Remove the "AI 连接配置" page under 扩展&增强 together with the theme's own OpenAI-compatible provider path. AI generation (SEO, summary, tags) now runs solely on the WordPress AI Client (wp_ai_client_prompt), with connections configured through WordPress AI Connectors. - AI.php keeps only the WP AI Client path: no provider config storage, no theme fallback, no rate limiting - Drop /ai/status, /ai/settings and legacy /ai/seo-suggest routes plus their handlers - Remove dead compat code: AISEO::suggest, AIPrompt::build_seo_prompt and the front-submit AI assist flow (its buttons were never rendered and its endpoints were never registered) - Update SEO schema/meta-box guidance that pointed at the deleted page - Add ai-config-removal contract test and refresh the toast and user-features contracts affected by the cleanup
228 lines
11 KiB
JavaScript
228 lines
11 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
|
|
const root = path.resolve(__dirname, '..', '..');
|
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
|
const exists = (file) => fs.existsSync(path.join(root, file));
|
|
|
|
const expectedFields = [
|
|
'avatar_upload_s',
|
|
'avatar_external',
|
|
'avatar_default_img',
|
|
'user_cover_upload_s',
|
|
'user_cover_img',
|
|
'user_desc_std',
|
|
// 打赏组(参考主题 8977 行:post_rewards_* 迁移至用户功能页)。
|
|
'post_rewards_s',
|
|
'post_rewards_text',
|
|
'post_rewards_amounts',
|
|
'post_rewards_custom',
|
|
'post_rewards_min',
|
|
'post_rewards_max',
|
|
];
|
|
|
|
const externalSources = ['', 'gravatar', 'weavatar', 'cravatar', 'geekzu', 'loli', 'sep', 'v2ex'];
|
|
|
|
test('user features schemas expose the canonical fields plus the migrated rewards group', () => {
|
|
const js = read('assets/admin/src/schema/sections/users/features.js');
|
|
const phpAll = read('src/Admin/Schema/SchemaUsers.php');
|
|
const php = phpAll.slice(phpAll.indexOf("'features' => ["), phpAll.indexOf("'verify' => ["));
|
|
|
|
const jsFields = [...js.matchAll(/\{\s*key:\s*'([^']+)',\s*label:/g)].map((match) => match[1]);
|
|
const phpFields = [...php.matchAll(/\['key'\s*=>\s*'([^']+)',\s*'label'/g)].map((match) => match[1]);
|
|
assert.deepEqual(jsFields, expectedFields);
|
|
assert.deepEqual(phpFields, expectedFields);
|
|
|
|
assert.match(js, /avatar_upload_s:\s*true/);
|
|
assert.match(js, /avatar_external:\s*''/);
|
|
assert.match(js, /assets\/img\/avatar-default\.png/);
|
|
assert.match(js, /user_cover_upload_s:\s*true/);
|
|
assert.match(js, /assets\/img\/user_t\.jpg/);
|
|
assert.match(js, /user_desc_std:\s*'这家伙很懒,什么都没有写\.\.\.'/);
|
|
assert.match(php, /avatar_default_img[^\n]+get_theme_file_uri\('assets\/img\/avatar-default\.png'\)/);
|
|
assert.match(php, /user_cover_img[^\n]+get_theme_file_uri\('assets\/img\/user_t\.jpg'\)/);
|
|
// 打赏组:开关默认开(对齐参考主题),依赖收敛于 post_rewards_s。
|
|
assert.match(js, /post_rewards_s:\s*true/);
|
|
assert.match(js, /post_rewards_text:\s*'赞赏'/);
|
|
assert.match(php, /\['key' => 'rewards', 'title' => '打赏', 'fields' => \[/);
|
|
assert.match(php, /'post_rewards_s', 'label' => '用户打赏功能', 'type' => 'switcher', 'default' => true/);
|
|
|
|
const jsSources = [...js.matchAll(/\{ label: '[^']+', value: '([^']*)' \}/g)].map((match) => match[1]);
|
|
const phpSources = [...php.matchAll(/\['label' => '[^']+', 'value' => '([^']*)'\]/g)].map((match) => match[1]);
|
|
assert.deepEqual(jsSources, externalSources);
|
|
assert.deepEqual(phpSources, externalSources);
|
|
});
|
|
|
|
test('avatar, cover, signature, and upload runtime use the canonical settings', () => {
|
|
const helpers = read('inc/helpers/aksrt-avatar.php');
|
|
const runtime = read('src/Settings/UserRuntime.php');
|
|
const upload = read('src/Admin/UserControllerProfileMediaOperations.php');
|
|
const center = read('inc/helpers/aksrt-user-center.php');
|
|
|
|
const customAt = helpers.indexOf("$custom = aksrt_get_custom_avatar_url");
|
|
const externalAt = helpers.indexOf("$external = aksrt_get_external_avatar_url", customAt);
|
|
const defaultAt = helpers.indexOf('return aksrt_get_default_avatar_url();', externalAt);
|
|
assert.ok(customAt >= 0 && customAt < externalAt && externalAt < defaultAt);
|
|
|
|
const hosts = {
|
|
gravatar: 'https://www.gravatar.com/avatar/',
|
|
weavatar: 'https://weavatar.com/avatar/',
|
|
cravatar: 'https://cn.cravatar.com/avatar/',
|
|
geekzu: 'https://sdn.geekzu.org/avatar/',
|
|
loli: 'https://gravatar.loli.net/avatar/',
|
|
sep: 'https://cdn.sep.cc/avatar/',
|
|
v2ex: 'https://cdn.v2ex.com/gravatar/',
|
|
};
|
|
for (const [source, host] of Object.entries(hosts)) {
|
|
assert.match(helpers, new RegExp(`'${source}'\\s*=>\\s*'${host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}'`));
|
|
}
|
|
assert.match(helpers, /hash\('sha256', strtolower\(trim\(\$email\)\)\)/);
|
|
assert.match(helpers, /'s'\s*=>\s*\$size/);
|
|
assert.match(helpers, /'d'\s*=>\s*aksrt_get_default_avatar_url\(\)/);
|
|
|
|
assert.match(helpers, /get_user_meta\(\$user_id, 'aksrt_background_image', true\)/);
|
|
assert.match(helpers, /return \(string\) \$settings\['user_cover_img'\]/);
|
|
assert.match(helpers, /get_user_meta\(\$user_id, 'description', true\)/);
|
|
assert.match(helpers, /return \(string\) \$settings\['user_desc_std'\]/);
|
|
assert.match(helpers, /!empty\(\$settings\['avatar_upload_s'\]\)/);
|
|
assert.match(helpers, /!empty\(\$settings\['user_cover_upload_s'\]\)/);
|
|
|
|
assert.match(runtime, /Settings::read_domain\('users', 'features'\)/);
|
|
assert.doesNotMatch(runtime, /user_center_tabs/);
|
|
assert.match(runtime, /'tabs'\s*=>\s*\['profile', 'level', 'comments', 'credits', 'revenue', 'favorites', 'auth'\]/);
|
|
assert.match(upload, /aksrt_is_avatar_upload_enabled\(\)/);
|
|
assert.match(upload, /aksrt_is_user_cover_upload_enabled\(\)/);
|
|
assert.match(center, /'show_avatar_edit'\s*=>\s*\$allow_avatar_upload/);
|
|
assert.match(center, /'allow_bg_upload'\s*=>\s*\$allow_bg_upload/);
|
|
});
|
|
|
|
test('public user surfaces consume the canonical cover and signature helpers', () => {
|
|
for (const file of [
|
|
'templates/parts/user/author-hero.php',
|
|
'src/WidgetSystem/Renderers/UserRenderer.php',
|
|
'inc/helpers/aksrt-user-dashboard.php',
|
|
]) {
|
|
assert.match(read(file), /aksrt_get_user_cover_url/);
|
|
}
|
|
|
|
for (const file of [
|
|
'templates/parts/user/author-hero.php',
|
|
'src/WidgetSystem/Renderers/UserRenderer.php',
|
|
'inc/helpers/aksrt-graphic-card.php',
|
|
'inc/helpers/aksrt-single-render.php',
|
|
'templates/parts/single-author-bar.php',
|
|
'templates/shortcodes/profile.php',
|
|
'inc/helpers/header/aksrt-header-render.php',
|
|
]) {
|
|
assert.match(read(file), /aksrt_get_user_description/);
|
|
}
|
|
});
|
|
|
|
test('retired admin pages, schemas, APIs, and metadata chains are absent', () => {
|
|
const deletedFiles = [
|
|
'assets/admin/src/pages/users/UsersFeatures.vue',
|
|
'assets/admin/src/pages/users/UsersSignin.vue',
|
|
'assets/admin/src/schema/sections/users/signin.js',
|
|
'assets/admin/src/pages/tools/ToolsEmailRollback.vue',
|
|
'assets/admin/src/schema/sections/tools/email-rollback.js',
|
|
'assets/admin/src/schema/sections/tools/frontend-modal-rollout.js',
|
|
'src/Admin/Schema/SchemaToolsEmailRollback.php',
|
|
'src/Admin/Schema/SchemaToolsFrontendModalRollout.php',
|
|
'src/Auth/EmailRollback.php',
|
|
'src/Settings/FrontendModalRuntime.php',
|
|
'assets/admin/src/pages/extensions/ExtensionTools.vue',
|
|
];
|
|
for (const file of deletedFiles) {
|
|
assert.equal(exists(file), false, `${file} should be deleted`);
|
|
}
|
|
|
|
const sources = [
|
|
'assets/admin/src/constants/sections/users.js',
|
|
'assets/admin/src/constants/sections/tools.js',
|
|
'assets/admin/src/constants/sections/extensions.js',
|
|
'assets/admin/src/router/component-loaders/users.js',
|
|
'assets/admin/src/router/component-loaders/tools.js',
|
|
'assets/admin/src/api/modules/tools.js',
|
|
'assets/admin/src/schema/sections/tools/index.js',
|
|
'src/Admin/Schema/SchemaTools.php',
|
|
'src/Admin/ToolsController.php',
|
|
'src/Settings/Settings.php',
|
|
'src/Settings/Capabilities.php',
|
|
'src/Settings/DomainGroups.php',
|
|
'src/Core/ScriptsPart1c.php',
|
|
'functions.php',
|
|
].map(read).join('\n');
|
|
|
|
for (const retired of [
|
|
'users/signin',
|
|
'email-rollback',
|
|
'EmailRollback',
|
|
'aksrt_email_previous',
|
|
'aksrt_email_changed_at',
|
|
'frontend_modal_rollout',
|
|
'FrontendModalRuntime',
|
|
'isRolloutEnabled',
|
|
'extensions/tools',
|
|
'/tools/maintenance',
|
|
'get_system_tools_settings',
|
|
'upload_avatar',
|
|
'upload_profile_background',
|
|
]) {
|
|
assert.equal(sources.includes(retired), false, `${retired} should be absent`);
|
|
}
|
|
|
|
assert.match(read('assets/admin/src/pages/tools/ToolsUpdate.vue'), /#\/users\/credits/);
|
|
assert.doesNotMatch(read('assets/admin/src/pages/tools/ToolsUpdate.vue'), /#\/users\/signin/);
|
|
assert.match(read('assets/admin/src/api/modules/tools.js'), /\/settings\/health/);
|
|
assert.match(read('assets/admin/src/constants/sections/tools.js'), /route: '\/tools\/health'/);
|
|
});
|
|
|
|
test('native user profile omits ledger tables and renders canonical images eagerly', () => {
|
|
const credits = read('src/User/CreditsAdmin.php');
|
|
const balance = read('src/User/BalanceAdmin.php');
|
|
const avatar = read('inc/helpers/aksrt-avatar.php');
|
|
const profileAdmin = read('src/User/ProfileAdmin.php');
|
|
const level = read('src/User/LevelAdmin.php');
|
|
|
|
assert.doesNotMatch(credits, /LedgerQuery::get_instance\(\)->history/);
|
|
assert.doesNotMatch(credits, /近期%s流水|近期积分流水/);
|
|
assert.doesNotMatch(balance, /LedgerQuery::get_instance\(\)->history/);
|
|
assert.doesNotMatch(balance, /近期流水/);
|
|
assert.match(profileAdmin, /get_avatar\(\$user_id, 96, '', \$user->display_name, \['loading' => 'eager'\]\)/);
|
|
assert.doesNotMatch(avatar, /aksrt_get_avatar_html\(\$user->ID, 96/);
|
|
assert.match(level, /Level::get_level_icon_url\(\$level\)/);
|
|
assert.match(level, /<img class="aksrt-user-level-icon aksrt-user-level-icon--admin" src="/);
|
|
assert.doesNotMatch(level, /Level::get_badge_html/);
|
|
});
|
|
|
|
test('identity marker and profile are editable on the native profile while applications use the review queue', () => {
|
|
const auth = read('src/User/AuthApply.php');
|
|
const verify = read('src/Admin/Native/UsersMenusVerify.php');
|
|
const transitions = read('src/Identity/TransitionService.php');
|
|
|
|
assert.match(auth, /add_action\('edit_user_profile_update', \[\$this, 'save_admin_profile'\]\)/);
|
|
assert.match(auth, /current_user_can\('aksrt_review_identity'\)/);
|
|
assert.match(auth, /wp_nonce_field\('aksrt_auth_profile_' \.[\s\S]*?'aksrt_auth_profile_nonce'\)/);
|
|
assert.match(auth, /name="aksrt_auth_enabled"/);
|
|
assert.match(auth, /name="aksrt_auth_name"/);
|
|
assert.match(auth, /name="aksrt_auth_desc"/);
|
|
assert.match(auth, /name="aksrt_auth_time"/);
|
|
assert.match(auth, /update_user_meta\(\$user_id, 'auth', 1\)/);
|
|
assert.match(auth, /update_user_meta\(\$user_id, 'auth_info'/);
|
|
assert.doesNotMatch(auth, /name="aksrt_identity_(?:application_uuid|type|status|public_name)"/);
|
|
|
|
assert.match(verify, /'general',[\s\S]*?'manual'/);
|
|
assert.match(verify, /if \(\$status === 'manual_pending'\)/);
|
|
assert.match(verify, /'verified' => __\('通过申请'/);
|
|
assert.match(verify, /'rejected' => __\('拒绝申请'/);
|
|
assert.match(verify, /AuthApply::review\(\$user_id, \$uuid, \$status, \$note, get_current_user_id\(\), \$fields\)/);
|
|
assert.match(auth, /\['verified', 'rejected'\]/);
|
|
assert.match(auth, /\(string\) \$application\['status'\] !== 'manual_pending'/);
|
|
assert.match(transitions, /'manual_pending' => 'pending'/);
|
|
assert.match(transitions, /'verified' => 'verified'/);
|
|
assert.match(transitions, /'rejected' => 'rejected'/);
|
|
assert.match(transitions, /default => ''/);
|
|
});
|