add: 首次提交油猴脚本

This commit is contained in:
2026-09-08 00:45:03 +08:00
parent 88372cfc55
commit 06f54f8e1c
17 changed files with 14736 additions and 0 deletions
+656
View File
@@ -0,0 +1,656 @@
// ==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();
})();