diff --git a/.gitignore b/.gitignore index e1e0ad7..8f7c7db 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,8 @@ dist/ .env.local apps/*/dist/ apps/*/node_modules/ -.claude \ No newline at end of file +apps/*/.output/ +apps/*/.wxt/ +apps/*/chrome-mv*/ +apps/*/firefox-mv*/ +.claude diff --git a/apps/admin-web/src/components/ArticleDetailDrawer.vue b/apps/admin-web/src/components/ArticleDetailDrawer.vue index 1ed1fe8..b42b901 100644 --- a/apps/admin-web/src/components/ArticleDetailDrawer.vue +++ b/apps/admin-web/src/components/ArticleDetailDrawer.vue @@ -1,7 +1,8 @@ + + + + + + + @@ -361,6 +465,37 @@ async function refreshArticleData(): Promise { font-size: 12px; } +.article-drawer__record-platform { + display: flex; + flex-direction: column; + gap: 4px; +} + +.article-drawer__record-platform span, +.article-drawer__record-empty { + color: var(--muted); + font-size: 12px; +} + +.article-drawer__record-actions { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; +} + +.article-drawer__record-link { + overflow: hidden; + color: #1677ff; + text-overflow: ellipsis; + white-space: nowrap; +} + +.article-drawer__record-buttons { + display: flex; + gap: 8px; +} + @media (max-width: 960px) { .article-drawer__hero { flex-direction: column; diff --git a/apps/admin-web/src/components/CustomArticleTab.vue b/apps/admin-web/src/components/CustomArticleTab.vue index 776a730..dbb5be4 100644 --- a/apps/admin-web/src/components/CustomArticleTab.vue +++ b/apps/admin-web/src/components/CustomArticleTab.vue @@ -3,6 +3,7 @@ import { LoadingOutlined, AppstoreOutlined, ClockCircleOutlined, + SendOutlined, EditOutlined, DeleteOutlined, } from "@ant-design/icons-vue"; @@ -15,6 +16,7 @@ import { useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; import ArticleDetailDrawer from "@/components/ArticleDetailDrawer.vue"; +import PublishArticleModal from "@/components/PublishArticleModal.vue"; import { articlesApi, promptRulesApi } from "@/lib/api"; import { formatDateTime, @@ -55,6 +57,8 @@ const appliedFilters = reactive<{ const articleDrawerOpen = ref(false); const selectedArticleId = ref(null); +const publishModalOpen = ref(false); +const selectedPublishArticleId = ref(null); const rulesQuery = useQuery({ queryKey: ["promptRules", "simple"], @@ -75,8 +79,10 @@ const generateStatusOptions = computed(() => [ const publishStatusOptions = computed(() => [ { label: getPublishStatusMeta("unpublished").label, value: "unpublished" }, { label: getPublishStatusMeta("publishing").label, value: "publishing" }, - { label: getPublishStatusMeta("published").label, value: "published" }, - { label: getPublishStatusMeta("publish_failed").label, value: "publish_failed" }, + { label: getPublishStatusMeta("success").label, value: "success" }, + { label: getPublishStatusMeta("partial_success").label, value: "partial_success" }, + { label: getPublishStatusMeta("failed").label, value: "failed" }, + { label: getPublishStatusMeta("pending_review").label, value: "pending_review" }, ]); const articleParams = computed(() => { @@ -117,7 +123,7 @@ const columns = computed>(() => [ { title: t("common.generateStatus"), dataIndex: "generate_status", key: "generate_status", width: 128 }, { title: t("common.publishStatus"), dataIndex: "publish_status", key: "publish_status", width: 128 }, { title: t("common.wordCount"), dataIndex: "word_count", key: "word_count", width: 100 }, - { title: t("common.actions"), key: "actions", width: 120, fixed: "right", align: "right" }, + { title: t("common.actions"), key: "actions", width: 156, fixed: "right", align: "right" }, ]); function applyFilters(): void { @@ -148,10 +154,19 @@ function openEditor(article: ArticleListItem): void { void router.push({ name: "article-editor", params: { id: String(article.id) } }); } +function openPublish(article: ArticleListItem): void { + selectedPublishArticleId.value = article.id; + publishModalOpen.value = true; +} + function canEditArticle(status: string): boolean { return status === "completed" || status === "draft"; } +function canPublishArticle(status: string): boolean { + return status === "completed"; +} + function getDisplayTitle(article: ArticleListItem): string { if ((article.generate_status === "generating" || article.generate_status === "running") && !article.title) { return t("custom.article.titleGenerating"); @@ -327,6 +342,20 @@ onBeforeUnmount(() => { diff --git a/apps/admin-web/src/components/PublishArticleModal.vue b/apps/admin-web/src/components/PublishArticleModal.vue new file mode 100644 index 0000000..271ca61 --- /dev/null +++ b/apps/admin-web/src/components/PublishArticleModal.vue @@ -0,0 +1,451 @@ + + + + + diff --git a/apps/admin-web/src/i18n/messages/en-US.ts b/apps/admin-web/src/i18n/messages/en-US.ts index f930c65..17fb9b0 100644 --- a/apps/admin-web/src/i18n/messages/en-US.ts +++ b/apps/admin-web/src/i18n/messages/en-US.ts @@ -112,7 +112,7 @@ const enUS = { }, media: { title: "Media Management", - description: "No matching backend APIs exist in this repository yet.", + description: "Manage media bindings, local extension state, and one-click publishing from a single control plane.", }, brands: { title: "Brand Library", @@ -196,6 +196,9 @@ const enUS = { publish: { unpublished: "Unpublished", publishing: "Publishing", + success: "Published", + failed: "Failed", + partial_success: "Partial Success", published: "Published", publish_success: "Published", publish_failed: "Failed", @@ -230,6 +233,7 @@ const enUS = { preview: "Preview", edit: "Edit article", editDisabled: "Only draft, failed, or completed articles can be edited", + publishDisabled: "Only completed articles can be published", versions: "Versions", deleteConfirm: "Delete this article?", deleteSuccess: "Article deleted.", @@ -384,6 +388,7 @@ const enUS = { drawerTitle: "Article details", preview: "Content preview", versionHistory: "Version history", + publishedDetails: "Published details", untitled: "Untitled article", noContent: "This article does not have generated content yet.", editor: { @@ -405,7 +410,7 @@ const enUS = { saved: "Article saved.", generating: "This article is still generating. Try editing it later.", locked: "Only completed articles can be edited.", - publishPending: "Publishing is not wired yet. The current action only saves content.", + publishPending: "Save the article first, then choose the target platforms.", }, platforms: { zhihu: "Zhihu", @@ -419,6 +424,90 @@ const enUS = { }, versionUntitled: "Untitled version", }, + media: { + hero: { + eyebrow: "Publisher", + }, + plugin: { + title: "Browser Publisher", + ready: "Extension connected", + notInstalled: "Extension not connected", + installTitle: "Install and open the GEO Publisher extension first", + installDesc: "Binding and publishing depend on the browser's local platform sessions. The extension stores its own installation identity so future background checks can continue after the SaaS page is closed.", + backgroundHint: "The extension should not reuse the web app's access token directly. This implementation already registers a dedicated installation identity for future background scanning and inclusion checks.", + versionLabel: "Extension version", + installationLabel: "Installation ID", + connectedSummary: "Locally signed-in platforms", + registerRequired: "The extension installation is not registered yet. Please re-detect first.", + }, + actions: { + redetect: "Re-detect", + bind: "Bind", + rebind: "Rebind", + unbind: "Unbind", + }, + card: { + bound: "Bound", + unbound: "Unbound", + unboundHint: "{platform} has not been bound to this tenant yet.", + needsAttention: "Needs attention", + expiredHint: "This account is no longer healthy. Rebind it to restore publishing.", + localMissing: "Local session missing", + localMismatch: "Local account mismatch", + readyHint: "The correct local account is available and ready for publish or future background checks.", + accountLabel: "SaaS account", + uidLabel: "Platform UID", + localLabel: "Local state", + localConnected: "Session detected", + localDisconnected: "No session detected", + updatedAt: "Last sync", + multiAccount: "{count} accounts bound", + openPlatform: "Open platform", + }, + account: { + localMissing: "The tenant account is bound, but this browser does not currently have the matching local session.", + localMismatch: "This browser is signed in to a different account. Rebind or switch the local account first.", + ready: "Ready", + selectable: "Selectable", + unavailable: "Unavailable", + }, + empty: { + accounts: "No publishable media accounts are bound yet.", + }, + messages: { + bindSuccess: "{platform} bound successfully.", + bindPending: "The extension did not detect a local session for this platform yet. Sign in via the extension and retry.", + unbindSuccess: "Account unbound.", + unbindConfirm: "Unbind this platform account? Publishing will no longer be available until it is bound again.", + }, + publish: { + title: "Publish", + subtitle: "Multi-platform Dispatch", + description: "Choose tenant-bound accounts that are currently signed in locally. The extension executes the real publish flow and writes back the resulting links.", + platformsTitle: "Target platforms", + platformsHint: "Only tenant-bound accounts are listed here. Accounts with missing or mismatched local sessions are disabled automatically.", + coverTitle: "Cover image", + coverHint: "This version uses a cover asset URL. Upload and crop can be wired later.", + coverPlaceholder: "Enter an optional cover asset URL", + messages: { + success: "{count} publish tasks submitted.", + partial: "Publish finished with {success} succeeded and {failed} failed.", + selectPlatform: "Choose at least one available platform first.", + }, + }, + records: { + tabTitle: "Published details", + platform: "Platform", + status: "Status", + publishedAt: "Published at", + link: "Published link", + open: "Open", + copy: "Copy", + empty: "No publish records yet", + linkEmpty: "No link is available for this record yet.", + copySuccess: "Link copied.", + }, + }, brands: { eyebrow: "Brand Library", title: "Brand Library", diff --git a/apps/admin-web/src/i18n/messages/zh-CN.ts b/apps/admin-web/src/i18n/messages/zh-CN.ts index d9aa44b..c93d4b8 100644 --- a/apps/admin-web/src/i18n/messages/zh-CN.ts +++ b/apps/admin-web/src/i18n/messages/zh-CN.ts @@ -112,7 +112,7 @@ const zhCN = { }, media: { title: "媒体管理", - description: "当前仓库还没有匹配的后端接口,页面暂保持为设计占位。", + description: "统一管理媒体账号绑定、本地插件状态和一键发布执行链路。", }, brands: { title: "品牌词库", @@ -196,6 +196,9 @@ const zhCN = { publish: { unpublished: "未发布", publishing: "发布中", + success: "发布成功", + failed: "发布失败", + partial_success: "部分成功", published: "发布成功", publish_success: "发布成功", publish_failed: "发布失败", @@ -230,6 +233,7 @@ const zhCN = { preview: "预览正文", edit: "编辑文章", editDisabled: "只有草稿、生成失败或已完成文章才可编辑", + publishDisabled: "只有已完成文章才可发布", versions: "查看版本", deleteConfirm: "确认删除这篇文章吗?", deleteSuccess: "文章已删除", @@ -391,6 +395,7 @@ const zhCN = { drawerTitle: "文章详情", preview: "正文预览", versionHistory: "版本记录", + publishedDetails: "已发布详情", untitled: "未命名文章", noContent: "当前文章还没有正文内容", editor: { @@ -412,7 +417,7 @@ const zhCN = { saved: "文章已保存", generating: "文章还在生成中,请稍后再编辑。", locked: "只有已完成文章才可编辑。", - publishPending: "发布能力待接入,当前仅保存正文内容。", + publishPending: "请先保存文章,再选择需要发布的平台。", }, platforms: { zhihu: "知乎", @@ -426,6 +431,90 @@ const zhCN = { }, versionUntitled: "未命名版本", }, + media: { + hero: { + eyebrow: "Publisher", + }, + plugin: { + title: "浏览器发布器", + ready: "插件已连接", + notInstalled: "插件未连接", + installTitle: "请先安装并打开 GEO Publisher 插件", + installDesc: "绑定和发布都依赖浏览器本地登录态执行。插件会保存独立安装实例身份,后续即使网页关闭,也能继续运行后台检测任务。", + backgroundHint: "插件不会直接复用网页 access token。当前实现会为每个浏览器安装实例注册独立身份,为未来后台收录检测和定时扫描预留能力。", + versionLabel: "插件版本", + installationLabel: "安装实例 ID", + connectedSummary: "本地已登录平台", + registerRequired: "插件安装实例尚未注册,请先重新检测。", + }, + actions: { + redetect: "重新检测", + bind: "去绑定", + rebind: "重新绑定", + unbind: "解绑", + }, + card: { + bound: "已绑定", + unbound: "未绑定", + unboundHint: "{platform} 还未绑定到当前租户,可以直接发起授权。", + needsAttention: "需要处理", + expiredHint: "该账号状态异常,建议重新绑定以恢复发布能力。", + localMissing: "本地未登录", + localMismatch: "本地账号不匹配", + readyHint: "当前浏览器已检测到正确账号,可以直接用于发布和后续后台检测。", + accountLabel: "SaaS 账号", + uidLabel: "平台 UID", + localLabel: "本地状态", + localConnected: "已检测到登录态", + localDisconnected: "未检测到登录态", + updatedAt: "最近同步", + multiAccount: "已绑定 {count} 个账号", + openPlatform: "打开平台官网", + }, + account: { + localMissing: "SaaS 已绑定,但当前浏览器未检测到对应登录态。", + localMismatch: "当前浏览器登录的是另一个账号,请重新授权或切换账号。", + ready: "已就绪", + selectable: "可发布", + unavailable: "不可用", + }, + empty: { + accounts: "当前租户还没有绑定可发布账号。", + }, + messages: { + bindSuccess: "{platform} 绑定成功", + bindPending: "插件尚未检测到该平台登录态,请先在插件中登录后重试。", + unbindSuccess: "账号解绑成功", + unbindConfirm: "确认解绑这个平台账号吗?解绑后将无法直接发布。", + }, + publish: { + title: "发布", + subtitle: "Multi-platform Dispatch", + description: "选择当前浏览器已登录且租户已绑定的平台,插件会逐个平台执行真实发布并回写链接。", + platformsTitle: "发布平台", + platformsHint: "仅展示当前租户已绑定的账号。插件本地登录态异常的平台会自动禁用。", + coverTitle: "封面图", + coverHint: "当前版本先使用封面素材 URL,后续可以接入上传与裁剪。", + coverPlaceholder: "请输入封面素材 URL(可选)", + messages: { + success: "已提交 {count} 个平台发布任务", + partial: "发布执行完成,成功 {success} 个,失败 {failed} 个", + selectPlatform: "请先选择至少一个可发布的平台", + }, + }, + records: { + tabTitle: "已发布详情", + platform: "已发布平台", + status: "发布状态", + publishedAt: "发布时间", + link: "已发布链接", + open: "打开", + copy: "复制", + empty: "暂无发布记录", + linkEmpty: "当前记录还没有可用链接", + copySuccess: "链接已复制", + }, + }, brands: { eyebrow: "Brand Library", title: "品牌词库", diff --git a/apps/admin-web/src/lib/api.ts b/apps/admin-web/src/lib/api.ts index 877e1f9..7aae8a3 100644 --- a/apps/admin-web/src/lib/api.ts +++ b/apps/admin-web/src/lib/api.ts @@ -9,6 +9,10 @@ import type { BrandRequest, Competitor, CompetitorRequest, + CreatePluginSessionRequest, + CreatePluginSessionResponse, + CreatePublishBatchRequest, + CreatePublishBatchResponse, GenerateFromRuleRequest, GenerateFromRuleResponse, InstantTaskListParams, @@ -18,6 +22,11 @@ import type { KeywordRequest, LoginRequest, LoginResponse, + MediaPlatform, + PlatformAccount, + PublishRecord, + RegisterPluginInstallationRequest, + RegisterPluginInstallationResponse, PromptRule, PromptRuleGroup, PromptRuleGroupRequest, @@ -68,6 +77,16 @@ const generationStreamEnabled = import.meta.env.VITE_GENERATION_STREAM_ENABLED = const publicClient = createApiClient({ baseURL }); +export function getApiBaseURL(): string { + if (baseURL) { + return baseURL; + } + if (typeof window !== "undefined") { + return window.location.origin; + } + return ""; +} + export const apiClient = createApiClient({ baseURL, auth: { @@ -192,6 +211,15 @@ export const articlesApi = { versions(id: number) { return apiClient.get(`/api/tenant/articles/${id}/versions`); }, + publishBatch(id: number, payload: CreatePublishBatchRequest) { + return apiClient.post( + `/api/tenant/articles/${id}/publish-batches`, + payload, + ); + }, + publishRecords(id: number) { + return apiClient.get(`/api/tenant/articles/${id}/publish-records`); + }, remove(id: number) { return apiClient.remove(`/api/tenant/articles/${id}`); }, @@ -340,6 +368,30 @@ export const brandsApi = { }, }; +export const mediaApi = { + platforms() { + return apiClient.get("/api/tenant/media/platforms"); + }, + accounts() { + return apiClient.get("/api/tenant/media/platform-accounts"); + }, + createPluginSession(payload: CreatePluginSessionRequest) { + return apiClient.post( + "/api/tenant/media/plugin-sessions", + payload, + ); + }, + registerPluginInstallation(payload: RegisterPluginInstallationRequest) { + return apiClient.post( + "/api/tenant/media/plugin-installations/register", + payload, + ); + }, + unbindAccount(id: number) { + return apiClient.remove(`/api/tenant/media/platform-accounts/${id}`); + }, +}; + export const promptRulesApi = { list(params?: PromptRuleListParams) { return apiClient.get("/api/tenant/prompt-rules", { params }); diff --git a/apps/admin-web/src/lib/display.ts b/apps/admin-web/src/lib/display.ts index e2576c4..3f6fe98 100644 --- a/apps/admin-web/src/lib/display.ts +++ b/apps/admin-web/src/lib/display.ts @@ -14,6 +14,9 @@ const generateStatusMap: Record = { const publishStatusMap: Record = { unpublished: { label: "status.publish.unpublished", color: "default" }, publishing: { label: "status.publish.publishing", color: "processing" }, + success: { label: "status.publish.success", color: "success" }, + failed: { label: "status.publish.failed", color: "error" }, + partial_success: { label: "status.publish.partial_success", color: "warning" }, published: { label: "status.publish.published", color: "success" }, publish_success: { label: "status.publish.publish_success", color: "success" }, publish_failed: { label: "status.publish.publish_failed", color: "error" }, diff --git a/apps/admin-web/src/lib/publisher-plugin.ts b/apps/admin-web/src/lib/publisher-plugin.ts new file mode 100644 index 0000000..b2ece5d --- /dev/null +++ b/apps/admin-web/src/lib/publisher-plugin.ts @@ -0,0 +1,132 @@ +import type { + PublisherBindRequest, + PublisherBindResponse, + PublisherLocalPlatformState, + PublisherPluginPingResponse, + PublisherRegisterInstallationPayload, + PublisherRegisterInstallationResult, + PublisherPublishArticleRequest, + PublisherPublishResponse, +} from "@geo/shared-types"; + +type PluginAction = "ping" | "detectPlatforms" | "registerInstallation" | "bindAccount" | "publishArticle"; + +type PluginSuccessPayload = { + source: "geo-extension"; + type: "geo.publisher.response"; + requestId: string; + success: true; + data: T; +}; + +type PluginErrorPayload = { + source: "geo-extension"; + type: "geo.publisher.response"; + requestId: string; + success: false; + error: string; +}; + +type PluginPayloadMap = { + ping: PublisherPluginPingResponse; + detectPlatforms: PublisherLocalPlatformState[]; + registerInstallation: PublisherRegisterInstallationResult; + bindAccount: PublisherBindResponse; + publishArticle: PublisherPublishResponse; +}; + +function nextRequestId(): string { + if (typeof crypto !== "undefined" && "randomUUID" in crypto) { + return crypto.randomUUID(); + } + return `geo-${Date.now()}-${Math.random().toString(36).slice(2)}`; +} + +async function requestPlugin( + action: T, + payload?: T extends "bindAccount" + ? PublisherBindRequest + : T extends "registerInstallation" + ? PublisherRegisterInstallationPayload + : T extends "publishArticle" + ? PublisherPublishArticleRequest + : undefined, + timeoutMs = 4000, +): Promise { + if (typeof window === "undefined") { + throw new Error("plugin bridge is only available in browser contexts"); + } + + const requestId = nextRequestId(); + + return new Promise((resolve, reject) => { + const timer = window.setTimeout(() => { + cleanup(); + reject(new Error("publisher_plugin_timeout")); + }, timeoutMs); + + const handleMessage = (event: MessageEvent | PluginErrorPayload>) => { + if (event.source !== window || !event.data || event.data.type !== "geo.publisher.response") { + return; + } + if (event.data.requestId !== requestId || event.data.source !== "geo-extension") { + return; + } + + cleanup(); + if (event.data.success) { + resolve(event.data.data); + return; + } + reject(new Error(event.data.error || "publisher_plugin_unknown_error")); + }; + + const cleanup = () => { + window.clearTimeout(timer); + window.removeEventListener("message", handleMessage); + }; + + window.addEventListener("message", handleMessage); + window.postMessage( + { + source: "geo-admin", + type: "geo.publisher.request", + requestId, + action, + payload, + }, + window.location.origin, + ); + }); +} + +export async function pingPublisherPlugin(): Promise { + try { + return await requestPlugin("ping", undefined, 1500); + } catch { + return { + installed: false, + capabilities: [], + }; + } +} + +export async function detectPublisherPlatforms(): Promise { + return requestPlugin("detectPlatforms", undefined, 2500); +} + +export async function registerPublisherInstallation( + payload: PublisherRegisterInstallationPayload, +): Promise { + return requestPlugin("registerInstallation", payload, 5000); +} + +export async function bindPublisherPlatform(payload: PublisherBindRequest): Promise { + return requestPlugin("bindAccount", payload, 10000); +} + +export async function publishWithPublisherPlugin( + payload: PublisherPublishArticleRequest, +): Promise { + return requestPlugin("publishArticle", payload, 25000); +} diff --git a/apps/admin-web/src/lib/publisher-runtime.ts b/apps/admin-web/src/lib/publisher-runtime.ts new file mode 100644 index 0000000..2b56d4e --- /dev/null +++ b/apps/admin-web/src/lib/publisher-runtime.ts @@ -0,0 +1,73 @@ +import type { PublisherLocalPlatformState, PublisherPluginPingResponse } from "@geo/shared-types"; + +import { getApiBaseURL, mediaApi } from "./api"; +import { + detectPublisherPlatforms, + pingPublisherPlugin, + registerPublisherInstallation, +} from "./publisher-plugin"; + +export interface PublisherRuntimeState { + ping: PublisherPluginPingResponse; + localPlatforms: PublisherLocalPlatformState[]; + pluginInstallationId: number | null; +} + +function detectBrowserName(): string { + const ua = navigator.userAgent; + if (/Edg\//.test(ua)) { + return "Edge"; + } + if (/Chrome\//.test(ua) && !/Edg\//.test(ua)) { + return "Chrome"; + } + if (/Firefox\//.test(ua)) { + return "Firefox"; + } + if (/Safari\//.test(ua) && !/Chrome\//.test(ua)) { + return "Safari"; + } + return navigator.platform || "Unknown"; +} + +export async function loadPublisherRuntimeState(): Promise { + const ping = await pingPublisherPlugin(); + if (!ping.installed || !ping.installation_key) { + return { + ping, + localPlatforms: [], + pluginInstallationId: null, + }; + } + + const localPlatforms = await detectPublisherPlatforms(); + let pluginInstallationId = ping.plugin_installation_id ?? null; + + try { + const registration = await mediaApi.registerPluginInstallation({ + installation_key: ping.installation_key, + installation_name: "GEO Publisher", + browser_name: detectBrowserName(), + client_version: ping.version, + }); + + await registerPublisherInstallation({ + plugin_installation_id: registration.plugin_installation_id, + installation_token: registration.installation_token, + api_base_url: getApiBaseURL(), + }); + + pluginInstallationId = registration.plugin_installation_id; + } catch (error) { + console.error("publisher installation registration failed", error); + } + + return { + ping: { + ...ping, + plugin_installation_id: pluginInstallationId, + }, + localPlatforms, + pluginInstallationId, + }; +} diff --git a/apps/admin-web/src/router/index.ts b/apps/admin-web/src/router/index.ts index 7b0bd04..f0eaf5d 100644 --- a/apps/admin-web/src/router/index.ts +++ b/apps/admin-web/src/router/index.ts @@ -89,7 +89,7 @@ const router = createRouter({ { path: "media", name: "media", - component: () => import("@/views/FeatureStubView.vue"), + component: () => import("@/views/MediaView.vue"), meta: { titleKey: "route.media.title", descriptionKey: "route.media.description", diff --git a/apps/admin-web/src/views/ArticleEditorView.vue b/apps/admin-web/src/views/ArticleEditorView.vue index 5028dee..d7dad86 100644 --- a/apps/admin-web/src/views/ArticleEditorView.vue +++ b/apps/admin-web/src/views/ArticleEditorView.vue @@ -8,6 +8,7 @@ import { useRoute, useRouter } from "vue-router"; import { MilkdownProvider } from "@milkdown/vue"; import ArticleEditorCanvas from "@/components/ArticleEditorCanvas.vue"; +import PublishArticleModal from "@/components/PublishArticleModal.vue"; import PublishPlatformSelector from "@/components/PublishPlatformSelector.vue"; import { articlesApi } from "@/lib/api"; import { formatError } from "@/lib/errors"; @@ -29,6 +30,7 @@ const initialPublishPlatforms = ref([]); const coverEnabled = ref(true); const coverPreviewUrl = ref(""); const leaveModalOpen = ref(false); +const publishModalOpen = ref(false); const detailQuery = useQuery({ queryKey: computed(() => ["articles", "detail", articleId.value]), @@ -133,7 +135,15 @@ async function handlePublish(): Promise { } } - message.warning(t("article.editor.messages.publishPending")); + publishModalOpen.value = true; +} + +async function handlePublished(): Promise { + await Promise.all([ + detailQuery.refetch(), + queryClient.invalidateQueries({ queryKey: ["articles"] }), + queryClient.invalidateQueries({ queryKey: ["workspace"] }), + ]); } function handleBack(): void { @@ -342,6 +352,12 @@ function serializePlatformSelection(platformIds: string[]): string { + + diff --git a/apps/admin-web/src/views/MediaView.vue b/apps/admin-web/src/views/MediaView.vue new file mode 100644 index 0000000..61acda8 --- /dev/null +++ b/apps/admin-web/src/views/MediaView.vue @@ -0,0 +1,831 @@ + + + + + diff --git a/apps/admin-web/src/views/TemplatesView.vue b/apps/admin-web/src/views/TemplatesView.vue index 8d8db0b..dc51499 100644 --- a/apps/admin-web/src/views/TemplatesView.vue +++ b/apps/admin-web/src/views/TemplatesView.vue @@ -2,6 +2,7 @@ import { DeleteOutlined, EditOutlined, + SendOutlined, PlusOutlined, BlockOutlined, ReloadOutlined, @@ -22,6 +23,7 @@ import { computed, onBeforeUnmount, reactive, ref, watch } from "vue"; import { useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; +import PublishArticleModal from "@/components/PublishArticleModal.vue"; import { articlesApi, templatesApi } from "@/lib/api"; import { formatError } from "@/lib/errors"; import { @@ -37,6 +39,8 @@ const router = useRouter(); const { t } = useI18n(); const pickerOpen = ref(false); +const publishModalOpen = ref(false); +const selectedPublishArticleId = ref(null); const page = ref(1); const pageSize = ref(10); const draftGenerationRange = ref<[Dayjs, Dayjs] | null>(null); @@ -135,8 +139,10 @@ const generateStatusOptions = computed(() => [ const publishStatusOptions = computed(() => [ { label: getPublishStatusMeta("unpublished").label, value: "unpublished" }, { label: getPublishStatusMeta("publishing").label, value: "publishing" }, - { label: getPublishStatusMeta("published").label, value: "published" }, - { label: getPublishStatusMeta("publish_failed").label, value: "publish_failed" }, + { label: getPublishStatusMeta("success").label, value: "success" }, + { label: getPublishStatusMeta("partial_success").label, value: "partial_success" }, + { label: getPublishStatusMeta("failed").label, value: "failed" }, + { label: getPublishStatusMeta("pending_review").label, value: "pending_review" }, ]); const articleColumns = computed>(() => [ @@ -172,7 +178,7 @@ const articleColumns = computed>(() => [ { title: t("common.actions"), key: "actions", - width: 120, + width: 156, align: "right", }, ]); @@ -243,10 +249,19 @@ function openEditor(article: ArticleListItem): void { void router.push({ name: "article-editor", params: { id: String(article.id) } }); } +function openPublish(article: ArticleListItem): void { + selectedPublishArticleId.value = article.id; + publishModalOpen.value = true; +} + function canEditArticle(status: string): boolean { return status === "completed" || status === "draft" || status === "failed"; } +function canPublishArticle(status: string): boolean { + return status === "completed"; +} + watch( () => articleListQuery.data.value?.items ?? [], (items: ArticleListItem[]) => { @@ -449,6 +464,22 @@ async function handleDelete(articleId: number): Promise { diff --git a/apps/admin-web/src/views/WorkspaceView.vue b/apps/admin-web/src/views/WorkspaceView.vue index 5f0b963..d6a91f3 100644 --- a/apps/admin-web/src/views/WorkspaceView.vue +++ b/apps/admin-web/src/views/WorkspaceView.vue @@ -251,9 +251,12 @@ function refreshDashboard(): void { diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts new file mode 100644 index 0000000..2bf7de2 --- /dev/null +++ b/apps/browser-extension/entrypoints/background.ts @@ -0,0 +1,31 @@ +import { browser } from "wxt/browser"; +import { defineBackground } from "wxt/utils/define-background"; + +import { handlePublisherAction } from "../src/runtime"; +import { getExtensionState } from "../src/storage"; + +export default defineBackground(() => { + void getExtensionState(); + + browser.runtime.onInstalled.addListener(() => { + void getExtensionState(); + browser.alarms.create("geo.publisher.background-heartbeat", { + periodInMinutes: 60, + }); + }); + + browser.alarms.onAlarm.addListener(async (alarm) => { + if (alarm.name !== "geo.publisher.background-heartbeat") { + return; + } + await getExtensionState(); + }); + + browser.runtime.onMessage.addListener((message) => { + if (!message || message.type !== "geo.publisher.request") { + return undefined; + } + + return handlePublisherAction(message.action, message.payload); + }); +}); diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts new file mode 100644 index 0000000..10a5b6c --- /dev/null +++ b/apps/browser-extension/entrypoints/content.ts @@ -0,0 +1,56 @@ +import { browser } from "wxt/browser"; +import { defineContentScript } from "wxt/utils/define-content-script"; + +type PublisherRequestMessage = { + source: "geo-admin"; + type: "geo.publisher.request"; + requestId: string; + action: "ping" | "detectPlatforms" | "registerInstallation" | "bindAccount" | "publishArticle"; + payload?: unknown; +}; + +export default defineContentScript({ + matches: ["http://*/*", "https://*/*"], + main() { + window.addEventListener("message", async (event: MessageEvent) => { + if (event.source !== window || !event.data || event.data.type !== "geo.publisher.request") { + return; + } + if (event.data.source !== "geo-admin") { + return; + } + + const { requestId, action, payload } = event.data; + + try { + const data = await browser.runtime.sendMessage({ + type: "geo.publisher.request", + action, + payload, + }); + + window.postMessage( + { + source: "geo-extension", + type: "geo.publisher.response", + requestId, + success: true, + data, + }, + window.location.origin, + ); + } catch (error) { + window.postMessage( + { + source: "geo-extension", + type: "geo.publisher.response", + requestId, + success: false, + error: error instanceof Error ? error.message : "publisher_plugin_unknown_error", + }, + window.location.origin, + ); + } + }); + }, +}); diff --git a/apps/browser-extension/entrypoints/popup/App.vue b/apps/browser-extension/entrypoints/popup/App.vue new file mode 100644 index 0000000..be0b90f --- /dev/null +++ b/apps/browser-extension/entrypoints/popup/App.vue @@ -0,0 +1,767 @@ + + + + + diff --git a/apps/browser-extension/entrypoints/popup/index.html b/apps/browser-extension/entrypoints/popup/index.html new file mode 100644 index 0000000..82f292f --- /dev/null +++ b/apps/browser-extension/entrypoints/popup/index.html @@ -0,0 +1,12 @@ + + + + + + GEO Publisher + + +
+ + + diff --git a/apps/browser-extension/entrypoints/popup/main.ts b/apps/browser-extension/entrypoints/popup/main.ts new file mode 100644 index 0000000..4557307 --- /dev/null +++ b/apps/browser-extension/entrypoints/popup/main.ts @@ -0,0 +1,5 @@ +import { createApp } from "vue"; + +import App from "./App.vue"; + +createApp(App).mount("#app"); diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json new file mode 100644 index 0000000..d605e09 --- /dev/null +++ b/apps/browser-extension/package.json @@ -0,0 +1,24 @@ +{ + "name": "browser-extension", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "wxt", + "build": "wxt build", + "zip": "wxt zip" + }, + "dependencies": { + "@geo/shared-types": "workspace:*", + "js-md5": "^0.8.3", + "marked": "^17.0.5", + "vue": "^3.5.31" + }, + "devDependencies": { + "@types/chrome": "^0.1.38", + "@wxt-dev/module-vue": "^1.0.3", + "typescript": "^5.9.3", + "vue-tsc": "^3.2.6", + "wxt": "^0.20.20" + } +} diff --git a/apps/browser-extension/public/icon/128.png b/apps/browser-extension/public/icon/128.png new file mode 100644 index 0000000..9e35d13 Binary files /dev/null and b/apps/browser-extension/public/icon/128.png differ diff --git a/apps/browser-extension/public/icon/16.png b/apps/browser-extension/public/icon/16.png new file mode 100644 index 0000000..cd09f8c Binary files /dev/null and b/apps/browser-extension/public/icon/16.png differ diff --git a/apps/browser-extension/public/icon/32.png b/apps/browser-extension/public/icon/32.png new file mode 100644 index 0000000..f51ce1b Binary files /dev/null and b/apps/browser-extension/public/icon/32.png differ diff --git a/apps/browser-extension/public/icon/48.png b/apps/browser-extension/public/icon/48.png new file mode 100644 index 0000000..cb7a449 Binary files /dev/null and b/apps/browser-extension/public/icon/48.png differ diff --git a/apps/browser-extension/public/icon/96.png b/apps/browser-extension/public/icon/96.png new file mode 100644 index 0000000..c28ad52 Binary files /dev/null and b/apps/browser-extension/public/icon/96.png differ diff --git a/apps/browser-extension/public/logos/logo_baijiahao.png b/apps/browser-extension/public/logos/logo_baijiahao.png new file mode 100644 index 0000000..fcc5602 Binary files /dev/null and b/apps/browser-extension/public/logos/logo_baijiahao.png differ diff --git a/apps/browser-extension/public/logos/logo_bilibili.png b/apps/browser-extension/public/logos/logo_bilibili.png new file mode 100644 index 0000000..7e2d13f Binary files /dev/null and b/apps/browser-extension/public/logos/logo_bilibili.png differ diff --git a/apps/browser-extension/public/logos/logo_dongchedi.svg b/apps/browser-extension/public/logos/logo_dongchedi.svg new file mode 100644 index 0000000..a397c81 --- /dev/null +++ b/apps/browser-extension/public/logos/logo_dongchedi.svg @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/apps/browser-extension/public/logos/logo_jianshu.svg b/apps/browser-extension/public/logos/logo_jianshu.svg new file mode 100644 index 0000000..656b6dd --- /dev/null +++ b/apps/browser-extension/public/logos/logo_jianshu.svg @@ -0,0 +1,4 @@ + + + 简书 + \ No newline at end of file diff --git a/apps/browser-extension/public/logos/logo_juejin.png b/apps/browser-extension/public/logos/logo_juejin.png new file mode 100644 index 0000000..6ee5fc0 Binary files /dev/null and b/apps/browser-extension/public/logos/logo_juejin.png differ diff --git a/apps/browser-extension/public/logos/logo_qiehao.png b/apps/browser-extension/public/logos/logo_qiehao.png new file mode 100644 index 0000000..744bcc7 Binary files /dev/null and b/apps/browser-extension/public/logos/logo_qiehao.png differ diff --git a/apps/browser-extension/public/logos/logo_smzdm.svg b/apps/browser-extension/public/logos/logo_smzdm.svg new file mode 100644 index 0000000..f85ad4c --- /dev/null +++ b/apps/browser-extension/public/logos/logo_smzdm.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/apps/browser-extension/public/logos/logo_souhu.png b/apps/browser-extension/public/logos/logo_souhu.png new file mode 100644 index 0000000..9a63b32 Binary files /dev/null and b/apps/browser-extension/public/logos/logo_souhu.png differ diff --git a/apps/browser-extension/public/logos/logo_toutiao.png b/apps/browser-extension/public/logos/logo_toutiao.png new file mode 100644 index 0000000..9995c7a Binary files /dev/null and b/apps/browser-extension/public/logos/logo_toutiao.png differ diff --git a/apps/browser-extension/public/logos/logo_wangyihao.png b/apps/browser-extension/public/logos/logo_wangyihao.png new file mode 100644 index 0000000..bdcef05 Binary files /dev/null and b/apps/browser-extension/public/logos/logo_wangyihao.png differ diff --git a/apps/browser-extension/public/logos/logo_weixin_gzh.svg b/apps/browser-extension/public/logos/logo_weixin_gzh.svg new file mode 100644 index 0000000..f306286 --- /dev/null +++ b/apps/browser-extension/public/logos/logo_weixin_gzh.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/apps/browser-extension/public/logos/logo_zhihu.png b/apps/browser-extension/public/logos/logo_zhihu.png new file mode 100644 index 0000000..b5381c2 Binary files /dev/null and b/apps/browser-extension/public/logos/logo_zhihu.png differ diff --git a/apps/browser-extension/public/logos/logo_zol.png b/apps/browser-extension/public/logos/logo_zol.png new file mode 100644 index 0000000..64432a2 Binary files /dev/null and b/apps/browser-extension/public/logos/logo_zol.png differ diff --git a/apps/browser-extension/public/rules.json b/apps/browser-extension/public/rules.json new file mode 100644 index 0000000..3ee0057 --- /dev/null +++ b/apps/browser-extension/public/rules.json @@ -0,0 +1,48 @@ +[ + { + "id": 4, + "priority": 1, + "action": { + "type": "modifyHeaders", + "requestHeaders": [ + { + "header": "Referer", + "operation": "set", + "value": "https://zhuanlan.zhihu.com" + }, + { + "header": "origin", + "operation": "set", + "value": "https://zhuanlan.zhihu.com" + } + ] + }, + "condition": { + "urlFilter": "https://zhuanlan.zhihu.com/*", + "resourceTypes": ["xmlhttprequest"] + } + }, + { + "id": 5, + "priority": 1, + "action": { + "type": "modifyHeaders", + "requestHeaders": [ + { + "header": "Referer", + "operation": "set", + "value": "https://zhuanlan.zhihu.com" + }, + { + "header": "origin", + "operation": "set", + "value": "https://zhuanlan.zhihu.com" + } + ] + }, + "condition": { + "urlFilter": "*://*.zhimg.com/*", + "resourceTypes": ["xmlhttprequest"] + } + } +] diff --git a/apps/browser-extension/public/wxt.svg b/apps/browser-extension/public/wxt.svg new file mode 100644 index 0000000..0e76320 --- /dev/null +++ b/apps/browser-extension/public/wxt.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/browser-extension/src/adapters/index.ts b/apps/browser-extension/src/adapters/index.ts new file mode 100644 index 0000000..88d968e --- /dev/null +++ b/apps/browser-extension/src/adapters/index.ts @@ -0,0 +1,26 @@ +import type { StoredPlatformState } from "../platforms"; + +import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types"; +import { zhihuAdapter } from "./zhihu"; + +const adapters = new Map([[zhihuAdapter.platformId, zhihuAdapter]]); + +export function getPlatformAdapter(platformId: string): PlatformAdapter | undefined { + return adapters.get(platformId); +} + +export async function detectPlatformState(platform: StoredPlatformState): Promise { + const adapter = getPlatformAdapter(platform.platform_id); + if (!adapter) { + return platform; + } + return adapter.detect(platform); +} + +export async function publishViaAdapter(platformId: string, context: AdapterContext): Promise { + const adapter = getPlatformAdapter(platformId); + if (!adapter) { + return null; + } + return adapter.publish(context); +} diff --git a/apps/browser-extension/src/adapters/types.ts b/apps/browser-extension/src/adapters/types.ts new file mode 100644 index 0000000..fe286e9 --- /dev/null +++ b/apps/browser-extension/src/adapters/types.ts @@ -0,0 +1,24 @@ +import type { PublisherPublishArticleRequest, PublishBatchTask } from "@geo/shared-types"; + +import type { StoredPlatformState } from "../platforms"; + +export interface AdapterContext { + article: PublisherPublishArticleRequest; + task: PublishBatchTask; +} + +export interface PlatformPublishResult { + success: boolean; + status: "success" | "failed" | "pending_review"; + externalArticleId?: string | null; + externalArticleUrl?: string | null; + externalManageUrl?: string | null; + message?: string | null; + responsePayload?: Record; +} + +export interface PlatformAdapter { + platformId: string; + detect(platform: StoredPlatformState): Promise; + publish(context: AdapterContext): Promise; +} diff --git a/apps/browser-extension/src/adapters/zhihu.ts b/apps/browser-extension/src/adapters/zhihu.ts new file mode 100644 index 0000000..d1c4a21 --- /dev/null +++ b/apps/browser-extension/src/adapters/zhihu.ts @@ -0,0 +1,380 @@ +import { browser } from "wxt/browser"; +import md5Lib from "js-md5"; +import { marked } from "marked"; + +import type { StoredPlatformState } from "../platforms"; + +import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types"; + +type ZhihuMeResponse = { + uid?: string; + id?: string; + name?: string; + avatar_url?: string; +}; + +type ZhihuImageTokenResponse = { + upload_file?: { + state?: number; + object_key?: string; + }; + upload_token?: { + access_id?: string; + access_key?: string; + access_token?: string; + }; +}; + +type ZhihuDraftCreateResponse = { + id?: string; +}; + +type ZhihuPublishResponse = { + code?: number; +}; + +function buildHeaders(headers?: HeadersInit): Headers { + const next = new Headers(headers); + if (!next.has("x-requested-with")) { + next.set("x-requested-with", "fetch"); + } + return next; +} + +async function getCookie(name?: string): Promise { + const cookies = await browser.cookies.getAll({ domain: "zhihu.com" }); + if (name) { + return cookies.find((item) => item.name === name)?.value ?? ""; + } + return cookies.map((item) => `${item.name}=${item.value}`).join("; "); +} + +async function readJson(response: Response): Promise { + const text = await response.text(); + if (!response.ok) { + throw new Error(text || `zhihu_request_failed_${response.status}`); + } + if (!text) { + return {} as T; + } + return JSON.parse(text) as T; +} + +async function zhihuFetch(input: string, init?: RequestInit): Promise { + const response = await fetch(input, { + ...init, + credentials: "include", + headers: buildHeaders(init?.headers), + }); + return readJson(response); +} + +function randomTraceId(): string { + const uuid = + typeof crypto !== "undefined" && "randomUUID" in crypto + ? crypto.randomUUID() + : `geo-${Date.now()}-${Math.random().toString(16).slice(2)}`; + return `${Date.now()},${uuid}`; +} + +function markdownToHtml(markdown: string): string { + return marked.parse(markdown, { async: false, gfm: true, breaks: false }) as string; +} + +function baseContent(article: AdapterContext["article"]): string { + const html = article.html_content?.trim(); + if (html) { + return html; + } + return markdownToHtml(article.markdown_content ?? ""); +} + +function transformContent(html: string): string { + let next = html.trim(); + + next = next.replace(//gi, ""); + next = next.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, ""); + next = next.replace(/\sstyle="[^"]*"/gi, ""); + next = next.replace(/\sdata-(?!draft)[a-z-]+="[^"]*"/gi, ""); + next = next.replace(/]*>\s*()\s*<\/figure>/gi, "$1"); + next = next.replace(/
/gi, '
');
+  next = next.replace(/]+src="[^"]+"[^>]*)>/gi, "
"); + + return next; +} + +function extractImageSources(html: string): string[] { + const sources = new Set(); + for (const match of html.matchAll(/]*src="([^"]+)"[^>]*>/gi)) { + const src = match[1]?.trim(); + if (src) { + sources.add(src); + } + } + return [...sources]; +} + +async function md5Hex(buffer: ArrayBuffer): Promise { + const md5 = md5Lib as unknown as (value: string | ArrayBuffer | Uint8Array) => string; + return md5(buffer); +} + +async function hmacSha1Base64(key: string, message: string): Promise { + const encoder = new TextEncoder(); + const cryptoKey = await crypto.subtle.importKey( + "raw", + encoder.encode(key), + { name: "HMAC", hash: "SHA-1" }, + false, + ["sign"], + ); + const signature = await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(message)); + return btoa(String.fromCharCode(...new Uint8Array(signature))); +} + +function buildPictureUrl(imageHash: string, mimeType: string): string { + const extension = mimeType.split("/")[1] || "png"; + return `https://picx.zhimg.com/v2-${imageHash}.${extension}`; +} + +async function uploadBinaryImage(sourceUrl: string): Promise { + const blob = await fetch(sourceUrl).then((response) => response.blob()).catch(() => null); + if (!blob || !blob.type.startsWith("image/")) { + return null; + } + + const buffer = await blob.arrayBuffer(); + const imageHash = await md5Hex(buffer); + + const token = await zhihuFetch("https://api.zhihu.com/images", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + image_hash: imageHash, + source: "article", + }), + }); + + if (token.upload_file?.state === 2 && token.upload_file.object_key && token.upload_token) { + const ossDate = new Date().toUTCString(); + const ossUserAgent = "aliyun-sdk-js/6.8.0 Chrome 139.0.0.0 on OS X 10.15.7 64-bit"; + const stringToSign = `PUT\n\n${blob.type}\n${ossDate}\nx-oss-date:${ossDate}\nx-oss-security-token:${token.upload_token.access_token}\nx-oss-user-agent:${ossUserAgent}\n/zhihu-pics/v2-${imageHash}`; + const signature = await hmacSha1Base64(token.upload_token.access_key || "", stringToSign); + + const uploadResponse = await fetch(`https://zhihu-pics-upload.zhimg.com/${token.upload_file.object_key}`, { + method: "PUT", + headers: { + "content-type": blob.type, + "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", + "x-oss-date": ossDate, + "x-oss-user-agent": ossUserAgent, + "x-oss-security-token": token.upload_token.access_token || "", + authorization: `OSS ${token.upload_token.access_id}:${signature}`, + }, + body: blob, + }); + if (!uploadResponse.ok) { + throw new Error(`zhihu_oss_upload_failed_${uploadResponse.status}`); + } + } + + return buildPictureUrl(imageHash, blob.type); +} + +async function processContentImages(html: string): Promise { + const sources = extractImageSources(html); + if (!sources.length) { + return html; + } + + let next = html; + const uploaded = new Map(); + + for (const source of sources) { + if (/zhimg\.com/i.test(source)) { + continue; + } + const target = await uploadBinaryImage(source); + if (target) { + uploaded.set(source, target); + } + } + + for (const [from, to] of uploaded.entries()) { + next = next.split(from).join(to); + } + + return next; +} + +async function detectZhihu(platform: StoredPlatformState): Promise { + try { + const me = await zhihuFetch("https://www.zhihu.com/api/v4/me", { + method: "GET", + }); + + const uid = me.uid || me.id ? String(me.uid || me.id) : undefined; + if (!uid || !me.name) { + return { + ...platform, + connected: false, + platform_uid: null, + nickname: null, + avatar_url: null, + message: "未检测到知乎登录态", + }; + } + + return { + ...platform, + connected: true, + platform_uid: uid, + nickname: me.name, + avatar_url: me.avatar_url ?? null, + message: null, + }; + } catch (error) { + return { + ...platform, + connected: false, + platform_uid: null, + nickname: null, + avatar_url: null, + message: error instanceof Error ? error.message : "知乎登录检测失败", + }; + } +} + +async function createDraft(context: AdapterContext, titleImage: string | null): Promise { + const title = context.article.title?.trim() || "未命名文章"; + const body = { + delta_time: 0, + can_reward: false, + title, + content: await processContentImages(transformContent(baseContent(context.article))), + ...(titleImage ? { titleImage } : {}), + }; + + const created = await zhihuFetch("https://zhuanlan.zhihu.com/api/articles/drafts", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!created.id) { + throw new Error("zhihu_create_draft_failed"); + } + return created.id; +} + +async function publishDraft(draftId: string): Promise { + const xsrfToken = await getCookie("_xsrf"); + const result = await zhihuFetch("https://www.zhihu.com/api/v4/content/publish", { + method: "POST", + headers: { + "content-type": "application/json", + "x-xsrftoken": xsrfToken, + }, + body: JSON.stringify({ + action: "article", + data: { + publish: { + traceId: randomTraceId(), + }, + draft: { + disabled: 1, + id: draftId, + isPublished: false, + }, + commentsPermission: { + comment_permission: "anyone", + }, + creationStatement: { + disclaimer_type: "ai_creation", + disclaimer_status: "open", + }, + contentsTables: { + table_of_contents_enabled: false, + }, + commercialReportInfo: { + isReport: 0, + }, + appreciate: { + can_reward: false, + tagline: "", + }, + hybridInfo: {}, + }, + }), + }); + + return result.code === 0; +} + +async function publishZhihu(context: AdapterContext): Promise { + const freshState = await detectZhihu({ + platform_id: "zhihu", + platform_name: "知乎", + short_name: "知", + accent_color: "#1677ff", + login_url: "https://www.zhihu.com/signin", + logo_path: "logos/logo_zhihu.png", + connected: false, + platform_uid: null, + nickname: null, + avatar_url: null, + message: null, + }); + + if (!freshState.connected) { + throw new Error("zhihu_not_logged_in"); + } + + const coverUrl = context.article.cover_asset_url?.trim() || null; + const titleImage = coverUrl ? await uploadBinaryImage(coverUrl) : null; + const draftId = await createDraft(context, titleImage); + + if (context.article.publish_type === "draft") { + return { + success: true, + status: "pending_review", + externalArticleId: draftId, + externalArticleUrl: `https://zhuanlan.zhihu.com/p/${draftId}/edit`, + externalManageUrl: `https://zhuanlan.zhihu.com/p/${draftId}/edit`, + message: "知乎草稿创建成功。", + responsePayload: { + mode: "zhihu-draft", + }, + }; + } + + const published = await publishDraft(draftId); + if (!published) { + throw new Error("zhihu_publish_failed"); + } + + const publishedUrl = `https://zhuanlan.zhihu.com/p/${draftId}`; + + return { + success: true, + status: "success", + externalArticleId: draftId, + externalArticleUrl: publishedUrl, + externalManageUrl: `${publishedUrl}/edit`, + message: "知乎正式发布成功。", + responsePayload: { + mode: "zhihu-direct-publish", + draft_id: draftId, + }, + }; +} + +export const zhihuAdapter: PlatformAdapter = { + platformId: "zhihu", + detect: detectZhihu, + publish: publishZhihu, +}; diff --git a/apps/browser-extension/src/platforms.ts b/apps/browser-extension/src/platforms.ts new file mode 100644 index 0000000..97ac950 --- /dev/null +++ b/apps/browser-extension/src/platforms.ts @@ -0,0 +1,146 @@ +import type { PublisherLocalPlatformState } from "@geo/shared-types"; + +export interface StoredPlatformState extends PublisherLocalPlatformState { + platform_name: string; + short_name: string; + accent_color: string; + login_url: string | null; + logo_path: string; +} + +const platformDefinitions = [ + { + platform_id: "toutiaohao", + platform_name: "头条号", + short_name: "头", + accent_color: "#f5222d", + login_url: "https://mp.toutiao.com/auth/page/login", + logo_path: "logos/logo_toutiao.png", + }, + { + platform_id: "baijiahao", + platform_name: "百家号", + short_name: "百", + accent_color: "#31445a", + login_url: "https://baijiahao.baidu.com/builder/theme/bjh/login", + logo_path: "logos/logo_baijiahao.png", + }, + { + platform_id: "sohuhao", + platform_name: "搜狐号", + short_name: "搜", + accent_color: "#fa8c16", + login_url: "https://mp.sohu.com/mpfe/v4/login", + logo_path: "logos/logo_souhu.png", + }, + { + platform_id: "qiehao", + platform_name: "企鹅号", + short_name: "Q", + accent_color: "#111827", + login_url: "https://om.qq.com", + logo_path: "logos/logo_qiehao.png", + }, + { + platform_id: "zhihu", + platform_name: "知乎", + short_name: "知", + accent_color: "#1677ff", + login_url: "https://www.zhihu.com/signin", + logo_path: "logos/logo_zhihu.png", + }, + { + platform_id: "wangyihao", + platform_name: "网易号", + short_name: "网", + accent_color: "#ff4d4f", + login_url: "https://mp.163.com/login.html", + logo_path: "logos/logo_wangyihao.png", + }, + { + platform_id: "jianshu", + platform_name: "简书", + short_name: "简", + accent_color: "#f56a5e", + login_url: "https://www.jianshu.com/sign_in", + logo_path: "logos/logo_jianshu.svg", + }, + { + platform_id: "bilibili", + platform_name: "bilibili", + short_name: "B", + accent_color: "#eb2f96", + login_url: "https://www.bilibili.com", + logo_path: "logos/logo_bilibili.png", + }, + { + platform_id: "juejin", + platform_name: "稀土掘金", + short_name: "掘", + accent_color: "#1677ff", + login_url: "https://juejin.cn/login", + logo_path: "logos/logo_juejin.png", + }, + { + platform_id: "smzdm", + platform_name: "什么值得买", + short_name: "值", + accent_color: "#f5222d", + login_url: "https://zhiyou.smzdm.com/user/login", + logo_path: "logos/logo_smzdm.svg", + }, + { + platform_id: "weixin_gzh", + platform_name: "微信公众号", + short_name: "微", + accent_color: "#13c26b", + login_url: "https://mp.weixin.qq.com/cgi-bin/loginpage", + logo_path: "logos/logo_weixin_gzh.svg", + }, + { + platform_id: "zol", + platform_name: "中关村在线", + short_name: "Z", + accent_color: "#ff4d4f", + login_url: "https://post.zol.com.cn/v2/manage/works/all", + logo_path: "logos/logo_zol.png", + }, + { + platform_id: "dongchedi", + platform_name: "懂车帝", + short_name: "懂", + accent_color: "#fadb14", + login_url: "https://mp.dcdapp.com/login", + logo_path: "logos/logo_dongchedi.svg", + }, +] satisfies Array>; + +export const supportedPlatforms = platformDefinitions; + +export function createDefaultPlatformStates(): StoredPlatformState[] { + return supportedPlatforms.map((platform) => ({ + ...platform, + connected: false, + platform_uid: null, + nickname: null, + avatar_url: null, + message: null, + })); +} + +export function buildPublishedUrl(platformId: string, externalArticleId: string): string { + switch (platformId) { + case "toutiaohao": + return `https://www.toutiao.com/article/${externalArticleId}/`; + case "zhihu": + return `https://zhuanlan.zhihu.com/p/${externalArticleId}`; + case "bilibili": + return `https://www.bilibili.com/read/cv${externalArticleId}/`; + case "juejin": + return `https://juejin.cn/post/${externalArticleId}`; + case "jianshu": + return `https://www.jianshu.com/p/${externalArticleId}`; + default: + return `https://example.com/${platformId}/${externalArticleId}`; + } +} diff --git a/apps/browser-extension/src/runtime.ts b/apps/browser-extension/src/runtime.ts new file mode 100644 index 0000000..5803ae4 --- /dev/null +++ b/apps/browser-extension/src/runtime.ts @@ -0,0 +1,310 @@ +import type { + ApiEnvelope, + PublisherBindRequest, + PublisherBindResponse, + PublisherLocalPlatformState, + PublisherPluginPingResponse, + PublisherPublishArticleRequest, + PublisherPublishResponse, + PublisherPublishTaskResult, + PublisherRegisterInstallationPayload, + PublisherRegisterInstallationResult, +} from "@geo/shared-types"; + +import { browser } from "wxt/browser"; + +import { detectPlatformState, publishViaAdapter } from "./adapters"; +import { buildPublishedUrl } from "./platforms"; +import { getExtensionState, patchExtensionState } from "./storage"; + +type PublisherAction = "ping" | "detectPlatforms" | "registerInstallation" | "bindAccount" | "publishArticle"; + +function normalizeBaseUrl(baseUrl: string): string { + return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; +} + +function normalizeText(value?: string | null): string | null { + const trimmed = value?.trim(); + return trimmed ? trimmed : null; +} + +async function postCallback( + baseUrl: string, + path: string, + payload: Record, +): Promise { + const state = await getExtensionState(); + const response = await fetch(new URL(path.replace(/^\//, ""), normalizeBaseUrl(baseUrl)).toString(), { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(state.installation_token ? { "X-Geo-Installation-Token": state.installation_token } : {}), + ...(state.plugin_installation_id ? { "X-Geo-Installation-Id": String(state.plugin_installation_id) } : {}), + }, + body: JSON.stringify(payload), + }); + + const body = (await response.json()) as ApiEnvelope | { message?: string }; + if (!response.ok || !("data" in body)) { + throw new Error(body.message || "publisher_callback_failed"); + } + return body.data; +} + +function nextExternalArticleId(taskId: number): string { + return `${Date.now()}${String(taskId).slice(-4)}`; +} + +function asFailedAdapterResult(error: unknown) { + return { + success: false, + status: "failed" as const, + externalArticleId: null, + externalArticleUrl: null, + externalManageUrl: null, + message: error instanceof Error ? error.message : "publisher_adapter_failed", + responsePayload: { + mode: "platform-adapter", + error: error instanceof Error ? error.message : "publisher_adapter_failed", + }, + }; +} + +export async function handlePublisherAction( + action: PublisherAction, + payload?: unknown, +): Promise< + | PublisherPluginPingResponse + | PublisherLocalPlatformState[] + | PublisherRegisterInstallationResult + | PublisherBindResponse + | PublisherPublishResponse +> { + switch (action) { + case "ping": + return handlePing(); + case "detectPlatforms": + return handleDetectPlatforms(); + case "registerInstallation": + return handleRegisterInstallation(payload as PublisherRegisterInstallationPayload); + case "bindAccount": + return handleBindAccount(payload as PublisherBindRequest); + case "publishArticle": + return handlePublishArticle(payload as PublisherPublishArticleRequest); + default: + throw new Error("publisher_action_not_supported"); + } +} + +async function handlePing(): Promise { + const state = await getExtensionState(); + return { + installed: true, + version: browser.runtime.getManifest().version, + capabilities: ["ping", "detect", "bind", "publish", "background"], + installation_key: state.installation_key, + plugin_installation_id: state.plugin_installation_id, + }; +} + +async function handleDetectPlatforms() { + const state = await getExtensionState(); + const detected = await Promise.all(state.platforms.map((platform) => detectPlatformState(platform))); + await patchExtensionState({ platforms: detected }); + return detected.map((platform) => ({ + platform_id: platform.platform_id, + connected: platform.connected, + platform_uid: platform.platform_uid ?? null, + nickname: platform.nickname ?? null, + avatar_url: platform.avatar_url ?? null, + message: platform.message ?? null, + })); +} + +async function handleRegisterInstallation( + payload: PublisherRegisterInstallationPayload, +): Promise { + await patchExtensionState({ + plugin_installation_id: payload.plugin_installation_id, + installation_token: payload.installation_token, + api_base_url: payload.api_base_url, + }); + + return { + success: true, + plugin_installation_id: payload.plugin_installation_id, + }; +} + +async function handleBindAccount(payload: PublisherBindRequest): Promise { + const state = await getExtensionState(); + const rawPlatform = state.platforms.find((item) => item.platform_id === payload.platform_id); + const platform = rawPlatform ? await detectPlatformState(rawPlatform) : null; + if (platform && rawPlatform) { + const nextPlatforms = state.platforms.map((item) => (item.platform_id === platform.platform_id ? platform : item)); + await patchExtensionState({ platforms: nextPlatforms }); + } + if (!platform?.connected) { + if (payload.login_url) { + await browser.tabs.create({ url: payload.login_url }); + } + return { + success: false, + platform_id: payload.platform_id, + message: "No local session detected for this platform. Configure it in the extension popup first.", + }; + } + + const callback = await postCallback<{ platform_account_id: number }>(payload.callback_base_url, "/api/callback/plugin/bind", { + plugin_session_id: payload.plugin_session_id, + session_token: payload.session_token, + platform_id: payload.platform_id, + platform_uid: platform.platform_uid, + nickname: platform.nickname, + avatar_url: platform.avatar_url, + status: "active", + client_version: browser.runtime.getManifest().version, + metadata: { + installation_key: state.installation_key, + plugin_installation_id: state.plugin_installation_id, + }, + }); + + return { + success: true, + platform_id: payload.platform_id, + platform_uid: platform.platform_uid ?? null, + nickname: platform.nickname ?? null, + avatar_url: platform.avatar_url ?? null, + platform_account_id: callback.platform_account_id, + message: "Platform account bound successfully.", + }; +} + +async function handlePublishArticle(payload: PublisherPublishArticleRequest): Promise { + const state = await getExtensionState(); + const results: PublisherPublishTaskResult[] = []; + + for (const task of payload.tasks) { + const platform = state.platforms.find((item) => item.platform_id === task.platform_id); + const adapterResult = await publishViaAdapter(task.platform_id, { + article: payload, + task, + }).catch(asFailedAdapterResult); + + if (adapterResult) { + await postCallback(payload.callback_base_url, "/api/callback/plugin/publish", { + plugin_session_id: task.plugin_session_id, + session_token: task.session_token, + article_id: payload.article_id, + publish_record_id: task.publish_record_id, + platform_account_id: task.platform_account_id, + platform_id: task.platform_id, + status: adapterResult.status, + external_article_id: normalizeText(adapterResult.externalArticleId), + external_article_url: normalizeText(adapterResult.externalArticleUrl), + external_manage_url: normalizeText(adapterResult.externalManageUrl), + published_at: adapterResult.success ? new Date().toISOString() : null, + error_message: adapterResult.success ? null : adapterResult.message, + client_version: browser.runtime.getManifest().version, + request_payload: { + title: payload.title, + cover_asset_url: normalizeText(payload.cover_asset_url), + publish_type: payload.publish_type, + }, + response_payload: adapterResult.responsePayload ?? { + mode: "platform-adapter", + }, + }); + + results.push({ + publish_record_id: task.publish_record_id, + platform_account_id: task.platform_account_id, + platform_id: task.platform_id, + success: adapterResult.success, + status: adapterResult.status, + external_article_id: normalizeText(adapterResult.externalArticleId), + external_article_url: normalizeText(adapterResult.externalArticleUrl), + external_manage_url: normalizeText(adapterResult.externalManageUrl), + message: adapterResult.message ?? null, + }); + continue; + } + + if (!platform?.connected) { + await postCallback(payload.callback_base_url, "/api/callback/plugin/publish", { + plugin_session_id: task.plugin_session_id, + session_token: task.session_token, + article_id: payload.article_id, + publish_record_id: task.publish_record_id, + platform_account_id: task.platform_account_id, + platform_id: task.platform_id, + status: "failed", + error_message: "Local platform session is missing in the extension runtime.", + client_version: browser.runtime.getManifest().version, + request_payload: { + title: payload.title, + publish_type: payload.publish_type, + }, + }); + + results.push({ + publish_record_id: task.publish_record_id, + platform_account_id: task.platform_account_id, + platform_id: task.platform_id, + success: false, + status: "failed", + message: "Local platform session is missing.", + }); + continue; + } + + const externalArticleId = nextExternalArticleId(task.publish_record_id); + const externalArticleUrl = buildPublishedUrl(task.platform_id, externalArticleId); + + await postCallback(payload.callback_base_url, "/api/callback/plugin/publish", { + plugin_session_id: task.plugin_session_id, + session_token: task.session_token, + article_id: payload.article_id, + publish_record_id: task.publish_record_id, + platform_account_id: task.platform_account_id, + platform_id: task.platform_id, + status: "success", + external_article_id: externalArticleId, + external_article_url: externalArticleUrl, + external_manage_url: normalizeText(platform.login_url), + published_at: new Date().toISOString(), + client_version: browser.runtime.getManifest().version, + request_payload: { + title: payload.title, + cover_asset_url: normalizeText(payload.cover_asset_url), + publish_type: payload.publish_type, + }, + response_payload: { + mode: "mock-adapter", + installation_key: state.installation_key, + }, + }); + + results.push({ + publish_record_id: task.publish_record_id, + platform_account_id: task.platform_account_id, + platform_id: task.platform_id, + success: true, + status: "success", + external_article_id: externalArticleId, + external_article_url: externalArticleUrl, + external_manage_url: normalizeText(platform.login_url), + message: "Published through the mock adapter.", + }); + } + + const successCount = results.filter((item) => item.success).length; + const failedCount = results.length - successCount; + + return { + success: successCount > 0, + batch_status: failedCount === 0 ? "success" : successCount > 0 ? "partial_success" : "failed", + results, + }; +} diff --git a/apps/browser-extension/src/storage.ts b/apps/browser-extension/src/storage.ts new file mode 100644 index 0000000..7bf0c9d --- /dev/null +++ b/apps/browser-extension/src/storage.ts @@ -0,0 +1,99 @@ +import { browser } from "wxt/browser"; + +import { createDefaultPlatformStates, supportedPlatforms, type StoredPlatformState } from "./platforms"; + +export interface ExtensionStorageState { + installation_key: string; + plugin_installation_id: number | null; + installation_token: string | null; + api_base_url: string | null; + platforms: StoredPlatformState[]; + last_updated_at: string | null; +} + +const STORAGE_KEY = "geo.publisher.state"; + +function nextInstallationKey(): string { + if (typeof crypto !== "undefined" && "randomUUID" in crypto) { + return crypto.randomUUID(); + } + return `geo-extension-${Date.now()}-${Math.random().toString(36).slice(2)}`; +} + +function mergePlatformStates(platforms?: StoredPlatformState[]): StoredPlatformState[] { + const defaults = createDefaultPlatformStates(); + if (!platforms?.length) { + return defaults; + } + + const incoming = new Map(platforms.map((platform) => [platform.platform_id, platform])); + return defaults.map((platform) => { + const stored = incoming.get(platform.platform_id); + return stored ? { ...platform, ...stored } : platform; + }); +} + +async function writeState(nextState: ExtensionStorageState): Promise { + await browser.storage.local.set({ + [STORAGE_KEY]: nextState, + }); +} + +export async function getExtensionState(): Promise { + const storageValue = await browser.storage.local.get(STORAGE_KEY); + const stored = storageValue[STORAGE_KEY] as Partial | undefined; + + const nextState: ExtensionStorageState = { + installation_key: stored?.installation_key || nextInstallationKey(), + plugin_installation_id: stored?.plugin_installation_id ?? null, + installation_token: stored?.installation_token ?? null, + api_base_url: stored?.api_base_url ?? null, + platforms: mergePlatformStates(stored?.platforms), + last_updated_at: stored?.last_updated_at ?? null, + }; + + const shouldPersist = + !stored?.installation_key || + !stored?.platforms || + stored.platforms.length !== supportedPlatforms.length; + + if (shouldPersist) { + await writeState(nextState); + } + + return nextState; +} + +export async function patchExtensionState( + patch: Partial> & { platforms?: StoredPlatformState[] }, +): Promise { + const current = await getExtensionState(); + const nextState: ExtensionStorageState = { + ...current, + ...patch, + platforms: patch.platforms ? mergePlatformStates(patch.platforms) : current.platforms, + last_updated_at: new Date().toISOString(), + }; + await writeState(nextState); + return nextState; +} + +export async function updatePlatformState( + platformId: string, + updater: Partial, +): Promise { + const current = await getExtensionState(); + const platforms = current.platforms.map((platform) => + platform.platform_id === platformId + ? { + ...platform, + ...updater, + } + : platform, + ); + return patchExtensionState({ platforms }); +} + +export async function resetPlatformStates(): Promise { + return patchExtensionState({ platforms: createDefaultPlatformStates() }); +} diff --git a/apps/browser-extension/tsconfig.json b/apps/browser-extension/tsconfig.json new file mode 100644 index 0000000..5f955a7 --- /dev/null +++ b/apps/browser-extension/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../packages/tsconfig/base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"], + "types": ["@types/chrome"] + }, + "include": ["**/*.ts", "**/*.vue", ".wxt/**/*.d.ts"] +} diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts new file mode 100644 index 0000000..f84343d --- /dev/null +++ b/apps/browser-extension/wxt.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "wxt"; + +export default defineConfig({ + modules: ["@wxt-dev/module-vue"], + manifest: { + name: "GEO Publisher", + description: "Bind media accounts, execute local publish flows, and keep background detection tasks running.", + permissions: ["storage", "tabs", "alarms", "cookies", "declarativeNetRequest"], + host_permissions: ["http://*/*", "https://*/*"], + declarative_net_request: { + rule_resources: [ + { + id: "publisher-rules", + enabled: true, + path: "rules.json", + }, + ], + }, + action: { + default_title: "GEO Publisher", + default_popup: "popup.html", + }, + }, +}); diff --git a/docs/media-publisher-extension-runtime-v1.md b/docs/media-publisher-extension-runtime-v1.md new file mode 100644 index 0000000..30368bb --- /dev/null +++ b/docs/media-publisher-extension-runtime-v1.md @@ -0,0 +1,58 @@ +# Media Publisher Extension Runtime V1 + +## Goal + +Browser extension actions must keep working for two different modes: + +- Foreground actions triggered from the SaaS UI, such as bind and publish. +- Future background actions that continue after the SaaS page is closed, such as checking Doubao/Kimi inclusion status. + +## Identity Model + +Do not reuse the SaaS web JWT inside the extension background runtime. + +Use two layers instead: + +1. SaaS user session + - Used only when the user is actively logged into the admin web app. + - Registers or refreshes a plugin installation record. + +2. Plugin installation identity + - One record per browser installation. + - Stored in `plugin_installations`. + - Holds a long-lived installation token hash in the backend. + - Lets future background jobs authenticate as a device/runtime, not as a browser tab. + +## Current Tables + +- `plugin_installations` + - Stable browser installation identity. +- `plugin_sessions` + - Short-lived action session for bind, check, publish. +- `publish_batches` + - One multi-platform publish request. +- `publish_records` + - One platform result row per publish. + +## Current Flow + +1. Admin page pings the extension. +2. Extension returns a stable `installation_key`. +3. Admin page calls `POST /api/tenant/media/plugin-installations/register`. +4. Backend returns `plugin_installation_id + installation_token`. +5. Admin page passes them back to the extension. +6. Extension stores that installation identity locally. +7. Bind or publish requests create short-lived `plugin_sessions`. +8. Callbacks write bind/publish results using session token validation. + +## Future Background Tasks + +Background jobs should be scheduled from the extension service worker using the stored installation identity. + +Typical examples: + +- Poll whether generated articles are included by Doubao/Kimi. +- Retry resolving a published article URL when only a platform draft ID is known. +- Re-check local platform login health. + +Those future endpoints should authenticate with the installation token, not the SaaS web JWT. diff --git a/package.json b/package.json index d5dbc66..bc46589 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "scripts": { "dev:admin": "pnpm --filter admin-web dev", "build:admin": "pnpm --filter admin-web build", - "typecheck:admin": "pnpm --filter admin-web typecheck" + "typecheck:admin": "pnpm --filter admin-web typecheck", + "dev:extension": "pnpm --filter browser-extension dev", + "build:extension": "pnpm --filter browser-extension build", + "typecheck:extension": "pnpm --filter browser-extension exec vue-tsc --noEmit" } } - diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts index 81d4205..0968666 100644 --- a/packages/shared-types/src/index.ts +++ b/packages/shared-types/src/index.ts @@ -282,6 +282,185 @@ export interface ArticleVersion { created_at: string; } +export const MIN_BROWSER_EXTENSION_VERSION = "0.1.0"; + +export interface MediaPlatform { + id: number; + platform_id: string; + name: string; + category: string; + short_name: string; + accent_color: string; + login_url: string | null; + logo_url: string | null; + status: string; + sort_order: number; +} + +export interface PlatformAccount { + id: number; + platform_id: string; + platform_name: string; + platform_category: string; + platform_uid: string; + nickname: string; + avatar_url: string | null; + status: string; + login_url: string | null; + last_check_at: string | null; + metadata?: Record | null; + created_at: string; + updated_at: string; +} + +export interface CreatePluginSessionRequest { + action_type: "bind" | "check"; + platform_id: string; + platform_account_id?: number | null; + plugin_installation_id?: number | null; + resource_type?: string | null; + resource_id?: number | null; +} + +export interface CreatePluginSessionResponse { + plugin_session_id: number; + session_token: string; + expire_at: string; + min_plugin_version: string; +} + +export interface CreatePublishBatchRequest { + platform_account_ids: number[]; + plugin_installation_id?: number | null; + publish_type?: "publish" | "draft"; + cover_asset_url?: string | null; +} + +export interface PublishBatchTask { + publish_record_id: number; + platform_account_id: number; + platform_id: string; + platform_name: string; + platform_uid: string; + platform_nickname: string; + plugin_session_id: number; + session_token: string; + expire_at: string; +} + +export interface CreatePublishBatchResponse { + publish_batch_id: number; + status: string; + tasks: PublishBatchTask[]; +} + +export interface PublishRecord { + id: number; + publish_batch_id: number; + article_id: number; + platform_account_id: number; + platform_id: string; + platform_name: string; + platform_nickname: string; + status: string; + external_article_id: string | null; + external_article_url: string | null; + external_manage_url: string | null; + published_at: string | null; + error_message: string | null; + created_at: string; + updated_at: string; +} + +export interface PublisherLocalPlatformState { + platform_id: string; + connected: boolean; + platform_uid?: string | null; + nickname?: string | null; + avatar_url?: string | null; + message?: string | null; +} + +export interface PublisherPluginPingResponse { + installed: boolean; + version?: string; + capabilities?: Array<"ping" | "detect" | "bind" | "publish" | "background">; + installation_key?: string; + plugin_installation_id?: number | null; +} + +export interface RegisterPluginInstallationRequest { + installation_key: string; + installation_name: string; + browser_name?: string | null; + client_version?: string | null; +} + +export interface RegisterPluginInstallationResponse { + plugin_installation_id: number; + installation_token: string; + status: string; + last_seen_at: string; +} + +export interface PublisherRegisterInstallationPayload { + plugin_installation_id: number; + installation_token: string; + api_base_url: string; +} + +export interface PublisherRegisterInstallationResult { + success: boolean; + plugin_installation_id: number; +} + +export interface PublisherBindRequest { + platform_id: string; + login_url?: string | null; + callback_base_url: string; + plugin_session_id: number; + session_token: string; +} + +export interface PublisherBindResponse { + success: boolean; + platform_id: string; + platform_uid?: string | null; + nickname?: string | null; + avatar_url?: string | null; + platform_account_id?: number | null; + message?: string | null; +} + +export interface PublisherPublishArticleRequest { + article_id: number; + title: string; + markdown_content: string; + html_content?: string | null; + cover_asset_url?: string | null; + callback_base_url: string; + publish_type: "publish" | "draft"; + tasks: PublishBatchTask[]; +} + +export interface PublisherPublishTaskResult { + publish_record_id: number; + platform_account_id: number; + platform_id: string; + success: boolean; + status: "success" | "failed" | "pending_review"; + external_article_id?: string | null; + external_article_url?: string | null; + external_manage_url?: string | null; + message?: string | null; +} + +export interface PublisherPublishResponse { + success: boolean; + batch_status: string; + results: PublisherPublishTaskResult[]; +} + export interface BrandRequest { name: string; website?: string | null; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6e1349..6327156 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,17 +64,48 @@ importers: version: 24.12.0 '@vitejs/plugin-vue': specifier: ^6.0.5 - version: 6.0.5(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0))(vue@3.5.31(typescript@5.9.3)) + version: 6.0.5(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1))(vue@3.5.31(typescript@5.9.3)) typescript: specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^8.0.3 - version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0) + version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1) vue-tsc: specifier: ^3.2.6 version: 3.2.6(typescript@5.9.3) + apps/browser-extension: + dependencies: + '@geo/shared-types': + specifier: workspace:* + version: link:../../packages/shared-types + js-md5: + specifier: ^0.8.3 + version: 0.8.3 + marked: + specifier: ^17.0.5 + version: 17.0.5 + vue: + specifier: ^3.5.31 + version: 3.5.31(typescript@5.9.3) + devDependencies: + '@types/chrome': + specifier: ^0.1.38 + version: 0.1.38 + '@wxt-dev/module-vue': + specifier: ^1.0.3 + version: 1.0.3(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1))(vue@3.5.31(typescript@5.9.3))(wxt@0.20.20(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1)) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vue-tsc: + specifier: ^3.2.6 + version: 3.2.6(typescript@5.9.3) + wxt: + specifier: ^0.20.20 + version: 0.20.20(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1) + packages/http-client: dependencies: '@geo/shared-types': @@ -88,6 +119,19 @@ importers: packages: + '@1natsu/wait-element@4.1.2': + resolution: {integrity: sha512-qWxSJD+Q5b8bKOvESFifvfZ92DuMsY+03SBNjTO34ipJLP6mZ9yK4bQz/vlh48aEQXoJfaZBqUwKL5BdI5iiWw==} + + '@aklinker1/rollup-plugin-visualizer@5.12.0': + resolution: {integrity: sha512-X24LvEGw6UFmy0lpGJDmXsMyBD58XmX1bbwsaMLhNoM+UMQfQ3b2RtC+nz4b/NoRK5r6QJSKJHBNVeUdwqybaQ==} + engines: {node: '>=14'} + hasBin: true + peerDependencies: + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rollup: + optional: true + '@ant-design/colors@6.0.0': resolution: {integrity: sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==} @@ -99,6 +143,10 @@ packages: peerDependencies: vue: '>=3.0.3' + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -112,6 +160,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/runtime@7.28.2': + resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} + engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -217,6 +269,19 @@ packages: resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} engines: {node: '>=10'} + '@devicefarmer/adbkit-logcat@2.1.3': + resolution: {integrity: sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==} + engines: {node: '>= 4'} + + '@devicefarmer/adbkit-monkey@1.2.1': + resolution: {integrity: sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==} + engines: {node: '>= 0.10.4'} + + '@devicefarmer/adbkit@3.3.8': + resolution: {integrity: sha512-7rBLLzWQnBwutH2WZ0EWUkQdihqrnLYCUMaB44hSol9e0/cdIhuNFcqZO0xNheAU6qqHVA8sMiLofkYTgb+lmw==} + engines: {node: '>= 0.10.4'} + hasBin: true + '@emnapi/core@1.9.1': resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} @@ -232,6 +297,162 @@ packages: '@emotion/unitless@0.8.1': resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + '@esbuild/aix-ppc64@0.27.5': + resolution: {integrity: sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.5': + resolution: {integrity: sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.5': + resolution: {integrity: sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.5': + resolution: {integrity: sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.5': + resolution: {integrity: sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.5': + resolution: {integrity: sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.5': + resolution: {integrity: sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.5': + resolution: {integrity: sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.5': + resolution: {integrity: sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.5': + resolution: {integrity: sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.5': + resolution: {integrity: sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.5': + resolution: {integrity: sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.5': + resolution: {integrity: sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.5': + resolution: {integrity: sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.5': + resolution: {integrity: sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.5': + resolution: {integrity: sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.5': + resolution: {integrity: sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.5': + resolution: {integrity: sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.5': + resolution: {integrity: sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.5': + resolution: {integrity: sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.5': + resolution: {integrity: sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.5': + resolution: {integrity: sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.5': + resolution: {integrity: sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.5': + resolution: {integrity: sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.5': + resolution: {integrity: sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.5': + resolution: {integrity: sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -253,9 +474,22 @@ packages: resolution: {integrity: sha512-BcmHpb5bQyeVNrptC3UhzpBZB/YHHDoEREOUERrmF2BRxsyOEuRrq+Z96C/D4+2KJb8kuHiouzAei7BXlG0YYw==} engines: {node: '>= 16'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lezer/common@1.5.1': resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==} @@ -397,6 +631,18 @@ packages: '@oxc-project/types@0.122.0': resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@3.0.2': + resolution: {integrity: sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==} + engines: {node: '>=12'} + '@rolldown/binding-android-arm64@1.0.0-rc.12': resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -520,9 +766,24 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/chrome@0.1.38': + resolution: {integrity: sha512-5aK4m9wZqoWAoB98aElESLm/5pXpqJnFWMNoiCs/XdPsXR6wNdVkJFSdQ9Wr4PnTuUrxD0SuNuDHh3EG5QeBzA==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/filesystem@0.0.36': + resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==} + + '@types/filewriter@0.0.33': + resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} + + '@types/har-format@1.2.16': + resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -538,6 +799,9 @@ packages: '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/minimatch@3.0.5': + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -626,43 +890,206 @@ packages: peerDependencies: vue: ^3.5.0 + '@webext-core/fake-browser@1.3.4': + resolution: {integrity: sha512-nZcVWr3JpwpS5E6hKpbAwAMBM/AXZShnfW0F76udW8oLd6Kv0nbW6vFS07md4Na/0ntQonk3hFnlQYGYBAlTrA==} + + '@webext-core/isolated-element@1.1.5': + resolution: {integrity: sha512-4m6oP8Vzm/68YO1QmkUOZqqUcmyBtA53tji2g00/nYXE3E3IceYgeub7eIqvXDV2Z7xU6cm6qO1IMt4XFVwtvQ==} + + '@webext-core/match-patterns@1.0.3': + resolution: {integrity: sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A==} + + '@wxt-dev/browser@0.1.38': + resolution: {integrity: sha512-Y9nUfNOMqgsoO8KQ1BssrwzHEmrSr/2pUowAG4Wcr9EyKyhOK7mC7Vdyj2kXAmp5NOUXHjhghzJ6qIb5h+RbCA==} + + '@wxt-dev/module-vue@1.0.3': + resolution: {integrity: sha512-xUNygcj4w0b1MzQGOQIp5V2ZwFE4y+5AwKPi/IPKyo2aNeFVJTfItIxzWI5vTj7031WCLzHgQrrBRS5C63D5iw==} + peerDependencies: + wxt: '>=0.19.16' + + '@wxt-dev/storage@1.2.8': + resolution: {integrity: sha512-GWCFKgF5+d7eslOxUDFC70ypA9njupmJb1nQM8uZoX0J3sWT2BO5xJLzb1sYahWAfID9p2BMtnUBN1lkWxPsbQ==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + adm-zip@0.5.17: + resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} + engines: {node: '>=12.0'} + alien-signals@3.1.2: resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + ant-design-vue@4.2.6: resolution: {integrity: sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==} engines: {node: '>=12.22.0'} peerDependencies: vue: '>=3.2.0' + array-differ@4.0.0: + resolution: {integrity: sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + array-tree-filter@2.1.0: resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==} + array-union@3.0.1: + resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==} + engines: {node: '>=12'} + + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + async-validator@4.2.5: resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + atomically@2.1.1: + resolution: {integrity: sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==} + axios@1.14.0: resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==} bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + chrome-launcher@1.2.0: + resolution: {integrity: sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==} + engines: {node: '>=12.13.0'} + hasBin: true + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -670,17 +1097,56 @@ packages: codemirror@6.0.2: resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@2.9.0: + resolution: {integrity: sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==} + engines: {node: '>= 0.6.x'} + commander@8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + compute-scroll-into-view@1.0.20: resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + configstore@7.1.0: + resolution: {integrity: sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==} + engines: {node: '>=18'} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -688,15 +1154,40 @@ packages: core-js@3.49.0: resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} dayjs@1.11.20: resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -709,6 +1200,29 @@ packages: decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.6: + resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -717,6 +1231,9 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -730,17 +1247,66 @@ packages: dom-scroll-into-view@2.0.1: resolution: {integrity: sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dompurify@3.3.3: resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-prop@9.0.0: + resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} + engines: {node: '>=18'} + + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.4.0: + resolution: {integrity: sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -749,6 +1315,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -757,6 +1326,26 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild@0.27.5: + resolution: {integrity: sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -764,9 +1353,22 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -776,6 +1378,15 @@ packages: picomatch: optional: true + filesize@11.0.15: + resolution: {integrity: sha512-30TpbYxQxCpi4XdVjkwXYQ37CzZltV38+P7MYroQ+4NK/Dmx9mxixFNrolzcmEIBsjT/uowC9T7kiy2+C12r1A==} + engines: {node: '>= 10.8.0'} + + firefox-profile@4.7.0: + resolution: {integrity: sha512-aGApEu5bfCNbA4PGUZiRJAIU6jKmghV2UVdklXAofnNtiDjqYw0czLS46W7IfFqVKgKhFB8Ao2YoNGHY4BoIMQ==} + engines: {node: '>=18'} + hasBin: true + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -785,10 +1396,22 @@ packages: debug: optional: true + form-data-encoder@4.1.0: + resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} + engines: {node: '>= 18'} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + formdata-node@6.0.3: + resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==} + engines: {node: '>= 18'} + + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -797,18 +1420,56 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + fx-runner@1.4.0: + resolution: {integrity: sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==} + hasBin: true + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + giget@3.2.0: + resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} + hasBin: true + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graceful-readlink@1.0.1: + resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} + + growly@1.3.0: + resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -824,25 +1485,194 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hookable@6.1.0: + resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + is-absolute@0.1.7: + resolution: {integrity: sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA==} + engines: {node: '>=0.10.0'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-in-ci@1.0.0: + resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==} + engines: {node: '>=18'} + hasBin: true + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-installed-globally@1.0.0: + resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} + engines: {node: '>=18'} + + is-npm@6.1.0: + resolution: {integrity: sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + is-plain-object@3.0.1: resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-primitive@3.0.1: + resolution: {integrity: sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==} + engines: {node: '>=0.10.0'} + + is-relative@0.1.3: + resolution: {integrity: sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA==} + engines: {node: '>=0.10.0'} + is-what@5.5.0: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@1.1.2: + resolution: {integrity: sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-md5@0.8.3: + resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + json-parse-even-better-errors@3.0.2: + resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + katex@0.16.44: resolution: {integrity: sha512-EkxoDTk8ufHqHlf9QxGwcxeLkWRR3iOuYfRpfORgYfqc8s13bgb+YtRY59NK5ZpRaCwq1kqA6a5lpX8C/eLphQ==} hasBin: true + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + ky@1.14.3: + resolution: {integrity: sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==} + engines: {node: '>=18'} + + latest-version@9.0.0: + resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==} + engines: {node: '>=18'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lighthouse-logger@2.0.2: + resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -917,12 +1747,61 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lines-and-columns@2.0.4: + resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + linkedom@0.18.12: + resolution: {integrity: sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==} + engines: {node: '>=16'} + peerDependencies: + canvas: '>= 2' + peerDependenciesMeta: + canvas: + optional: true + + listr2@10.2.1: + resolution: {integrity: sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==} + engines: {node: '>=22.13.0'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -933,9 +1812,26 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + many-keys-map@2.0.1: + resolution: {integrity: sha512-DHnZAD4phTbZ+qnJdjoNEVU1NecYoSdbOOoVmTDH46AuxDkEVh3MxTVpXq10GtcTC6mndN9dkv1rNfpjRcLnOw==} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@17.0.5: + resolution: {integrity: sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==} + engines: {node: '>= 20'} + hasBin: true + + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1074,15 +1970,40 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + multimatch@6.0.0: + resolution: {integrity: sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nano-spawn@2.1.0: + resolution: {integrity: sha512-yTW+2okrElHiH4fsiz/+/zc0EDo9BDDoC3iKk8dpv1GeRc9nUWzUZHx6TofMWErchhUQR8hY9/Eu1Uja9x1nqA==} + engines: {node: '>=20.17'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1096,18 +2017,93 @@ packages: nanopop@2.4.2: resolution: {integrity: sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==} + nanospinner@1.2.2: + resolution: {integrity: sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + engines: {node: '>= 6.13.0'} + + node-notifier@10.0.1: + resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nypm@0.6.5: + resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==} + engines: {node: '>=18'} + hasBin: true + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + orderedmap@2.1.1: resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + os-shim@0.1.3: + resolution: {integrity: sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==} + engines: {node: '>= 0.4.0'} + + package-json@10.0.1: + resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==} + engines: {node: '>=18'} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parse-json@7.1.1: + resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} + engines: {node: '>=16'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -1121,10 +2117,44 @@ packages: typescript: optional: true + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@9.7.0: + resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} + hasBin: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + promise-toolbox@0.21.0: + resolution: {integrity: sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg==} + engines: {node: '>=6'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + prosemirror-changeset@2.4.0: resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==} @@ -1184,10 +2214,54 @@ packages: prosemirror-view: optional: true + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} + publish-browser-extension@4.0.4: + resolution: {integrity: sha512-QMQbWL0FWgBfnkJ6w8HOJoIPaWLE7vTpewM4ae2vLs7SrD4eKdAk+SxOzqAICwbhEPuaLAOA+XkT9sZS5R0PmA==} + engines: {node: '>=18.0.0'} + hasBin: true + + pupa@3.3.0: + resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} + engines: {node: '>=12.20'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + rc9@3.0.1: + resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + registry-auth-token@5.1.1: + resolution: {integrity: sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} @@ -1209,9 +2283,17 @@ packages: remove-accents@0.5.0: resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -1223,20 +2305,142 @@ packages: rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + scroll-into-view-if-needed@2.2.31: resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-value@4.1.0: + resolution: {integrity: sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==} + engines: {node: '>=11.0'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shallow-equal@1.2.1: resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + shell-quote@1.7.3: + resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} + + shellwords@0.1.1: + resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + spawn-sync@1.0.15: + resolution: {integrity: sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==} + speakingurl@14.0.1: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@5.0.0: + resolution: {integrity: sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@5.0.2: + resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==} + engines: {node: '>=14.16'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + stubborn-fs@2.0.0: + resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} + + stubborn-utils@1.0.2: + resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} + style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} @@ -1247,31 +2451,66 @@ packages: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + throttle-debounce@5.0.2: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + uhyphen@0.2.0: + resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unimport@6.0.2: + resolution: {integrity: sha512-ZSOkrDw380w+KIPniY3smyXh2h7H9v2MNr9zejDuh239o5sdea44DRAYrv+rfUi2QGT186P2h0GPGKvy8avQ5g==} + engines: {node: '>=18.12.0'} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -1287,12 +2526,40 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + + update-notifier@7.3.1: + resolution: {integrity: sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==} + engines: {node: '>=18'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@6.0.0: + resolution: {integrity: sha512-oj4PVrT+pDh6GYf5wfUXkcZyekYS8kKPfLPXVl8qe324Ec6l4K2DUKNadRbZ3LQl0qGcDz+PyOo7ZAh00Y+JjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + vite@8.0.3: resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1388,6 +2655,91 @@ packages: warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + engines: {node: '>=10.13.0'} + + web-ext-run@0.2.4: + resolution: {integrity: sha512-rQicL7OwuqWdQWI33JkSXKcp7cuv1mJG8u3jRQwx/8aDsmhbTHs9ZRmNYOL+LX0wX8edIEQX8jj4bB60GoXtKA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + when-exit@2.1.5: + resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} + + when@3.7.7: + resolution: {integrity: sha512-9lFZp/KHoqH6bPKjbWqa+3Dg/K/r2v0X/3/G2x4DBGchVS2QX2VXL3cZV994WQVnTM1/PD71Az25nAzryEUugw==} + + which@1.2.4: + resolution: {integrity: sha512-zDRAqDSBudazdfM9zpiI30Fu9ve47htYXcGi3ln0wfKu2a7SmrT6F3VDoYONu//48V8Vz4TdCRNPjtvyRO3yBA==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + winreg@0.0.12: + resolution: {integrity: sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ==} + + wrap-ansi@10.0.0: + resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} + engines: {node: '>=20'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + wxt@0.20.20: + resolution: {integrity: sha512-OGvOD1YEXwasjlOmfYzCGlIa88Jm9mxjM+hqx7zw+Xctg+TKjhF1bIt7vVJ1oT1t4RqvczTAcD2mUduiDltZaw==} + hasBin: true + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + zip-dir@2.0.0: + resolution: {integrity: sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg==} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -1396,6 +2748,18 @@ packages: snapshots: + '@1natsu/wait-element@4.1.2': + dependencies: + defu: 6.1.6 + many-keys-map: 2.0.1 + + '@aklinker1/rollup-plugin-visualizer@5.12.0': + dependencies: + open: 8.4.2 + picomatch: 2.3.2 + source-map: 0.7.6 + yargs: 17.7.2 + '@ant-design/colors@6.0.0': dependencies: '@ctrl/tinycolor': 3.6.1 @@ -1408,6 +2772,12 @@ snapshots: '@ant-design/icons-svg': 4.4.2 vue: 3.5.31(typescript@5.9.3) + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -1416,6 +2786,8 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@babel/runtime@7.28.2': {} + '@babel/runtime@7.29.2': {} '@babel/types@7.29.0': @@ -1680,6 +3052,22 @@ snapshots: '@ctrl/tinycolor@3.6.1': {} + '@devicefarmer/adbkit-logcat@2.1.3': {} + + '@devicefarmer/adbkit-monkey@1.2.1': {} + + '@devicefarmer/adbkit@3.3.8': + dependencies: + '@devicefarmer/adbkit-logcat': 2.1.3 + '@devicefarmer/adbkit-monkey': 1.2.1 + bluebird: 3.7.2 + commander: 9.5.0 + debug: 4.3.7 + node-forge: 1.4.0 + split: 1.0.1 + transitivePeerDependencies: + - supports-color + '@emnapi/core@1.9.1': dependencies: '@emnapi/wasi-threads': 1.2.0 @@ -1700,6 +3088,84 @@ snapshots: '@emotion/unitless@0.8.1': {} + '@esbuild/aix-ppc64@0.27.5': + optional: true + + '@esbuild/android-arm64@0.27.5': + optional: true + + '@esbuild/android-arm@0.27.5': + optional: true + + '@esbuild/android-x64@0.27.5': + optional: true + + '@esbuild/darwin-arm64@0.27.5': + optional: true + + '@esbuild/darwin-x64@0.27.5': + optional: true + + '@esbuild/freebsd-arm64@0.27.5': + optional: true + + '@esbuild/freebsd-x64@0.27.5': + optional: true + + '@esbuild/linux-arm64@0.27.5': + optional: true + + '@esbuild/linux-arm@0.27.5': + optional: true + + '@esbuild/linux-ia32@0.27.5': + optional: true + + '@esbuild/linux-loong64@0.27.5': + optional: true + + '@esbuild/linux-mips64el@0.27.5': + optional: true + + '@esbuild/linux-ppc64@0.27.5': + optional: true + + '@esbuild/linux-riscv64@0.27.5': + optional: true + + '@esbuild/linux-s390x@0.27.5': + optional: true + + '@esbuild/linux-x64@0.27.5': + optional: true + + '@esbuild/netbsd-arm64@0.27.5': + optional: true + + '@esbuild/netbsd-x64@0.27.5': + optional: true + + '@esbuild/openbsd-arm64@0.27.5': + optional: true + + '@esbuild/openbsd-x64@0.27.5': + optional: true + + '@esbuild/openharmony-arm64@0.27.5': + optional: true + + '@esbuild/sunos-x64@0.27.5': + optional: true + + '@esbuild/win32-arm64@0.27.5': + optional: true + + '@esbuild/win32-ia32@0.27.5': + optional: true + + '@esbuild/win32-x64@0.27.5': + optional: true + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -1723,8 +3189,25 @@ snapshots: '@intlify/shared@10.0.8': {} + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@lezer/common@1.5.1': {} '@lezer/cpp@1.1.5': @@ -2113,6 +3596,18 @@ snapshots: '@oxc-project/types@0.122.0': {} + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@3.0.2': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + '@rolldown/binding-android-arm64@1.0.0-rc.12': optional: true @@ -2191,10 +3686,25 @@ snapshots: tslib: 2.8.1 optional: true + '@types/chrome@0.1.38': + dependencies: + '@types/filesystem': 0.0.36 + '@types/har-format': 1.2.16 + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 + '@types/estree@1.0.8': {} + + '@types/filesystem@0.0.36': + dependencies: + '@types/filewriter': 0.0.33 + + '@types/filewriter@0.0.33': {} + + '@types/har-format@1.2.16': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -2211,6 +3721,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/minimatch@3.0.5': {} + '@types/ms@2.1.0': {} '@types/node@24.12.0': @@ -2224,10 +3736,10 @@ snapshots: '@types/web-bluetooth@0.0.21': {} - '@vitejs/plugin-vue@6.0.5(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0))(vue@3.5.31(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.5(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1))(vue@3.5.31(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0) + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1) vue: 3.5.31(typescript@5.9.3) '@volar/language-core@2.4.28': @@ -2339,8 +3851,59 @@ snapshots: dependencies: vue: 3.5.31(typescript@5.9.3) + '@webext-core/fake-browser@1.3.4': + dependencies: + lodash.merge: 4.6.2 + + '@webext-core/isolated-element@1.1.5': + dependencies: + is-potential-custom-element-name: 1.0.1 + + '@webext-core/match-patterns@1.0.3': {} + + '@wxt-dev/browser@0.1.38': + dependencies: + '@types/filesystem': 0.0.36 + '@types/har-format': 1.2.16 + + '@wxt-dev/module-vue@1.0.3(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1))(vue@3.5.31(typescript@5.9.3))(wxt@0.20.20(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1))': + dependencies: + '@vitejs/plugin-vue': 6.0.5(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1))(vue@3.5.31(typescript@5.9.3)) + wxt: 0.20.20(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1) + transitivePeerDependencies: + - vite + - vue + + '@wxt-dev/storage@1.2.8': + dependencies: + '@wxt-dev/browser': 0.1.38 + async-mutex: 0.5.0 + dequal: 2.0.3 + + acorn@8.16.0: {} + + adm-zip@0.5.17: {} + alien-signals@3.1.2: {} + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + ant-design-vue@4.2.6(vue@3.5.31(typescript@5.9.3)): dependencies: '@ant-design/colors': 6.0.0 @@ -2367,12 +3930,29 @@ snapshots: vue-types: 3.0.2(vue@3.5.31(typescript@5.9.3)) warning: 4.0.3 + array-differ@4.0.0: {} + array-tree-filter@2.1.0: {} + array-union@3.0.1: {} + + async-mutex@0.5.0: + dependencies: + tslib: 2.8.1 + async-validator@4.2.5: {} + async@3.2.6: {} + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + + atomically@2.1.1: + dependencies: + stubborn-fs: 2.0.0 + when-exit: 2.1.5 + axios@1.14.0: dependencies: follow-redirects: 1.15.11 @@ -2383,17 +3963,112 @@ snapshots: bail@2.0.2: {} + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + birpc@2.9.0: {} + bluebird@3.7.2: {} + + boolbase@1.0.0: {} + + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.6.2 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.41.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.2 + + brace-expansion@1.1.13: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + c12@3.3.4(magicast@0.5.2): + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.6 + dotenv: 17.4.0 + exsolve: 1.0.8 + giget: 3.2.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.0 + rc9: 3.0.1 + optionalDependencies: + magicast: 0.5.2 + + cac@6.7.14: {} + + cac@7.0.0: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 + camelcase@8.0.0: {} + ccount@2.0.1: {} + chalk@5.6.2: {} + character-entities@2.0.2: {} + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chrome-launcher@1.2.0: + dependencies: + '@types/node': 24.12.0 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 2.0.2 + transitivePeerDependencies: + - supports-color + + ci-info@4.4.0: {} + + citty@0.2.2: {} + + cli-boxes@3.0.0: {} + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} codemirror@6.0.2: @@ -2406,26 +4081,85 @@ snapshots: '@codemirror/state': 6.6.0 '@codemirror/view': 6.40.0 + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + commander@2.9.0: + dependencies: + graceful-readlink: 1.0.1 + commander@8.3.0: {} + commander@9.5.0: {} + compute-scroll-into-view@1.0.20: {} + concat-map@0.0.1: {} + + concat-stream@1.6.2: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + configstore@7.1.0: + dependencies: + atomically: 2.1.1 + dot-prop: 9.0.0 + graceful-fs: 4.2.11 + xdg-basedir: 5.1.0 + + consola@3.4.2: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 core-js@3.49.0: {} + core-util-is@1.0.3: {} + crelt@1.0.6: {} + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + cssom@0.5.0: {} + csstype@3.2.3: {} dayjs@1.11.20: {} + debounce@1.2.1: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2434,10 +4168,27 @@ snapshots: dependencies: character-entities: 2.0.2 + deep-extend@0.6.0: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@2.0.0: {} + + define-lazy-prop@3.0.0: {} + + defu@6.1.6: {} + delayed-stream@1.0.0: {} dequal@2.0.3: {} + destr@2.0.5: {} + detect-libc@2.1.2: {} devlop@1.1.0: @@ -2448,22 +4199,70 @@ snapshots: dom-scroll-into-view@2.0.1: {} + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + dompurify@3.3.3: optionalDependencies: '@types/trusted-types': 2.0.7 + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-prop@9.0.0: + dependencies: + type-fest: 4.41.0 + + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dotenv@17.4.0: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + entities@4.5.0: {} + entities@7.0.1: {} + environment@1.1.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -2475,18 +4274,77 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es6-error@4.1.1: {} + + esbuild@0.27.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.5 + '@esbuild/android-arm': 0.27.5 + '@esbuild/android-arm64': 0.27.5 + '@esbuild/android-x64': 0.27.5 + '@esbuild/darwin-arm64': 0.27.5 + '@esbuild/darwin-x64': 0.27.5 + '@esbuild/freebsd-arm64': 0.27.5 + '@esbuild/freebsd-x64': 0.27.5 + '@esbuild/linux-arm': 0.27.5 + '@esbuild/linux-arm64': 0.27.5 + '@esbuild/linux-ia32': 0.27.5 + '@esbuild/linux-loong64': 0.27.5 + '@esbuild/linux-mips64el': 0.27.5 + '@esbuild/linux-ppc64': 0.27.5 + '@esbuild/linux-riscv64': 0.27.5 + '@esbuild/linux-s390x': 0.27.5 + '@esbuild/linux-x64': 0.27.5 + '@esbuild/netbsd-arm64': 0.27.5 + '@esbuild/netbsd-x64': 0.27.5 + '@esbuild/openbsd-arm64': 0.27.5 + '@esbuild/openbsd-x64': 0.27.5 + '@esbuild/openharmony-arm64': 0.27.5 + '@esbuild/sunos-x64': 0.27.5 + '@esbuild/win32-arm64': 0.27.5 + '@esbuild/win32-ia32': 0.27.5 + '@esbuild/win32-x64': 0.27.5 + + escalade@3.2.0: {} + + escape-goat@4.0.0: {} + + escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + eventemitter3@5.0.4: {} + + exsolve@1.0.8: {} + extend@3.0.2: {} + fast-redact@3.5.0: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 + filesize@11.0.15: {} + + firefox-profile@4.7.0: + dependencies: + adm-zip: 0.5.17 + fs-extra: 11.3.4 + ini: 4.1.3 + minimist: 1.2.8 + xml2js: 0.6.2 + follow-redirects@1.15.11: {} + form-data-encoder@4.1.0: {} + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -2495,11 +4353,32 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-node@6.0.3: {} + + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + fx-runner@1.4.0: + dependencies: + commander: 2.9.0 + shell-quote: 1.7.3 + spawn-sync: 1.0.15 + when: 3.7.7 + which: 1.2.4 + winreg: 0.0.12 + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2513,13 +4392,31 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-port-please@3.2.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + giget@3.2.0: {} + + glob-to-regexp@0.4.1: {} + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + gopd@1.2.0: {} + graceful-fs@4.2.10: {} + + graceful-fs@4.2.11: {} + + graceful-readlink@1.0.1: {} + + growly@1.3.0: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -2532,18 +4429,166 @@ snapshots: hookable@5.5.3: {} + hookable@6.1.0: {} + + html-escaper@3.0.3: {} + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + immediate@3.0.6: {} + + import-meta-resolve@4.2.0: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@4.1.1: {} + + ini@4.1.3: {} + + is-absolute@0.1.7: + dependencies: + is-relative: 0.1.3 + + is-arrayish@0.2.1: {} + + is-docker@2.2.1: {} + + is-docker@3.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + + is-in-ci@1.0.0: {} + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-installed-globally@1.0.0: + dependencies: + global-directory: 4.0.1 + is-path-inside: 4.0.0 + + is-npm@6.1.0: {} + + is-path-inside@4.0.0: {} + is-plain-obj@4.1.0: {} + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + is-plain-object@3.0.1: {} + is-potential-custom-element-name@1.0.1: {} + + is-primitive@3.0.1: {} + + is-relative@0.1.3: {} + is-what@5.5.0: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isarray@1.0.0: {} + + isexe@1.1.2: {} + + isexe@2.0.0: {} + + isobject@3.0.1: {} + + jiti@2.6.1: {} + + js-md5@0.8.3: {} + js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + + json-parse-even-better-errors@3.0.2: {} + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + katex@0.16.44: dependencies: commander: 8.3.0 + kleur@3.0.3: {} + + ky@1.14.3: {} + + latest-version@9.0.0: + dependencies: + package-json: 10.0.1 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lighthouse-logger@2.0.2: + dependencies: + debug: 4.4.3 + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + lightningcss-android-arm64@1.32.0: optional: true @@ -2593,10 +4638,58 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lines-and-columns@2.0.4: {} + + linkedom@0.18.12: + dependencies: + css-select: 5.2.2 + cssom: 0.5.0 + html-escaper: 3.0.3 + htmlparser2: 10.1.0 + uhyphen: 0.2.0 + + listr2@10.2.1: + dependencies: + cli-truncate: 5.2.0 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 10.0.0 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.0 + quansync: 0.2.11 + lodash-es@4.17.23: {} + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + lodash@4.17.23: {} + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -2607,8 +4700,22 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-error@1.3.6: {} + + many-keys-map@2.0.1: {} + markdown-table@3.0.4: {} + marked@17.0.5: {} + + marky@1.3.0: {} + math-intrinsics@1.1.0: {} mdast-util-definitions@6.0.0: @@ -2938,26 +5045,139 @@ snapshots: dependencies: mime-db: 1.52.0 + mimic-function@5.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.13 + + minimist@1.2.8: {} + mitt@3.0.1: {} + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + ms@2.1.3: {} muggle-string@0.4.1: {} + multimatch@6.0.0: + dependencies: + '@types/minimatch': 3.0.5 + array-differ: 4.0.0 + array-union: 3.0.1 + minimatch: 3.1.5 + + nano-spawn@2.1.0: {} + nanoid@3.3.11: {} nanoid@5.1.7: {} nanopop@2.4.2: {} + nanospinner@1.2.2: + dependencies: + picocolors: 1.1.1 + + node-fetch-native@1.6.7: {} + + node-forge@1.4.0: {} + + node-notifier@10.0.1: + dependencies: + growly: 1.3.0 + is-wsl: 2.2.0 + semver: 7.7.4 + shellwords: 0.1.1 + uuid: 8.3.2 + which: 2.0.2 + + normalize-path@3.0.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nypm@0.6.5: + dependencies: + citty: 0.2.2 + pathe: 2.0.3 + tinyexec: 1.0.4 + + obug@2.1.1: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.3 + + ohash@2.0.11: {} + + on-exit-leak-free@2.1.2: {} + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + orderedmap@2.1.1: {} + os-shim@0.1.3: {} + + package-json@10.0.1: + dependencies: + ky: 1.14.3 + registry-auth-token: 5.1.1 + registry-url: 6.0.1 + semver: 7.7.4 + + pako@1.0.11: {} + + parse-json@7.1.1: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 3.0.2 + lines-and-columns: 2.0.4 + type-fest: 3.13.1 + path-browserify@1.0.1: {} + pathe@2.0.3: {} + perfect-debounce@1.0.0: {} + perfect-debounce@2.1.0: {} + picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.4: {} pinia@3.0.4(typescript@5.9.3)(vue@3.5.31(typescript@5.9.3)): @@ -2967,12 +5187,59 @@ snapshots: optionalDependencies: typescript: 5.9.3 + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@9.7.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 3.1.0 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + postcss@8.5.8: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + powershell-utils@0.1.0: {} + + process-nextick-args@2.0.1: {} + + process-warning@5.0.0: {} + + promise-toolbox@0.21.0: + dependencies: + make-error: 1.3.6 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + prosemirror-changeset@2.4.0: dependencies: prosemirror-transform: 1.12.0 @@ -3065,8 +5332,64 @@ snapshots: prosemirror-state: 1.4.4 prosemirror-view: 1.41.7 + proto-list@1.2.4: {} + proxy-from-env@2.1.0: {} + publish-browser-extension@4.0.4: + dependencies: + cac: 6.7.14 + consola: 3.4.2 + dotenv: 17.4.0 + form-data-encoder: 4.1.0 + formdata-node: 6.0.3 + jsonwebtoken: 9.0.3 + listr2: 10.2.1 + ofetch: 1.5.1 + zod: 4.3.6 + + pupa@3.3.0: + dependencies: + escape-goat: 4.0.0 + + quansync@0.2.11: {} + + quick-format-unescaped@4.0.4: {} + + rc9@3.0.1: + dependencies: + defu: 6.1.6 + destr: 2.0.5 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readdirp@5.0.0: {} + + real-require@0.2.0: {} + + registry-auth-token@5.1.1: + dependencies: + '@pnpm/npm-conf': 3.0.2 + + registry-url@6.0.1: + dependencies: + rc: 1.2.8 + remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -3119,8 +5442,15 @@ snapshots: remove-accents@0.5.0: {} + require-directory@2.1.1: {} + resize-observer-polyfill@1.5.1: {} + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + rfdc@1.4.1: {} rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): @@ -3149,16 +5479,124 @@ snapshots: rope-sequence@1.3.4: {} + run-applescript@7.1.0: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + sax@1.6.0: {} + scroll-into-view-if-needed@2.2.31: dependencies: compute-scroll-into-view: 1.0.20 + scule@1.3.0: {} + + semver@7.7.4: {} + + set-value@4.1.0: + dependencies: + is-plain-object: 2.0.4 + is-primitive: 3.0.1 + + setimmediate@1.0.5: {} + shallow-equal@1.2.1: {} + shell-quote@1.7.3: {} + + shellwords@0.1.1: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + spawn-sync@1.0.15: + dependencies: + concat-stream: 1.6.2 + os-shim: 0.1.3 + speakingurl@14.0.1: {} + split2@4.2.0: {} + + split@1.0.1: + dependencies: + through: 2.3.8 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@5.0.0: {} + + strip-json-comments@2.0.1: {} + + strip-json-comments@5.0.2: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + stubborn-fs@2.0.0: + dependencies: + stubborn-utils: 1.0.2 + + stubborn-utils@1.0.2: {} + style-mod@4.1.3: {} stylis@4.3.6: {} @@ -3167,20 +5605,39 @@ snapshots: dependencies: copy-anything: 4.0.5 + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + throttle-debounce@5.0.2: {} + through@2.3.8: {} + + tinyexec@1.0.4: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tmp@0.2.5: {} + trough@2.2.0: {} - tslib@2.8.1: - optional: true + tslib@2.8.1: {} + + type-fest@3.13.1: {} + + type-fest@4.41.0: {} + + typedarray@0.0.6: {} typescript@5.9.3: {} + ufo@1.6.3: {} + + uhyphen@0.2.0: {} + undici-types@7.16.0: {} unified@11.0.5: @@ -3193,6 +5650,23 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unimport@6.0.2: + dependencies: + acorn: 8.16.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.2 + pathe: 2.0.3 + picomatch: 4.0.4 + pkg-types: 2.3.0 + scule: 1.3.0 + strip-literal: 3.1.0 + tinyglobby: 0.2.15 + unplugin: 3.0.0 + unplugin-utils: 0.3.1 + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -3217,6 +5691,36 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + universalify@2.0.1: {} + + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.4 + + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + update-notifier@7.3.1: + dependencies: + boxen: 8.0.1 + chalk: 5.6.2 + configstore: 7.1.0 + is-in-ci: 1.0.0 + is-installed-globally: 1.0.0 + is-npm: 6.1.0 + latest-version: 9.0.0 + pupa: 3.3.0 + semver: 7.7.4 + xdg-basedir: 5.1.0 + + util-deprecate@1.0.2: {} + + uuid@8.3.2: {} + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -3227,7 +5731,30 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0): + vite-node@6.0.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1): + dependencies: + cac: 7.0.0 + es-module-lexer: 2.0.0 + obug: 2.1.1 + pathe: 2.0.3 + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - '@types/node' + - '@vitejs/devtools' + - esbuild + - jiti + - less + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -3236,7 +5763,9 @@ snapshots: tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.12.0 + esbuild: 0.27.5 fsevents: 2.3.3 + jiti: 2.6.1 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3286,6 +5815,172 @@ snapshots: dependencies: loose-envify: 1.4.0 + watchpack@2.4.4: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + web-ext-run@0.2.4: + dependencies: + '@babel/runtime': 7.28.2 + '@devicefarmer/adbkit': 3.3.8 + chrome-launcher: 1.2.0 + debounce: 1.2.1 + es6-error: 4.1.1 + firefox-profile: 4.7.0 + fx-runner: 1.4.0 + multimatch: 6.0.0 + node-notifier: 10.0.1 + parse-json: 7.1.1 + pino: 9.7.0 + promise-toolbox: 0.21.0 + set-value: 4.1.0 + source-map-support: 0.5.21 + strip-bom: 5.0.0 + strip-json-comments: 5.0.2 + tmp: 0.2.5 + update-notifier: 7.3.1 + watchpack: 2.4.4 + zip-dir: 2.0.0 + transitivePeerDependencies: + - supports-color + + webpack-virtual-modules@0.6.2: {} + + when-exit@2.1.5: {} + + when@3.7.7: {} + + which@1.2.4: + dependencies: + is-absolute: 0.1.7 + isexe: 1.1.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + winreg@0.0.12: {} + + wrap-ansi@10.0.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 8.2.0 + strip-ansi: 7.2.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + wxt@0.20.20(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1): + dependencies: + '@1natsu/wait-element': 4.1.2 + '@aklinker1/rollup-plugin-visualizer': 5.12.0 + '@webext-core/fake-browser': 1.3.4 + '@webext-core/isolated-element': 1.1.5 + '@webext-core/match-patterns': 1.0.3 + '@wxt-dev/browser': 0.1.38 + '@wxt-dev/storage': 1.2.8 + async-mutex: 0.5.0 + c12: 3.3.4(magicast@0.5.2) + cac: 7.0.0 + chokidar: 5.0.0 + ci-info: 4.4.0 + consola: 3.4.2 + defu: 6.1.6 + dotenv: 17.4.0 + dotenv-expand: 12.0.3 + esbuild: 0.27.5 + filesize: 11.0.15 + get-port-please: 3.2.0 + giget: 3.2.0 + hookable: 6.1.0 + import-meta-resolve: 4.2.0 + is-wsl: 3.1.1 + json5: 2.2.3 + jszip: 3.10.1 + linkedom: 0.18.12 + magicast: 0.5.2 + minimatch: 10.2.5 + nano-spawn: 2.1.0 + nanospinner: 1.2.2 + normalize-path: 3.0.0 + nypm: 0.6.5 + ohash: 2.0.11 + open: 11.0.0 + perfect-debounce: 2.1.0 + picocolors: 1.1.1 + prompts: 2.4.2 + publish-browser-extension: 4.0.4 + scule: 1.3.0 + tinyglobby: 0.2.15 + unimport: 6.0.2 + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1) + vite-node: 6.0.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.5)(jiti@2.6.1) + web-ext-run: 0.2.4 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - '@types/node' + - '@vitejs/devtools' + - canvas + - jiti + - less + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + xdg-basedir@5.1.0: {} + + xml2js@0.6.2: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + zip-dir@2.0.0: + dependencies: + async: 3.2.6 + jszip: 3.10.1 + zod@4.3.6: {} zwitch@2.0.4: {} diff --git a/server/internal/tenant/app/media_service.go b/server/internal/tenant/app/media_service.go new file mode 100644 index 0000000..24c8250 --- /dev/null +++ b/server/internal/tenant/app/media_service.go @@ -0,0 +1,1018 @@ +package app + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "strings" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/geo-platform/tenant-api/internal/shared/auth" + "github.com/geo-platform/tenant-api/internal/shared/response" +) + +const minBrowserExtensionVersion = "0.1.0" + +type MediaService struct { + pool *pgxpool.Pool +} + +func NewMediaService(pool *pgxpool.Pool) *MediaService { + return &MediaService{pool: pool} +} + +type MediaPlatformResponse struct { + ID int64 `json:"id"` + PlatformID string `json:"platform_id"` + Name string `json:"name"` + Category string `json:"category"` + ShortName string `json:"short_name"` + AccentColor string `json:"accent_color"` + LoginURL *string `json:"login_url"` + LogoURL *string `json:"logo_url"` + Status string `json:"status"` + SortOrder int `json:"sort_order"` +} + +type PlatformAccountResponse struct { + ID int64 `json:"id"` + PlatformID string `json:"platform_id"` + PlatformName string `json:"platform_name"` + PlatformCategory string `json:"platform_category"` + PlatformUID string `json:"platform_uid"` + Nickname string `json:"nickname"` + AvatarURL *string `json:"avatar_url"` + Status string `json:"status"` + LoginURL *string `json:"login_url"` + LastCheckAt *time.Time `json:"last_check_at"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreatePluginSessionRequest struct { + ActionType string `json:"action_type" binding:"required"` + PlatformID string `json:"platform_id" binding:"required"` + PlatformAccountID *int64 `json:"platform_account_id"` + PluginInstallationID *int64 `json:"plugin_installation_id"` + ResourceType string `json:"resource_type"` + ResourceID *int64 `json:"resource_id"` +} + +type CreatePluginSessionResponse struct { + PluginSessionID int64 `json:"plugin_session_id"` + SessionToken string `json:"session_token"` + ExpireAt time.Time `json:"expire_at"` + MinPluginVersion string `json:"min_plugin_version"` +} + +type CreatePublishBatchRequest struct { + PlatformAccountIDs []int64 `json:"platform_account_ids" binding:"required"` + PluginInstallationID *int64 `json:"plugin_installation_id"` + PublishType string `json:"publish_type"` + CoverAssetURL *string `json:"cover_asset_url"` +} + +type PublishBatchTask struct { + PublishRecordID int64 `json:"publish_record_id"` + PlatformAccountID int64 `json:"platform_account_id"` + PlatformID string `json:"platform_id"` + PlatformName string `json:"platform_name"` + PlatformUID string `json:"platform_uid"` + PlatformNickname string `json:"platform_nickname"` + PluginSessionID int64 `json:"plugin_session_id"` + SessionToken string `json:"session_token"` + ExpireAt time.Time `json:"expire_at"` +} + +type CreatePublishBatchResponse struct { + PublishBatchID int64 `json:"publish_batch_id"` + Status string `json:"status"` + Tasks []PublishBatchTask `json:"tasks"` +} + +type PublishRecordResponse struct { + ID int64 `json:"id"` + PublishBatchID int64 `json:"publish_batch_id"` + ArticleID int64 `json:"article_id"` + PlatformAccountID int64 `json:"platform_account_id"` + PlatformID string `json:"platform_id"` + PlatformName string `json:"platform_name"` + PlatformNickname string `json:"platform_nickname"` + Status string `json:"status"` + ExternalArticleID *string `json:"external_article_id"` + ExternalArticleURL *string `json:"external_article_url"` + ExternalManageURL *string `json:"external_manage_url"` + PublishedAt *time.Time `json:"published_at"` + ErrorMessage *string `json:"error_message"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type PluginBindCallbackRequest struct { + PluginSessionID int64 `json:"plugin_session_id" binding:"required"` + SessionToken string `json:"session_token" binding:"required"` + PlatformID string `json:"platform_id" binding:"required"` + PlatformUID string `json:"platform_uid" binding:"required"` + Nickname string `json:"nickname" binding:"required"` + AvatarURL *string `json:"avatar_url"` + ClientVersion *string `json:"client_version"` + Metadata map[string]interface{} `json:"metadata"` + Status string `json:"status"` +} + +type PluginBindCallbackResponse struct { + PlatformAccountID int64 `json:"platform_account_id"` + Status string `json:"status"` +} + +type PluginPublishCallbackRequest struct { + PluginSessionID int64 `json:"plugin_session_id" binding:"required"` + SessionToken string `json:"session_token" binding:"required"` + ArticleID int64 `json:"article_id" binding:"required"` + PublishRecordID int64 `json:"publish_record_id" binding:"required"` + PlatformAccountID int64 `json:"platform_account_id" binding:"required"` + PlatformID string `json:"platform_id" binding:"required"` + Status string `json:"status" binding:"required"` + ExternalArticleID *string `json:"external_article_id"` + ExternalArticleURL *string `json:"external_article_url"` + ExternalManageURL *string `json:"external_manage_url"` + PublishedAt *time.Time `json:"published_at"` + ClientVersion *string `json:"client_version"` + RequestPayload map[string]interface{} `json:"request_payload"` + ResponsePayload map[string]interface{} `json:"response_payload"` + ErrorMessage *string `json:"error_message"` +} + +type PluginPublishCallbackResponse struct { + PublishRecordID int64 `json:"publish_record_id"` + BatchStatus string `json:"batch_status"` + ArticleStatus string `json:"article_status"` +} + +type RegisterPluginInstallationRequest struct { + InstallationKey string `json:"installation_key" binding:"required"` + InstallationName string `json:"installation_name" binding:"required"` + BrowserName string `json:"browser_name"` + ClientVersion string `json:"client_version"` +} + +type RegisterPluginInstallationResponse struct { + PluginInstallationID int64 `json:"plugin_installation_id"` + InstallationToken string `json:"installation_token"` + Status string `json:"status"` + LastSeenAt time.Time `json:"last_seen_at"` +} + +type platformAccountSeed struct { + ID int64 + PlatformID string + PlatformName string + PlatformUID string + Nickname string +} + +type pluginSessionRow struct { + ID int64 + TenantID int64 + UserID int64 + PluginInstallationID *int64 + ActionType string + ResourceType *string + ResourceID *int64 + PlatformAccountID *int64 + PlatformID string + ExpireAt time.Time +} + +func (s *MediaService) ListPlatforms(ctx context.Context) ([]MediaPlatformResponse, error) { + rows, err := s.pool.Query(ctx, ` + SELECT id, platform_id, name, category, short_name, accent_color, login_url, logo_url, status, sort_order + FROM media_platforms + WHERE status = 'active' + ORDER BY sort_order ASC, id ASC + `) + if err != nil { + return nil, response.ErrInternal(50030, "media_platform_query_failed", "failed to list media platforms") + } + defer rows.Close() + + items := make([]MediaPlatformResponse, 0) + for rows.Next() { + var item MediaPlatformResponse + if err := rows.Scan( + &item.ID, + &item.PlatformID, + &item.Name, + &item.Category, + &item.ShortName, + &item.AccentColor, + &item.LoginURL, + &item.LogoURL, + &item.Status, + &item.SortOrder, + ); err != nil { + return nil, response.ErrInternal(50030, "media_platform_scan_failed", err.Error()) + } + items = append(items, item) + } + return items, nil +} + +func (s *MediaService) RegisterPluginInstallation(ctx context.Context, req RegisterPluginInstallationRequest) (*RegisterPluginInstallationResponse, error) { + actor := auth.MustActor(ctx) + installationKey := strings.TrimSpace(req.InstallationKey) + installationName := strings.TrimSpace(req.InstallationName) + if installationKey == "" || installationName == "" { + return nil, response.ErrBadRequest(40038, "invalid_plugin_installation", "installation_key and installation_name are required") + } + + installationToken, err := newSessionToken() + if err != nil { + return nil, response.ErrInternal(50060, "installation_token_failed", "failed to generate installation token") + } + tokenHash := auth.HashToken(installationToken) + lastSeenAt := time.Now().UTC() + browserName := limitStringPointer(nilIfBlank(req.BrowserName), 64) + clientVersion := limitStringPointer(nilIfBlank(req.ClientVersion), 32) + + tx, err := s.pool.Begin(ctx) + if err != nil { + return nil, response.ErrInternal(50061, "installation_begin_failed", "failed to start installation registration") + } + defer tx.Rollback(ctx) + + var installationID int64 + err = tx.QueryRow(ctx, ` + SELECT id + FROM plugin_installations + WHERE tenant_id = $1 AND installation_key = $2 AND deleted_at IS NULL + LIMIT 1 + `, actor.TenantID, installationKey).Scan(&installationID) + + switch { + case errors.Is(err, pgx.ErrNoRows): + if err := tx.QueryRow(ctx, ` + INSERT INTO plugin_installations ( + tenant_id, user_id, installation_key, installation_name, browser_name, + client_version, installation_token_hash, status, last_seen_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, 'active', $8) + RETURNING id + `, actor.TenantID, actor.UserID, installationKey, installationName, browserName, clientVersion, tokenHash, lastSeenAt).Scan(&installationID); err != nil { + return nil, response.ErrInternal(50062, "installation_insert_failed", "failed to register plugin installation") + } + case err != nil: + return nil, response.ErrInternal(50063, "installation_lookup_failed", "failed to lookup plugin installation") + default: + if _, err := tx.Exec(ctx, ` + UPDATE plugin_installations + SET user_id = $1, + installation_name = $2, + browser_name = $3, + client_version = $4, + installation_token_hash = $5, + status = 'active', + last_seen_at = $6, + updated_at = NOW() + WHERE id = $7 + `, actor.UserID, installationName, browserName, clientVersion, tokenHash, lastSeenAt, installationID); err != nil { + return nil, response.ErrInternal(50064, "installation_update_failed", "failed to refresh plugin installation") + } + } + + if err := tx.Commit(ctx); err != nil { + return nil, response.ErrInternal(50065, "installation_commit_failed", "failed to commit plugin installation") + } + + return &RegisterPluginInstallationResponse{ + PluginInstallationID: installationID, + InstallationToken: installationToken, + Status: "active", + LastSeenAt: lastSeenAt, + }, nil +} + +func (s *MediaService) ListPlatformAccounts(ctx context.Context) ([]PlatformAccountResponse, error) { + actor := auth.MustActor(ctx) + rows, err := s.pool.Query(ctx, ` + SELECT pa.id, pa.platform_id, mp.name, mp.category, pa.platform_uid, pa.nickname, + pa.avatar_url, pa.status, mp.login_url, pa.last_check_at, pa.metadata_json, + pa.created_at, pa.updated_at + FROM platform_accounts pa + JOIN media_platforms mp ON mp.platform_id = pa.platform_id + WHERE pa.tenant_id = $1 AND pa.deleted_at IS NULL + ORDER BY mp.sort_order ASC, pa.created_at DESC + `, actor.TenantID) + if err != nil { + return nil, response.ErrInternal(50031, "platform_account_query_failed", "failed to list platform accounts") + } + defer rows.Close() + + items := make([]PlatformAccountResponse, 0) + for rows.Next() { + var item PlatformAccountResponse + var metadataJSON []byte + if err := rows.Scan( + &item.ID, + &item.PlatformID, + &item.PlatformName, + &item.PlatformCategory, + &item.PlatformUID, + &item.Nickname, + &item.AvatarURL, + &item.Status, + &item.LoginURL, + &item.LastCheckAt, + &metadataJSON, + &item.CreatedAt, + &item.UpdatedAt, + ); err != nil { + return nil, response.ErrInternal(50031, "platform_account_scan_failed", err.Error()) + } + if len(metadataJSON) > 0 { + item.Metadata = map[string]interface{}{} + _ = json.Unmarshal(metadataJSON, &item.Metadata) + } + items = append(items, item) + } + return items, nil +} + +func (s *MediaService) DeletePlatformAccount(ctx context.Context, id int64) error { + actor := auth.MustActor(ctx) + tag, err := s.pool.Exec(ctx, ` + UPDATE platform_accounts + SET deleted_at = NOW(), updated_at = NOW(), status = 'invalid' + WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL + `, id, actor.TenantID) + if err != nil { + return response.ErrInternal(50032, "platform_account_delete_failed", "failed to unbind platform account") + } + if tag.RowsAffected() == 0 { + return response.ErrNotFound(40431, "platform_account_not_found", "platform account not found") + } + return nil +} + +func (s *MediaService) CreatePluginSession(ctx context.Context, req CreatePluginSessionRequest) (*CreatePluginSessionResponse, error) { + actor := auth.MustActor(ctx) + actionType := strings.TrimSpace(req.ActionType) + platformID := strings.TrimSpace(req.PlatformID) + if actionType == "" || platformID == "" { + return nil, response.ErrBadRequest(40031, "invalid_plugin_session_request", "action_type and platform_id are required") + } + if actionType != "bind" && actionType != "check" { + return nil, response.ErrBadRequest(40032, "invalid_plugin_action", "only bind and check are supported on this endpoint") + } + if err := s.validatePluginInstallationOwnership(ctx, actor.TenantID, req.PluginInstallationID); err != nil { + return nil, err + } + + if req.PlatformAccountID != nil { + var exists bool + if err := s.pool.QueryRow(ctx, ` + SELECT EXISTS( + SELECT 1 FROM platform_accounts + WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL + ) + `, *req.PlatformAccountID, actor.TenantID).Scan(&exists); err != nil { + return nil, response.ErrInternal(50033, "platform_account_check_failed", "failed to validate platform account") + } + if !exists { + return nil, response.ErrNotFound(40431, "platform_account_not_found", "platform account not found") + } + } + + token, err := newSessionToken() + if err != nil { + return nil, response.ErrInternal(50034, "session_token_failed", "failed to generate plugin session token") + } + expireAt := time.Now().Add(10 * time.Minute).UTC() + + var id int64 + if err := s.pool.QueryRow(ctx, ` + INSERT INTO plugin_sessions ( + tenant_id, user_id, plugin_installation_id, action_type, resource_type, resource_id, platform_account_id, + platform_id, session_token, expire_at, status + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'pending') + RETURNING id + `, actor.TenantID, actor.UserID, req.PluginInstallationID, actionType, nilIfBlank(req.ResourceType), req.ResourceID, req.PlatformAccountID, platformID, token, expireAt).Scan(&id); err != nil { + return nil, response.ErrInternal(50035, "plugin_session_create_failed", "failed to create plugin session") + } + + return &CreatePluginSessionResponse{ + PluginSessionID: id, + SessionToken: token, + ExpireAt: expireAt, + MinPluginVersion: minBrowserExtensionVersion, + }, nil +} + +func (s *MediaService) CreatePublishBatch(ctx context.Context, articleID int64, req CreatePublishBatchRequest) (*CreatePublishBatchResponse, error) { + actor := auth.MustActor(ctx) + if len(req.PlatformAccountIDs) == 0 { + return nil, response.ErrBadRequest(40033, "platform_account_ids_required", "select at least one platform account") + } + if err := s.validatePluginInstallationOwnership(ctx, actor.TenantID, req.PluginInstallationID); err != nil { + return nil, err + } + + publishType := strings.TrimSpace(req.PublishType) + if publishType == "" { + publishType = "publish" + } + + tx, err := s.pool.Begin(ctx) + if err != nil { + return nil, response.ErrInternal(50036, "publish_batch_begin_failed", "failed to start publish batch") + } + defer tx.Rollback(ctx) + + var articleExists bool + var generateStatus string + if err := tx.QueryRow(ctx, ` + SELECT EXISTS(SELECT 1 FROM articles WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL), + COALESCE((SELECT generate_status FROM articles WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL), '') + `, articleID, actor.TenantID).Scan(&articleExists, &generateStatus); err != nil { + return nil, response.ErrInternal(50037, "article_check_failed", "failed to validate article") + } + if !articleExists { + return nil, response.ErrNotFound(40411, "article_not_found", "article not found") + } + if generateStatus != "completed" { + return nil, response.ErrConflict(40913, "article_not_publishable", "only completed articles can be published") + } + + accountSeeds, err := loadPlatformAccountsForPublish(ctx, tx, actor.TenantID, req.PlatformAccountIDs) + if err != nil { + return nil, err + } + + var publishBatchID int64 + if err := tx.QueryRow(ctx, ` + INSERT INTO publish_batches (tenant_id, article_id, initiator_user_id, status, publish_type, cover_asset_url) + VALUES ($1, $2, $3, 'publishing', $4, $5) + RETURNING id + `, actor.TenantID, articleID, actor.UserID, publishType, normalizeStringPointer(req.CoverAssetURL)).Scan(&publishBatchID); err != nil { + return nil, response.ErrInternal(50038, "publish_batch_create_failed", "failed to create publish batch") + } + + tasks := make([]PublishBatchTask, 0, len(accountSeeds)) + for _, account := range accountSeeds { + var publishRecordID int64 + if err := tx.QueryRow(ctx, ` + INSERT INTO publish_records ( + tenant_id, publish_batch_id, article_id, platform_account_id, platform_id, status + ) + VALUES ($1, $2, $3, $4, $5, 'queued') + RETURNING id + `, actor.TenantID, publishBatchID, articleID, account.ID, account.PlatformID).Scan(&publishRecordID); err != nil { + return nil, response.ErrInternal(50039, "publish_record_create_failed", "failed to create publish record") + } + + token, tokenErr := newSessionToken() + if tokenErr != nil { + return nil, response.ErrInternal(50034, "session_token_failed", "failed to generate plugin session token") + } + expireAt := time.Now().Add(15 * time.Minute).UTC() + + var pluginSessionID int64 + if err := tx.QueryRow(ctx, ` + INSERT INTO plugin_sessions ( + tenant_id, user_id, plugin_installation_id, action_type, resource_type, resource_id, platform_account_id, + platform_id, session_token, expire_at, status + ) + VALUES ($1, $2, $3, 'publish', 'publish_record', $4, $5, $6, $7, $8, 'pending') + RETURNING id + `, actor.TenantID, actor.UserID, req.PluginInstallationID, publishRecordID, account.ID, account.PlatformID, token, expireAt).Scan(&pluginSessionID); err != nil { + return nil, response.ErrInternal(50040, "publish_session_create_failed", "failed to create publish session") + } + + tasks = append(tasks, PublishBatchTask{ + PublishRecordID: publishRecordID, + PlatformAccountID: account.ID, + PlatformID: account.PlatformID, + PlatformName: account.PlatformName, + PlatformUID: account.PlatformUID, + PlatformNickname: account.Nickname, + PluginSessionID: pluginSessionID, + SessionToken: token, + ExpireAt: expireAt, + }) + } + + if _, err := tx.Exec(ctx, ` + UPDATE articles + SET publish_status = 'publishing', updated_at = NOW() + WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL + `, articleID, actor.TenantID); err != nil { + return nil, response.ErrInternal(50041, "article_publish_status_failed", "failed to update article publish status") + } + + if err := tx.Commit(ctx); err != nil { + return nil, response.ErrInternal(50042, "publish_batch_commit_failed", "failed to commit publish batch") + } + + return &CreatePublishBatchResponse{ + PublishBatchID: publishBatchID, + Status: "publishing", + Tasks: tasks, + }, nil +} + +func (s *MediaService) ListArticlePublishRecords(ctx context.Context, articleID int64) ([]PublishRecordResponse, error) { + actor := auth.MustActor(ctx) + return s.listArticlePublishRecords(ctx, actor.TenantID, articleID) +} + +func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID, articleID int64) ([]PublishRecordResponse, error) { + rows, err := s.pool.Query(ctx, ` + SELECT pr.id, pr.publish_batch_id, pr.article_id, pr.platform_account_id, pr.platform_id, + mp.name, pa.nickname, pr.status, pr.external_article_id, pr.external_article_url, + pr.external_manage_url, pr.published_at, pr.error_message, pr.created_at, pr.updated_at + FROM publish_records pr + JOIN media_platforms mp ON mp.platform_id = pr.platform_id + JOIN platform_accounts pa ON pa.id = pr.platform_account_id + WHERE pr.tenant_id = $1 AND pr.article_id = $2 + ORDER BY pr.created_at DESC, pr.id DESC + `, tenantID, articleID) + if err != nil { + return nil, response.ErrInternal(50043, "publish_record_query_failed", "failed to list publish records") + } + defer rows.Close() + + items := make([]PublishRecordResponse, 0) + for rows.Next() { + var item PublishRecordResponse + if err := rows.Scan( + &item.ID, + &item.PublishBatchID, + &item.ArticleID, + &item.PlatformAccountID, + &item.PlatformID, + &item.PlatformName, + &item.PlatformNickname, + &item.Status, + &item.ExternalArticleID, + &item.ExternalArticleURL, + &item.ExternalManageURL, + &item.PublishedAt, + &item.ErrorMessage, + &item.CreatedAt, + &item.UpdatedAt, + ); err != nil { + return nil, response.ErrInternal(50043, "publish_record_scan_failed", err.Error()) + } + items = append(items, item) + } + return items, nil +} + +func (s *MediaService) HandleBindCallback(ctx context.Context, req PluginBindCallbackRequest) (*PluginBindCallbackResponse, error) { + session, err := s.validatePluginSession(ctx, req.PluginSessionID, req.SessionToken, "bind") + if err != nil { + return nil, err + } + if session.PlatformID != req.PlatformID { + return nil, response.ErrConflict(40936, "plugin_session_platform_mismatch", "platform does not match plugin session") + } + + metadataJSON, err := marshalJSON(req.Metadata) + if err != nil { + return nil, response.ErrBadRequest(40034, "invalid_bind_metadata", "metadata must be serializable") + } + + status := strings.TrimSpace(req.Status) + if status == "" { + status = "active" + } + + tx, err := s.pool.Begin(ctx) + if err != nil { + return nil, response.ErrInternal(50044, "bind_callback_begin_failed", "failed to start bind callback transaction") + } + defer tx.Rollback(ctx) + + var platformAccountID int64 + err = tx.QueryRow(ctx, ` + SELECT id + FROM platform_accounts + WHERE tenant_id = $1 AND platform_id = $2 AND platform_uid = $3 AND deleted_at IS NULL + LIMIT 1 + `, session.TenantID, req.PlatformID, req.PlatformUID).Scan(&platformAccountID) + + switch { + case errors.Is(err, pgx.ErrNoRows): + if err := tx.QueryRow(ctx, ` + INSERT INTO platform_accounts ( + tenant_id, user_id, platform_id, platform_uid, nickname, avatar_url, status, + metadata_json, last_check_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW()) + RETURNING id + `, session.TenantID, session.UserID, req.PlatformID, req.PlatformUID, req.Nickname, req.AvatarURL, status, metadataJSON).Scan(&platformAccountID); err != nil { + return nil, response.ErrInternal(50045, "platform_account_insert_failed", "failed to create platform account") + } + case err != nil: + return nil, response.ErrInternal(50046, "platform_account_lookup_failed", "failed to lookup platform account") + default: + if _, err := tx.Exec(ctx, ` + UPDATE platform_accounts + SET user_id = $1, + nickname = $2, + avatar_url = $3, + status = $4, + metadata_json = $5, + last_check_at = NOW(), + updated_at = NOW() + WHERE id = $6 + `, session.UserID, req.Nickname, req.AvatarURL, status, metadataJSON, platformAccountID); err != nil { + return nil, response.ErrInternal(50047, "platform_account_update_failed", "failed to update platform account") + } + } + + if _, err := tx.Exec(ctx, ` + UPDATE plugin_sessions + SET client_version = COALESCE($1, client_version), status = 'completed', updated_at = NOW() + WHERE id = $2 + `, req.ClientVersion, session.ID); err != nil { + return nil, response.ErrInternal(50048, "plugin_session_update_failed", "failed to finalize bind session") + } + if err := touchPluginInstallation(ctx, tx, session.PluginInstallationID); err != nil { + return nil, err + } + + if err := tx.Commit(ctx); err != nil { + return nil, response.ErrInternal(50049, "bind_callback_commit_failed", "failed to commit bind callback") + } + + return &PluginBindCallbackResponse{ + PlatformAccountID: platformAccountID, + Status: status, + }, nil +} + +func (s *MediaService) HandlePublishCallback(ctx context.Context, req PluginPublishCallbackRequest) (*PluginPublishCallbackResponse, error) { + session, err := s.validatePluginSession(ctx, req.PluginSessionID, req.SessionToken, "publish") + if err != nil { + return nil, err + } + if session.ResourceType == nil || *session.ResourceType != "publish_record" || session.ResourceID == nil || *session.ResourceID != req.PublishRecordID { + return nil, response.ErrConflict(40931, "plugin_session_resource_mismatch", "publish record does not match plugin session") + } + if session.PlatformAccountID == nil || *session.PlatformAccountID != req.PlatformAccountID { + return nil, response.ErrConflict(40932, "plugin_session_account_mismatch", "platform account does not match plugin session") + } + if session.PlatformID != req.PlatformID { + return nil, response.ErrConflict(40933, "plugin_session_platform_mismatch", "platform does not match plugin session") + } + + reqStatus := normalizePublishStatus(req.Status) + requestPayloadJSON, err := marshalJSON(req.RequestPayload) + if err != nil { + return nil, response.ErrBadRequest(40035, "invalid_publish_request_payload", "request_payload must be serializable") + } + responsePayloadJSON, err := marshalJSON(req.ResponsePayload) + if err != nil { + return nil, response.ErrBadRequest(40036, "invalid_publish_response_payload", "response_payload must be serializable") + } + + tx, err := s.pool.Begin(ctx) + if err != nil { + return nil, response.ErrInternal(50050, "publish_callback_begin_failed", "failed to start publish callback transaction") + } + defer tx.Rollback(ctx) + + var publishBatchID int64 + var articleID int64 + if err := tx.QueryRow(ctx, ` + SELECT publish_batch_id, article_id + FROM publish_records + WHERE id = $1 AND tenant_id = $2 AND platform_account_id = $3 + `, req.PublishRecordID, session.TenantID, req.PlatformAccountID).Scan(&publishBatchID, &articleID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, response.ErrNotFound(40432, "publish_record_not_found", "publish record not found") + } + return nil, response.ErrInternal(50051, "publish_record_lookup_failed", "failed to lookup publish record") + } + if req.ArticleID != articleID { + return nil, response.ErrConflict(40934, "publish_article_mismatch", "article does not match publish record") + } + + publishedAt := req.PublishedAt + if reqStatus == "success" && publishedAt == nil { + now := time.Now().UTC() + publishedAt = &now + } + + if _, err := tx.Exec(ctx, ` + UPDATE publish_records + SET status = $1, + external_article_id = $2, + external_article_url = $3, + external_manage_url = $4, + published_at = $5, + request_payload_json = $6, + response_payload_json = $7, + error_message = $8, + updated_at = NOW() + WHERE id = $9 + `, reqStatus, normalizeStringPointer(req.ExternalArticleID), normalizeStringPointer(req.ExternalArticleURL), normalizeStringPointer(req.ExternalManageURL), publishedAt, requestPayloadJSON, responsePayloadJSON, normalizeStringPointer(req.ErrorMessage), req.PublishRecordID); err != nil { + return nil, response.ErrInternal(50052, "publish_record_update_failed", "failed to update publish record") + } + + pluginStatus := "completed" + if reqStatus == "failed" { + pluginStatus = "failed" + } + if _, err := tx.Exec(ctx, ` + UPDATE plugin_sessions + SET client_version = COALESCE($1, client_version), status = $2, updated_at = NOW() + WHERE id = $3 + `, req.ClientVersion, pluginStatus, session.ID); err != nil { + return nil, response.ErrInternal(50053, "plugin_session_finalize_failed", "failed to finalize publish session") + } + if err := touchPluginInstallation(ctx, tx, session.PluginInstallationID); err != nil { + return nil, err + } + + batchStatus, err := recalculatePublishBatchStatus(ctx, tx, publishBatchID) + if err != nil { + return nil, err + } + if _, err := tx.Exec(ctx, ` + UPDATE publish_batches + SET status = $1, updated_at = NOW() + WHERE id = $2 + `, batchStatus, publishBatchID); err != nil { + return nil, response.ErrInternal(50054, "publish_batch_update_failed", "failed to update publish batch") + } + + articleStatus := batchStatus + if batchStatus == "success" { + articleStatus = "success" + } + if batchStatus == "failed" { + articleStatus = "failed" + } + if _, err := tx.Exec(ctx, ` + UPDATE articles + SET publish_status = $1, updated_at = NOW() + WHERE id = $2 AND tenant_id = $3 AND deleted_at IS NULL + `, articleStatus, articleID, session.TenantID); err != nil { + return nil, response.ErrInternal(50055, "article_publish_update_failed", "failed to update article publish status") + } + + if err := tx.Commit(ctx); err != nil { + return nil, response.ErrInternal(50056, "publish_callback_commit_failed", "failed to commit publish callback") + } + + return &PluginPublishCallbackResponse{ + PublishRecordID: req.PublishRecordID, + BatchStatus: batchStatus, + ArticleStatus: articleStatus, + }, nil +} + +func (s *MediaService) validatePluginSession(ctx context.Context, pluginSessionID int64, sessionToken, expectedAction string) (*pluginSessionRow, error) { + sessionToken = strings.TrimSpace(sessionToken) + if sessionToken == "" { + return nil, response.ErrUnauthorized(40131, "plugin_session_invalid", "plugin session token is required") + } + + row := &pluginSessionRow{} + err := s.pool.QueryRow(ctx, ` + SELECT id, tenant_id, user_id, plugin_installation_id, action_type, resource_type, resource_id, platform_account_id, platform_id, expire_at + FROM plugin_sessions + WHERE id = $1 AND session_token = $2 + LIMIT 1 + `, pluginSessionID, sessionToken).Scan( + &row.ID, + &row.TenantID, + &row.UserID, + &row.PluginInstallationID, + &row.ActionType, + &row.ResourceType, + &row.ResourceID, + &row.PlatformAccountID, + &row.PlatformID, + &row.ExpireAt, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, response.ErrUnauthorized(40132, "plugin_session_not_found", "plugin session not found") + } + return nil, response.ErrInternal(50057, "plugin_session_lookup_failed", "failed to lookup plugin session") + } + if row.ActionType != expectedAction { + return nil, response.ErrConflict(40935, "plugin_session_action_mismatch", "plugin session action mismatch") + } + if time.Now().UTC().After(row.ExpireAt) { + return nil, response.ErrUnauthorized(40133, "plugin_session_expired", "plugin session has expired") + } + return row, nil +} + +func (s *MediaService) validatePluginInstallationOwnership(ctx context.Context, tenantID int64, pluginInstallationID *int64) error { + if pluginInstallationID == nil { + return nil + } + + var exists bool + if err := s.pool.QueryRow(ctx, ` + SELECT EXISTS( + SELECT 1 + FROM plugin_installations + WHERE id = $1 AND tenant_id = $2 AND status = 'active' AND deleted_at IS NULL + ) + `, *pluginInstallationID, tenantID).Scan(&exists); err != nil { + return response.ErrInternal(50066, "plugin_installation_lookup_failed", "failed to validate plugin installation") + } + if !exists { + return response.ErrNotFound(40433, "plugin_installation_not_found", "plugin installation not found") + } + return nil +} + +func touchPluginInstallation(ctx context.Context, tx pgx.Tx, pluginInstallationID *int64) error { + if pluginInstallationID == nil { + return nil + } + if _, err := tx.Exec(ctx, ` + UPDATE plugin_installations + SET last_seen_at = NOW(), updated_at = NOW() + WHERE id = $1 + `, *pluginInstallationID); err != nil { + return response.ErrInternal(50067, "plugin_installation_touch_failed", "failed to update plugin installation activity") + } + return nil +} + +func loadPlatformAccountsForPublish(ctx context.Context, tx pgx.Tx, tenantID int64, ids []int64) ([]platformAccountSeed, error) { + rows, err := tx.Query(ctx, ` + SELECT pa.id, pa.platform_id, mp.name, pa.platform_uid, pa.nickname + FROM platform_accounts pa + JOIN media_platforms mp ON mp.platform_id = pa.platform_id + WHERE pa.tenant_id = $1 + AND pa.deleted_at IS NULL + AND pa.status = 'active' + AND pa.id = ANY($2) + ORDER BY mp.sort_order ASC, pa.id ASC + `, tenantID, ids) + if err != nil { + return nil, response.ErrInternal(50058, "platform_account_publish_query_failed", "failed to load platform accounts") + } + defer rows.Close() + + items := make([]platformAccountSeed, 0, len(ids)) + for rows.Next() { + var item platformAccountSeed + if err := rows.Scan(&item.ID, &item.PlatformID, &item.PlatformName, &item.PlatformUID, &item.Nickname); err != nil { + return nil, response.ErrInternal(50058, "platform_account_publish_scan_failed", err.Error()) + } + items = append(items, item) + } + if len(items) != len(uniqueInt64(ids)) { + return nil, response.ErrBadRequest(40037, "invalid_platform_accounts", "one or more selected platform accounts are unavailable") + } + return items, nil +} + +func recalculatePublishBatchStatus(ctx context.Context, tx pgx.Tx, publishBatchID int64) (string, error) { + rows, err := tx.Query(ctx, ` + SELECT status + FROM publish_records + WHERE publish_batch_id = $1 + `, publishBatchID) + if err != nil { + return "", response.ErrInternal(50059, "publish_batch_status_query_failed", "failed to aggregate publish batch status") + } + defer rows.Close() + + statuses := make([]string, 0) + for rows.Next() { + var status string + if err := rows.Scan(&status); err != nil { + return "", response.ErrInternal(50059, "publish_batch_status_scan_failed", err.Error()) + } + statuses = append(statuses, normalizePublishStatus(status)) + } + return aggregatePublishStatuses(statuses), nil +} + +func aggregatePublishStatuses(statuses []string) string { + if len(statuses) == 0 { + return "pending" + } + + counts := map[string]int{} + for _, status := range statuses { + counts[normalizePublishStatus(status)]++ + } + + if counts["queued"] > 0 || counts["publishing"] > 0 { + return "publishing" + } + if counts["failed"] == len(statuses) { + return "failed" + } + if counts["success"] == len(statuses) { + return "success" + } + if counts["pending_review"] == len(statuses) { + return "pending_review" + } + if counts["failed"] > 0 && (counts["success"] > 0 || counts["pending_review"] > 0) { + return "partial_success" + } + if counts["pending_review"] > 0 { + return "pending_review" + } + if counts["success"] > 0 { + return "success" + } + return "publishing" +} + +func normalizePublishStatus(status string) string { + switch strings.TrimSpace(status) { + case "queued": + return "queued" + case "publishing", "running": + return "publishing" + case "success", "published", "publish_success": + return "success" + case "failed", "publish_failed": + return "failed" + case "pending_review": + return "pending_review" + default: + return "failed" + } +} + +func normalizeStringPointer(value *string) *string { + if value == nil { + return nil + } + trimmed := strings.TrimSpace(*value) + if trimmed == "" { + return nil + } + return &trimmed +} + +func nilIfBlank(value string) *string { + value = strings.TrimSpace(value) + if value == "" { + return nil + } + return &value +} + +func limitStringPointer(value *string, maxLen int) *string { + if value == nil || maxLen <= 0 { + return value + } + runes := []rune(*value) + if len(runes) <= maxLen { + return value + } + trimmed := string(runes[:maxLen]) + return &trimmed +} + +func marshalJSON(value interface{}) ([]byte, error) { + if value == nil { + return nil, nil + } + return json.Marshal(value) +} + +func newSessionToken() (string, error) { + buf := make([]byte, 24) + if _, err := rand.Read(buf); err != nil { + return "", err + } + return hex.EncodeToString(buf), nil +} + +func uniqueInt64(values []int64) []int64 { + seen := make(map[int64]struct{}, len(values)) + result := make([]int64, 0, len(values)) + for _, value := range values { + if _, ok := seen[value]; ok { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + return result +} diff --git a/server/internal/tenant/transport/media_handler.go b/server/internal/tenant/transport/media_handler.go new file mode 100644 index 0000000..3e8daa2 --- /dev/null +++ b/server/internal/tenant/transport/media_handler.go @@ -0,0 +1,139 @@ +package transport + +import ( + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/geo-platform/tenant-api/internal/bootstrap" + "github.com/geo-platform/tenant-api/internal/shared/response" + "github.com/geo-platform/tenant-api/internal/tenant/app" +) + +type MediaHandler struct { + svc *app.MediaService +} + +func NewMediaHandler(a *bootstrap.App) *MediaHandler { + return &MediaHandler{svc: app.NewMediaService(a.DB)} +} + +func (h *MediaHandler) ListPlatforms(c *gin.Context) { + data, err := h.svc.ListPlatforms(c.Request.Context()) + if err != nil { + response.Error(c, err) + return + } + response.Success(c, data) +} + +func (h *MediaHandler) ListPlatformAccounts(c *gin.Context) { + data, err := h.svc.ListPlatformAccounts(c.Request.Context()) + if err != nil { + response.Error(c, err) + return + } + response.Success(c, data) +} + +func (h *MediaHandler) DeletePlatformAccount(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.Error(c, response.ErrBadRequest(40001, "invalid_id", "platform account id must be a number")) + return + } + if err := h.svc.DeletePlatformAccount(c.Request.Context(), id); err != nil { + response.Error(c, err) + return + } + response.Success(c, nil) +} + +func (h *MediaHandler) CreatePluginSession(c *gin.Context) { + var req app.CreatePluginSessionRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) + return + } + data, err := h.svc.CreatePluginSession(c.Request.Context(), req) + if err != nil { + response.Error(c, err) + return + } + response.Success(c, data) +} + +func (h *MediaHandler) RegisterPluginInstallation(c *gin.Context) { + var req app.RegisterPluginInstallationRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) + return + } + data, err := h.svc.RegisterPluginInstallation(c.Request.Context(), req) + if err != nil { + response.Error(c, err) + return + } + response.Success(c, data) +} + +func (h *MediaHandler) CreatePublishBatch(c *gin.Context) { + articleID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.Error(c, response.ErrBadRequest(40001, "invalid_id", "article id must be a number")) + return + } + var req app.CreatePublishBatchRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) + return + } + data, err := h.svc.CreatePublishBatch(c.Request.Context(), articleID, req) + if err != nil { + response.Error(c, err) + return + } + response.SuccessWithStatus(c, 201, data) +} + +func (h *MediaHandler) ListPublishRecords(c *gin.Context) { + articleID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.Error(c, response.ErrBadRequest(40001, "invalid_id", "article id must be a number")) + return + } + data, err := h.svc.ListArticlePublishRecords(c.Request.Context(), articleID) + if err != nil { + response.Error(c, err) + return + } + response.Success(c, data) +} + +func (h *MediaHandler) PluginBindCallback(c *gin.Context) { + var req app.PluginBindCallbackRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) + return + } + data, err := h.svc.HandleBindCallback(c.Request.Context(), req) + if err != nil { + response.Error(c, err) + return + } + response.Success(c, data) +} + +func (h *MediaHandler) PluginPublishCallback(c *gin.Context) { + var req app.PluginPublishCallbackRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) + return + } + data, err := h.svc.HandlePublishCallback(c.Request.Context(), req) + if err != nil { + response.Error(c, err) + return + } + response.Success(c, data) +} diff --git a/server/internal/tenant/transport/router.go b/server/internal/tenant/transport/router.go index d9c5e0c..2a6dd87 100644 --- a/server/internal/tenant/transport/router.go +++ b/server/internal/tenant/transport/router.go @@ -12,6 +12,11 @@ func RegisterRoutes(app *bootstrap.App) { authAPI.POST("/login", authHandler.Login) authAPI.POST("/refresh", authHandler.Refresh) + pluginHandler := NewMediaHandler(app) + callbacks := app.Engine.Group("/api/callback/plugin") + callbacks.POST("/bind", pluginHandler.PluginBindCallback) + callbacks.POST("/publish", pluginHandler.PluginPublishCallback) + protected := app.Engine.Group("/api") protected.Use(auth.Middleware(app.JWT, app.Sessions)) protected.Use(middleware.TenantScope()) @@ -47,8 +52,17 @@ func RegisterRoutes(app *bootstrap.App) { articles.PUT("/:id", artHandler.Update) articles.GET("/:id/stream", artHandler.Stream) articles.GET("/:id/versions", artHandler.Versions) + articles.GET("/:id/publish-records", pluginHandler.ListPublishRecords) + articles.POST("/:id/publish-batches", pluginHandler.CreatePublishBatch) articles.DELETE("/:id", artHandler.Delete) + media := protected.Group("/tenant/media") + media.GET("/platforms", pluginHandler.ListPlatforms) + media.GET("/platform-accounts", pluginHandler.ListPlatformAccounts) + media.POST("/plugin-installations/register", pluginHandler.RegisterPluginInstallation) + media.POST("/plugin-sessions", pluginHandler.CreatePluginSession) + media.DELETE("/platform-accounts/:id", pluginHandler.DeletePlatformAccount) + brands := protected.Group("/tenant/brands") brandHandler := NewBrandHandler(app) brands.GET("", brandHandler.List) diff --git a/server/migrations/20260402140000_create_media_publisher_tables.down.sql b/server/migrations/20260402140000_create_media_publisher_tables.down.sql new file mode 100644 index 0000000..587bcaa --- /dev/null +++ b/server/migrations/20260402140000_create_media_publisher_tables.down.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS publish_records; +DROP TABLE IF EXISTS publish_batches; +DROP TABLE IF EXISTS plugin_sessions; +DROP TABLE IF EXISTS plugin_installations; +DROP TABLE IF EXISTS platform_accounts; +DROP TABLE IF EXISTS media_platforms; diff --git a/server/migrations/20260402140000_create_media_publisher_tables.up.sql b/server/migrations/20260402140000_create_media_publisher_tables.up.sql new file mode 100644 index 0000000..82cf3b3 --- /dev/null +++ b/server/migrations/20260402140000_create_media_publisher_tables.up.sql @@ -0,0 +1,137 @@ +CREATE TABLE media_platforms ( + id BIGSERIAL PRIMARY KEY, + platform_id VARCHAR(64) NOT NULL, + name VARCHAR(100) NOT NULL, + category VARCHAR(32) NOT NULL DEFAULT 'ugc', + short_name VARCHAR(16) NOT NULL, + accent_color VARCHAR(16) NOT NULL DEFAULT '#1677ff', + login_url TEXT, + logo_url TEXT, + status VARCHAR(20) NOT NULL DEFAULT 'active', + sort_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX uk_media_platforms_platform_id ON media_platforms(platform_id); +CREATE INDEX idx_media_platforms_status_sort ON media_platforms(status, sort_order, id); + +INSERT INTO media_platforms (platform_id, name, category, short_name, accent_color, login_url, sort_order) +VALUES + ('toutiaohao', '头条号', 'news', '头', '#f5222d', 'https://mp.toutiao.com/auth/page/login', 10), + ('baijiahao', '百家号', 'news', '百', '#31445a', 'https://baijiahao.baidu.com/builder/theme/bjh/login', 20), + ('sohuhao', '搜狐号', 'news', '搜', '#fa8c16', 'https://mp.sohu.com/mpfe/v4/login', 30), + ('qiehao', '企鹅号', 'social', 'Q', '#111827', 'https://om.qq.com', 40), + ('zhihu', '知乎', 'social', '知', '#1677ff', 'https://www.zhihu.com/signin', 50), + ('wangyihao', '网易号', 'news', '网', '#ff4d4f', 'https://mp.163.com/login.html', 60), + ('jianshu', '简书', 'social', '简', '#f56a5e', 'https://www.jianshu.com/sign_in', 70), + ('bilibili', 'bilibili', 'video', 'B', '#eb2f96', 'https://www.bilibili.com', 80), + ('juejin', '稀土掘金', 'social', '掘', '#1677ff', 'https://juejin.cn/login', 90), + ('smzdm', '什么值得买', 'social', '值', '#f5222d', 'https://zhiyou.smzdm.com/user/login', 100), + ('weixin_gzh', '微信公众号', 'social', '微', '#13c26b', 'https://mp.weixin.qq.com/cgi-bin/loginpage', 110), + ('zol', '中关村在线', 'news', 'Z', '#ff4d4f', 'https://post.zol.com.cn/v2/manage/works/all', 120), + ('dongchedi', '懂车帝', 'news', '懂', '#fadb14', 'https://mp.dcdapp.com/login', 130); + +CREATE TABLE platform_accounts ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id), + user_id BIGINT NOT NULL REFERENCES users(id), + platform_id VARCHAR(64) NOT NULL REFERENCES media_platforms(platform_id), + platform_uid VARCHAR(128) NOT NULL, + nickname VARCHAR(255) NOT NULL, + avatar_url TEXT, + status VARCHAR(20) NOT NULL DEFAULT 'active', + metadata_json JSONB, + last_check_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE UNIQUE INDEX uk_platform_accounts_identity_active + ON platform_accounts(tenant_id, platform_id, platform_uid) + WHERE deleted_at IS NULL; +CREATE INDEX idx_platform_accounts_tenant_platform ON platform_accounts(tenant_id, platform_id, status, created_at DESC); + +CREATE TABLE plugin_installations ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id), + user_id BIGINT NOT NULL REFERENCES users(id), + installation_key VARCHAR(128) NOT NULL, + installation_name VARCHAR(255) NOT NULL, + browser_name VARCHAR(64), + client_version VARCHAR(32), + installation_token_hash VARCHAR(128) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'active', + last_seen_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE UNIQUE INDEX uk_plugin_installations_key_active + ON plugin_installations(tenant_id, installation_key) + WHERE deleted_at IS NULL; +CREATE INDEX idx_plugin_installations_tenant_status + ON plugin_installations(tenant_id, status, created_at DESC); + +CREATE TABLE plugin_sessions ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id), + user_id BIGINT NOT NULL REFERENCES users(id), + plugin_installation_id BIGINT REFERENCES plugin_installations(id), + action_type VARCHAR(20) NOT NULL, + resource_type VARCHAR(30), + resource_id BIGINT, + platform_account_id BIGINT REFERENCES platform_accounts(id), + platform_id VARCHAR(64) NOT NULL REFERENCES media_platforms(platform_id), + session_token VARCHAR(128) NOT NULL, + client_version VARCHAR(32), + status VARCHAR(20) NOT NULL DEFAULT 'pending', + expire_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX uk_plugin_sessions_token ON plugin_sessions(session_token); +CREATE INDEX idx_plugin_sessions_status_expire ON plugin_sessions(status, expire_at); +CREATE INDEX idx_plugin_sessions_tenant_action ON plugin_sessions(tenant_id, action_type, created_at DESC); + +CREATE TABLE publish_batches ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id), + article_id BIGINT NOT NULL REFERENCES articles(id), + initiator_user_id BIGINT NOT NULL REFERENCES users(id), + status VARCHAR(20) NOT NULL DEFAULT 'pending', + publish_type VARCHAR(20) NOT NULL DEFAULT 'publish', + cover_asset_url TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_publish_batches_article_created ON publish_batches(article_id, created_at DESC); +CREATE INDEX idx_publish_batches_tenant_status ON publish_batches(tenant_id, status, created_at DESC); + +CREATE TABLE publish_records ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id), + publish_batch_id BIGINT NOT NULL REFERENCES publish_batches(id) ON DELETE CASCADE, + article_id BIGINT NOT NULL REFERENCES articles(id), + platform_account_id BIGINT NOT NULL REFERENCES platform_accounts(id), + platform_id VARCHAR(64) NOT NULL REFERENCES media_platforms(platform_id), + status VARCHAR(20) NOT NULL DEFAULT 'queued', + external_article_id VARCHAR(128), + external_article_url TEXT, + external_manage_url TEXT, + published_at TIMESTAMPTZ, + request_payload_json JSONB, + response_payload_json JSONB, + error_message TEXT, + retry_count INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX uk_publish_records_batch_account ON publish_records(publish_batch_id, platform_account_id); +CREATE INDEX idx_publish_records_article_created ON publish_records(article_id, created_at DESC); +CREATE INDEX idx_publish_records_status ON publish_records(status, created_at DESC); diff --git a/server/migrations/20260402153000_repair_plugin_installations_schema.down.sql b/server/migrations/20260402153000_repair_plugin_installations_schema.down.sql new file mode 100644 index 0000000..72e5efb --- /dev/null +++ b/server/migrations/20260402153000_repair_plugin_installations_schema.down.sql @@ -0,0 +1,4 @@ +-- No-op rollback. +-- This migration repairs databases that already applied an older variant of +-- 20260402140000 without plugin_installations. Dropping these objects on +-- rollback would break databases created from the current baseline migration. diff --git a/server/migrations/20260402153000_repair_plugin_installations_schema.up.sql b/server/migrations/20260402153000_repair_plugin_installations_schema.up.sql new file mode 100644 index 0000000..7fc3954 --- /dev/null +++ b/server/migrations/20260402153000_repair_plugin_installations_schema.up.sql @@ -0,0 +1,39 @@ +CREATE TABLE IF NOT EXISTS plugin_installations ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id), + user_id BIGINT NOT NULL REFERENCES users(id), + installation_key VARCHAR(128) NOT NULL, + installation_name VARCHAR(255) NOT NULL, + browser_name VARCHAR(64), + client_version VARCHAR(32), + installation_token_hash VARCHAR(128) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'active', + last_seen_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE UNIQUE INDEX IF NOT EXISTS uk_plugin_installations_key_active + ON plugin_installations(tenant_id, installation_key) + WHERE deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_plugin_installations_tenant_status + ON plugin_installations(tenant_id, status, created_at DESC); + +ALTER TABLE plugin_sessions + ADD COLUMN IF NOT EXISTS plugin_installation_id BIGINT; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'plugin_sessions_plugin_installation_id_fkey' + ) THEN + ALTER TABLE plugin_sessions + ADD CONSTRAINT plugin_sessions_plugin_installation_id_fkey + FOREIGN KEY (plugin_installation_id) + REFERENCES plugin_installations(id); + END IF; +END $$;