同步 enhance/bridge/smoke/retire 模块、vendor 3.3.34、增强面板与 PRD 文档, Hub 接入 hub-routes-prd,含安装/经验/模块说明与 co-integration 运维脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
219 lines
6.7 KiB
JavaScript
219 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* W7 补丁编排:恢复 co-chat storage → Cursor 未运行时 headless 打补丁;
|
||
* Cursor 运行中则排队(不强杀),Hub watcher 在退出后自动续跑。
|
||
*/
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { spawn, spawnSync } from 'node:child_process';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
const HOME = process.env.HOME || '';
|
||
const PCHAT_ROOT = path.join(HOME, '.persistent-chat-local');
|
||
const PENDING_FILE = path.join(PCHAT_ROOT, 'pending-w7.json');
|
||
const COCHAT_STORAGE = path.join(
|
||
HOME,
|
||
'Library/Application Support/Cursor/User/globalStorage/co-chat.co-chat-panel/storage.json',
|
||
);
|
||
const CURSOR_APP = '/Applications/Cursor.app';
|
||
const WORKBENCH = path.join(CURSOR_APP, 'Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js');
|
||
const EXTHOST = path.join(CURSOR_APP, 'Contents/Resources/app/out/vs/workbench/api/node/extensionHostProcess.js');
|
||
const PATCH_ACTIVATE = path.join(__dirname, 'patch-activate.mjs');
|
||
|
||
const PATCH_FLAGS = {
|
||
'kc.corePatchBundle': true,
|
||
'kc.agentSortPerfPatch': true,
|
||
'kc.seamlessPatch': true,
|
||
'kc.nameTabHook': true,
|
||
'kc.noQuotaMcp': true,
|
||
'kc.extendStallTimeout': true,
|
||
'kc.mcpSelfHealing': true,
|
||
'kc.agentRetries': true,
|
||
'kc.endlessRetries': true,
|
||
'kc.extensionProtect': true,
|
||
'kc.stripeOverride': true,
|
||
'kc.disableTelemetry': true,
|
||
'kc.cursorCleanAuto': true,
|
||
'kc.bgKeepalive': true,
|
||
'kc.loopReminderHook': true,
|
||
'kc.noQuotaTogglePort': true,
|
||
'kc.disableCursorUpdate': true,
|
||
};
|
||
|
||
function emit(obj) {
|
||
console.log(JSON.stringify(obj));
|
||
}
|
||
|
||
function readJson(p, fb = {}) {
|
||
try {
|
||
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
||
} catch {
|
||
return fb;
|
||
}
|
||
}
|
||
|
||
function writeJson(p, obj) {
|
||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||
fs.writeFileSync(p, JSON.stringify(obj, null, 2));
|
||
}
|
||
|
||
function verifyMarkers() {
|
||
const wb = fs.existsSync(WORKBENCH) ? fs.readFileSync(WORKBENCH, 'utf8') : '';
|
||
const eh = fs.existsSync(EXTHOST) ? fs.readFileSync(EXTHOST, 'utf8') : '';
|
||
const checks = [
|
||
{ id: 'noQuotaMcp', start: 'CO_NO_QUOTA_MCP_V1_START', src: wb },
|
||
{ id: 'composerBridge', start: 'CO_COMPOSER_BRIDGE_START', src: wb },
|
||
{ id: 'exthost', start: 'CO_NO_QUOTA_EXTHOST_V1_START', src: eh },
|
||
];
|
||
const found = checks.filter((m) => m.src.includes(m.start)).map((m) => m.id);
|
||
return {
|
||
patchOk: found.length >= 3,
|
||
found,
|
||
total: checks.length,
|
||
details: checks.map((m) => ({ id: m.id, start: m.start, found: m.src.includes(m.start) })),
|
||
};
|
||
}
|
||
|
||
function isCursorRunning() {
|
||
const r = spawnSync('ps', ['aux'], { encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 });
|
||
return /\/Applications\/Cursor\.app\/Contents\/MacOS\/Cursor(\s|$)/.test(r.stdout || '');
|
||
}
|
||
|
||
function restoreCoChatStorage() {
|
||
const now = new Date().toISOString();
|
||
const cur = readJson(COCHAT_STORAGE, {});
|
||
const merged = {
|
||
...cur,
|
||
...PATCH_FLAGS,
|
||
'coMcp.aclSetupComplete': true,
|
||
'coMcp.aclGrantedPath': WORKBENCH,
|
||
'kc.noQuotaMcp.lastMode': cur['kc.noQuotaMcp.lastMode'] || 'extremely_low',
|
||
'kc.noQuotaMcp.lastEnabled': true,
|
||
'kc.noQuotaMcp.lastExtVersion': cur['kc.noQuotaMcp.lastExtVersion'] || '3.3.34',
|
||
'kc.noQuotaMcp.reloadStamps': now,
|
||
'kc.noQuotaMcp.reloadReasonTs': now,
|
||
'kc.corePatchBundle.reapplyPending': now,
|
||
};
|
||
writeJson(COCHAT_STORAGE, merged);
|
||
return { storagePath: COCHAT_STORAGE, keys: Object.keys(PATCH_FLAGS).length };
|
||
}
|
||
|
||
function queuePending(reason) {
|
||
writeJson(PENDING_FILE, { queuedAt: new Date().toISOString(), reason });
|
||
}
|
||
|
||
function clearPending() {
|
||
try {
|
||
fs.unlinkSync(PENDING_FILE);
|
||
} catch {}
|
||
}
|
||
|
||
function runPatchActivate(timeoutMs = 28000) {
|
||
return new Promise((resolve) => {
|
||
if (!fs.existsSync(PATCH_ACTIVATE)) {
|
||
resolve({ ok: false, error: 'patch-activate.mjs 未找到' });
|
||
return;
|
||
}
|
||
const child = spawn(process.execPath, [PATCH_ACTIVATE, 'apply'], {
|
||
env: { ...process.env, CO_SKIP_LICENSE: '1', PCHAT_NO_LICENSE: '1' },
|
||
});
|
||
let out = '';
|
||
let err = '';
|
||
const timer = setTimeout(() => {
|
||
try {
|
||
child.kill('SIGTERM');
|
||
} catch {}
|
||
}, timeoutMs);
|
||
child.stdout.on('data', (c) => {
|
||
out += c;
|
||
});
|
||
child.stderr.on('data', (c) => {
|
||
err += c;
|
||
});
|
||
child.on('close', () => {
|
||
clearTimeout(timer);
|
||
let parsed = null;
|
||
try {
|
||
parsed = JSON.parse(out.trim().split('\n').filter(Boolean).pop());
|
||
} catch {}
|
||
if (parsed) return resolve(parsed);
|
||
resolve({ ok: false, stdout: out.slice(-2000), stderr: err.slice(-2000) });
|
||
});
|
||
});
|
||
}
|
||
|
||
async function applyPatchFlow() {
|
||
const before = verifyMarkers();
|
||
if (before.patchOk) {
|
||
clearPending();
|
||
return { ok: true, ...before, method: 'already', hint: 'W8 已通过' };
|
||
}
|
||
|
||
const storage = restoreCoChatStorage();
|
||
|
||
if (isCursorRunning()) {
|
||
queuePending('cursor_running');
|
||
return {
|
||
ok: false,
|
||
queued: true,
|
||
needsQuit: true,
|
||
needsCoChatToggle: true,
|
||
storage,
|
||
...before,
|
||
method: 'queued',
|
||
hint:
|
||
'已写入 co-chat 补丁配置。请在 Cursor 内:co-chat 面板 → 设置 → 关闭再打开「核心增强包」→ 再 Cmd+Q 完全退出 → 重开 Cursor → Hub 点「诊断」。Hub 不会强杀 Cursor。',
|
||
};
|
||
}
|
||
|
||
const result = await runPatchActivate();
|
||
const after = verifyMarkers();
|
||
const ok = after.patchOk || result.patchOk;
|
||
if (ok) clearPending();
|
||
else queuePending('patch_activate_failed');
|
||
return {
|
||
ok,
|
||
queued: !ok,
|
||
storage,
|
||
patchResult: result,
|
||
...after,
|
||
method: ok ? 'patch-activate' : 'patch-activate-failed',
|
||
hint: ok
|
||
? 'W8 OK — 请重新打开 Cursor 使补丁生效'
|
||
: result.hint ||
|
||
'补丁未写入:请 Cmd+Q 退出 Cursor 后 Hub 将自动重试,或重开 Cursor 让 co-chat 扩展激活时打补丁',
|
||
};
|
||
}
|
||
|
||
async function tryPendingPatch() {
|
||
if (!fs.existsSync(PENDING_FILE)) return null;
|
||
if (isCursorRunning()) return { waiting: true, reason: 'cursor_still_running' };
|
||
restoreCoChatStorage();
|
||
const result = await runPatchActivate();
|
||
const after = verifyMarkers();
|
||
if (after.patchOk) {
|
||
clearPending();
|
||
return { ok: true, applied: true, ...after, patchResult: result };
|
||
}
|
||
return { ok: false, applied: false, ...after, patchResult: result };
|
||
}
|
||
|
||
async function main() {
|
||
const mode = process.argv[2] || 'apply';
|
||
if (mode === 'verify') {
|
||
emit({ ok: true, ...verifyMarkers(), method: 'verify' });
|
||
return;
|
||
}
|
||
if (mode === 'pending') {
|
||
emit({ ...(await tryPendingPatch()), method: 'pending' });
|
||
return;
|
||
}
|
||
emit(await applyPatchFlow());
|
||
}
|
||
|
||
main().catch((e) => {
|
||
emit({ ok: false, error: String(e.stack || e.message || e) });
|
||
process.exitCode = 1;
|
||
});
|