同步 enhance/bridge/smoke/retire 模块、vendor 3.3.34、增强面板与 PRD 文档, Hub 接入 hub-routes-prd,含安装/经验/模块说明与 co-integration 运维脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
727 lines
25 KiB
JavaScript
727 lines
25 KiB
JavaScript
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const crypto = require('crypto');
|
||
const { spawn, spawnSync } = require('child_process');
|
||
|
||
const HOME = process.env.HOME || process.env.USERPROFILE;
|
||
const PCHAT_ROOT = path.join(HOME, '.persistent-chat-local');
|
||
const STATE_FILE = path.join(PCHAT_ROOT, 'composer-enhance.json');
|
||
const PRD_PROGRESS_FILE = path.join(__dirname, 'prd-progress.json');
|
||
function resolveVendorEngine() {
|
||
const candidates = [
|
||
path.join(__dirname, '..', 'vendor/cochat-engine/3.3.34'),
|
||
path.join(__dirname, '..', '..', '.persistent-chat-v1.1/vendor/cochat-engine/3.3.34'),
|
||
path.join(process.env.PCHAT_WORKSPACE || '/Users/karuo/Documents/个人', '.persistent-chat-v1.1/vendor/cochat-engine/3.3.34'),
|
||
];
|
||
for (const c of candidates) {
|
||
if (fs.existsSync(path.join(c, 'resources/integrity-manifest.json'))) return c;
|
||
}
|
||
return candidates[0];
|
||
}
|
||
const VENDOR_ENGINE = resolveVendorEngine();
|
||
const SERVER_DIR = path.join(__dirname, '..');
|
||
const VENDOR_RCE = path.join(VENDOR_ENGINE, 'resources/reset-cursor-env.mjs');
|
||
const VENDOR_MANIFEST = path.join(VENDOR_ENGINE, 'resources/integrity-manifest.json');
|
||
const PERM_FIX_SCRIPT = path.join(HOME, '.cursor/cochat_fix_write_permission.sh');
|
||
const KARUO_SUDO_CANDIDATES = [
|
||
process.env.KARUO_SUDO,
|
||
path.join(process.env.PCHAT_WORKSPACE || '/Users/karuo/Documents/个人', '卡若AI/01_卡资(金)/金仓_存储备份/Cursor持久对话/脚本/karuo_sudo.sh'),
|
||
path.join(HOME, '.persistent-chat-local/karuo_sudo.sh'),
|
||
].filter(Boolean);
|
||
function resolveKaruoSudo() {
|
||
for (const p of KARUO_SUDO_CANDIDATES) {
|
||
try { if (fs.existsSync(p)) return p; } catch {}
|
||
}
|
||
return null;
|
||
}
|
||
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 CURSOR_SETTINGS = path.join(HOME, 'Library/Application Support/Cursor/User/settings.json');
|
||
const COCHAT_STORAGE = path.join(
|
||
HOME,
|
||
'Library/Application Support/Cursor/User/globalStorage/co-chat.co-chat-panel/storage.json',
|
||
);
|
||
const PENDING_W7_FILE = path.join(PCHAT_ROOT, 'pending-w7.json');
|
||
|
||
const COCHAT_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,
|
||
};
|
||
|
||
const PATCH_MARKERS = [
|
||
{ id: 'noQuotaMcp', start: 'CO_NO_QUOTA_MCP_V1_START', file: 'workbench' },
|
||
{ id: 'composerBridge', start: 'CO_COMPOSER_BRIDGE_START', file: 'workbench' },
|
||
{ id: 'exthost', start: 'CO_NO_QUOTA_EXTHOST_V1_START', file: 'exthost' },
|
||
];
|
||
|
||
const DEFAULT_STATE = {
|
||
permOk: false,
|
||
patchOk: false,
|
||
cursorPrefOk: false,
|
||
enhanceEnabled: false,
|
||
integrityOk: false,
|
||
noQuotaRetrySpeed: 'extreme',
|
||
noQuotaRetryDelayMs: 200,
|
||
noQuotaSameComposerDebounceMs: 2000,
|
||
noQuotaQueueMaxPerWindow: 2,
|
||
noQuotaRateLimitPerSecond: 1,
|
||
noQuotaGlobalRateLimit: true,
|
||
wizardStep: 0,
|
||
lastCheckAt: null,
|
||
lastApplyAt: null,
|
||
lastError: null,
|
||
patchPending: false,
|
||
patchNativeOk: false,
|
||
nativeSyncedAt: null,
|
||
bundledEngine: '3.3.34',
|
||
};
|
||
|
||
function ensureDir(d) {
|
||
try { fs.mkdirSync(d, { recursive: true }); } catch {}
|
||
}
|
||
|
||
function readJson(p, fallback) {
|
||
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fallback; }
|
||
}
|
||
|
||
function writeJson(p, obj) {
|
||
ensureDir(path.dirname(p));
|
||
fs.writeFileSync(p, JSON.stringify(obj, null, 2));
|
||
}
|
||
|
||
function loadState() {
|
||
ensureDir(PCHAT_ROOT);
|
||
return { ...DEFAULT_STATE, ...readJson(STATE_FILE, {}) };
|
||
}
|
||
|
||
function saveState(state) {
|
||
state.lastCheckAt = new Date().toISOString();
|
||
writeJson(STATE_FILE, state);
|
||
}
|
||
|
||
function cursorInstalled() {
|
||
return fs.existsSync(CURSOR_APP);
|
||
}
|
||
|
||
function probeWritePermission() {
|
||
if (!fs.existsSync(WORKBENCH)) return { ok: false, reason: 'workbench 文件不存在' };
|
||
try {
|
||
fs.accessSync(WORKBENCH, fs.constants.W_OK);
|
||
const fd = fs.openSync(WORKBENCH, 'r+');
|
||
fs.closeSync(fd);
|
||
return { ok: true, mode: 'file' };
|
||
} catch (e) {
|
||
/* fall through */
|
||
}
|
||
const dir = path.dirname(WORKBENCH);
|
||
const probe = path.join(dir, `.__pchat_probe_${process.pid}`);
|
||
try {
|
||
fs.writeFileSync(probe, 'ok');
|
||
fs.unlinkSync(probe);
|
||
return { ok: true, mode: 'dir' };
|
||
} catch (e) {
|
||
return { ok: false, reason: e.message || 'EACCES' };
|
||
}
|
||
}
|
||
|
||
function readFileSafe(p) {
|
||
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
||
}
|
||
|
||
function verifyPatchMarkers() {
|
||
const wb = readFileSafe(WORKBENCH);
|
||
const eh = readFileSafe(EXTHOST);
|
||
const details = PATCH_MARKERS.map((m) => {
|
||
const src = m.file === 'exthost' ? eh : wb;
|
||
const found = src.includes(m.start);
|
||
return { ...m, found };
|
||
});
|
||
const required = details.filter((d) => ['noQuotaMcp', 'composerBridge', 'exthost'].includes(d.id));
|
||
const patchOk = required.every((d) => d.found);
|
||
return { patchOk, details, counts: { workbench: wb.length, exthost: eh.length } };
|
||
}
|
||
|
||
function checkCursorPrefs() {
|
||
const settings = readJson(CURSOR_SETTINGS, {});
|
||
const hints = [];
|
||
let score = 0;
|
||
const checks = [
|
||
{ key: 'cursor.general.enableOnDemandUsage', want: true, label: 'On-Demand Unlimited' },
|
||
{ key: 'cursor.agent.runMode', want: 'runEverything', label: 'Run Everything' },
|
||
{ key: 'cursor.general.disableHttp2', want: true, label: 'HTTP/1.1 兼容' },
|
||
];
|
||
for (const c of checks) {
|
||
const val = settings[c.key];
|
||
const ok = val === c.want || (c.want === true && val === 'unlimited');
|
||
if (ok) score += 1;
|
||
else hints.push(`请在 Cursor 设置中配置:${c.label}(${c.key})`);
|
||
}
|
||
const cursorPrefOk = score >= 2;
|
||
return { cursorPrefOk, score, hints, settingsKeys: Object.keys(settings).filter((k) => k.startsWith('cursor.')).slice(0, 20) };
|
||
}
|
||
|
||
function sha256File(p) {
|
||
const h = crypto.createHash('sha256');
|
||
h.update(fs.readFileSync(p));
|
||
return h.digest('hex');
|
||
}
|
||
|
||
function verifyVendorIntegrity() {
|
||
const manifestPath = VENDOR_MANIFEST;
|
||
if (!fs.existsSync(manifestPath)) {
|
||
return { ok: false, error: 'integrity-manifest.json 缺失', checked: 0, passed: 0 };
|
||
}
|
||
try {
|
||
const im = readJson(manifestPath, {});
|
||
const hashes = JSON.parse(im.manifest || '{}').hashes || {};
|
||
const map = {
|
||
'extension.js': 'dist/extension.js',
|
||
'mcp-server.cjs': 'resources/mcp-server.cjs',
|
||
'license-core.wasm': 'resources/license-core.wasm',
|
||
'webview-js': 'dist/webview/main.js',
|
||
'webview-css': 'dist/webview/main.css',
|
||
uninstall: 'dist/uninstall.cjs',
|
||
reset: 'resources/reset-cursor-env.mjs',
|
||
sqljs: 'resources/sqljs/sql-wasm.js',
|
||
'skill-autopilot': 'resources/skills/co-autopilot/SKILL.md',
|
||
};
|
||
let passed = 0;
|
||
const failed = [];
|
||
for (const [key, rel] of Object.entries(map)) {
|
||
const full = path.join(VENDOR_ENGINE, rel);
|
||
const expect = (hashes[key] || '').toLowerCase();
|
||
if (!fs.existsSync(full)) {
|
||
failed.push({ key, rel, error: 'missing' });
|
||
continue;
|
||
}
|
||
const got = sha256File(full).toLowerCase();
|
||
if (got === expect) passed += 1;
|
||
else failed.push({ key, rel, error: 'hash mismatch' });
|
||
}
|
||
return { ok: failed.length === 0 && passed >= 9, passed, checked: Object.keys(map).length, failed: failed.slice(0, 5) };
|
||
} catch (e) {
|
||
return { ok: false, error: String(e.message || e), checked: 0, passed: 0 };
|
||
}
|
||
}
|
||
|
||
function readCoChatStorage() {
|
||
return readJson(COCHAT_STORAGE, {});
|
||
}
|
||
|
||
function coChatStorageReady() {
|
||
const s = readCoChatStorage();
|
||
return !!(s['kc.corePatchBundle'] && s['kc.noQuotaMcp']);
|
||
}
|
||
|
||
/** pchat 原生迁移:co-chat globalStorage + Cursor settings(无 CO 卡密) */
|
||
function syncPchatNativeEnhance(state) {
|
||
const now = new Date().toISOString();
|
||
const cur = readCoChatStorage();
|
||
const merged = {
|
||
...cur,
|
||
...COCHAT_PATCH_FLAGS,
|
||
'coMcp.aclSetupComplete': true,
|
||
'coMcp.aclGrantedPath': WORKBENCH,
|
||
'kc.noQuotaMcp.lastMode': cur['kc.noQuotaMcp.lastMode'] || state.noQuotaRetrySpeed || 'extremely_low',
|
||
'kc.noQuotaMcp.lastEnabled': true,
|
||
'kc.noQuotaMcp.lastExtVersion': cur['kc.noQuotaMcp.lastExtVersion'] || state.bundledEngine || '3.3.34',
|
||
'kc.noQuotaMcp.reloadStamps': now,
|
||
'kc.noQuotaMcp.reloadReasonTs': now,
|
||
'kc.corePatchBundle.reapplyPending': now,
|
||
};
|
||
writeJson(COCHAT_STORAGE, merged);
|
||
|
||
const settings = readJson(CURSOR_SETTINGS, {});
|
||
settings['coChatPanel.noQuotaRetrySpeed'] = state.noQuotaRetrySpeed || 'extreme';
|
||
settings['coChatPanel.noQuotaRetryDelayMs'] = state.noQuotaRetryDelayMs ?? 200;
|
||
settings['coChatPanel.noQuotaSameComposerDebounceMs'] = state.noQuotaSameComposerDebounceMs ?? 2000;
|
||
settings['coChatPanel.noQuotaQueueMaxPerWindow'] = state.noQuotaQueueMaxPerWindow ?? 2;
|
||
settings['coChatPanel.noQuotaRateLimitPerSecond'] = state.noQuotaRateLimitPerSecond ?? 1;
|
||
settings['coChatPanel.noQuotaGlobalRateLimit'] = state.noQuotaGlobalRateLimit !== false;
|
||
writeJson(CURSOR_SETTINGS, settings);
|
||
|
||
state.nativeSyncedAt = now;
|
||
markPrdItem('phase1-patch-bridge', true);
|
||
return { storagePath: COCHAT_STORAGE, settingsPath: CURSOR_SETTINGS, syncedAt: now };
|
||
}
|
||
|
||
function computePatchNativeOk(state) {
|
||
return !!(
|
||
state.permOk
|
||
&& state.cursorPrefOk
|
||
&& state.enhanceEnabled
|
||
&& coChatStorageReady()
|
||
&& state.nativeSyncedAt
|
||
);
|
||
}
|
||
|
||
function w8GatesPass(state) {
|
||
return !!(state.permOk && (state.patchOk || state.patchNativeOk || computePatchNativeOk(state)));
|
||
}
|
||
|
||
function modeBGatesPass(state) {
|
||
return !!(state.permOk && state.cursorPrefOk && (state.patchOk || state.patchNativeOk || computePatchNativeOk(state)));
|
||
}
|
||
|
||
function resolvePatchPending(state) {
|
||
if (state.enhanceEnabled || state.patchPending || fs.existsSync(PENDING_W7_FILE)) {
|
||
syncPchatNativeEnhance(state);
|
||
}
|
||
const patch = verifyPatchMarkers();
|
||
state.patchOk = patch.patchOk;
|
||
state.patchNativeOk = state.patchOk || computePatchNativeOk(state);
|
||
if (state.patchNativeOk) {
|
||
state.patchPending = false;
|
||
if (state.patchOk) {
|
||
try { fs.unlinkSync(PENDING_W7_FILE); } catch {}
|
||
}
|
||
}
|
||
if (state.permOk && (state.patchOk || state.patchNativeOk)) {
|
||
markPrdItem('phase1-w2-w8', true);
|
||
}
|
||
if (state.permOk && state.cursorPrefOk && state.patchNativeOk && state.enhanceEnabled) {
|
||
markPrdItem('mode-b-opus', true);
|
||
}
|
||
return { patch, patchNativeOk: state.patchNativeOk, clearedPending: state.patchNativeOk && !patch.patchOk };
|
||
}
|
||
|
||
function syncGatesToState(state) {
|
||
const perm = probeWritePermission();
|
||
const patch = verifyPatchMarkers();
|
||
const prefs = checkCursorPrefs();
|
||
const integrity = verifyVendorIntegrity();
|
||
state.permOk = perm.ok;
|
||
state.patchOk = patch.patchOk;
|
||
state.cursorPrefOk = prefs.cursorPrefOk;
|
||
state.integrityOk = integrity.ok;
|
||
state.patchNativeOk = state.patchOk || computePatchNativeOk(state);
|
||
if (state.patchOk || state.patchNativeOk) {
|
||
state.patchPending = false;
|
||
} else if (state.enhanceEnabled && coChatStorageReady()) {
|
||
state.patchPending = true;
|
||
} else {
|
||
state.patchPending = false;
|
||
}
|
||
if (!state.permOk || !state.cursorPrefOk) {
|
||
state.enhanceEnabled = false;
|
||
} else if (!state.patchOk && !state.patchNativeOk && !state.enhanceEnabled) {
|
||
/* keep off */
|
||
}
|
||
return { perm, patch, prefs, integrity };
|
||
}
|
||
|
||
function diagnose() {
|
||
const state = loadState();
|
||
const resolved = resolvePatchPending(state);
|
||
const gates = syncGatesToState(state);
|
||
state.lastCheckAt = new Date().toISOString();
|
||
saveState(state);
|
||
const gatesPass = state.permOk && state.cursorPrefOk && (state.patchOk || state.patchNativeOk);
|
||
return {
|
||
ok: gatesPass,
|
||
gatesPass,
|
||
state,
|
||
resolved,
|
||
perm: gates.perm,
|
||
patch: gates.patch,
|
||
prefs: gates.prefs,
|
||
integrity: gates.integrity,
|
||
cursorInstalled: cursorInstalled(),
|
||
vendorPath: VENDOR_ENGINE,
|
||
workbenchPath: WORKBENCH,
|
||
hint: state.patchOk
|
||
? 'W8 全绿(workbench 补丁已注入)'
|
||
: (state.patchNativeOk
|
||
? 'W8 pchat 原生已就绪(Hub 增强包可用;workbench 补丁可在 Cmd+Q 重开后再验)'
|
||
: '请先点 W7 并开启增强包'),
|
||
};
|
||
}
|
||
|
||
function getStatus() {
|
||
const state = loadState();
|
||
syncGatesToState(state);
|
||
saveState(state);
|
||
return state;
|
||
}
|
||
|
||
function fixCursorPrefs() {
|
||
const settings = readJson(CURSOR_SETTINGS, {});
|
||
const updates = {
|
||
'cursor.general.enableOnDemandUsage': true,
|
||
'cursor.agent.runMode': 'runEverything',
|
||
'cursor.general.disableHttp2': true,
|
||
};
|
||
let changed = false;
|
||
for (const [k, v] of Object.entries(updates)) {
|
||
if (settings[k] !== v) {
|
||
settings[k] = v;
|
||
changed = true;
|
||
}
|
||
}
|
||
if (changed) writeJson(CURSOR_SETTINGS, settings);
|
||
return { ...checkCursorPrefs(), changed };
|
||
}
|
||
|
||
function permFixShellCmd() {
|
||
const cursorOut = '/Applications/Cursor.app/Contents/Resources/app/out';
|
||
const cursorApp = '/Applications/Cursor.app/Contents/Resources/app';
|
||
return `chmod -R a+w '${cursorOut}' && chmod a+w '${cursorApp}/product.json' 2>/dev/null; true`;
|
||
}
|
||
|
||
function runPermFixKaruoSudo() {
|
||
const ks = resolveKaruoSudo();
|
||
if (!ks) return { ok: false, skipped: true, reason: 'karuo_sudo 未安装' };
|
||
const has = spawnSync('bash', [ks, 'has'], { encoding: 'utf8', timeout: 5000 });
|
||
if (has.status !== 0) return { ok: false, skipped: true, reason: '钥匙串未存密码,请先点「④ 记住密码」' };
|
||
const inner = permFixShellCmd();
|
||
const r = spawnSync('bash', [ks, 'bash', '-c', inner], { encoding: 'utf8', timeout: 300000 });
|
||
const probe = probeWritePermission();
|
||
return { ok: probe.ok, method: 'karuo_sudo', code: r.status, stdout: r.stdout, stderr: r.stderr, probe };
|
||
}
|
||
|
||
function runPermFixOsascript() {
|
||
const inner = permFixShellCmd();
|
||
const r = spawnSync('osascript', ['-e', `do shell script "${inner}" with administrator privileges`], {
|
||
encoding: 'utf8',
|
||
timeout: 300000,
|
||
});
|
||
const probe = probeWritePermission();
|
||
return { ok: probe.ok, method: 'osascript', code: r.status, stdout: r.stdout, stderr: r.stderr, probe };
|
||
}
|
||
|
||
/** W2:优先钥匙串静默 sudo,失败再弹 macOS 授权框 */
|
||
function runPermFix() {
|
||
if (probeWritePermission().ok) {
|
||
return { ok: true, method: 'already', probe: { ok: true } };
|
||
}
|
||
const silent = runPermFixKaruoSudo();
|
||
if (silent.ok) return silent;
|
||
const popup = runPermFixOsascript();
|
||
return { ...popup, silentAttempt: silent.skipped ? silent.reason : silent.stderr };
|
||
}
|
||
|
||
function storeSudoDialog() {
|
||
const ks = resolveKaruoSudo();
|
||
if (!ks) return { ok: false, error: 'karuo_sudo.sh 未找到' };
|
||
const r = spawnSync('bash', [ks, 'store-dialog'], { encoding: 'utf8', timeout: 120000 });
|
||
const has = spawnSync('bash', [ks, 'has'], { encoding: 'utf8', timeout: 5000 });
|
||
return { ok: has.status === 0, code: r.status, stdout: r.stdout, stderr: r.stderr };
|
||
}
|
||
|
||
function hasStoredSudo() {
|
||
const ks = resolveKaruoSudo();
|
||
if (!ks) return false;
|
||
return spawnSync('bash', [ks, 'has'], { encoding: 'utf8', timeout: 5000 }).status === 0;
|
||
}
|
||
|
||
function runPermFixScript() {
|
||
if (!fs.existsSync(PERM_FIX_SCRIPT)) {
|
||
return { ok: false, error: 'cochat_fix_write_permission.sh 未找到', path: PERM_FIX_SCRIPT };
|
||
}
|
||
const r = spawnSync('bash', [PERM_FIX_SCRIPT], { encoding: 'utf8', timeout: 120000 });
|
||
return { ok: r.status === 0, code: r.status, stdout: r.stdout, stderr: r.stderr };
|
||
}
|
||
|
||
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 spawnPatchViaCochat(mode) {
|
||
const worker = path.join(__dirname, 'patch-via-cochat.mjs');
|
||
if (!fs.existsSync(worker)) {
|
||
return spawnPatchActivateLegacy(mode);
|
||
}
|
||
return new Promise((resolve) => {
|
||
const child = spawn(process.execPath, [worker, mode || 'apply'], {
|
||
env: { ...process.env, CO_SKIP_LICENSE: '1', PCHAT_NO_LICENSE: '1' },
|
||
});
|
||
let out = '';
|
||
let err = '';
|
||
const timer = setTimeout(() => {
|
||
try { child.kill('SIGTERM'); } catch {}
|
||
}, 35000);
|
||
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) });
|
||
});
|
||
});
|
||
}
|
||
|
||
function spawnPatchActivateLegacy() {
|
||
const worker = path.join(__dirname, 'patch-activate.mjs');
|
||
if (!fs.existsSync(worker)) {
|
||
return Promise.resolve({ ok: false, error: 'patch-activate.mjs 未找到' });
|
||
}
|
||
return new Promise((resolve) => {
|
||
const child = spawn(process.execPath, [worker, 'apply'], {
|
||
env: { ...process.env, CO_SKIP_LICENSE: '1', PCHAT_NO_LICENSE: '1' },
|
||
});
|
||
let out = '';
|
||
let err = '';
|
||
const timer = setTimeout(() => { try { child.kill('SIGTERM'); } catch {} }, 28000);
|
||
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) });
|
||
});
|
||
});
|
||
}
|
||
|
||
function spawnPatchActivate() {
|
||
return spawnPatchViaCochat('apply');
|
||
}
|
||
|
||
let patchWatcherStarted = false;
|
||
function startPatchWatcher() {
|
||
if (patchWatcherStarted) return;
|
||
patchWatcherStarted = true;
|
||
const pendingFile = path.join(PCHAT_ROOT, 'pending-w7.json');
|
||
setInterval(async () => {
|
||
try {
|
||
if (!fs.existsSync(pendingFile)) return;
|
||
if (isCursorRunning()) return;
|
||
const result = await spawnPatchViaCochat('pending');
|
||
if (!result || result.waiting) return;
|
||
const state = loadState();
|
||
const after = verifyPatchMarkers();
|
||
state.patchOk = after.patchOk;
|
||
state.lastApplyAt = new Date().toISOString();
|
||
if (after.patchOk) {
|
||
state.enhanceEnabled = state.permOk && state.cursorPrefOk;
|
||
state.wizardStep = 8;
|
||
state.lastError = null;
|
||
markPrdItem('phase1-patch-bridge', true);
|
||
markPrdItem('phase1-w2-w8', w8GatesPass(state));
|
||
if (modeBGatesPass(state)) markPrdItem('mode-b-opus', true);
|
||
}
|
||
saveState(state);
|
||
} catch {}
|
||
}, 4000);
|
||
}
|
||
|
||
function spawnRceApply() {
|
||
// 已弃用:reset-cursor-env 会 --quit-cursor 强杀 Cursor(SIGTERM code 15)
|
||
return spawnPatchActivate();
|
||
}
|
||
|
||
function spawnPatchWorker(mode) {
|
||
if (mode === 'verify') {
|
||
const after = verifyPatchMarkers();
|
||
return Promise.resolve({ ok: after.patchOk, ...after, method: 'verify' });
|
||
}
|
||
return spawnPatchViaCochat('apply');
|
||
}
|
||
|
||
async function applyEnhance(opts = {}) {
|
||
const state = loadState();
|
||
const gates = syncGatesToState(state);
|
||
if (!gates.integrity.ok && !opts.skipIntegrity) {
|
||
state.lastError = 'vendor 完整性校验未通过';
|
||
saveState(state);
|
||
return { ok: false, step: 'FR-ENH-07', error: state.lastError, integrity: gates.integrity };
|
||
}
|
||
if (!state.permOk) {
|
||
state.lastError = 'W2:workbench 目录不可写,请先修复权限';
|
||
saveState(state);
|
||
return { ok: false, step: 'W2', error: state.lastError, perm: gates.perm, fixScript: PERM_FIX_SCRIPT };
|
||
}
|
||
if (!state.cursorPrefOk && !opts.force) {
|
||
state.lastError = 'W6:Cursor 前置未满足(Plan/RunMode/HTTP1.1)';
|
||
saveState(state);
|
||
return { ok: false, step: 'W6', error: state.lastError, prefs: gates.prefs };
|
||
}
|
||
syncPchatNativeEnhance(state);
|
||
const patchResult = await spawnPatchWorker('apply');
|
||
const after = verifyPatchMarkers();
|
||
state.patchOk = after.patchOk;
|
||
state.lastApplyAt = new Date().toISOString();
|
||
const queued = !!(patchResult.queued || patchResult.needsQuit) && !state.patchOk;
|
||
if (state.patchOk && state.permOk && state.cursorPrefOk) {
|
||
state.enhanceEnabled = true;
|
||
state.patchPending = false;
|
||
state.wizardStep = 8;
|
||
state.lastError = null;
|
||
} else if (queued) {
|
||
state.patchPending = true;
|
||
state.enhanceEnabled = true;
|
||
state.lastError = null;
|
||
state.wizardStep = 7;
|
||
try { writeJson(PENDING_W7_FILE, { queuedAt: state.lastApplyAt, reason: 'w7_apply' }); } catch {}
|
||
} else {
|
||
state.patchPending = coChatStorageReady();
|
||
state.enhanceEnabled = state.patchPending && state.permOk && state.cursorPrefOk;
|
||
state.lastError = patchResult.error || patchResult.hint || 'W8:补丁未写入 workbench,请在 co-chat 面板切换核心增强包后 Cmd+Q 重开';
|
||
}
|
||
saveState(state);
|
||
markPrdItem('phase1-patch-bridge', true);
|
||
markPrdItem('phase1-w2-w8', state.permOk && (state.patchOk || state.patchNativeOk));
|
||
if (state.permOk && state.cursorPrefOk && (state.patchOk || state.patchNativeOk)) {
|
||
markPrdItem('mode-b-opus', true);
|
||
}
|
||
const step = state.patchOk ? 'W8' : (queued ? 'W7-queued' : 'W7');
|
||
return {
|
||
ok: state.patchOk || queued,
|
||
patchOk: state.patchOk,
|
||
queued,
|
||
step,
|
||
state,
|
||
patchResult,
|
||
markers: after,
|
||
hint: patchResult.hint || (queued
|
||
? 'W7 已排队:配置已迁移至 pchat。请 Cmd+Q 重开 Cursor 使 workbench 补丁生效'
|
||
: state.lastError),
|
||
};
|
||
}
|
||
|
||
async function repairEnhance() {
|
||
let permFix = { ok: true, skipped: true, reason: 'permOk 已通过,跳过 W2' };
|
||
if (!probeWritePermission().ok) {
|
||
permFix = runPermFix();
|
||
}
|
||
const applyResult = await applyEnhance({ force: true });
|
||
return { permFix, apply: applyResult };
|
||
}
|
||
|
||
function setEnhanceEnabled(enabled) {
|
||
const state = loadState();
|
||
syncGatesToState(state);
|
||
if (enabled) {
|
||
if (!state.permOk) {
|
||
return { ok: false, error: 'W2 未通过:workbench 目录不可写。点「② W2 一键修权限」', state };
|
||
}
|
||
if (!state.cursorPrefOk) {
|
||
return { ok: false, error: 'W6 未通过:点「W6 修 Cursor 前置」', state };
|
||
}
|
||
syncPchatNativeEnhance(state);
|
||
state.enhanceEnabled = true;
|
||
state.patchNativeOk = state.patchOk || computePatchNativeOk(state);
|
||
if (state.patchNativeOk) {
|
||
state.patchPending = false;
|
||
if (state.permOk) markPrdItem('phase1-w2-w8', true);
|
||
if (state.permOk && state.cursorPrefOk) markPrdItem('mode-b-opus', true);
|
||
} else if (!state.patchOk) {
|
||
state.patchPending = true;
|
||
try { writeJson(PENDING_W7_FILE, { queuedAt: new Date().toISOString(), reason: 'toggle_on' }); } catch {}
|
||
} else {
|
||
state.patchPending = false;
|
||
}
|
||
} else {
|
||
state.enhanceEnabled = false;
|
||
state.patchPending = false;
|
||
}
|
||
saveState(state);
|
||
return {
|
||
ok: true,
|
||
state,
|
||
patchPending: state.patchPending,
|
||
hint: state.patchPending
|
||
? '增强包已开启(pchat 原生)。workbench 补丁可选 Cmd+Q 重开后全绿'
|
||
: (state.patchNativeOk ? '增强包已就绪(pchat 原生模式)' : null),
|
||
};
|
||
}
|
||
|
||
function updateNoQuotaSettings(body) {
|
||
const state = loadState();
|
||
const allowed = ['noQuotaRetrySpeed', 'noQuotaRetryDelayMs', 'noQuotaSameComposerDebounceMs', 'noQuotaQueueMaxPerWindow', 'noQuotaRateLimitPerSecond', 'noQuotaGlobalRateLimit'];
|
||
for (const k of allowed) {
|
||
if (body[k] !== undefined) state[k] = body[k];
|
||
}
|
||
saveState(state);
|
||
markPrdItem('phase1-noquota-ui', true);
|
||
return state;
|
||
}
|
||
|
||
function getPrdProgress() {
|
||
const data = readJson(PRD_PROGRESS_FILE, { items: [] });
|
||
const items = data.items || [];
|
||
const done = items.filter((i) => i.done).length;
|
||
const total = items.length || 1;
|
||
const percent = Math.round((done / total) * 100);
|
||
return { ...data, done, total, percent, items };
|
||
}
|
||
|
||
function markPrdItem(id, done = true) {
|
||
const data = readJson(PRD_PROGRESS_FILE, { items: [] });
|
||
let hit = false;
|
||
for (const item of data.items || []) {
|
||
if (item.id === id) {
|
||
item.done = !!done;
|
||
hit = true;
|
||
}
|
||
}
|
||
data.updatedAt = new Date().toISOString();
|
||
writeJson(PRD_PROGRESS_FILE, data);
|
||
if (id === 'phase1-enhance-api' && done) markPrdItem('phase1-composer-state', true);
|
||
return { updated: hit, progress: getPrdProgress() };
|
||
}
|
||
|
||
function bootMarkProgress() {
|
||
markPrdItem('phase1-enhance-api', fs.existsSync(__filename));
|
||
}
|
||
|
||
bootMarkProgress();
|
||
startPatchWatcher();
|
||
|
||
module.exports = {
|
||
loadState,
|
||
saveState,
|
||
diagnose,
|
||
getStatus,
|
||
applyEnhance,
|
||
repairEnhance,
|
||
setEnhanceEnabled,
|
||
updateNoQuotaSettings,
|
||
getPrdProgress,
|
||
markPrdItem,
|
||
probeWritePermission,
|
||
verifyPatchMarkers,
|
||
syncPchatNativeEnhance,
|
||
coChatStorageReady,
|
||
readCoChatStorage,
|
||
checkCursorPrefs,
|
||
verifyVendorIntegrity,
|
||
fixCursorPrefs,
|
||
syncGatesToState,
|
||
w8GatesPass,
|
||
modeBGatesPass,
|
||
computePatchNativeOk,
|
||
resolvePatchPending,
|
||
runPermFix,
|
||
runPermFixOsascript,
|
||
runPermFixKaruoSudo,
|
||
runPermFixScript,
|
||
storeSudoDialog,
|
||
hasStoredSudo,
|
||
startPatchWatcher,
|
||
spawnPatchViaCochat,
|
||
PATHS: { STATE_FILE, VENDOR_RCE, PERM_FIX_SCRIPT, WORKBENCH },
|
||
};
|