// R150 251 langgraph 派发器:kick(分离启动)+ read(单次读日志),不在等待期新建 shell(B-125) const BASE = process.env.BASE || 'http://192.168.16.251:4096'; const DIR = process.env.DIR || '/sc-data/cfc/cfc-langgraph'; const LOG = process.env.LOG || '/tmp/r150_pytest.log'; const AUTH = 'Basic ' + Buffer.from('opencode:' + (process.env.OPENCODE_SERVER_PASSWORD || 'IwinTrue@123')).toString('base64'); const H = { Authorization: AUTH, 'Content-Type': 'application/json' }; const LOC = '?location%5Bdirectory%5D=' + encodeURIComponent(DIR); async function j(url, opt) { try { const r = await fetch(url, Object.assign({ headers: H }, opt || {})); const t = await r.text(); try { return { status: r.status, json: JSON.parse(t) }; } catch { return { status: r.status, text: t }; } } catch (e) { return { status: 0, err: e.message }; } } async function sh(cmd, capMs) { const cr = await j(BASE + '/api/shell' + LOC, { method: 'POST', body: JSON.stringify({ command: cmd, cwd: DIR }) }); const id = cr.json && cr.json.data && cr.json.data.id; if (!id) return { err: 'create ' + cr.status + ' ' + JSON.stringify(cr).slice(0, 200) }; const t0 = Date.now(); while (Date.now() - t0 < (capMs || 60000)) { await new Promise(r => setTimeout(r, 2500)); const s = await j(BASE + '/api/shell/' + id + LOC); const d = (s.json && s.json.data) || null; if (!d) continue; if (d.status && d.status !== 'running') { const o = await j(BASE + '/api/shell/' + id + '/output' + LOC + '&limit=200000'); return { code: d.status, out: (o.json && o.json.data && o.json.data.output) || '' }; } } return { err: 'timeout' }; } (async () => { const mode = process.argv[2] || 'kick'; // R173 增:LOG 若从本机 env 注入,Git Bash(MSYS) 会把 /tmp/x 改写成 C:/Users/.../x, // 远端 `tail` 恒报 No such file,且 kick 的重定向也会失败 → 看起来像「日志没生成」。 // 这里直接挡住 Windows 风格路径,强制调用方使用远端 POSIX 路径。 if (LOG.includes(':') || LOG.includes('\\')) { console.error('BAD_LOG_PATH=' + LOG + ' 请使用远端 POSIX 路径(如 /tmp/rX_pytest.log),不要经由本地 env 注入'); process.exit(2); } if (mode === 'kick') { // R156 修:251 系统 /usr/bin/python 无 pytest,必须用节点 venv(.venv/bin/python → python3.11); // 退出码另存变量后再写盘(B-126:$? 直接内联在 kick 串里可能落盘失败)。 // R173 复现(B-126 第 6 轮):手工 kick 用 `echo EXIT=$?` 时 186 行日志零 EXIT 行; // 已在 /tmp/r173_probe.log 验证「先存 rc 再落盘」可写入 → 保留 rc 写法为唯一正确姿势。 const inner = "cd " + DIR + " && .venv/bin/python -m pytest -q --ignore=tests/qna --ignore=tests/test_chat.py > " + LOG + " 2>&1; rc=$?; echo EXIT=$rc >> " + LOG; const cmd = "rm -f " + LOG + "; setsid nohup bash -c '" + inner + "' >/dev/null 2>&1 & echo KICKED"; const r = await sh(cmd, 40000); console.log('KICK=' + JSON.stringify(r).slice(0, 400)); const h = await sh('cd ' + DIR + ' && git rev-parse --short HEAD; date', 30000); console.log('NODE_HEAD=' + ((h.out || h.err || '').trim())); } else if (mode === 'read') { const r = await sh('tail -c 4000 ' + LOG, 45000); console.log(r.err ? 'READ_ERR=' + r.err : r.out); } else if (mode === 'run') { const r = await sh(process.argv[3], 120000); console.log(r.err ? 'ERR=' + r.err : r.out); } })();