feat(platform): Implement legacy system integration with Wrapper Bridge architecture

Complete integration of the old clinical research platform (www.xunzhengyixue.com)
into the new AI platform via Token injection + iframe embedding:

Backend:
- Add legacy-bridge module (MySQL pool, auth service, routes)
- POST /api/v1/legacy/auth: JWT -> phone lookup -> Token injection into old MySQL
- Auto-create user in old system if not found (matched by phone number)

Frontend:
- LegacySystemPage: iframe container with Bridge URL construction
- ResearchManagement + StatisticalTools entry components
- Module registry updated from external links to iframe embed mode

ECS (token-bridge.html deployed to www.xunzhengyixue.com):
- Wrapper Bridge: sets cookies within same-origin context
- Storage Access API for cross-site dev environments
- CSS injection: hide old system nav/footer, remove padding gaps
- Inner iframe loads target page with full DOM access (same-origin)

Key technical decisions:
- Token injection (direct MySQL write) instead of calling login API
- Wrapper Bridge instead of parent-page cookie setting (cross-origin fix)
- Storage Access API + SameSite=None;Secure for third-party cookie handling
- User isolation guaranteed by phone number matching

Documentation:
- Integration plan v4.0 with full implementation record
- Implementation summary with 6 pitfalls documented
- System status guide updated (ST module now integrated)

Tested: Local E2E verified - auto login, research management, 126 statistical
tools, report generation, download, UI layout all working correctly

Made-with: Cursor
This commit is contained in:
2026-02-27 21:54:38 +08:00
parent 6124c7abc6
commit c3f7d54fdf
21 changed files with 1407 additions and 63 deletions

View File

@@ -33,6 +33,7 @@
"jsonrepair": "^3.13.1",
"jsonwebtoken": "^9.0.2",
"jspdf": "^3.0.3",
"mysql2": "^3.18.2",
"openai": "^6.16.0",
"p-queue": "^9.0.1",
"pg-boss": "^12.5.2",
@@ -1458,6 +1459,15 @@
"fastq": "^1.17.1"
}
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.12.2.tgz",
@@ -2902,6 +2912,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"license": "MIT",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -3302,6 +3321,12 @@
"node": ">=0.12.0"
}
},
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz",
@@ -3766,6 +3791,27 @@
"node": ">= 12.0.0"
}
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/lru.min": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz",
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/luxon": {
"version": "3.7.2",
"resolved": "https://registry.npmmirror.com/luxon/-/luxon-3.7.2.tgz",
@@ -3938,6 +3984,44 @@
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
}
},
"node_modules/mysql2": {
"version": "3.18.2",
"resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.18.2.tgz",
"integrity": "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.2",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.7.2",
"long": "^5.3.2",
"lru.min": "^1.1.4",
"named-placeholders": "^1.1.6",
"sql-escaper": "^1.3.3"
},
"engines": {
"node": ">= 8.0"
},
"peerDependencies": {
"@types/node": ">= 8"
}
},
"node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz",
@@ -3949,6 +4033,18 @@
"thenify-all": "^1.0.0"
}
},
"node_modules/named-placeholders": {
"version": "1.1.6",
"resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.6.tgz",
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
"license": "MIT",
"dependencies": {
"lru.min": "^1.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
@@ -5180,6 +5276,21 @@
"node": ">= 10.x"
}
},
"node_modules/sql-escaper": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/sql-escaper/-/sql-escaper-1.3.3.tgz",
"integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=2.0.0",
"node": ">=12.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",

View File

@@ -50,6 +50,7 @@
"jsonrepair": "^3.13.1",
"jsonwebtoken": "^9.0.2",
"jspdf": "^3.0.3",
"mysql2": "^3.18.2",
"openai": "^6.16.0",
"p-queue": "^9.0.1",
"pg-boss": "^12.5.2",

View File

@@ -0,0 +1,120 @@
/**
* Legacy Auth Token Injection Test
*
* Tests the full flow:
* 1. Connect to old system MySQL (xzyx_online)
* 2. Find user by phone
* 3. Generate MD5 token (same formula as old Java system)
* 4. Insert token into u_user_token
* 5. Print cookie-setting instructions for manual browser verification
*
* Usage:
* npx tsx scripts/test-legacy-auth.ts <phone>
* npx tsx scripts/test-legacy-auth.ts 18611348738
*/
import mysql from 'mysql2/promise';
import crypto from 'crypto';
const MYSQL_CONFIG = {
host: process.env.LEGACY_MYSQL_HOST || '8.154.22.149',
port: parseInt(process.env.LEGACY_MYSQL_PORT || '3306'),
user: process.env.LEGACY_MYSQL_USER || 'xzyx_rw',
password: process.env.LEGACY_MYSQL_PASSWORD || 'SKJfdwalkd',
database: process.env.LEGACY_MYSQL_DATABASE || 'xzyx_online',
};
function generateToken(userId: number): string {
const timestamp = Date.now();
const raw = `KyKz1.0:${userId}:${timestamp}`;
return crypto.createHash('md5').update(raw).digest('hex');
}
async function main() {
const phone = process.argv[2];
if (!phone) {
console.error('Usage: npx tsx scripts/test-legacy-auth.ts <phone>');
console.error('Example: npx tsx scripts/test-legacy-auth.ts 18611348738');
process.exit(1);
}
console.log('='.repeat(60));
console.log('Legacy Auth Token Injection Test');
console.log('='.repeat(60));
// Step 1: Connect
console.log('\n[1/5] Connecting to MySQL...');
const conn = await mysql.createConnection(MYSQL_CONFIG);
console.log(` OK - connected to ${MYSQL_CONFIG.host}:${MYSQL_CONFIG.port}/${MYSQL_CONFIG.database}`);
// Step 2: Find user
console.log(`\n[2/5] Looking up user by phone: ${phone}`);
const [rows] = await conn.execute<any[]>(
'SELECT id, phone, nickname, real_name, user_role FROM u_user_info WHERE phone = ?',
[phone],
);
if (rows.length === 0) {
console.log(' User NOT found. To test, pick an existing user:');
const [sample] = await conn.execute<any[]>(
'SELECT id, phone, nickname, real_name FROM u_user_info LIMIT 5',
);
console.table(sample);
await conn.end();
process.exit(1);
}
const user = rows[0];
console.log(` Found: id=${user.id}, nickname="${user.nickname}", role=${user.user_role}`);
// Step 3: Generate token
console.log('\n[3/5] Generating MD5 token...');
const token = generateToken(user.id);
console.log(` Token: ${token}`);
// Step 4: Delete old tokens + insert new one
// gen_time must be milliseconds since epoch (Java System.currentTimeMillis()),
// NOT MySQL NOW(). The old system's isTimeout() does millisecond arithmetic.
console.log('\n[4/5] Deleting old tokens and inserting new one...');
await conn.execute('DELETE FROM u_user_token WHERE user_id = ?', [user.id]);
const genTime = Date.now();
await conn.execute(
'INSERT INTO u_user_token (user_id, token, gen_time, user_role) VALUES (?, ?, ?, ?)',
[user.id, token, genTime, user.user_role],
);
console.log(` OK - token inserted (gen_time=${genTime}, which is ${new Date(genTime).toISOString()})`);
// Step 5: Verify
console.log('\n[5/5] Verifying token in database...');
const [verify] = await conn.execute<any[]>(
'SELECT id, user_id, token, gen_time, user_role FROM u_user_token WHERE token = ?',
[token],
);
console.log(' Verified:');
console.table(verify);
await conn.end();
// Print manual test instructions
console.log('\n' + '='.repeat(60));
console.log('TOKEN INJECTION SUCCESSFUL!');
console.log('='.repeat(60));
console.log('\nManual browser test:');
console.log('1. Open browser, go to https://www.xunzhengyixue.com');
console.log('2. Open DevTools (F12) -> Console');
console.log('3. Paste the following 3 lines:\n');
const nickname = user.nickname || user.real_name || phone;
console.log(`document.cookie = "token=${token}; domain=.xunzhengyixue.com; path=/";`);
console.log(`document.cookie = "nickname=${encodeURIComponent(nickname)}; domain=.xunzhengyixue.com; path=/";`);
console.log(`document.cookie = "id=${user.id}; domain=.xunzhengyixue.com; path=/";`);
console.log('\n4. Refresh the page (F5)');
console.log('5. You should see the user\'s projects without logging in manually');
console.log('\n' + '='.repeat(60));
}
main().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -214,6 +214,13 @@ import { ssaRoutes } from './modules/ssa/index.js';
await fastify.register(ssaRoutes, { prefix: '/api/v1/ssa' });
logger.info('✅ SSA智能统计分析路由已注册: /api/v1/ssa');
// ============================================
// 【集成模块】Legacy Bridge - 旧系统集成桥接
// ============================================
import { legacyBridgeRoutes } from './modules/legacy-bridge/routes.js';
await fastify.register(legacyBridgeRoutes, { prefix: '/api/v1/legacy' });
logger.info('✅ Legacy Bridge路由已注册: /api/v1/legacy');
// 启动服务器
const start = async () => {
try {
@@ -340,6 +347,11 @@ const gracefulShutdown = async (signal: string) => {
await prisma.$disconnect();
console.log('✅ 数据库连接已关闭');
// 4. 关闭 Legacy MySQL 连接池
const { closeLegacyMysqlPool } = await import('./modules/legacy-bridge/mysql-pool.js');
await closeLegacyMysqlPool();
console.log('✅ Legacy MySQL连接池已关闭');
console.log('👋 优雅关闭完成,再见!');
process.exit(0);
} catch (error) {

View File

@@ -0,0 +1,118 @@
/**
* Legacy System Auth Service
*
* Handles token injection into the old Java system's MySQL database.
* Generates MD5 tokens matching the old system's formula and inserts
* them into u_user_token, bypassing the login API entirely.
*/
import crypto from 'crypto';
import { getLegacyMysqlPool } from './mysql-pool.js';
import { logger } from '../../common/logging/index.js';
interface LegacyUser {
id: number;
phone: string;
nickname: string;
real_name: string;
user_role: string;
}
interface LegacyAuthResult {
token: string;
nickname: string;
id: number;
userRole: string;
}
const DEFAULT_PASSWORD_MD5 = 'E10ADC3949BA59ABBE56E057F20F883E'; // MD5 of "123456"
/**
* Token formula from old system: MD5("KyKz1.0:" + userId + ":" + timestamp)
*/
function generateLegacyToken(userId: number): string {
const timestamp = Date.now();
const raw = `KyKz1.0:${userId}:${timestamp}`;
return crypto.createHash('md5').update(raw).digest('hex');
}
/**
* Find user in old system by phone number.
*/
async function findUserByPhone(phone: string): Promise<LegacyUser | null> {
const pool = getLegacyMysqlPool();
const [rows] = await pool.execute<any[]>(
'SELECT id, phone, nickname, real_name, user_role FROM u_user_info WHERE phone = ?',
[phone],
);
return rows.length > 0 ? rows[0] : null;
}
/**
* Create user in old system with default password.
*/
async function createUser(phone: string, realName: string): Promise<LegacyUser> {
const pool = getLegacyMysqlPool();
const [result] = await pool.execute<any>(
`INSERT INTO u_user_info
(phone, nickname, real_name, hospital, company, academy, user_role, user_status, password, hosptial_id, email_validate_status, register_time)
VALUES (?, ?, ?, '本地', '', '', 'NORMAL', 'NORMAL', ?, 0, 0, UNIX_TIMESTAMP())`,
[phone, realName || phone, realName || phone, DEFAULT_PASSWORD_MD5],
);
const insertId = result.insertId;
logger.info('Created legacy user', { phone, insertId });
return {
id: insertId,
phone,
nickname: realName || phone,
real_name: realName || phone,
user_role: 'NORMAL',
};
}
/**
* Inject a fresh token into old system's u_user_token table.
* gen_time must be Java System.currentTimeMillis() (milliseconds since epoch),
* NOT MySQL NOW(). The old system's isTimeout() does millisecond arithmetic.
* Also delete existing tokens for this user first (matches Java generateToken behavior).
*/
async function injectToken(userId: number, token: string, userRole: string): Promise<void> {
const pool = getLegacyMysqlPool();
await pool.execute('DELETE FROM u_user_token WHERE user_id = ?', [userId]);
await pool.execute(
`INSERT INTO u_user_token (user_id, token, gen_time, user_role) VALUES (?, ?, ?, ?)`,
[userId, token, Date.now(), userRole],
);
}
/**
* Main entry: authenticate a user in the old system.
* - Finds or creates the user by phone
* - Generates and injects a token
* - Returns credentials for cookie-setting on the frontend
*/
export async function authenticateLegacyUser(
phone: string,
displayName?: string,
): Promise<LegacyAuthResult> {
let user = await findUserByPhone(phone);
if (!user) {
logger.info('Legacy user not found, creating', { phone });
user = await createUser(phone, displayName || '');
}
const token = generateLegacyToken(user.id);
await injectToken(user.id, token, user.user_role);
logger.info('Legacy token injected', { phone, userId: user.id });
return {
token,
nickname: user.nickname || user.real_name || phone,
id: user.id,
userRole: user.user_role,
};
}

View File

@@ -0,0 +1,39 @@
/**
* Legacy System MySQL Connection Pool
*
* Connects to the old Java system's MariaDB/MySQL database (xzyx_online)
* on ECS 8.154.22.149 for user sync and token injection.
*/
import mysql from 'mysql2/promise';
import { logger } from '../../common/logging/index.js';
const LEGACY_MYSQL_CONFIG = {
host: process.env.LEGACY_MYSQL_HOST || '8.154.22.149',
port: parseInt(process.env.LEGACY_MYSQL_PORT || '3306'),
user: process.env.LEGACY_MYSQL_USER || 'xzyx_rw',
password: process.env.LEGACY_MYSQL_PASSWORD || 'SKJfdwalkd',
database: process.env.LEGACY_MYSQL_DATABASE || 'xzyx_online',
waitForConnections: true,
connectionLimit: 5,
queueLimit: 0,
connectTimeout: 10000,
};
let pool: mysql.Pool | null = null;
export function getLegacyMysqlPool(): mysql.Pool {
if (!pool) {
pool = mysql.createPool(LEGACY_MYSQL_CONFIG);
logger.info('Legacy MySQL pool created', { host: LEGACY_MYSQL_CONFIG.host });
}
return pool;
}
export async function closeLegacyMysqlPool(): Promise<void> {
if (pool) {
await pool.end();
pool = null;
logger.info('Legacy MySQL pool closed');
}
}

View File

@@ -0,0 +1,61 @@
/**
* Legacy Bridge Routes
*
* POST /api/v1/legacy/auth - Authenticate current user in the old Java system
*/
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { authenticate } from '../../common/auth/auth.middleware.js';
import { authenticateLegacyUser } from './legacy-auth.service.js';
import { logger } from '../../common/logging/index.js';
import { prisma } from '../../config/database.js';
export async function legacyBridgeRoutes(fastify: FastifyInstance): Promise<void> {
/**
* POST /auth
*
* Uses the current user's JWT to generate a valid token for the old system.
* Frontend should set the returned values as cookies on .xunzhengyixue.com
*/
fastify.post('/auth', {
preHandler: [authenticate],
}, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const { userId, phone } = request.user!;
if (!phone) {
return reply.status(400).send({
error: 'Bad Request',
message: '当前用户无手机号,无法同步到旧系统',
});
}
// Look up display name from new system's user table
let displayName = phone;
try {
const newUser = await prisma.user.findUnique({
where: { id: userId },
select: { name: true },
});
if (newUser?.name) {
displayName = newUser.name;
}
} catch {
// Non-critical, fall back to phone
}
const result = await authenticateLegacyUser(phone, displayName);
return reply.send({
success: true,
data: result,
});
} catch (error) {
logger.error('Legacy auth failed', { error });
return reply.status(500).send({
error: 'Internal Server Error',
message: '旧系统认证失败,请稍后重试',
});
}
});
}

View File

@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>加载中...</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif; }
#frame { width: 100%; height: 100%; border: none; display: none; }
#status { display: flex; flex-direction: column; align-items: center; justify-content: center;
height: 100%; gap: 16px; background: #f5f7fa; }
#status .spinner { width: 32px; height: 32px; border: 3px solid #e0e0e0;
border-top-color: #4f6ef7; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
#status p { color: #888; font-size: 14px; }
#status button { padding: 10px 28px; font-size: 15px; border-radius: 8px; border: none;
background: #4f6ef7; color: #fff; cursor: pointer; transition: background 0.2s; }
#status button:hover { background: #3a5ce5; }
</style>
</head>
<body>
<div id="status">
<div class="spinner"></div>
<p id="msg">正在连接...</p>
<button id="btn" style="display:none;">点击授权并继续</button>
</div>
<iframe id="frame"></iframe>
<script>
(function() {
var P = new URLSearchParams(location.search);
var token = P.get('token');
var nickname = P.get('nickname');
var id = P.get('id');
var role = P.get('userRole');
var redirect = P.get('redirect') || '/index.html';
var cookieOpt = '; domain=.xunzhengyixue.com; path=/; SameSite=None; Secure';
function setCookies() {
if (!token) return;
document.cookie = 'token=' + encodeURIComponent(token) + cookieOpt;
document.cookie = 'nickname=' + encodeURIComponent(nickname) + cookieOpt;
document.cookie = 'id=' + encodeURIComponent(id) + cookieOpt;
document.cookie = 'userRole=' + encodeURIComponent(role) + cookieOpt;
}
// ---- Custom CSS injected into every page loaded in the inner iframe ----
var CUSTOM_CSS = [
'#header-navbar { display: none !important; }',
'#footer-bar { display: none !important; }',
'#menu { display: none !important; }',
'#footer { display: none !important; }',
'body { padding-top: 0 !important; margin-top: 0 !important; }',
'#page-wrapper { padding-top: 0 !important; margin-top: 0 !important; }',
'.navbar-fixed-top + * { margin-top: 0 !important; padding-top: 0 !important; }',
'section#portfolio { padding-top: 10px !important; }',
].join('\n');
function showFrame() {
var frame = document.getElementById('frame');
document.getElementById('status').style.display = 'none';
frame.style.display = 'block';
frame.onload = function() {
try {
var doc = frame.contentDocument;
if (!doc || doc.getElementById('_bridge_css')) return;
var s = doc.createElement('style');
s.id = '_bridge_css';
s.textContent = CUSTOM_CSS;
(doc.head || doc.documentElement).appendChild(s);
} catch(e) {}
};
frame.src = redirect;
}
function grantAndProceed() {
if (document.requestStorageAccess) {
document.requestStorageAccess()
.then(function() { setCookies(); showFrame(); })
.catch(function() { setCookies(); showFrame(); });
} else {
setCookies();
showFrame();
}
}
function showGrantButton() {
document.querySelector('#status .spinner').style.display = 'none';
document.getElementById('msg').textContent = '需要授权以访问旧系统';
var btn = document.getElementById('btn');
btn.style.display = 'inline-block';
btn.onclick = function() {
btn.style.display = 'none';
document.querySelector('#status .spinner').style.display = 'block';
document.getElementById('msg').textContent = '正在连接...';
grantAndProceed();
};
}
// ---- Entry point: check storage access, then set cookies + load frame ----
if (document.hasStorageAccess) {
document.hasStorageAccess().then(function(has) {
if (has) {
setCookies();
showFrame();
} else {
showGrantButton();
}
});
} else {
setCookies();
showFrame();
}
})();
</script>
</body>
</html>