dispatch_shell.cjs 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. // opencode 直连 shell 派发器(R115 修正 B-113 误判:session 路由 204 是设计行为,不是失败)
  2. // 关键事实(openapi.json 实证):
  3. // 1. POST /api/shell?location[directory]=<dir> ← 正确入口,直接返回 Shell.Info(含 id)
  4. // 2. POST /api/session/{id}/shell 恒返 204(无 body),且 shell 退出后即被 GC
  5. // → 旧脚本轮询 /api/shell(list 只列 running)必然拿到 NONE,误判为「建不出对象」
  6. // 3. 长任务必须「重定向到日志文件 + 另起 shell 读文件」,否则 shell 对象被回收后
  7. // /api/shell/{id}/output 返回 ShellNotFoundError
  8. // 用法(Git Bash 必须带 MSYS_NO_PATHCONV=1,否则 /sc-data 会被改写成本机路径):
  9. // MSYS_NO_PATHCONV=1 BASE=http://192.168.16.251:4096 DIR=/sc-data/cfc/cfc-langgraph \
  10. // CMD="pytest -q" LOG=/tmp/x.log node dispatch_shell.cjs
  11. const BASE = process.env.BASE;
  12. const DIR = process.env.DIR;
  13. const CMD = process.env.CMD;
  14. const LOG = process.env.LOG || '/tmp/dispatch_shell.log';
  15. const CAP_MS = parseInt(process.env.CAP_MS || '900000', 10);
  16. const AUTH = 'Basic ' + Buffer.from('opencode:' + (process.env.OPENCODE_SERVER_PASSWORD || 'IwinTrue@123')).toString('base64');
  17. const H = { Authorization: AUTH, 'Content-Type': 'application/json' };
  18. const LOC = '?location%5Bdirectory%5D=' + encodeURIComponent(DIR || '');
  19. async function j(url, opt) {
  20. try {
  21. const r = await fetch(url, Object.assign({ headers: H }, opt));
  22. const t = await r.text();
  23. try { return { status: r.status, json: JSON.parse(t) }; } catch { return { status: r.status, text: t }; }
  24. } catch (e) { return { status: 0, err: e.message }; }
  25. }
  26. const sleep = ms => new Promise(r => setTimeout(r, ms));
  27. async function run(cmd, capMs) {
  28. const cr = await j(BASE + '/api/shell' + LOC, { method: 'POST', body: JSON.stringify({ command: cmd, cwd: DIR }) });
  29. const id = cr.json && cr.json.data && cr.json.data.id;
  30. if (!id) return { err: 'create failed ' + cr.status + ' ' + JSON.stringify(cr).slice(0, 200) };
  31. const t0 = Date.now();
  32. while (Date.now() - t0 < capMs) {
  33. await sleep(3000);
  34. const s = await j(BASE + '/api/shell/' + id + LOC);
  35. const d = (s.json && s.json.data) || null;
  36. if (!d) continue; // GC/竞态:继续等下一次
  37. if (d.status && d.status !== 'running') {
  38. const o = await j(BASE + '/api/shell/' + id + '/output' + LOC + '&limit=200000');
  39. return { status: d.status, out: (o.json && o.json.data && o.json.data.output) || '' };
  40. }
  41. }
  42. return { err: 'timeout ' + capMs + 'ms' };
  43. }
  44. (async () => {
  45. // 1) 后台启动长任务并写盘(避免 shell 对象被回收)
  46. const kick = await run('rm -f ' + LOG + '; ( ' + CMD + ' ) > ' + LOG + ' 2>&1; echo DONE_$? >> ' + LOG, 30000);
  47. console.log('KICK', kick.err || kick.status);
  48. // 2) 轮询日志文件
  49. const t0 = Date.now();
  50. let tail = '';
  51. while (Date.now() - t0 < CAP_MS) {
  52. await sleep(8000);
  53. const r = await run('tail -60 ' + LOG, 45000);
  54. if (r.err) { console.log('POLL_ERR', r.err); continue; }
  55. tail = r.out || '';
  56. if (/DONE_\d+/.test(tail)) break;
  57. }
  58. console.log('OUT_BEGIN');
  59. console.log(tail);
  60. console.log('OUT_END');
  61. })().catch(e => { console.error('ERR', e.message); process.exit(1); });