Skip to content

Instantly share code, notes, and snippets.

@Mr-xn
Created May 29, 2026 09:35
Show Gist options
  • Select an option

  • Save Mr-xn/d012169b720d12308010cdb78bd5c50d to your computer and use it in GitHub Desktop.

Select an option

Save Mr-xn/d012169b720d12308010cdb78bd5c50d to your computer and use it in GitHub Desktop.
typora mac 激活
#!/usr/bin/env node
/**
* Typora macOS 激活脚本
* 原理:替换二进制中的 RSA 公钥,用自己的私钥签名生成 Activation Token
*/
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const readlineSync = require('readline-sync');
const os = require('os');
// ================================
// 配置
// ================================
const CONFIG_FILE = path.join(os.homedir(), '.typora_crack_config.json');
const DEFAULT_APP_PATH = '/Applications/Typora.app';
const ORIGINAL_PUB_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7nVoGCHqIMJyqgALEUrc
5JJhap0+HtJqzPE04pz4y+nrOmY7/12f3HvZyyoRsxKdXTZbO0wEHFIh0cRqsuaJ
PyaOOPbA0BsalofIAY3mRhQQ3vSf+rn3g+w0S+udWmKV9DnmJlpWqizFajU4T/E4
5ZgMNcXt3E1ips32rdbTR0Nnen9PVITvrbJ3l6CI2BFBImZQZ2P8N+LsqfJsqyVV
wDkt3mHAVxV7FZbfYWG+8FDSuKQHaCmvgAtChx9hwl3J6RekkqDVa6GIV13D23LS
qdk0Jb521wFJi/V6QAK6SLBiby5gYN6zQQ5RQpjXtR53MwzTdiAzGEuKdOtrY2Me
DwIDAQAB
-----END PUBLIC KEY-----`;
// ================================
// 工具函数
// ================================
function getBinaryPath(appPath) {
return path.join(appPath, 'Contents', 'MacOS', 'Typora');
}
function readCache() {
try {
if (fs.existsSync(CONFIG_FILE)) {
const data = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
if (data.path && fs.existsSync(getBinaryPath(data.path))) {
return data;
}
}
} catch (e) {}
return { path: null, machineCode: null, email: null };
}
function saveCache(obj) {
try { fs.writeFileSync(CONFIG_FILE, JSON.stringify(obj, null, 2), 'utf8'); } catch (e) {}
}
function getNowDateStr() {
const now = new Date();
const dd = String(now.getDate()).padStart(2, '0');
const mm = String(now.getMonth() + 1).padStart(2, '0');
return `${mm}/${dd}/${now.getFullYear()}`;
}
// ================================
// 选择 Typora 路径
// ================================
function chooseTyporaPath() {
const cache = readCache();
if (cache.path) {
console.log(`\n✅ 检测到缓存路径: ${cache.path}`);
const use = readlineSync.question('是否使用此路径?[Y/n]:').trim().toLowerCase();
if (use !== 'n' && use !== 'no') return cache.path;
}
if (fs.existsSync(getBinaryPath(DEFAULT_APP_PATH))) {
console.log(`\n✅ 检测到默认路径: ${DEFAULT_APP_PATH}`);
const use = readlineSync.question('是否使用此路径?[Y/n]:').trim().toLowerCase();
if (use !== 'n' && use !== 'no') {
saveCache({ ...cache, path: DEFAULT_APP_PATH });
return DEFAULT_APP_PATH;
}
}
try {
const output = spawnSync('osascript', ['-e',
'POSIX path of (choose folder with prompt "请选择 Typora.app 所在目录")'
], { encoding: 'utf8' }).stdout.trim();
if (output) {
let p = path.resolve(output.replace(/\/$/, ''));
if (!p.endsWith('.app')) p = path.join(p, 'Typora.app');
if (fs.existsSync(getBinaryPath(p))) {
saveCache({ ...cache, path: p });
return p;
}
}
} catch (e) {}
while (true) {
const input = readlineSync.question('请输入 Typora.app 路径:').trim();
if (!input) continue;
const p = path.resolve(input);
if (fs.existsSync(getBinaryPath(p))) {
saveCache({ ...cache, path: p });
return p;
}
console.log('❌ 路径无效,未找到 Typora 二进制');
}
}
// ================================
// 获取机器码和邮箱
// ================================
function getMachineCodeAndEmail() {
const cache = readCache();
if (cache.machineCode && cache.email) {
console.log(`\n✅ 检测到缓存的激活信息:`);
console.log(` 机器码: ${cache.machineCode.substring(0, 30)}...`);
console.log(` 邮箱: ${cache.email}`);
const use = readlineSync.question('是否使用此信息?[Y/n]:').trim().toLowerCase();
if (use !== 'n' && use !== 'no') {
return { machineCode: cache.machineCode, email: cache.email };
}
}
let machineCode, email;
while (true) {
machineCode = readlineSync.question('\n请输入机器码: ').trim();
email = readlineSync.question('请输入邮箱: ').trim();
if (!machineCode) { console.log('❌ 机器码不能为空'); continue; }
if (!email) { console.log('❌ 邮箱不能为空'); continue; }
break;
}
saveCache({ ...cache, machineCode, email });
return { machineCode, email };
}
// ================================
// 回滚还原
// ================================
function rollback(appPath) {
console.log('\n🔄 开始回滚还原...');
const binPath = getBinaryPath(appPath);
const bakPath = binPath + '.bak';
spawnSync('killall', ['Typora'], { stdio: 'ignore' });
if (fs.existsSync(bakPath)) {
fs.copyFileSync(bakPath, binPath);
fs.rmSync(bakPath, { force: true });
console.log('✅ 已还原 Typora 二进制');
} else {
console.log('⚠️ 未找到备份文件,无需回滚');
return;
}
spawnSync('defaults', ['delete', 'abnerworks.Typora', 'typora-license'], { stdio: 'ignore' });
spawnSync('codesign', ['--force', '--deep', '-s', '-', appPath], { stdio: 'ignore' });
console.log('✅ 已清理授权信息并重新签名');
console.log('\n🎉 回滚完成!');
}
// ================================
// 主流程
// ================================
(function main() {
process.stdout.write('\x1Bc');
console.log('=================================');
console.log(' Typora macOS 激活脚本');
console.log('=================================');
const appPath = chooseTyporaPath();
const binPath = getBinaryPath(appPath);
const bakPath = binPath + '.bak';
if (fs.existsSync(bakPath)) {
console.log('\n⚠️ 检测到已激活过(二进制已 patch)');
const choice = readlineSync.question('[R]回滚还原 / [P]重新Patch / [Q]退出:').trim().toUpperCase();
if (choice === 'R') { rollback(appPath); process.exit(0); }
if (choice !== 'P') { process.exit(0); }
console.log('还原原始二进制...');
fs.copyFileSync(bakPath, binPath);
fs.rmSync(bakPath, { force: true });
spawnSync('defaults', ['delete', 'abnerworks.Typora', 'typora-license'], { stdio: 'ignore' });
}
const { machineCode, email } = getMachineCodeAndEmail();
const mc = JSON.parse(Buffer.from(machineCode, 'base64').toString('utf8'));
console.log(`\ndeviceId: ${mc.l}`);
console.log(`fingerprint: ${mc.i}`);
console.log(`version: ${mc.v}`);
spawnSync('killall', ['Typora'], { stdio: 'ignore' });
const nowDateStr = getNowDateStr();
// 一、生成密钥对
console.log('\n一、生成密钥对...');
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
const pubKeyBase64 = publicKey
.replace('-----BEGIN PUBLIC KEY-----', '')
.replace('-----END PUBLIC KEY-----', '')
.replace(/\s/g, '');
const lines = [];
for (let i = 0; i < pubKeyBase64.length; i += 64) {
lines.push(pubKeyBase64.slice(i, i + 64));
}
const formattedPubKey = '-----BEGIN PUBLIC KEY-----\n' + lines.join('\n') + '\n-----END PUBLIC KEY-----';
const newKeyBuf = Buffer.from(formattedPubKey, 'utf8');
const originalKeyBuf = Buffer.from(ORIGINAL_PUB_KEY, 'utf8');
if (newKeyBuf.length !== originalKeyBuf.length) {
console.log(`❌ 公钥长度不匹配: 期望 ${originalKeyBuf.length}, 实际 ${newKeyBuf.length}`);
process.exit(1);
}
console.log(`✅ 密钥对生成成功 (${newKeyBuf.length} 字节)`);
// 二、Patch 二进制
console.log('\n二、Patch 二进制...');
fs.copyFileSync(binPath, bakPath);
let binaryData = fs.readFileSync(binPath);
let offset = 0, replacements = 0;
while (true) {
const idx = binaryData.indexOf(originalKeyBuf, offset);
if (idx === -1) break;
newKeyBuf.copy(binaryData, idx);
offset = idx + newKeyBuf.length;
replacements++;
}
if (replacements === 0) {
console.log('❌ 未在二进制中找到原始公钥,可能版本不兼容');
fs.copyFileSync(bakPath, binPath);
fs.rmSync(bakPath, { force: true });
process.exit(1);
}
fs.writeFileSync(binPath, binaryData);
console.log(`✅ 已替换 ${replacements} 处公钥`);
// 三、生成 Activation Token
console.log('\n三、生成 Activation Token...');
const ACT_ENTITY = {
deviceId: mc.l,
fingerprint: mc.i,
email: email,
license: "Cracked_By_DreamNya&Letr",
version: mc.v,
date: nowDateStr,
type: "DreamNya"
};
const licenseJSON = JSON.stringify(ACT_ENTITY);
const jsonBase64 = Buffer.from(licenseJSON, 'utf8').toString('base64');
// 模拟浏览器 atob:UTF-8 bytes → Latin1 string
const atobResult = Buffer.from(jsonBase64, 'base64').toString('latin1');
const parsedAfterAtob = JSON.parse(atobResult);
// 按 key 字母排序,跳过 sig/oldSig/oldFinger,拼接值
const skipKeys = new Set(['sig', 'oldSig', 'oldFinger']);
const sortedKeys = Object.keys(parsedAfterAtob).filter(k => !skipKeys.has(k)).sort();
const dataToSign = sortedKeys.map(k => String(parsedAfterAtob[k])).join('');
// SHA256 + PKCS1v15 签名
const signature = crypto.sign('SHA256', Buffer.from(dataToSign, 'utf8'), {
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PADDING
});
const activationToken = '+' + jsonBase64 + '|' + signature.toString('base64') + '#';
console.log('✅ Token 生成成功');
// 四、重新签名
console.log('\n四、重新签名应用...');
const signResult = spawnSync('codesign', ['--force', '--deep', '-s', '-', appPath], {
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
});
if (signResult.status !== 0) {
console.log(`⚠️ 签名警告: ${(signResult.stderr || '').trim()}`);
} else {
console.log('✅ 应用重新签名成功');
}
// 完成
console.log('\n🎉 全部完成!');
console.log('\n使用方法:');
console.log('1. 打开 Typora → 帮助 → 我的许可证');
console.log('2. 点击「获取 Activation Token」进入离线激活页面');
console.log('3. 在 Activation Token 框中粘贴以下内容:\n');
console.log(activationToken);
console.log('\n(Token 已复制到剪贴板)');
try { spawnSync('pbcopy', [], { input: activationToken, encoding: 'utf8' }); } catch (e) {}
})();
@Mr-xn

Mr-xn commented May 29, 2026

Copy link
Copy Markdown
Author

运行要求有node.js环境即可,复制保存为mac.js,然后 node mac.js
接下来就是跟着脚本走,打开typora转到离线激活复制机器码粘贴回脚本,自动patch然后重启后复制计算出的token粘贴回typora中激活即可

@Mr-xn

Mr-xn commented May 29, 2026

Copy link
Copy Markdown
Author
#!/usr/bin/env node
/**
 * Typora macOS 激活脚本
 * 原理:替换二进制中的 RSA 公钥,用自己的私钥签名生成 Activation Token
 */

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const readlineSync = require('readline-sync');
const os = require('os');

// ================================
// 配置
// ================================
const CONFIG_FILE = path.join(os.homedir(), '.typora_crack_config.json');
const DEFAULT_APP_PATH = '/Applications/Typora.app';
const ORIGINAL_PUB_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7nVoGCHqIMJyqgALEUrc
5JJhap0+HtJqzPE04pz4y+nrOmY7/12f3HvZyyoRsxKdXTZbO0wEHFIh0cRqsuaJ
PyaOOPbA0BsalofIAY3mRhQQ3vSf+rn3g+w0S+udWmKV9DnmJlpWqizFajU4T/E4
5ZgMNcXt3E1ips32rdbTR0Nnen9PVITvrbJ3l6CI2BFBImZQZ2P8N+LsqfJsqyVV
wDkt3mHAVxV7FZbfYWG+8FDSuKQHaCmvgAtChx9hwl3J6RekkqDVa6GIV13D23LS
qdk0Jb521wFJi/V6QAK6SLBiby5gYN6zQQ5RQpjXtR53MwzTdiAzGEuKdOtrY2Me
DwIDAQAB
-----END PUBLIC KEY-----`;

// ================================
// 工具函数
// ================================
function getBinaryPath(appPath) {
    return path.join(appPath, 'Contents', 'MacOS', 'Typora');
}

function readCache() {
    try {
        if (fs.existsSync(CONFIG_FILE)) {
            const data = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
            if (data.path && fs.existsSync(getBinaryPath(data.path))) {
                return data;
            }
        }
    } catch (e) {}
    return { path: null, machineCode: null, email: null };
}

function saveCache(obj) {
    try { fs.writeFileSync(CONFIG_FILE, JSON.stringify(obj, null, 2), 'utf8'); } catch (e) {}
}

function getNowDateStr() {
    const now = new Date();
    const dd = String(now.getDate()).padStart(2, '0');
    const mm = String(now.getMonth() + 1).padStart(2, '0');
    return `${mm}/${dd}/${now.getFullYear()}`;
}

// ================================
// 选择 Typora 路径
// ================================
function chooseTyporaPath() {
    const cache = readCache();

    // 1. 缓存
    if (cache.path) {
        console.log(`\n✅ 检测到缓存路径: ${cache.path}`);
        const use = readlineSync.question('是否使用此路径?[Y/n]:').trim().toLowerCase();
        if (use !== 'n' && use !== 'no') return cache.path;
    }

    // 2. 默认路径
    if (fs.existsSync(getBinaryPath(DEFAULT_APP_PATH))) {
        console.log(`\n✅ 检测到默认路径: ${DEFAULT_APP_PATH}`);
        const use = readlineSync.question('是否使用此路径?[Y/n]:').trim().toLowerCase();
        if (use !== 'n' && use !== 'no') {
            saveCache({ ...cache, path: DEFAULT_APP_PATH });
            return DEFAULT_APP_PATH;
        }
    }

    // 3. 文件夹选择
    try {
        const output = spawnSync('osascript', ['-e',
            'POSIX path of (choose folder with prompt "请选择 Typora.app 所在目录")'
        ], { encoding: 'utf8' }).stdout.trim();
        if (output) {
            let p = path.resolve(output.replace(/\/$/, ''));
            if (!p.endsWith('.app')) p = path.join(p, 'Typora.app');
            if (fs.existsSync(getBinaryPath(p))) {
                saveCache({ ...cache, path: p });
                return p;
            }
        }
    } catch (e) {}

    // 4. 手动输入
    while (true) {
        const input = readlineSync.question('请输入 Typora.app 路径:').trim();
        if (!input) continue;
        const p = path.resolve(input);
        if (fs.existsSync(getBinaryPath(p))) {
            saveCache({ ...cache, path: p });
            return p;
        }
        console.log('❌ 路径无效,未找到 Typora 二进制');
    }
}

// ================================
// 获取机器码和邮箱
// ================================
function getMachineCodeAndEmail() {
    const cache = readCache();

    if (cache.machineCode && cache.email) {
        console.log(`\n✅ 检测到缓存的激活信息:`);
        console.log(`   机器码: ${cache.machineCode.substring(0, 30)}...`);
        console.log(`   邮箱: ${cache.email}`);
        const use = readlineSync.question('是否使用此信息?[Y/n]:').trim().toLowerCase();
        if (use !== 'n' && use !== 'no') {
            return { machineCode: cache.machineCode, email: cache.email };
        }
    }

    let machineCode, email;
    while (true) {
        machineCode = readlineSync.question('\n请输入机器码: ').trim();
        email = readlineSync.question('请输入邮箱: ').trim();
        if (!machineCode) { console.log('❌ 机器码不能为空'); continue; }
        if (!email) { console.log('❌ 邮箱不能为空'); continue; }
        break;
    }

    saveCache({ ...cache, machineCode, email });
    return { machineCode, email };
}

// ================================
// 回滚还原
// ================================
function rollback(appPath) {
    console.log('\n🔄 开始回滚还原...');
    const binPath = getBinaryPath(appPath);
    const bakPath = binPath + '.bak';

    spawnSync('killall', ['Typora'], { stdio: 'ignore' });

    if (fs.existsSync(bakPath)) {
        fs.copyFileSync(bakPath, binPath);
        fs.rmSync(bakPath, { force: true });
        console.log('✅ 已还原 Typora 二进制');
    } else {
        console.log('⚠️  未找到备份文件,无需回滚');
        return;
    }

    spawnSync('defaults', ['delete', 'abnerworks.Typora', 'typora-license'], { stdio: 'ignore' });
    spawnSync('codesign', ['--force', '--deep', '-s', '-', appPath], { stdio: 'ignore' });
    console.log('✅ 已清理授权信息并重新签名');
    console.log('\n🎉 回滚完成!');
}

// ================================
// 主流程
// ================================
(function main() {
    process.stdout.write('\x1Bc');
    console.log('=================================');
    console.log('    Typora macOS 激活脚本');
    console.log('=================================');

    // 选择路径
    const appPath = chooseTyporaPath();
    const binPath = getBinaryPath(appPath);
    const bakPath = binPath + '.bak';

    // 检查是否已 patch
    if (fs.existsSync(bakPath)) {
        console.log('\n⚠️  检测到已激活过(二进制已 patch)');
        const choice = readlineSync.question('[R]回滚还原 / [P]重新Patch / [Q]退出:').trim().toUpperCase();
        if (choice === 'R') { rollback(appPath); process.exit(0); }
        if (choice !== 'P') { process.exit(0); }
        // 还原后重新 patch
        console.log('还原原始二进制...');
        fs.copyFileSync(bakPath, binPath);
        fs.rmSync(bakPath, { force: true });
        spawnSync('defaults', ['delete', 'abnerworks.Typora', 'typora-license'], { stdio: 'ignore' });
    }

    // 获取机器码
    const { machineCode, email } = getMachineCodeAndEmail();
    const mc = JSON.parse(Buffer.from(machineCode, 'base64').toString('utf8'));
    console.log(`\ndeviceId: ${mc.l}`);
    console.log(`fingerprint: ${mc.i}`);
    console.log(`version: ${mc.v}`);

    // 关闭 Typora
    spawnSync('killall', ['Typora'], { stdio: 'ignore' });

    const nowDateStr = getNowDateStr();

    // ====== 一、生成密钥对 ======
    console.log('\n一、生成密钥对...');
    const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
        modulusLength: 2048,
        publicKeyEncoding: { type: 'spki', format: 'pem' },
        privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
    });

    // 格式化公钥为与原始公钥相同的字节长度
    const pubKeyBase64 = publicKey
        .replace('-----BEGIN PUBLIC KEY-----', '')
        .replace('-----END PUBLIC KEY-----', '')
        .replace(/\s/g, '');
    const lines = [];
    for (let i = 0; i < pubKeyBase64.length; i += 64) {
        lines.push(pubKeyBase64.slice(i, i + 64));
    }
    const formattedPubKey = '-----BEGIN PUBLIC KEY-----\n' + lines.join('\n') + '\n-----END PUBLIC KEY-----';
    const newKeyBuf = Buffer.from(formattedPubKey, 'utf8');
    const originalKeyBuf = Buffer.from(ORIGINAL_PUB_KEY, 'utf8');

    if (newKeyBuf.length !== originalKeyBuf.length) {
        console.log(`❌ 公钥长度不匹配: 期望 ${originalKeyBuf.length}, 实际 ${newKeyBuf.length}`);
        process.exit(1);
    }
    console.log(`✅ 密钥对生成成功 (${newKeyBuf.length} 字节)`);

    // ====== 二、Patch 二进制 ======
    console.log('\n二、Patch 二进制...');
    fs.copyFileSync(binPath, bakPath);

    let binaryData = fs.readFileSync(binPath);
    let offset = 0, replacements = 0;
    while (true) {
        const idx = binaryData.indexOf(originalKeyBuf, offset);
        if (idx === -1) break;
        newKeyBuf.copy(binaryData, idx);
        offset = idx + newKeyBuf.length;
        replacements++;
    }

    if (replacements === 0) {
        console.log('❌ 未在二进制中找到原始公钥,可能版本不兼容');
        fs.copyFileSync(bakPath, binPath);
        fs.rmSync(bakPath, { force: true });
        process.exit(1);
    }

    // 同时 patch 验证服务器 URL,防止 renew 请求导致掉激活
    const urlReplacements = [
        { from: 'https://store.typora.io', to: 'https://127.0.0.1' },
        { from: 'https://dian.typora.com.cn', to: 'https://127.0.0.1' }
    ];
    let urlPatched = 0;
    for (const { from, to } of urlReplacements) {
        const fromBuf = Buffer.from(from, 'utf8');
        const toBuf = Buffer.alloc(fromBuf.length, 0); // null 填充
        Buffer.from(to, 'utf8').copy(toBuf);
        let off = 0;
        while (true) {
            const idx = binaryData.indexOf(fromBuf, off);
            if (idx === -1) break;
            toBuf.copy(binaryData, idx);
            off = idx + toBuf.length;
            urlPatched++;
        }
    }

    fs.writeFileSync(binPath, binaryData);
    console.log(`✅ 已替换 ${replacements} 处公钥, ${urlPatched} 处服务器地址`);

    // ====== 三、生成 Activation Token ======
    console.log('\n三、生成 Activation Token...');

    const ACT_ENTITY = {
        deviceId: mc.l,
        fingerprint: mc.i,
        email: email,
        license: "Cracked_By_DreamNya&Letr",
        version: mc.v,
        date: nowDateStr,
        type: "DreamNya"
    };

    // Token 格式: +base64(JSON)|signature#
    // 关键:浏览器 atob() 把 UTF-8 多字节字符按 Latin1 解码
    // 原生验证时用的是 atob 乱码后的值,所以签名必须基于乱码后的数据
    const licenseJSON = JSON.stringify(ACT_ENTITY);
    const jsonBase64 = Buffer.from(licenseJSON, 'utf8').toString('base64');

    // 模拟浏览器 atob:UTF-8 bytes → Latin1 string
    const atobResult = Buffer.from(jsonBase64, 'base64').toString('latin1');
    const parsedAfterAtob = JSON.parse(atobResult);

    // 按 key 字母排序,跳过 sig/oldSig/oldFinger,拼接值
    const skipKeys = new Set(['sig', 'oldSig', 'oldFinger']);
    const sortedKeys = Object.keys(parsedAfterAtob).filter(k => !skipKeys.has(k)).sort();
    const dataToSign = sortedKeys.map(k => String(parsedAfterAtob[k])).join('');

    // SHA256 + PKCS1v15 签名
    const signature = crypto.sign('SHA256', Buffer.from(dataToSign, 'utf8'), {
        key: privateKey,
        padding: crypto.constants.RSA_PKCS1_PADDING
    });

    const activationToken = '+' + jsonBase64 + '|' + signature.toString('base64') + '#';
    console.log('✅ Token 生成成功');

    // ====== 四、重新签名 ======
    console.log('\n四、重新签名应用...');
    const signResult = spawnSync('codesign', ['--force', '--deep', '-s', '-', appPath], {
        encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
    });
    if (signResult.status !== 0) {
        console.log(`⚠️  签名警告: ${(signResult.stderr || '').trim()}`);
    } else {
        console.log('✅ 应用重新签名成功');
    }

    // ====== 完成 ======
    console.log('\n🎉 全部完成!');
    console.log('\n使用方法:');
    console.log('1. 打开 Typora → 帮助 → 我的许可证');
    console.log('2. 点击「获取 Activation Token」进入离线激活页面');
    console.log('3. 在 Activation Token 框中粘贴以下内容:\n');
    console.log(activationToken);
    console.log('\n(Token 已复制到剪贴板)');

    try { spawnSync('pbcopy', [], { input: activationToken, encoding: 'utf8' }); } catch (e) {}
})();

同时patch了renew相关的url,所有renew请求全部发往127.0.0.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment