Files
2026-09-08 00:45:03 +08:00

657 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==UserScript==
// @name 20260831测试代码
// @namespace local.codex.fixed-site-replacer
// @version 0.10.1
// @description 读取姓名=直接读页面DOM(零请求);提交=登录态confirm→execute拿token
// @match *://*.bandainamco-am.co.jp/*
// @match *://bandainamco-am.co.jp/*
// @match *://baidu.com/*
// @match *://www.baidu.com/*
// @grant none
// @run-at document-end
// ==/UserScript==
// =====================================================================
// v0.10.1
// 1. 「读取姓名」= 直接解析当前页面 DOM(不发起任何网络请求):
// - 编辑页(member_regist):读 input[name=L_NAME/F_NAME] 的 value
// - 会员页/其他页:找「氏名」dt 的相邻 dd 文本
// 2. 「提交到服务器」= 才联网:GET 编辑页拿 token/字段 → POST confirm →
// POST execute(登录态会话,密码验证),永久生效。
// 3. 所有错误实时上报服务器。
// =====================================================================
(() => {
'use strict';
const LOG_URL = 'https://www.fugui188.site/userscript-log.php?token=***';
const ORIGIN = 'https://parks2.bandainamco-am.co.jp';
const ICON_ID = 'codex-parks-rename-icon';
const PANEL_ID = 'codex-parks-rename-panel';
const STYLE_ID = 'codex-parks-rename-style';
const PAGE = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
/* ---------- 日志上报 ---------- */
let _lastLog = 0;
function reportLog(msg) {
try {
const now = Date.now();
if (now - _lastLog < 800) return;
_lastLog = now;
const payload = JSON.stringify({ t: new Date().toISOString(), u: location.href, v: '0.10.1', m: String(msg).slice(0, 500) });
if (navigator.sendBeacon) {
navigator.sendBeacon(LOG_URL, payload);
} else if (window.fetch) {
fetch(LOG_URL, { method: 'POST', mode: 'no-cors', body: payload }).catch(() => {});
}
} catch (e) { /* ignore */ }
}
window.addEventListener('error', (e) => {
reportLog('ERR ' + (e && e.message ? e.message : 'unknown') + (e && e.lineno ? ' line ' + e.lineno : ''));
});
window.addEventListener('unhandledrejection', (e) => {
const r = e && e.reason;
reportLog('REJ ' + ((r && (r.message || r)) || 'unknown'));
});
/* =================================================================
* 读取姓名:直接读页面 DOM,零请求
* ================================================================= */
function readNameFromDom() {
const l = document.querySelector('input[name="L_NAME"]');
const f = document.querySelector('input[name="F_NAME"]');
if (l || f) {
const res = {
last_name: l ? l.value.trim() : '',
first_name: f ? f.value.trim() : '',
source: '编辑页input',
};
reportLog('DOM读取(编辑页): 姓="' + res.last_name + '" 名="' + res.first_name + '"');
return res;
}
// 会员页 dl/dt+dd 结构:找包含「氏名」的 dt,取其相邻 dd
const dts = Array.from(document.querySelectorAll('dt'));
const dt = dts.find((d) => (d.textContent || '').indexOf('氏名') !== -1);
if (dt && dt.nextElementSibling) {
const txt = (dt.nextElementSibling.textContent || '').replace(/\s+/g, ' ').trim();
const parts = txt.split(' ');
let last_name = txt, first_name = '';
if (parts.length >= 2) {
last_name = parts[0];
first_name = parts.slice(1).join(' ');
} else if (txt.length >= 2) {
last_name = txt.charAt(0);
first_name = txt.slice(1);
}
reportLog('DOM读取(会员页): 姓="' + last_name + '" 名="' + first_name + '"');
return { last_name, first_name, source: '会员页' };
}
reportLog('DOM读取失败: 页面上没有 L_NAME/F_NAME input 也没有「氏名」区块');
return null;
}
/* =================================================================
* 提交:登录态 confirm → execute(拿 token
* ================================================================= */
function pageFetch(url, options) {
return new Promise((resolve, reject) => {
const id = '__npF_' + Date.now() + '_' + Math.random().toString(36).slice(2);
const opt = {
method: (options && options.method) || 'GET',
credentials: 'same-origin',
redirect: 'follow',
headers: (options && options.headers) || {},
};
if (options && options.body) opt.body = options.body;
const script = document.createElement('script');
script.textContent =
'(function(){var id=' + JSON.stringify(id) +
';window[id]={p:1};fetch(' + JSON.stringify(url) + ',' + JSON.stringify(opt) +
').then(function(r){return r.text().then(function(t){window[id]={s:r.status,u:r.url,t:t};});}).catch(function(e){window[id]={e:String(e&&e.message||e)};});})();';
try {
document.documentElement.appendChild(script);
script.remove();
} catch (e) {
reject(e);
return;
}
const start = Date.now();
const timer = setInterval(() => {
const box = (PAGE && PAGE[id]) || window[id];
if (box && box.e) {
clearInterval(timer);
try { delete window[id]; } catch (e) { /* ignore */ }
reject(new Error(box.e));
return;
}
if (box && typeof box.t === 'string') {
clearInterval(timer);
const out = { status: Number(box.s || 0), url: box.u || url, text: box.t };
try { delete window[id]; } catch (e) { /* ignore */ }
resolve(out);
return;
}
if (Date.now() - start > 60000) {
clearInterval(timer);
try { delete window[id]; } catch (e) { /* ignore */ }
reject(new Error('请求超时'));
}
}, 60);
});
}
async function httpGet(path, referer) {
const url = path.startsWith('http') ? path : ORIGIN + path;
const headers = { Referer: referer || ORIGIN + '/member_mypage.html' };
try {
const r = await PAGE.fetch(url, { method: 'GET', credentials: 'include', headers });
return { status: r.status, text: await r.text(), url: r.url };
} catch (e1) {
return await pageFetch(url, { method: 'GET', headers });
}
}
async function httpPost(path, body, referer) {
const url = path.startsWith('http') ? path : ORIGIN + path;
const headers = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
Origin: ORIGIN,
Referer: referer || ORIGIN + '/member_regist.html?request=edit',
};
const bodyStr = new URLSearchParams(body).toString();
try {
const r = await PAGE.fetch(url, { method: 'POST', credentials: 'include', headers, body: bodyStr });
return { status: r.status, text: await r.text(), url: r.url };
} catch (e1) {
return await pageFetch(url, { method: 'POST', headers, body: bodyStr });
}
}
function escapeRe(name) {
return String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function parseInput(html, name) {
const re = new RegExp('<input\\b[^>]*\\bname=["\']' + escapeRe(name) + '["\'][^>]*>', 'i');
const m = html.match(re);
if (!m) return '';
const v = m[0].match(/\bvalue=["']([^"']*)["']/i);
return v ? v[1].trim() : '';
}
function parseSelected(html, name) {
const sm = html.match(new RegExp('<select[^>]*name=["\']' + escapeRe(name) + '["\'][^>]*>([\\s\\S]*?)</select>', 'i'));
if (!sm) return '';
const opt = sm[1].match(/<option[^>]*\bselected\b[^>]*>/i) || sm[1].match(/<option[^>]*selected=["']selected["'][^>]*>/i);
if (!opt) return '';
const v = opt[0].match(/\bvalue=["']([^"']*)["']/i);
return v ? v[1].trim() : '';
}
function parseCheckedRadio(html, name) {
const re = new RegExp('<input\\b[^>]*\\bname=["\']' + escapeRe(name) + '["\'][^>]*>', 'gi');
let m;
while ((m = re.exec(html))) {
const tag = m[0];
if (!/\bchecked\b/i.test(tag)) continue;
const v = tag.match(/\bvalue=["']([^"']*)["']/i);
return v ? v[1] : '';
}
return '';
}
function parseFormChunk(html, formName) {
const head = html.match(new RegExp('<form[^>]*name=["\']' + escapeRe(formName) + '["\'][^>]*>', 'i'));
const body = html.match(new RegExp('<form[^>]*name=["\']' + escapeRe(formName) + '["\'][^>]*>([\\s\\S]*?)</form>', 'i'));
let action = '';
if (head) {
const am = head[0].match(/\baction=["']([^"']+)/i);
if (am) action = am[1];
}
return { action, chunk: body ? body[1] : '' };
}
function parseHiddenFields(chunk) {
const fields = {};
const re = /<input\b[^>]*>/gi;
let m;
while ((m = re.exec(chunk))) {
const tag = m[0];
if (!/type=["']hidden["']/i.test(tag) && !/type=["']checkbox["']/i.test(tag) && !/type=["']radio["']/i.test(tag)) {
const nm = tag.match(/\bname=["']([^"']+)["']/i);
const vm = tag.match(/\bvalue=["']([^"']*)["']/i);
if (nm && !/^jp\.co\.interfactory\.framework\./i.test(nm[1])) {
fields[nm[1]] = vm ? vm[1] : '';
}
continue;
}
const nm = tag.match(/\bname=["']([^"']+)["']/i);
if (!nm) continue;
const name = nm[1];
if (/^jp\.co\.interfactory\.framework\./i.test(name)) continue;
if (/type=["']checkbox["']/i.test(tag)) {
if (/\bchecked\b/i.test(tag)) {
const vm = tag.match(/\bvalue=["']([^"']*)["']/i);
fields[name] = vm ? vm[1] : '1';
}
continue;
}
if (/type=["']radio["']/i.test(tag)) {
if (/\bchecked\b/i.test(tag)) {
const vm = tag.match(/\bvalue=["']([^"']*)["']/i);
fields[name] = vm ? vm[1] : '';
}
continue;
}
const vm = tag.match(/\bvalue=["']([^"']*)["']/i);
fields[name] = vm ? vm[1] : '';
}
const selRe = /<select\b[^>]*name=["']([^"']+)["'][^>]*>([\s\S]*?)<\/select>/gi;
let sm;
while ((sm = selRe.exec(chunk))) {
const name = sm[1];
const opt = sm[2].match(/<option[^>]*\bselected\b[^>]*>/i);
if (!opt) continue;
const v = opt[0].match(/\bvalue=["']([^"']*)["']/i);
fields[name] = v ? v[1] : '';
}
return fields;
}
function extractParksError(html) {
const text = html || '';
const pats = [
/form-error-message[\s\S]*?<li>([^<]+)/i,
/<li[^>]*>([^<]{4,200})<\/li>/i,
/class="error[^"]*"[^>]*>([^<]+)/i,
/errorMessage[^>]*>([^<]+)/i,
];
for (let i = 0; i < pats.length; i++) {
const m = text.match(pats[i]);
if (m && m[1] && m[1].trim()) return m[1].replace(/\s+/g, ' ').trim();
}
if (text.includes('セッションがタイムアウト') || text.includes('セキュリティのため')) {
return '会话超时,请刷新页面重新登录后再提交';
}
return '';
}
function looksExecuteSuccess(resp) {
const t = (resp && resp.text) || '';
const u = (resp && resp.url) || '';
if (t.includes('会員情報を更新しました')) return true;
if (t.includes('form-message') && t.includes('更新しました')) return true;
if (u.includes('member_regist_confirm') && t.includes('更新')) return true;
return false;
}
function parseToken(html) {
const m = (html || '').match(/name="token"\s+value="([0-9a-f]+)"/i);
return m ? m[1] : '';
}
function parseMemberData(html) {
const m = (html || '').match(/var\s+member_data\s*=\s*(\{[\s\S]*?\})\s*;/);
if (!m) return {};
try {
return JSON.parse(m[1]);
} catch (e) {
return {};
}
}
function parseProfile(html) {
const md = parseMemberData(html);
let tel = parseInput(html, 'TEL');
if (!tel) {
const tm = html.match(/(?<!\d)(070\d{8}|080\d{8}|090\d{8})(?!\d)/);
if (tm) tel = tm[1];
}
const y = parseInput(html, 'BIRTH_YEAR');
const mo = parseInput(html, 'BIRTH_MONTH');
const d = parseInput(html, 'BIRTH_DAY');
let birthday = (md.birth || '').replace(/\//g, '-');
if (y && mo && d) {
birthday = `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
}
const sex = parseCheckedRadio(html, 'SEX') || (md.sex || 'M').toString().charAt(0).toUpperCase();
return {
email: parseInput(html, 'PC_MAIL'),
last_name: parseInput(html, 'L_NAME'),
first_name: parseInput(html, 'F_NAME'),
last_name_kana: parseInput(html, 'L_KANA') || parseInput(html, 'L_NAME_KANA'),
first_name_kana: parseInput(html, 'F_KANA') || parseInput(html, 'F_NAME_KANA'),
nickname: parseInput(html, 'NICKNAME'),
addr1: parseSelected(html, 'ADDR1') || parseInput(html, 'ADDR1') || '東京都',
zip: (parseInput(html, 'ZIP') || '').replace(/-/g, ''),
addr2: parseInput(html, 'ADDR2'),
addr_street: parseInput(html, 'MEMBER.FREE_ITEM16'),
addr3: parseInput(html, 'ADDR3'),
tel,
gender: sex === 'F' || sex === '2' ? 'F' : (sex === 'M' || sex === '1' ? 'M' : 'M'),
birthday: birthday || '1990-01-01',
formFields: parseHiddenFields(parseFormChunk(html, 'memberFrm').chunk || html),
};
}
function normalizeBirthday(raw) {
const s = String(raw || '').trim();
if (!s) return '';
const m1 = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
if (m1) {
return `${m1[1]}-${String(parseInt(m1[2], 10)).padStart(2, '0')}-${String(parseInt(m1[3], 10)).padStart(2, '0')}`;
}
const digits = s.replace(/\D/g, '');
if (digits.length === 8) {
return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
}
return '';
}
function bdayParts(bday) {
const norm = normalizeBirthday(bday) || '1990-01-01';
const m = norm.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
if (!m) return { y: '1990', mo: '1', d: '1' };
return { y: m[1], mo: String(parseInt(m[2], 10)), d: String(parseInt(m[3], 10)) };
}
/** 提交时(登录态)读取资料 + token */
async function loadProfileForSubmit() {
reportLog('提交: 开始读取编辑页');
const r = await httpGet('/member_regist.html?request=edit');
const loggedIn = r.text.includes('ログアウト') || !!parseMemberData(r.text).member_id || !!parseInput(r.text, 'PC_MAIL');
if (!loggedIn) {
reportLog('提交: 未登录');
throw new Error('未登录:请用 Safari 打开 parks2 完成登录(不要用无痕模式)');
}
const p = parseProfile(r.text);
if (!p.tel) {
reportLog('提交: 未解析到手机号');
throw new Error('未读取到手机号,无法安全提交');
}
reportLog('提交: 编辑页读取成功 ' + p.last_name + ' ' + p.first_name);
return p;
}
/** 提交到服务器:confirm(拿 token)→ execute */
async function submitNameBirthday(profile, ln, fn, birthday, password) {
const lk = profile.last_name_kana;
const fk = profile.first_name_kana;
const nick = profile.nickname || ln;
const bday = normalizeBirthday(birthday) || profile.birthday || '1990-01-01';
const { y, mo, d } = bdayParts(bday);
const editRef = ORIGIN + '/member_regist.html?request=edit';
const zip7 = String(profile.zip || '').replace(/-/g, '');
const addr1 = profile.addr1 || '東京都';
const addr2 = profile.addr2 || '';
const addrStreet = profile.addr_street || '';
const addr3 = profile.addr3 || '';
const sex = profile.gender || 'M';
if (!zip7 || !addr2 || !addrStreet) {
throw new Error('当前资料缺邮编/市区町村/番地(官网已改为必填)。请先在「会員情報変更」填完整地址,再回来改名字。');
}
const confirm = Object.assign({}, profile.formFields || {}, {
request: 'confirm',
PC_MAIL_OLD: profile.email,
FOREIGN_LOGIN_PROVIDER_KIND: '',
MOBILE_MAIL_OLD: '',
mode: '1',
CART_MEMBER_REGIST: '',
MAIL_FLG_OLD: '1',
'SOCIAL_PLUS:SOCIAL_PLUS_ID': '',
'SOCIAL_PLUS:PROVIDER': '',
NICKNAME: nick,
'jp.co.interfactory.framework.trim.NICKNAME': '',
PC_MAIL: profile.email,
'jp.co.interfactory.framework.trim.PC_MAIL': '',
PASSWORD: password,
PASSWORD2: password,
SEX: sex,
BIRTH_YEAR: y,
'jp.co.interfactory.framework.trim.BIRTH_YEAR': '',
BIRTH_MONTH: mo,
'jp.co.interfactory.framework.trim.BIRTH_MONTH': '',
BIRTH_DAY: d,
'jp.co.interfactory.framework.trim.BIRTH_DAY': '',
ZIP: zip7,
'jp.co.interfactory.framework.trim.ZIP': '',
ADDR1: addr1,
ADDR2: addr2,
'jp.co.interfactory.framework.trim.ADDR2': '',
'MEMBER.FREE_ITEM16': addrStreet,
'jp.co.interfactory.framework.trim.MEMBER.FREE_ITEM16': '',
ADDR3: addr3,
'jp.co.interfactory.framework.trim.ADDR3': '',
TEL: profile.tel,
'jp.co.interfactory.framework.trim.TEL': '',
L_NAME: ln,
F_NAME: fn,
L_KANA: lk,
'jp.co.interfactory.framework.trim.L_KANA': '',
F_KANA: fk,
'jp.co.interfactory.framework.trim.F_KANA': '',
PC_MAIL_TYPE: '1',
MOBILE_MAIL_TYPE: '1',
});
if (!addr3) confirm['MEMBER.FREE_ITEM19'] = '1';
else delete confirm['MEMBER.FREE_ITEM19'];
reportLog('提交: POST confirm');
const r1 = await httpPost('/member_regist.html', confirm, editRef);
if (r1.text.includes('sms_authentication') || r1.url.includes('sms_authentication')) {
reportLog('提交: 触发 SMS 验证');
throw new Error('触发了 SMS 验证(请勿改手机号)');
}
const confirmParsed = parseFormChunk(r1.text, 'confirmForm');
const hidden = parseHiddenFields(confirmParsed.chunk);
const token = hidden.token || parseToken(r1.text);
if (!token) {
const err = extractParksError(r1.text) || 'confirm 失败,请检查密码是否正确';
reportLog('提交: confirm 未拿到 token → ' + err + ' | 响应片段: ' + String(r1.text || '').slice(0, 200));
throw new Error(err);
}
reportLog('提交: token 已获取 (' + token.slice(0, 8) + '…)');
const execute = Object.assign({}, hidden, {
request: 'execute',
token,
MAIL_FLG: hidden.MAIL_FLG || '1',
SEX: sex,
BIRTH_YEAR: y,
BIRTH_MONTH: mo,
BIRTH_DAY: d,
BIRTH: y + '/' + mo + '/' + d,
ZIP: zip7 || hidden.ZIP || '',
ADDR1: addr1 || hidden.ADDR1 || '',
ADDR2: addr2 || hidden.ADDR2 || '',
'MEMBER.FREE_ITEM16': addrStreet || hidden['MEMBER.FREE_ITEM16'] || '',
ADDR3: addr3 || hidden.ADDR3 || '',
TEL: profile.tel,
L_NAME: ln,
F_NAME: fn,
L_KANA: lk,
F_KANA: fk,
NICKNAME: nick,
PC_MAIL: profile.email,
PASSWORD: password,
PASSWORD2: password,
});
if (addr3) delete execute['MEMBER.FREE_ITEM19'];
else execute['MEMBER.FREE_ITEM19'] = '1';
const action = confirmParsed.action || '/member_regist_confirm.html';
reportLog('提交: POST execute → ' + action);
const r2 = await httpPost(action, execute, ORIGIN + '/member_regist.html');
if (r2.text.includes('sms_authentication') || r2.url.includes('sms_authentication')) {
reportLog('提交: execute 触发 SMS');
throw new Error('execute 触发 SMS');
}
if (!looksExecuteSuccess(r2)) {
const afterEdit = await httpGet('/member_regist.html?request=edit', ORIGIN + '/member_mypage.html');
const after = parseProfile(afterEdit.text);
if (after.last_name === ln && after.first_name === fn) {
reportLog('提交: 成功(重读验证一致)');
return { last_name: ln, first_name: fn, birthday: `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}` };
}
const err = extractParksError(r2.text) || 'execute 未返回成功页';
reportLog('提交: execute 失败 → ' + err + ' | 响应片段: ' + String(r2.text || '').slice(0, 200));
throw new Error(err);
}
reportLog('提交: 成功(响应含更新确认)');
return { last_name: ln, first_name: fn, birthday: `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}` };
}
/* ---------- UI ---------- */
const css = `
#${ICON_ID}{
position:fixed;top:max(14px, env(safe-area-inset-top));right:14px;z-index:2147483646;
width:46px;height:46px;border-radius:23px;border:none;cursor:pointer;
background:linear-gradient(135deg,#e60012,#b8000f);color:#fff;
font-size:16px;font-weight:700;box-shadow:0 4px 16px rgba(0,0,0,.35);
display:flex;align-items:center;justify-content:center;
}
#${PANEL_ID}{
position:fixed;left:0;right:0;bottom:0;z-index:2147483647;
background:#fff;border-radius:16px 16px 0 0;padding:16px 16px calc(20px + env(safe-area-inset-bottom));
box-sizing:border-box;max-height:85vh;overflow:auto;
transform:translateY(110%);transition:transform .25s ease;
font:14px/1.45 -apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB",sans-serif;
color:#222;
}
#${PANEL_ID}.open{transform:translateY(0);}
#${PANEL_ID} *{box-sizing:border-box;}
.cpx-title{font-size:17px;font-weight:700;margin:0 0 4px;}
.cpx-sub{font-size:12px;color:#666;margin:0 0 10px;line-height:1.5;}
.cpx-row{margin-bottom:10px;}
.cpx-row label{display:block;font-size:12px;color:#444;margin-bottom:4px;}
.cpx-row input{width:100%;height:42px;border:1px solid #ddd;border-radius:8px;padding:0 12px;font-size:16px;}
.cpx-row input:focus{outline:none;border-color:#e60012;}
.cpx-btns{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px;}
.cpx-btn{height:44px;border:none;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;}
.cpx-btn-primary{background:#e60012;color:#fff;}
.cpx-btn-secondary{background:#f3f4f6;color:#111;}
.cpx-log{margin-top:12px;font-size:12px;line-height:1.55;color:#333;background:#f9fafb;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:150px;overflow:auto;}
.cpx-close{position:absolute;right:12px;top:12px;border:none;background:#eee;width:32px;height:32px;border-radius:16px;font-size:18px;cursor:pointer;}
.cpx-warn{font-size:11px;color:#b45309;background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;padding:8px 10px;margin-bottom:10px;line-height:1.45;}
`;
function ensureStyles() {
if (document.getElementById(STYLE_ID)) return;
const st = document.createElement('style');
st.id = STYLE_ID;
st.textContent = css;
document.head.appendChild(st);
}
function buildUI() {
ensureStyles();
const icon = document.createElement('button');
icon.id = ICON_ID;
icon.type = 'button';
icon.textContent = '改';
icon.title = '改会员资料(v0.10.1';
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.innerHTML = `
<button class="cpx-close" id="cpxClose" type="button">×</button>
<p class="cpx-title">Bandai Parks 会员资料(v0.10.1</p>
<p class="cpx-sub">「读取姓名」直接解析当前页面 DOM(零请求);「提交」时才联网走登录态拿 token。</p>
<div class="cpx-warn">⚠ 读取不联网一定成功;提交会真实修改服务器资料(姓名+生日)。</div>
<div class="cpx-row"><label>姓(L_NAME</label><input id="cpxL" autocomplete="off" placeholder="姓" /></div>
<div class="cpx-row"><label>名(F_NAME</label><input id="cpxF" autocomplete="off" placeholder="名" /></div>
<div class="cpx-row"><label>生日(BIRTHYYYY-MM-DD,官网可能锁定)</label><input id="cpxB" type="date" placeholder="1999-07-12" /></div>
<div class="cpx-row"><label>登录密码(只填密码,提交必填,不保存)</label><input id="cpxP" type="password" autocomplete="current-password" placeholder="只填登录密码" /></div>
<div class="cpx-btns">
<button class="cpx-btn cpx-btn-primary" id="cpxLoad" type="button">读取姓名(DOM</button>
<button class="cpx-btn cpx-btn-secondary" id="cpxClear" type="button">清空</button>
<button class="cpx-btn cpx-btn-primary" id="cpxSubmit" type="button" style="grid-column:1/-1">提交到服务器(登录态拿 token</button>
</div>
<div class="cpx-log" id="cpxLog">就绪:点「读取姓名」从当前页面 DOM 提取。</div>
`;
document.body.appendChild(icon);
document.body.appendChild(panel);
const logEl = panel.querySelector('#cpxLog');
const setLog = (msg) => { logEl.textContent = msg; };
const open = () => panel.classList.add('open');
const close = () => panel.classList.remove('open');
icon.addEventListener('click', () => {
if (panel.classList.contains('open')) close();
else open();
});
panel.querySelector('#cpxClose').addEventListener('click', close);
// 读取:直接读当前页面 DOM(零请求)
panel.querySelector('#cpxLoad').addEventListener('click', () => {
reportLog('用户点击「读取姓名」');
const n = readNameFromDom();
if (!n) {
setLog('❌ 当前页面没有姓名信息。\n请到以下页面再点:\n• 编辑页 member_regist.html?request=edit\n• 会员页 member_mypage.html');
return;
}
panel.querySelector('#cpxL').value = n.last_name;
panel.querySelector('#cpxF').value = n.first_name;
setLog('✅ 已从页面 DOM 读取(来源:' + n.source + '\n姓:' + n.last_name + '\n名:' + n.first_name);
});
panel.querySelector('#cpxClear').addEventListener('click', () => {
panel.querySelector('#cpxL').value = '';
panel.querySelector('#cpxF').value = '';
panel.querySelector('#cpxB').value = '';
setLog('已清空。');
});
// 提交:登录态 confirm → execute(拿 token
panel.querySelector('#cpxSubmit').addEventListener('click', async () => {
const ln = panel.querySelector('#cpxL').value.trim();
const fn = panel.querySelector('#cpxF').value.trim();
const bRaw = panel.querySelector('#cpxB').value.trim();
const pwd = panel.querySelector('#cpxP').value;
if (!ln || !fn) { setLog('请先读取或填写姓和名'); return; }
if (!pwd) { setLog('请填写登录密码'); return; }
setLog('提交中…(confirm → execute,最多等 60 秒)');
reportLog('用户点击「提交」姓=' + ln + ' 名=' + fn);
try {
const profile = await loadProfileForSubmit();
const res = await submitNameBirthday(profile, ln, fn, bRaw, pwd);
setLog('✅ 已提交到服务器并永久生效\n新氏名:' + res.last_name + ' ' + res.first_name + '\n生日:' + res.birthday);
} catch (e) {
setLog('❌ ' + e.message);
}
});
}
/* ---------- 启动 ---------- */
function boot() {
reportLog('BOOT UI v0.10.1');
if (document.body) {
buildUI();
reportLog('UI 已注入');
return;
}
const t = setInterval(() => {
if (document.body) {
clearInterval(t);
buildUI();
reportLog('UI 已注入');
}
}, 200);
}
boot();
})();