| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364 |
- // opencode 直连 shell 派发器(R115 修正 B-113 误判:session 路由 204 是设计行为,不是失败)
- // 关键事实(openapi.json 实证):
- // 1. POST /api/shell?location[directory]=<dir> ← 正确入口,直接返回 Shell.Info(含 id)
- // 2. POST /api/session/{id}/shell 恒返 204(无 body),且 shell 退出后即被 GC
- // → 旧脚本轮询 /api/shell(list 只列 running)必然拿到 NONE,误判为「建不出对象」
- // 3. 长任务必须「重定向到日志文件 + 另起 shell 读文件」,否则 shell 对象被回收后
- // /api/shell/{id}/output 返回 ShellNotFoundError
- // 用法(Git Bash 必须带 MSYS_NO_PATHCONV=1,否则 /sc-data 会被改写成本机路径):
- // MSYS_NO_PATHCONV=1 BASE=http://192.168.16.251:4096 DIR=/sc-data/cfc/cfc-langgraph \
- // CMD="pytest -q" LOG=/tmp/x.log node dispatch_shell.cjs
- const BASE = process.env.BASE;
- const DIR = process.env.DIR;
- const CMD = process.env.CMD;
- const LOG = process.env.LOG || '/tmp/dispatch_shell.log';
- const CAP_MS = parseInt(process.env.CAP_MS || '900000', 10);
- 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 }; }
- }
- const sleep = ms => new Promise(r => setTimeout(r, ms));
- async function run(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 failed ' + cr.status + ' ' + JSON.stringify(cr).slice(0, 200) };
- const t0 = Date.now();
- while (Date.now() - t0 < capMs) {
- await sleep(3000);
- const s = await j(BASE + '/api/shell/' + id + LOC);
- const d = (s.json && s.json.data) || null;
- if (!d) continue; // GC/竞态:继续等下一次
- if (d.status && d.status !== 'running') {
- const o = await j(BASE + '/api/shell/' + id + '/output' + LOC + '&limit=200000');
- return { status: d.status, out: (o.json && o.json.data && o.json.data.output) || '' };
- }
- }
- return { err: 'timeout ' + capMs + 'ms' };
- }
- (async () => {
- // 1) 后台启动长任务并写盘(避免 shell 对象被回收)
- const kick = await run('rm -f ' + LOG + '; ( ' + CMD + ' ) > ' + LOG + ' 2>&1; echo DONE_$? >> ' + LOG, 30000);
- console.log('KICK', kick.err || kick.status);
- // 2) 轮询日志文件
- const t0 = Date.now();
- let tail = '';
- while (Date.now() - t0 < CAP_MS) {
- await sleep(8000);
- const r = await run('tail -60 ' + LOG, 45000);
- if (r.err) { console.log('POLL_ERR', r.err); continue; }
- tail = r.out || '';
- if (/DONE_\d+/.test(tail)) break;
- }
- console.log('OUT_BEGIN');
- console.log(tail);
- console.log('OUT_END');
- })().catch(e => { console.error('ERR', e.message); process.exit(1); });
|