/ clis / ones / common.js
common.js
  1  /**
  2   * ONES 旧版 Project API — 经 Browser Bridge 在已登录标签页内 fetch(携带 Cookie)。
  3   * 文档:https://developer.ones.cn/zh-CN/docs/api/readme/
  4   */
  5  import { CliError } from '@jackwener/opencli/errors';
  6  export const API_PREFIX = '/project/api/project';
  7  export function getOnesBaseUrl() {
  8      const u = process.env.ONES_BASE_URL?.trim().replace(/\/+$/, '');
  9      if (!u) {
 10          throw new CliError('CONFIG', 'Missing ONES_BASE_URL', 'Set ONES_BASE_URL to your deployment origin, e.g. https://your-team.ones.cn (no trailing slash).');
 11      }
 12      return u;
 13  }
 14  export function onesApiUrl(apiPath) {
 15      const base = getOnesBaseUrl();
 16      const p = apiPath.replace(/^\/+/, '');
 17      return `${base}${API_PREFIX}/${p}`;
 18  }
 19  /** 打开 ONES 根地址,确保后续 fetch 与页面同源、带上登录 Cookie */
 20  export async function gotoOnesHome(page) {
 21      await page.goto(getOnesBaseUrl(), { waitUntil: 'load' });
 22      await page.wait(2);
 23  }
 24  /**
 25   * 在页面内发起请求。默认带 credentials;若设置了 ONES_USER_ID + ONES_AUTH_TOKEN,则附加文档要求的 Header(与纯 Cookie 二选一或并存,取决于部署)。
 26   */
 27  function buildHeaders(auth, includeJsonContentType) {
 28      const ref = getOnesBaseUrl();
 29      const out = { Referer: ref };
 30      if (auth) {
 31          const uid = process.env.ONES_USER_ID?.trim() ||
 32              process.env.ONES_USER_UUID?.trim() ||
 33              process.env.Ones_User_Id?.trim();
 34          const tok = process.env.ONES_AUTH_TOKEN?.trim() || process.env.Ones_Auth_Token?.trim();
 35          if (uid && tok) {
 36              out['Ones-User-Id'] = uid;
 37              out['Ones-Auth-Token'] = tok;
 38          }
 39      }
 40      if (includeJsonContentType)
 41          out['Content-Type'] = 'application/json';
 42      return out;
 43  }
 44  export function summarizeOnesError(status, body) {
 45      if (body && typeof body === 'object') {
 46          const o = body;
 47          const parts = [];
 48          if (typeof o.type === 'string')
 49              parts.push(o.type);
 50          if (typeof o.reason === 'string')
 51              parts.push(o.reason);
 52          if (typeof o.errcode === 'string')
 53              parts.push(o.errcode);
 54          if (typeof o.message === 'string')
 55              parts.push(o.message);
 56          if (o.code !== undefined && o.code !== null)
 57              parts.push(`code=${String(o.code)}`);
 58          if (parts.length)
 59              return parts.filter(Boolean).join(' · ');
 60      }
 61      return status === 401 ? 'Unauthorized' : `HTTP ${status}`;
 62  }
 63  /** ONES 部分接口 HTTP 200 但 body 仍为错误(如 reason: ServerError) */
 64  function throwIfOnesPeekBusinessError(apiPath, parsed) {
 65      if (parsed === null || typeof parsed !== 'object')
 66          return;
 67      const o = parsed;
 68      if (Array.isArray(o.groups))
 69          return;
 70      const hasErr = (typeof o.reason === 'string' && o.reason.length > 0) ||
 71          (typeof o.errcode === 'string' && o.errcode.length > 0) ||
 72          (typeof o.type === 'string' && o.type.length > 0);
 73      if (!hasErr)
 74          return;
 75      const detail = summarizeOnesError(200, parsed);
 76      throw new CliError('FETCH_ERROR', `ONES ${apiPath}: ${detail}`, '若 query 不合法会返回 ServerError;可试 opencli ones tasks(空 must)或检查筛选器文档。响应全文可用 -v 或临时打日志。');
 77  }
 78  export async function onesFetchInPageWithMeta(page, apiPath, options = {}) {
 79      if (!options.skipGoto) {
 80          await gotoOnesHome(page);
 81      }
 82      const url = onesApiUrl(apiPath);
 83      const method = (options.method ?? 'GET').toUpperCase();
 84      const auth = options.auth !== false;
 85      const body = options.body ?? null;
 86      const includeCt = body !== null || method === 'POST' || method === 'PUT' || method === 'PATCH';
 87      const headers = buildHeaders(auth, includeCt);
 88      const urlJs = JSON.stringify(url);
 89      const methodJs = JSON.stringify(method);
 90      const headersJs = JSON.stringify(headers);
 91      const bodyJs = body === null ? 'null' : JSON.stringify(body);
 92      const raw = await page.evaluate(`
 93      (async () => {
 94        const url = ${urlJs};
 95        const method = ${methodJs};
 96        const headers = ${headersJs};
 97        const body = ${bodyJs};
 98        const init = {
 99          method,
100          headers: { ...headers },
101          credentials: 'include',
102        };
103        if (body !== null) init.body = body;
104        const res = await fetch(url, init);
105        const text = await res.text();
106        let parsed = null;
107        try {
108          parsed = text ? JSON.parse(text) : null;
109        } catch {
110          parsed = text;
111        }
112        return { ok: res.ok, status: res.status, parsed };
113      })()
114    `);
115      return raw;
116  }
117  /** 当前操作用户 8 位 uuid(Header 或 GET users/me) */
118  export async function resolveOnesUserUuid(page, opts) {
119      const fromEnv = process.env.ONES_USER_ID?.trim() ||
120          process.env.ONES_USER_UUID?.trim() ||
121          process.env.Ones_User_Id?.trim();
122      if (fromEnv)
123          return fromEnv;
124      const data = (await onesFetchInPage(page, 'users/me', { skipGoto: opts?.skipGoto }));
125      const u = data.user && typeof data.user === 'object' ? data.user : data;
126      if (!u || typeof u.uuid !== 'string') {
127          throw new CliError('FETCH_ERROR', 'Could not read current user uuid from users/me', 'Set ONES_USER_ID or ensure Chrome is logged in; try: opencli ones me -f json');
128      }
129      return String(u.uuid);
130  }
131  export async function onesFetchInPage(page, apiPath, options = {}) {
132      const r = await onesFetchInPageWithMeta(page, apiPath, options);
133      if (!r.ok) {
134          const detail = summarizeOnesError(r.status, r.parsed);
135          const hint = r.status === 401
136              ? '在 Chrome 中打开 ONES 并登录;或先执行 opencli ones login 后按提示 export ONES_USER_ID / ONES_AUTH_TOKEN;并确认 ONES_BASE_URL 与浏览器地址一致。'
137              : '检查 ONES_BASE_URL、VPN/内网,以及实例是否仍为 Project API 路径。';
138          throw new CliError('FETCH_ERROR', `ONES ${apiPath}: ${detail}`, hint);
139      }
140      if (apiPath.includes('/filters/peek')) {
141          throwIfOnesPeekBusinessError(apiPath, r.parsed);
142      }
143      return r.parsed;
144  }