From 32d6a462cd09917b29c81538175cefe8a9f425c9 Mon Sep 17 00:00:00 2001 From: liangxu Date: Fri, 3 Apr 2026 00:39:15 +0800 Subject: [PATCH] feat: implement browser extension for media publishing and add backend support for media management --- .gitignore | 6 +- .../src/components/ArticleDetailDrawer.vue | 137 +- .../src/components/CustomArticleTab.vue | 40 +- .../src/components/PublishArticleModal.vue | 451 +++ apps/admin-web/src/i18n/messages/en-US.ts | 93 +- apps/admin-web/src/i18n/messages/zh-CN.ts | 93 +- apps/admin-web/src/lib/api.ts | 52 + apps/admin-web/src/lib/display.ts | 3 + apps/admin-web/src/lib/publisher-plugin.ts | 132 + apps/admin-web/src/lib/publisher-runtime.ts | 73 + apps/admin-web/src/router/index.ts | 2 +- .../admin-web/src/views/ArticleEditorView.vue | 18 +- apps/admin-web/src/views/MediaView.vue | 831 +++++ apps/admin-web/src/views/TemplatesView.vue | 42 +- apps/admin-web/src/views/WorkspaceView.vue | 9 +- .../entrypoints/background.ts | 31 + apps/browser-extension/entrypoints/content.ts | 56 + .../entrypoints/popup/App.vue | 767 +++++ .../entrypoints/popup/index.html | 12 + .../entrypoints/popup/main.ts | 5 + apps/browser-extension/package.json | 24 + apps/browser-extension/public/icon/128.png | Bin 0 -> 3074 bytes apps/browser-extension/public/icon/16.png | Bin 0 -> 559 bytes apps/browser-extension/public/icon/32.png | Bin 0 -> 916 bytes apps/browser-extension/public/icon/48.png | Bin 0 -> 1334 bytes apps/browser-extension/public/icon/96.png | Bin 0 -> 2366 bytes .../public/logos/logo_baijiahao.png | Bin 0 -> 1868 bytes .../public/logos/logo_bilibili.png | Bin 0 -> 3034 bytes .../public/logos/logo_dongchedi.svg | 11 + .../public/logos/logo_jianshu.svg | 4 + .../public/logos/logo_juejin.png | Bin 0 -> 12121 bytes .../public/logos/logo_qiehao.png | Bin 0 -> 2836 bytes .../public/logos/logo_smzdm.svg | 8 + .../public/logos/logo_souhu.png | Bin 0 -> 5117 bytes .../public/logos/logo_toutiao.png | Bin 0 -> 4527 bytes .../public/logos/logo_wangyihao.png | Bin 0 -> 2126 bytes .../public/logos/logo_weixin_gzh.svg | 5 + .../public/logos/logo_zhihu.png | Bin 0 -> 1373 bytes .../public/logos/logo_zol.png | Bin 0 -> 1913 bytes apps/browser-extension/public/rules.json | 48 + apps/browser-extension/public/wxt.svg | 15 + apps/browser-extension/src/adapters/index.ts | 26 + apps/browser-extension/src/adapters/types.ts | 24 + apps/browser-extension/src/adapters/zhihu.ts | 380 +++ apps/browser-extension/src/platforms.ts | 146 + apps/browser-extension/src/runtime.ts | 310 ++ apps/browser-extension/src/storage.ts | 99 + apps/browser-extension/tsconfig.json | 8 + apps/browser-extension/wxt.config.ts | 24 + docs/media-publisher-extension-runtime-v1.md | 58 + package.json | 6 +- packages/shared-types/src/index.ts | 179 ++ pnpm-lock.yaml | 2709 ++++++++++++++++- server/internal/tenant/app/media_service.go | 1018 +++++++ .../tenant/transport/media_handler.go | 139 + server/internal/tenant/transport/router.go | 14 + ...000_create_media_publisher_tables.down.sql | 6 + ...40000_create_media_publisher_tables.up.sql | 137 + ...epair_plugin_installations_schema.down.sql | 4 + ..._repair_plugin_installations_schema.up.sql | 39 + 60 files changed, 8268 insertions(+), 26 deletions(-) create mode 100644 apps/admin-web/src/components/PublishArticleModal.vue create mode 100644 apps/admin-web/src/lib/publisher-plugin.ts create mode 100644 apps/admin-web/src/lib/publisher-runtime.ts create mode 100644 apps/admin-web/src/views/MediaView.vue create mode 100644 apps/browser-extension/entrypoints/background.ts create mode 100644 apps/browser-extension/entrypoints/content.ts create mode 100644 apps/browser-extension/entrypoints/popup/App.vue create mode 100644 apps/browser-extension/entrypoints/popup/index.html create mode 100644 apps/browser-extension/entrypoints/popup/main.ts create mode 100644 apps/browser-extension/package.json create mode 100644 apps/browser-extension/public/icon/128.png create mode 100644 apps/browser-extension/public/icon/16.png create mode 100644 apps/browser-extension/public/icon/32.png create mode 100644 apps/browser-extension/public/icon/48.png create mode 100644 apps/browser-extension/public/icon/96.png create mode 100644 apps/browser-extension/public/logos/logo_baijiahao.png create mode 100644 apps/browser-extension/public/logos/logo_bilibili.png create mode 100644 apps/browser-extension/public/logos/logo_dongchedi.svg create mode 100644 apps/browser-extension/public/logos/logo_jianshu.svg create mode 100644 apps/browser-extension/public/logos/logo_juejin.png create mode 100644 apps/browser-extension/public/logos/logo_qiehao.png create mode 100644 apps/browser-extension/public/logos/logo_smzdm.svg create mode 100644 apps/browser-extension/public/logos/logo_souhu.png create mode 100644 apps/browser-extension/public/logos/logo_toutiao.png create mode 100644 apps/browser-extension/public/logos/logo_wangyihao.png create mode 100644 apps/browser-extension/public/logos/logo_weixin_gzh.svg create mode 100644 apps/browser-extension/public/logos/logo_zhihu.png create mode 100644 apps/browser-extension/public/logos/logo_zol.png create mode 100644 apps/browser-extension/public/rules.json create mode 100644 apps/browser-extension/public/wxt.svg create mode 100644 apps/browser-extension/src/adapters/index.ts create mode 100644 apps/browser-extension/src/adapters/types.ts create mode 100644 apps/browser-extension/src/adapters/zhihu.ts create mode 100644 apps/browser-extension/src/platforms.ts create mode 100644 apps/browser-extension/src/runtime.ts create mode 100644 apps/browser-extension/src/storage.ts create mode 100644 apps/browser-extension/tsconfig.json create mode 100644 apps/browser-extension/wxt.config.ts create mode 100644 docs/media-publisher-extension-runtime-v1.md create mode 100644 server/internal/tenant/app/media_service.go create mode 100644 server/internal/tenant/transport/media_handler.go create mode 100644 server/migrations/20260402140000_create_media_publisher_tables.down.sql create mode 100644 server/migrations/20260402140000_create_media_publisher_tables.up.sql create mode 100644 server/migrations/20260402153000_repair_plugin_installations_schema.down.sql create mode 100644 server/migrations/20260402153000_repair_plugin_installations_schema.up.sql 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 0000000000000000000000000000000000000000..9e35d130796978714468fa149241172744bf3992 GIT binary patch literal 3074 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 Q9RL6T07*qoM6N<$g0^9zvj6}9 literal 0 HcmV?d00001 diff --git a/apps/browser-extension/public/icon/16.png b/apps/browser-extension/public/icon/16.png new file mode 100644 index 0000000000000000000000000000000000000000..cd09f8cfbc020b5a077c6c491e2be5f1cb75fb50 GIT binary patch literal 559 zcmV+~0?_@5P)J*;soIYk(IETZ~(bMj$o8vnQ?Z`K&P@byKnRdgL9BcAPx)vHta#e(k_EN+QtB~a;#B7vY4-MPwd z0I*M%0Ii$vMXQ0J*IDiEx*HeHO#2~|QUWDPMwmjovlOSF45Is5Q{nqwq^gh#fRPv6 zS}@*-M+8d*2rWit4ITS_^ytbtKuJ{`srD|`y4ycdB$KV&w?aSmH!}7OlPlMt4AEQp zhZ%K%Z3q@VTdrAraF5Y9z3|}q+%#iwp}*e_69qRYzn6K?6mgj zj?N1`|7Fpj-nU)JS}?UmCFPhx`KK{AMERWeC5F}p!vOUOl x{o{41KG|W@*}t!GLho_Yiov7vl{n4;-T?Ii%lIRLgKym_bV-2v#fCmVkAh`iih%Om#5EqsU zEB$;ulbPxHNiqxxepRVTcfam_)BWD}zDCH1!#lJ1=QkYzLKTQg1TaFtD>Sfey|Nw1 z3Cv>jc4%%Q$sQXQ)5Zy*b+&%lh8&pdK=X%LqOt7p-_p#5NNihWyASt-m}fv^KK5X9 zbks(t8*DKHMv44r@nJitenlQlcBQ64mJYAwD3KR8Dmyf5Q6f}^(*cw^x1qBd8s%9kw)PqOF+ zc-#S zwQjUnP-RT=n$z;oa{$o^ej$o%)Kw)I9tuf6Q`Tn+#^dIVS&7sFkK3u|69@r)NF$HD zn5?fe?w80Fs5B&yA2ly4ut2Ew4a9w|>vbXq1Q}Wd!xyk%#42Pl;Q%w^j@J4G3&z1W zvl_P0Dd_YlpU67vy~AJ_#T7gd5&C5&WPNcQtPjvH(26c7^r`6R;v+X!sKdcrNe*yU zu%IoZ&tmCi;5IA?s^k;m?l}i#BnCL@`+N#*F3DTVC`^hA)cVg?aqWB(jFA(tHZEp< zwF)31XomugC@(-5{jAQilHIq;7k$02kY(b8Kv-7EiXJ9J%oAhjFm3N<5ujX#EH9TW9YBdNqdt0000<{22)0j@ZAli#Ip&84}hAsEPnZ0vICT zIil>g)=nMB5eRp{-n;rUl5nIZozTq{LL5B**?A2)AhQ9D%laV^98KF_fMq|gbh*W_VK z1jmlNdOFzMnWnR6z&J3c=k!HD!pQmK(ypfUD~#Df9-3~ld-VB_oA?C8H;_0Hc_u)( zkPLQNszn=p+I^HX5MyYO{Sc@2!+9Vp?Ft~F`P!l-lx{XHv`YA9z_Otk!1`zN$>8z6qBU3z6!!)@Cylpu z&2$r4dr z!Ph&1n@}}yZ|UZmlwLZ3yn({y_xC54U@7EOHs zmS9Ox%wtj5-FXeFu?9FB4(G0#0YAVJ!7g(VX(}eN5NByC!#oOv_#o0Wj5RxTHdK#0}VOB}LiJ$VTSLE0$n_c4LN3lA@FuMfFbX^Typ_M);{*DcI)i6`WkWsC0j9uXW3O zVuOZ!LPg1b(kgf{rMV>i4+)t4j0{Pu7zjk3{An}TrkIRbNlQWFk|JO4^rG#EB2j-C z$|~4rDvhoAGr1@OO2Pi^N;kHK|71{qA96S36HTf1nb0gBaFKb-q<8q)Wk`U5Mv3!J zsKJ!Llmig|{?`gPQPn>@48Ai~??4K2FD)WqrXqmSJFT^`-buopHnKdR2ti1B(Eeq* sU%9M=0p6t5!3M|Pz3;IZ)jLV}0)HJVZ}+VOtN;K207*qoM6N<$f?ddKj{pDw literal 0 HcmV?d00001 diff --git a/apps/browser-extension/public/icon/96.png b/apps/browser-extension/public/icon/96.png new file mode 100644 index 0000000000000000000000000000000000000000..c28ad52d56fc409195f52f57c447442326d94b6b GIT binary patch literal 2366 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fcc5602e6a419bae420a4ab5297c3c821e39abc1 GIT binary patch literal 1868 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6ee5fc02155db7fafadb549a8246139086aa2127 GIT binary patch literal 12121 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..744bcc7e603442502174b4dd0f0d6a3c78d725a0 GIT binary patch literal 2836 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 + + + \ 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 0000000000000000000000000000000000000000..9a63b32be2c5289413667e56515d14e3be006b96 GIT binary patch literal 5117 zcmVPx|w@E}nRA@uRnt6~_)t$#b=iK|2US2OWG+VQXBC@C<#toy01WQB|l+j2uiKCJb znYcwOX4II)nyTm!ljvw%;vO})1VK>*aSKjZ6~ht`LDq(Dn(ltRy}O;M-?{few_|Ed z{>a>_=5e=ke&65n{Vne)K64ZbAB;8~ORoxpO=n{|27@a9>@)x5^EPzp4wV|S*vU?O zIFlK@zWe^Fcw&r6ceXw@*LZ)Lt)o^A+TJpUp7!x_eRNEk8@!ODehZ|0DF@-qm1?LT9JEnW?&BvVgDvx(+^fujzc|8kB)Z zp%fSkBYxq~xiJ`}Vu;Bz@}My$zF@Tx|D%+=4XWEaht`bsX16oSpp4xqR+^YxuG!rW zANH1gB*3UrRM|E6Wh=+eQH8?#(KJ7@&iEU&#*wXK0g?rhrQ;2YiyDI;#KNXh3UmTA zHfI7R-adY(xN0nrqZL|f{4lZrqeFYf__ZiUH0{J7qe!j`+IZ>6_~`8TC{^*@n5k#IEVO zhBd45?A_z>l~XF|UE?Mm8(T$d5mMm?hV37B*fr}0XUUdy4@Lo9j;^}(9#uH-zdv+Ii>&*ky4JzS`GplV#@7Y_RzTT&;#pl%pyD875IB)tu+?1xe%B63= z6pjNXI^;`s!~pn`_G@~UG&IVuic`6mn`)~?F)-~*p>%!o3s zy>Tdi-Pq29_wS^s$z$TAKJ56klhP80+J0GTmE%wEG%{gQZ=%SsaebbyPKg^sLqmas%>~NKG7K7;W%7BooPK6Cm1S-Z zMiiHdO2)S-Z{MB9?OYIhsZ!{aCN!{NZ5zM6Zzo3%FX8%|hp_yeJZo3x`SOW51`f*b z;=FyhuHxdE{j9Ife_2?_ZIldiPa1rQNoCkgp74yw+s>`P5S@?e*pFwXlC* zfs1GMW&D|yJppKg_}030?fm#ho7rFQF?-Gs2J}yJ|9!hS=iEMAcu5T@SF^X?r(fR; znG(fgkL<pX)fkQE72nB$*_vglKe~BQ3l`A^<#eeLiE$`8*SDIaWIte_( zpdlrU9$n6wwXIZ?yG)qao6gRVTm=j}s)QBqx3X$go{C(GE3T~N)YB?3VNAoKv6dFY zvNu~;u%OX`l)$|Gjw7h6OY^I_yEyl}T27mgO)?9;Q&Vs&vMqJ3SL>z#9udc zaqfA&s2iAN!TcuN6wLT~FB@DhK6`+Vw-mVa@)}MVS0+&wr-cYsuI%K=#~a91Xihq* zf+rrY=Yr{dm^P!5=bmb!ti)yV^c*XfxAWBFjZ8kTnlYoxtp%n`t>&xab7a%PpFJ5a z$W1^6dw*=4wt0uMulSgw*w+y8_ToIF$CfbSxGeJ?J;1`3o5@wVbhbxq{kRLq)ztP* z;|CF)odIW^RmsfDtEn55!XyA>OV2m7w;3{}nomCVx$|ecsO^*C#vk?NqqQBp_f|Vs z&Z?oZ!e#E=`xyJ>QcfLL&eFxLjGs`>uwf~bxM2^9HUwp7qrBa>8K>c&k{iYSghK4Sx{veIGHs5CV-I?>c&CbCJ| zQFL}0?wQ-f;zb9ku6DWeu7T{>-o?u=wKDsAeHbz%!!PgI$H}LZG49k7-g&#gs4*#q z3~~-7`l1a4OPD(w6eW*5Ekie8MLlm*GUC^miM z^W^VZ*}B!^%n4dO6wKH>Oj=F(qe)F4VPB&4>N+bKzFrk^kOF^ zS@_P?Sq?NA7AzEjLneN$gb9;VHtj$5Kszf|cJl3iE#>sn(is0UwS2&WXwDm6d9{ma=VjTvB_vw{v#wMO9VBIG(ZUA5 zVj9Z-be3_NAGE@IzGCAhm#y1eMjRb7|7EDiMSS-=f$b9lU-8Lym*-x9_udceVp=Qu z^maJotD4it8R`ax|E{{GAAFVewAkUtZV%1uY))u(s2aqk%-1l3< zu3d)Sy*1@!F#fA9`kK&Qgovst(L!V`*U+F@_Kw5+R|QDK)X9pguMBNfx_N`r5S4HgE6B77wk0DKp zxU($b$zKRUU{W9s=&w2H1kDALVf5&L&Nj`;m72yT+1oI5a9|al0JzFHO%KQ2PU5qY z4?Iti%^Don*v;D86|RPCDK;xgtrNF+v5SNeR?AH-nu@B3_JZP-MT#X$L-y^H&I__B zNIAuEL0oNw;+j$#N8kf~B%UB(6#e^ZE}x)OS`zPs9fWr*%F34+tJQn0*{VJ+CmXn5k|&pUMwKO3kY8#`w1`;3ko04NH- z!y#IfMXk=_`i`X-<&U&iCsq+%nB{;VgtBm zK+AnO1V|xJeEl4kpIzsZ8~l#|5bhSv-XboOMQzBSjf-v19HEGqCJJFhs~;jW%@VP% zII{249)>LUJr;UCpO^q&))L``1d=$gS{DY4S<_w@SCexufZ>B29{h2{$WtW-HqSC{ z!xwNN(Yj>SW{BR;VBRXlZA{x{om7fQeBRC*4WZ0=f)Epg1VKO$2KKx@@Oh@-u`2Ru zlRTJ=d908qLi?^lLNvaXBs>cwCQkt(1>M}6H5XoXK@1?(@L>0O#s3cBBx8o#T+Fg8 z>cdi0!L`8}iN8Cs@KSn&L1={y0^bTB`jV9bwuK=tcs}oiz7@=dKyh7hk0j?@CIcNE~3Z#&Gnc3K<*!a|>W6q8r8CVHb3)|8?Trme<}>k?^=QQD3mBNN2} zd$G`wUm)~6qQIlxkC+!2mPR2_F%emph)p0_R|Z^`TJ7xfusaD5Iih$vYT2t$fXk*h z-1hGmpxPJCGO4zQ&;w*ro4}AVL0aBshv=getjwT4l^P<0$+{Q`R^<>WMG!`qFeLJQ z0?)Ss2cAbzC=hubT|vl_P$5n0er0SDl0wWngpmR~9{t(|MaG3mGLzCm+$b4#$IV#c za$W(ZPjL9jPp6@pZ<=kKHy%YD4{kq$#3Ok{wA(@LN~2m*sB{`tnjvyxtxIMVjfa6D z@h^Q-xhAIM)5ANJ_*7m4fP7zq<>E4;Fcm2;cY*Z(5eV6z^v;ex|MSQ zF!c0?qN~O`_E?QeZHI`k1Bsdc-dtHh91cU`c=qEJWjoWR_ zlN4aP9c6|8z%g_tSxsyqRB7F)7Lh2k1Mkrxp8qVbT>j|e<4ezBTV_##x1QS4`|{ioCLp=*ez zN{NdkL2LP5Mv{(_uGSu2@|}bA+<5+su+YtyJAD7T>8P&TZ!qo)Pg^>SBrwId?dFCs zkh#5l3YUlWQ!EhrLY0C25+V|zg(-_d#9sm(a*Y^-T!S?ZJe4kLJ_aiSxmr02HMVLtOs~>1C`sM|wj@zy`>bWNfEhS26D+`O( zV}WHVA>8hZzl*?;0E#O_5nP0@C2=COt>qJ+jz8mjO-ixYfp=5Jc3^2oQB$rMJy`LT z;f5pnNA#(I-o2o_T$(BA&f*D)G*=z%u&Y7w{$CX9KULg#S;)xKFG1z+z9S#rJGb0! zB*H}@p$V+!MeFkIN$4BNMR8yzW;^BH=&FuHK|O5I{NN5m7o9A`nFrD)yXwhYCEz`sI<8#a9Sw&$el%J$Haa=^*02!ksy>UYDmM&#RAYzO5<;CRl@TgVh zR&kLO4)_KPyXziRDC9?XZG325!{fK>_JwWe9kJ=8_KxEr4#Fa<6g3@Z%s61XYrMM! zC|9^cpz2t~kS|5l4VF36;o0xs*@46sk=iIz1IPa^O7}YFIJ<_|vhm*bwYOcsBNegX9#d9H3kD3AHOSd{%=GJgL3q_$3#!Jvz9kNw4)nex7ee`;_D zM92QQ?0kN<_wf_DbwQ{wLdQPnXi$V5CXNl!LdKv<6>g=Wv|3SKYp6&BE|aek z?C7A|DDxkaJbuf`qH9jV9ezdR4w!m>s}(D?4z!LFAF`_0?S z}IKezcG3^Px`X-PyuRA@u3nhCUAb$!P_|GUne<;~ldgkZv2P-BY~MK%>rD#~t4(iS;dEk{Ul zh=>YGEt^q6cBob9K~XeTv0^z|gS6HoN5zT~NKA;3?Ij^EdGAeTpS%5ge*bwBhL=Ey za?hFfX72qjzwdAR|CXzmb!n|%3w#Jz6vy%5VHoDaFhnV}Z=`&ekJj4Uvn*@&GW-2b z)6|T&ZQDGP?{ZHrS*v>uxi7yx&%<@ys#0nzuo}2mDYdqH@qJWjtv%pY;L=vBW!39- z8jS`?k{pn@ZU6_R{*N3WdkBai2+YTE96|g=;0mQwXu!{KLx}%Tnx^ll)oPT>WwWJB zk_9@%{;5PCXnwb3I}+{S73Q^CEkpxc*QK|&7vJ~g@qYtn3WyGXyS3I!h44zHVhHc) z=`rGdCF=1vinlr6$S#!o(u0I2NQvnMaaj*X2O z(F$?$TLubuh5OwG*x^_Qko*>cI>2AxXxq)s_zLq)=Exo*G2y5IFyo?MaMbNtGPWCl z{1(QFDh&({kPWg`6-AL2vB)K;I-841iBWg_w&?3@Q5#UH#zU$MkNUrZEust3B(~ut7$&E(ZWt*(pC#4w$iNAs#U4tkzcqF@37%6 z0G`L#@bEr$oG6v3l*^2ajNm#BJ9g|an`k*b)6^JIUvDqFcJ3goRT&u?#>S@n{4T7x zMVv%fvT>zGT5n?2>S94>63URYeT(4u*S~$%HZrH?Wwa{UcB#KDG z5L*C6EvhJ{d-`yeUB;Z{m!c}omfpK_8)-A9KggItb}OQ`dl%83DRi7-HyhZEIy#J? zyceof9800=Ws=Eh)KnR}RYjGnB&EHiwYuS$6-78Y!HpA~G)1MFM3g1%*hX6@^6P~= zFIY^+Cfdr_zF%`acF5jWtl-ESZ$wQ`?9^NT`C1B3uBEuQWVmCsLUgl+YPLwj2pySI z8;N3M!_G$*lt4o^=zb<+yQ8Cy+I5KNCblE;?lGNvvToB1y zMrrJN=DzHm*fy!}qI{1i@NuR~_zm$?F9D!DzEy8Le=);r*D}n~Ws@Y<&tZPZY@ z4M4~ga$^M@l@&#@Y(*v&zB?quG)_DT{rcD83=EO3eTv}WhjC}*By5nCrd>sP^Boi} zUksm`=u|TvOR6xDVNc; zDpsXRIyFT)xrccC4^-SDJ!ig$+}I!ri`s?_r01T8zhMLZ3^QI9z(~D@#fv3Sdh3Y< z-&}=%_+g~VX6l~1aX$75f@VmlQ!)?z0RNn`+jQOhIqZ*LjW70MLy~IBZ+Z*;tM131 za~Sc?om9X471EzPia%9CSN4*WrfG!{)?b}TZsn)RobX1JcBp)36^-jZOYgdM#%5e& zXZqzOFdKjnRkIyx*IYsWCvPP6T&yH!?af189ACWopT?RmPhl&i4U{sH&#UlZ95&WAsQ|Iv@&4UQ3)r>Wg`JNm9W zDNL5YR#>URF^A|O0uURBEU0F4ShwGbclOy(X^^b>HQ^Ok<8I!B`{PH*E_$c2m)f=0 zVqbTiIU~g0#ePDii3>fb3(mt@d@=6nC*t^dl6sZ!@h3>rgzRZ2p_~lW$A3Y%@+Ptm z{eX=4H)WYqTy$U76P~J4uqJ5VYr%XM#3U0Zb%#&;I!&ckl zS>mlNT*;FUbPjZTq1h@FTTom0deY+;LSKL`)ld)q2SKxG^fI)4oJUvVoqigU-&e1^ zlHk@`P!`yRy0+Vmlp@q3i9F%i7m)j>tMLBpIAUV-#1u|{KgwyxOi8s&{M#pJtoknY z&whqIK8{r{lZiD}3Kf6WLH4o$#EmV!5cl8i#mh-z)1-3kwKzB3K`w3*W&)hmtMN}g z#RPh_%P%K;*IkCQmiQ4lJAk_55S@N1>e8j;&pRJwTckV2seOADtsnju?w`K_7M(%v z*w-4BimP>;N{Qxge~VuA@A$v|6@eJ~p#l&dmMulNd z>}rJbzmJkV@y(=h#PkRL0rwl^oS>rcf*r^!CwQFpuDlTgV-CJUU6JJp5y=#P30W4!e#Zk$G+-)^DDmZ`UM}vxzI+&y5}kfW$t{v)_aBtyTEB{Jzjwq|phJBgCy~oP>R8 zXVeI(Z+aHB`B{>yK8g4ATI#2sjC#vkY55-MP%o+$V*T`g(2bCx?|&D&Z-C}APce1+ znPe_rLU7q7xIsVFhrWlibSe2rf<>uH#Ad&P0uU7v)v2Ae2!G`*=r|_cJwZ6R8*6f! zbjLRIU%ZLpx#yTuQfZqsY2o?-QJgS!^;LLZ`6Ahhgno(1-fB2_-?$h5-Di=MN>mpu z!d8mRggwa{8v=Zd>6q~iTa8a6uUQWpSXt1m+zsFyl@h~D4d>c1ENC42Htx<_R5v0LeXSLmSjnu zi*kc@0#ll%blGyeZ#{rihO1|tMe&>WV;6cM3EBIxkK^8P7p;>|CjaekX(rWSiy7aOWO| zs@I9uuO<5agQ$5&ppH8Zs%7j=+weASAod+vUVz^cR}_+oLfn}|xjVt1-E}bMfC1pa zmCI%USht4JMiWb192TX8$Dv+#0#;bYsaCOS4dO-BW3YW}T$pa7sEW zCf{2tf z>ILkQF3%OqSa98qT`IIs0*MKQg`ijak`)sLwpm)4gD({!Nd?1%-*VT~IZOl~(lm2s zDi?CUVYy6_Vt?$TEVyCi{x%>dvC{>hyFB|+kY1`V)nPzlN{sC?Ozc>+9FN#`jk_k3 z!!XWBtU{hBpCjoj;uMN_1O3ob!09QVdh#?ahh5KY!GFUWn0xBmQPbmF^`>(c zGx*dK3`kXO27RYiBSbi|ku*9y2Yi>rchFvd<7U9~NPUl1CQIzwsEm)DDd6O?q&<1# zg41k{G+%^5599)zfj)}!4x`qLOrDT>9*OM`+X7VV#4&&+uNa*>7e|^LoyuGov2p!+ zh6V?iGctzSGrmPXd&zS8q6R~+c?~L`NB88gdJ3eu0(MZq&IZIm2FnjnxeQ6*V`Xxv zAcvLpaWWo`7oa817gvbgwu!V;SP{pNnw-Hlf>1a@a4dn`OcQZG@roT`njzh7F6lpP z*s#IWY^C|3O0}Ba@ta=}IX=fMTxk5g<9M{fn5nXqQl+duQ$3UXQZ&@Wdy;I67Psr2 zraGtPUOS=`qtnJhC6j9Fw{TT!3*XPs-`7jIR5E>t{YrQ(?7yJNoFO&BV?n%|2!cOG!MmyU#iX8>C$&p zNpyen=FNt$!u@PED+amHD7V)7?j%W;$^hw?NyARi3z%*W9Ez&E+}Q4p{J|L0X{`!f zGFEiIzrSCWZ#TMU0GL+|!Z3Wtwr$&xmaAOCB@yMTr9Ows0FjW`pJ;(}HN{3nefE3R zpbNyUNs?STH8o`kV)411e(=ngeKZ`5a|a`~+fj6XpFK}Jkh~)y&BlB_f8durx+isB zw~3-?QLR=xyi_XX#n5KIh;c9i|J3IqeR=JnSS(gEnaozD)B#_&`F{&!ko7O%9XtR4 N002ovPDHLkV1lE7;{E^t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bdcef0537e04fa57bd861f32514739561956b9d9 GIT binary patch literal 2126 zcmV-U2(kBxP)HWU58gC@gf3>>(qs(e-E&FmgX z*30gV|L;k_8y|q(a{`_d@WvTv)641gw=K{QyZ1l+e2a{$1pHY61RJRRtzh3;ui(?}^us;(47=0rgWM6f<+DkE z;&&Bba;*KeK(S-9hUmX!l?&|lyI*#N*Zs?n$4|fR*FXpR-TwF7^|#aML-zA^lYn0Q zzJdw*_m3Z^paLlr!yUS`hS(TQe*EvjDEzehus`kpas2q}&oIxU$DxD6r%yl1n{G^l z%?eft7?+-n6)u>dXVJ90>|R&L(ZzGbNiqm}goOBJ`uO2a~b_frlbn9CwP% zj*dJH&^$7B6Dof`)44Va0!lVME)`tjU_TV~HSa82iAu!2Os0=B&}DMQd9qm9)bvF< zil}o6Pl0lB=ZIB$#m6T_k{xRr_(nJ06*<(pAfS)w2wtp7-d-|-C5aSNaV|dWV2oiU zXXQ61?!u`ZbWh@NsBSywPQ`hq<-|o?dV2^ly7FP?jm?P|47j z=<-uV4x6)-9Y`#g`zhpXe%HcYQM%5-eTqul=tE3zB>OEa2w*5!5kD@{0Q}>E+Ii+}cMeqJjOw!nz)&&6x=5o%?V4oMG=O{9Ai6Ftg zLkwVR?Ye7w_J~eVDLln#M*NYJM$mU+*i7&!PlwN&1px^*nQ(*>tc{gROErCxtRfTu zVj^uas?AnMO0^5o_TUa(?ZqJ!Nx9GCO3!`{xlFQ}o@rm33j!_-OX-qYrjJfbW{mrp zl(8=hI!VyiaZyC<=wMzXSukuXQALBoM*K36ldcb9>zq%YipZ>^rgN^&^GUrRpeL0J zIdw>;pZi`C(OZx8{+LkzK5r{#k6|wkum?p$3QyOOjUZ4c@MjC`O&{4nkt*KUpT8~$ zNMN0$pBs}wh?0Or$sWaY*A>SUq1$lbQ^g$W1myBt ztD@kRUDrQq|9?$DFYH_G2Lax;UOLb(+LCvhFWtLq3tt-ZOZ)}_<5yYz3Gp{`{OvqP z>hiPbt1O6h*CxKoE%yZ(1awn>J4b?D*!FZ=V78@;oK1(f^&DNF{g(e+&n~j8WLsDe zz-j)woh&dF{ywk4C4AR(ktI%6zbOv&r?(@ztR9v4_X&-jEPAUSLwxAvM7D;JE2BAX zO~GXE$4rAau!7Rj*yU`EEURCYlI@6YT0NfU!#p>BWYqoyPCwe34;_sATfTlftWE%9 zj9IV2kChI#`H>{JGaw}$cJ3I-OZE(ebiD#|9tG4v=V}?BZf`mX5drpd^n5jJNdSi8 zC8_LJRwQ+-nA{YNqTG2#Cyw(}VCkb9q)`^gMvU5fslv!<<-c9vH+1L!zGyri{5mi+ z1?-BoeI+$UF6)}j4VmdMHG!^l7fCxKA{};?yp_G_n(`ooYF=y{VbqmeYvsF~fU+C` zYrXYJ(phiWujv(L%sY}eS$qZ&RaDM-47>on^vLDWN52zBKPQ`Pl>myt=-6z3x%dJr z|Hs3&_g|W+FvSIbsdmQMR_6j|;1{Bt*&-vvkeO!dgo@wuWxq)PHt!)}3jUQh7v0~V z{xFr$JM2obf8}cspx9&m6Bl%+E>DauV}F7Aw?45GXVNt}{e|v)?_^u& zp)a3FR(pOhbY(+lPqv!V*@q3=H5}^q#~?tEw~8kLZ?*HO`#Aw&;;pXF33#iWPu + + \ 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 0000000000000000000000000000000000000000..b5381c234b69b34b29be4df7e35c24cb42cfc49c GIT binary patch literal 1373 zcmV-j1)}Px)7fD1xRA@u(ntxDKRTRg+=Pkh~7_KE0l$7yn1p6V4$~HC-YU-qojifk@Xr&J5 zh=4?-6-X1>B&bQIHd$jcP1XQb#AJnpVpD?(g-i)#nKc73;tzha`)=L6i@VD%%RXj@ z-8a+wYxmuE?>V1y&pq$lb01;ImHa-7I^BZj+dDB|Hn1rS%x~y3WBH#zosxzw-R_6u zy8H0%2dnA4rFNfgMp=hF=;H7bXpdXtkKZ^v}ci#8%_PZpAmp7Uv!97 zFvcEpdN4OWNJN$CYiS{eFal^Z*gk_^-rz=^6pPk)NyQz7w2`>am!4xkd92$EhejkX zeTeR-u$Ng;>os|^ViR~khd_U6H)+$^d4oZo<^lRBRzahY8p(f?e8b(LQnBWy4m`xt zt(y!?=>TyNghkPWWAzL*&5FA#73c7I#Q=p*OL*=P;7S(|mCMlGuOfhzvk3cFig(}J zRX|Ru>{4rvW-vuFP`gz^Sbz|dP{dICyL+u~d=opn1|GPJP@Q!%^T22Ifb2kjy$m$B zF%+KwuJh}c;Y6kYINbs)+&lbqTe{E_ z`+)MF?E}mS1FE;U5Wv@m(3nS)x99>$cTpu+k^Hl|^8RV?AH70PoMOOyxm8FnSo%~WDc0gi2u5ErgI zg|O_Q+sTmsq?-UHPat?}>;vR2Atc6#ws*b*_Ey?YPup$)AFp#0fFs+NrAmm85Kkl@ zW+D_zzQ9tGg-MQF?=Or9VB_bjt~IIC%wkVmbpiP6DzN4zGytu%eU-8J5U(Vknwg6SXIxe0$RTsK@5}_=G z5NfK*4H$ss2N}No#s1)aK7=1~2pS29e8qN?Gd)EXG9I8KJQvD&H>W^9Zf>8BAHwRGtrv`%%s{Po* fGOnuabDMtw%cH}IdV|a800000NkvXXu0mjfN4I(v literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..64432a232a96a7e5b502a90b3803499828bb4367 GIT binary patch literal 1913 zcmV-<2Zs2GP)Px+Gf6~2RA@uxn`vwmWf;f*&&+Jw-P#BW7Ex-!KnO(Cc<{k!G#&&Efe&irh=$?^ zG$I&d(5N4cSBxf@_=T!bNtDBYs8KwE(L_Kaf+gIFR~zeDx9uJ?^Seakh8>7axS^{-5}fuP#dId$+a0($3$^J{sf7Sq#4=R(nyEG z{*gX7BxOT;!hH@T?}?aXnx+9F0%a^l6;{R>DFG=ZTvxz$r3*6)5Lv^4dz(n%JVsucHz2su{ma~ zAvIIiOzi_~2XI`_1yg|b z){6c%SJ%Om6Xy_>&Cy87i{-(?kdHoH(r!othEb~#Y`KW5RbS6v#I}M0IYuHULr5ahlNv+W1&WyAcK~qsX{i zSX3D;y3je3L2o`$$_%FgP*=-vQSmD&7aC~;vWSA*%c2p`)t7v_ye3qo8GrLDyX|1^^57LBTB|q@ra4XEb_X6Jo%$sh|fsP|okT(-5Ks z6wK>`Y?A~G3B(wz$phmx+6-EC2he(UN%h#EGQgj^fz3P1x^Y1x)ecBKe#vhS0gm(@ zR5l_$58VY?cvZzNjHrTg?;gnQJ3Kh8 zTv$a`kIy?ItH#iZTR-1G)y(kTv@6QXp(e9$B1Rm?&oFft1E_CUVY z2^mj>lq(XVtfB;H-Yn3w>wt(gEcHf4!Tx_CH*Ey|{0A}|j-_(&yeTkVTo1G~4;!xj zfOZ;k+pEB@-I<~yS)F1z$RQeQ25op4G<9-SaPaPDmDhGxwKtzQlBuZ9{gwd;e)IOLaa0`a&Xl5SRU$x<*>WN7j^pvTq% zv1pCayxY|V&`AUPw?h8an*+s=*+nLJL=!+`48|jCfk~xj$A(K^Js?jn9DWbzJ>}(C zs3imiCZ|ns4#EW8zZ%ruHeBD!x~_V_Q=b7nU0&%A6%>_oe9YmP(+A0xz>4LdOD-zQ z$!d(M1MJxk{IW;=a8>FlC>@fy$3Y*Y1q*>|uc^lGvacOJ;NK^K6T7@Wf89>upES zb>v+1^*6&XB$^u1Fd)1s6@Il~1k)-!y`s;Fi4!3#L;brj2*Lzh-iD6Vd3EgD@V$87 z49?#=o3OCGyaB2RF#cB>n>P&LkKW)_8_z+rqmBuf{91v5Ov-Y~im#k^eQ<5Z#e~~t))?^kH-mV))0^sIl^m1cSb!7Zt-o&(VuJ!rNf|h&oz!8b z!m>QjJOzD4>!RSNTN2o}+f||F0r{)1o<5E^0+W7shXzz|0btQkRX{qtgKO#n_)h0P z__izJsWt>0pEq5JX+PVUkjf6#Q0V3;nGYJ|U~X8~1@MC%34FdsdZDSpQiFzp3DJZ= z+mH5;3JzUtG>{&I{34?Vp)P=jzZ*d3RztyYVX;Gbrxf)=b!xYRNxwV!Rn)JLx`22^ zqgXY~2h}-Q7eFp9FT5Pb(LWmkK!(aFO0XP>iw-2!>xb;pAL@C2XnLTPw2L4Y>jv1@ zm&Wtk9eh8)xtJZQmIKV`N}?$#MqwW;_d_6l@#DDIb + + + + + + + + + + + + + + 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 $$;