feat: 接入管理端跨站单点登录

This commit is contained in:
sunlei 2026-07-16 10:53:51 +08:00
parent 86b6cbdae4
commit 76b9513d80
6 changed files with 175 additions and 4 deletions

View File

@ -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 加载。

View File

@ -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',
);
};
}

View File

@ -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)
})
})

View File

@ -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();

View File

@ -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
}
}

View File

@ -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, '');