diff --git a/README.md b/README.md index 713715c..28cef46 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ pnpm exec playwright test e2e/argon-parity/baseline.spec.ts --project=chromium - `hljs-codeblock` 静态快照需要恢复 Argon `highlightjs-line-numbers` 运行时生成的 `data-line-number` 与 `.hljs-ln-n::before`,否则行号列会坍塌成 0px。 - 左栏 overview sticky/relative 切换用 no-headroom 回归用例固定,解除 fixed 后不得重放卡片入场动画或产生缩放闪烁。 - 左栏文章目录/站点概览切换必须保留 Bootstrap tab fade 节奏,采用 active/show 分帧保持 Argon 手感;回归用例需要断言切换中 opacity 处于 0 到 1 之间,而不是只检查最终显隐。 +- 左栏“管理”始终使用跨站 KT Admin SSO:生产默认跳到 `https://admin.kwitsukasa.top/#/auth/login?sso=1&redirect=%2Fblog%2Farticle`,本地默认使用 `http://localhost:5999/`,可通过 `VITE_KT_ADMIN_BASE_URL` 覆盖 Admin 基址。旧主题接口返回的 `/wp-admin/` 会在最终侧栏菜单中迁移到该入口;Blog 不跨域读取 Cookie,也不在 URL 中传递 token。 - Argon motion、滚动几何、延迟、RAF 调度、DOM id/selector、hash anchor 和跨组件 ref 必须从 `src/factories/blogAnimationFactory.ts` 与 `src/factories/blogDomFactory.ts` 取值;组件和 Hook 不再散写行为层 id、裸 `requestAnimationFrame`、裸 `setTimeout` 或重复 timing。 - Live2D 使用旧 WordPress 站点同款 Cubism2 MOC 资源,`BlogLive2D` 与 `src/components/blog/live2d/runtime/*` 负责 canvas、Pio/Tia catalog、MOC metadata、选择缓存、WebGL 渲染、真实 MTN 动作队列、全页面有界视线、canvas-local 触摸和模型/服装直达切换。点击命中保留原 `live2d.min.js` 的 canvas-local width 分母坐标;桌面鼠标视线以模型中心为零点,按中心到对应 viewport 边缘的距离连续归一化,使 canvas 外近/中/远距离仍可区分。运行时保持源码的 `loadParam -> motion/eye blink -> saveParam -> pointer/sine/breath -> update` 参数生命周期,并直接导入类型化、语义化的 TS Core;不包含 compatibility/global installer,也不得 append 原始 `live2d.min.js`。 - Cubism2 反混淆只以 `public/live2d/wordpress-moc/live2d.min.js` 为源码证据,并严格按“函数名恢复 -> 调用逻辑理清 -> 拆分压缩耦合 -> 变量名恢复 -> runtime 全覆盖”推进;生产运行时不得加载该 min.js。权威进度维护在 `docs/blog-live2d-cubism2-minjs-deobfuscation-ledger.md`,稳定决策位于 `docs/live2d-deobfuscation/`。当前主线已闭合:627/627 函数、1411/1411 调用、567 owned/60 omitted、578 参数/1169 局部/53 最终标识符;runtime 分类为 546 exact、14 intentional bug fix、7 defensive extension、60 omitted-unreachable。生产 Core 严格等于 38 个 owner module + 5 个 sealed support module、0 orphan,并直接由 `runtimeCore.ts`/renderer 消费;禁止重新引入 compatibility、legacy globals、alias、映射层或原始 min.js 加载。 diff --git a/e2e/argon-parity/interactions.spec.ts b/e2e/argon-parity/interactions.spec.ts index 95a4928..f501cfd 100644 --- a/e2e/argon-parity/interactions.spec.ts +++ b/e2e/argon-parity/interactions.spec.ts @@ -1064,7 +1064,10 @@ function createSidebarMenuSurfaceTest() { await expect(homeIcon).toHaveCSS('display', 'inline-block'); await expect(homeIcon).toHaveCSS('width', '15px'); await expect(homeIcon).toHaveCSS('margin-right', '8px'); - await expect(adminLink).toHaveAttribute('href', 'http://blog.kwitsukasa.top/wp-admin/'); + await expect(adminLink).toHaveAttribute( + 'href', + 'http://localhost:5999/#/auth/login?sso=1&redirect=%2Fblog%2Farticle', + ); }; } diff --git a/src/__tests__/BlogAdminSso.spec.ts b/src/__tests__/BlogAdminSso.spec.ts new file mode 100644 index 0000000..c622565 --- /dev/null +++ b/src/__tests__/BlogAdminSso.spec.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' + +import { + BLOG_ADMIN_MANAGEMENT_PATH, + buildBlogAdminSsoUrl, + isLegacyWordpressAdminHref, + resolveBlogAdminBaseUrl, +} from '@/factories/blogAdminSsoFactory' + +describe('Blog Admin SSO URL factory', () => { + it('builds a cross-site Admin login bootstrap without exposing a token', () => { + const href = buildBlogAdminSsoUrl('https://admin.kwitsukasa.top/') + const url = new URL(href) + const hashUrl = new URL(url.hash.slice(1), url.origin) + + expect(url.origin).toBe('https://admin.kwitsukasa.top') + expect(hashUrl.pathname).toBe('/auth/login') + expect(hashUrl.searchParams.get('sso')).toBe('1') + expect(hashUrl.searchParams.get('redirect')).toBe(BLOG_ADMIN_MANAGEMENT_PATH) + expect(href).not.toMatch(/token|credential/i) + }) + + it('uses the configured Admin origin and safe local/production fallbacks', () => { + expect( + resolveBlogAdminBaseUrl({ + PROD: true, + VITE_KT_ADMIN_BASE_URL: 'https://admin.example.com/base/', + }), + ).toBe('https://admin.example.com/base/') + expect(resolveBlogAdminBaseUrl({ PROD: true })).toBe('https://admin.kwitsukasa.top/') + expect(resolveBlogAdminBaseUrl({ PROD: false })).toBe('http://localhost:5999/') + }) + + it('identifies only legacy WordPress management destinations', () => { + expect(isLegacyWordpressAdminHref('http://blog.kwitsukasa.top/wp-admin/')).toBe(true) + expect(isLegacyWordpressAdminHref('/wp-admin')).toBe(true) + expect(isLegacyWordpressAdminHref('https://blog.kwitsukasa.top/post/wp-admin-migration')).toBe( + false, + ) + expect(isLegacyWordpressAdminHref('https://example.com/manage')).toBe(false) + }) +}) diff --git a/src/__tests__/useBlogTheme.spec.ts b/src/__tests__/useBlogTheme.spec.ts index 4173936..05a1b1b 100644 --- a/src/__tests__/useBlogTheme.spec.ts +++ b/src/__tests__/useBlogTheme.spec.ts @@ -1,6 +1,7 @@ import { nextTick } from 'vue'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { buildBlogAdminSsoUrl } from '@/factories/blogAdminSsoFactory'; import { useBlogTheme } from '@/hooks/useBlogTheme'; vi.mock('antdv-next', () => ({ @@ -120,7 +121,7 @@ describe('useBlogTheme', () => { }, { external: true, - href: 'http://blog.kwitsukasa.top/wp-admin/', + href: buildBlogAdminSsoUrl(), icon: 'fa-user', label: '管理', }, @@ -197,6 +198,45 @@ describe('useBlogTheme', () => { expect(siteConfig.value.headerMenuVisible).toBe(true); }); + it('migrates same-site WordPress management without changing unrelated sidebar links', async () => { + const { applyWordpressThemeConfig, siteConfig } = useBlogTheme(); + + applyWordpressThemeConfig({ + sidebarMenu: [ + { + href: 'https://blog.kwitsukasa.top/wp-admin/', + icon: 'fa-user', + label: '管理', + }, + { + external: true, + href: 'https://github.com/KwiTsukasa', + icon: 'fa-github', + label: 'GitHub', + }, + ], + site: { + home: 'https://blog.kwitsukasa.top', + }, + }); + await nextTick(); + + expect(siteConfig.value.sidebarMenu).toEqual([ + { + external: true, + href: buildBlogAdminSsoUrl(), + icon: 'fa-user', + label: '管理', + }, + { + external: true, + href: 'https://github.com/KwiTsukasa', + icon: 'fa-github', + label: 'GitHub', + }, + ]); + }); + it('maps legacy Argon placeholder image config back to previous blog static assets', async () => { const { applyWordpressThemeConfig, siteConfig } = useBlogTheme(); diff --git a/src/factories/blogAdminSsoFactory.ts b/src/factories/blogAdminSsoFactory.ts new file mode 100644 index 0000000..fb9b3a3 --- /dev/null +++ b/src/factories/blogAdminSsoFactory.ts @@ -0,0 +1,60 @@ +interface BlogAdminEnvironment { + PROD?: boolean + VITE_KT_ADMIN_BASE_URL?: string +} + +const LOCAL_ADMIN_BASE_URL = 'http://localhost:5999/' +const PRODUCTION_ADMIN_BASE_URL = 'https://admin.kwitsukasa.top/' +const ADMIN_SSO_LOGIN_PATH = '/auth/login' + +export const BLOG_ADMIN_MANAGEMENT_PATH = '/blog/article' + +/** + * Resolves the KT Admin origin used by the Blog management entry. + * @param env Vite environment values; a configured HTTP(S) URL wins over the local/production default. + * @returns Absolute KT Admin base URL. + */ +export function resolveBlogAdminBaseUrl(env: BlogAdminEnvironment = import.meta.env) { + const configured = env.VITE_KT_ADMIN_BASE_URL?.trim() + const fallback = env.PROD ? PRODUCTION_ADMIN_BASE_URL : LOCAL_ADMIN_BASE_URL + + if (!configured) return fallback + + try { + const url = new URL(configured) + return /^https?:$/.test(url.protocol) ? url.toString() : fallback + } catch { + return fallback + } +} + +/** + * Builds the cross-site Admin SSO bootstrap URL for Blog article management. + * @param adminBaseUrl Absolute KT Admin base URL; defaults to the Vite environment contract. + * @returns Token-free top-level navigation URL targeting the Admin SSO bootstrap. + */ +export function buildBlogAdminSsoUrl(adminBaseUrl = resolveBlogAdminBaseUrl()) { + const url = new URL(adminBaseUrl) + const query = new URLSearchParams({ + sso: '1', + redirect: BLOG_ADMIN_MANAGEMENT_PATH, + }) + + url.search = '' + url.hash = `${ADMIN_SSO_LOGIN_PATH}?${query.toString()}` + return url.toString() +} + +/** + * Detects the legacy WordPress dashboard destination that remote Argon theme data may still expose. + * @param href Normalized sidebar destination, absolute or site-relative. + * @returns Whether the destination is the WordPress administration root or one of its descendants. + */ +export function isLegacyWordpressAdminHref(href: string) { + try { + const pathname = new URL(href, 'https://blog.invalid/').pathname.replace(/\/+$/g, '') + return pathname === '/wp-admin' || pathname.startsWith('/wp-admin/') + } catch { + return false + } +} diff --git a/src/hooks/useBlogTheme.ts b/src/hooks/useBlogTheme.ts index 1d3cba3..aad0229 100644 --- a/src/hooks/useBlogTheme.ts +++ b/src/hooks/useBlogTheme.ts @@ -10,6 +10,10 @@ import { unwrapBlogCssImage, } from '@/data/blogStaticAssets'; import { createBlogMotionCssVariables } from '@/factories/blogAnimationFactory'; +import { + buildBlogAdminSsoUrl, + isLegacyWordpressAdminHref, +} from '@/factories/blogAdminSsoFactory'; import { BLOG_META_NAMES, blogDomId, blogDomSelector, blogMetaSelector } from '@/factories/blogDomFactory'; export type BlogThemeMode = 'dark' | 'light'; @@ -122,7 +126,7 @@ const ARGON_DEFAULT_AUTHOR_AVATAR = PREVIOUS_BLOG_AUTHOR_AVATAR; const defaultHeaderMenu: BlogThemeMenuItem[] = []; const defaultSidebarMenu: BlogThemeMenuItem[] = [ { href: '/', icon: 'fa-home', label: '首页' }, - { external: true, href: 'http://blog.kwitsukasa.top/wp-admin/', icon: 'fa-user', label: '管理' }, + { external: true, href: buildBlogAdminSsoUrl(), icon: 'fa-user', label: '管理' }, ]; const defaultPreferences: BlogThemePreferences = { @@ -336,7 +340,7 @@ function applyWordpressThemeConfig(config: WordpressArgonThemeConfig) { runtimeConfig.siteTitle = config.site?.title || runtimeConfig.siteTitle; runtimeConfig.siteUrl = config.site?.url || runtimeConfig.siteUrl; if (Array.isArray(config.sidebarMenu)) { - runtimeConfig.sidebarMenu = normalizeMenuItems( + runtimeConfig.sidebarMenu = normalizeSidebarMenuItems( config.sidebarMenu, config.site?.home || config.site?.url || runtimeConfig.siteHome || runtimeConfig.siteUrl, ); @@ -1014,6 +1018,27 @@ function normalizeMenuItems(items: BlogThemeMenuItem[], siteHome = ''): BlogThem }, []); } +/** + * Normalizes sidebar items and migrates the legacy WordPress dashboard entry to KT Admin SSO. + * @param items Raw sidebar items supplied by the Blog theme API. + * @param siteHome Public Blog home URL used to collapse same-site links into RouterLink paths. + * @returns Sidebar items with only the legacy management destination replaced by an external SSO URL. + */ +function normalizeSidebarMenuItems( + items: BlogThemeMenuItem[], + siteHome = '', +): BlogThemeMenuItem[] { + return normalizeMenuItems(items, siteHome).map((item) => + isLegacyWordpressAdminHref(item.href) + ? { + ...item, + external: true, + href: buildBlogAdminSsoUrl(), + } + : item, + ); +} + function normalizeMenuHref(href: string, siteHome = '') { if (!href) return ''; const normalizedSiteHome = siteHome.replace(/\/+$/g, '');