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 }