| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- /**
- * parse-client.mjs — Parse REST 客户端 + 凭据四级解析(零硬编码)
- *
- * 上报后端:https://server.fmode.cn/parse/classes/<Table>
- * 鉴权:X-Parse-Application-Id + X-Parse-Master-Key(或用户 sessionToken)
- *
- * 凭据解析链(优先级从高到低,全部运行时读取,零密钥入库):
- * 1. 环境变量 PARSE_APP_ID / PARSE_MASTER_KEY(或 sessionToken 走 X-Parse-Session-Token)
- * 2. ~/.fmode/config.json → parse 段({ appId, masterKey } 或顶层同名键)
- * 3. future-server/config/config.nova.json → parse 段(平台开发机标准位置)
- * 4. 都找不到 → 打印指引并退出(绝不猜、绝不硬编码)
- *
- * Date 字段铁律(schema 111 坑):
- * 裸字符串 "2026-09-12T08:00:00.000Z" 直接写 Parse 报 schema 111;
- * 必须 wrapDate() 包装为 {"__type":"Date","iso":"..."} —— 本模块统一处理。
- */
- import fs from 'fs';
- import path from 'path';
- import os from 'os';
- const DEFAULT_SERVER_URL = process.env.PARSE_SERVER_URL || 'https://server.fmode.cn/parse';
- function readJsonMaybe(filePath) {
- try {
- if (!filePath || !fs.existsSync(filePath)) return null;
- return JSON.parse(fs.readFileSync(filePath, 'utf-8').replace(/^/, ''));
- } catch {
- return null;
- }
- }
- /** 在任意层级找 parse 配置段:{parse:{appId,masterKey}} 或顶层 {appId,masterKey} */
- function extractParseSection(obj) {
- if (!obj || typeof obj !== 'object') return null;
- const sec = obj.parse && typeof obj.parse === 'object' ? obj.parse : obj;
- if (sec.appId && sec.masterKey) {
- return { appId: String(sec.appId), masterKey: String(sec.masterKey), serverURL: sec.serverURLOnline || sec.publicServerURL || sec.serverURL || null };
- }
- return null;
- }
- /** 凭据四级解析。返回 {appId, masterKey, serverURL, source} 或 null。 */
- export function resolveCredentials() {
- // 第 1 级:环境变量
- if (process.env.PARSE_APP_ID && process.env.PARSE_MASTER_KEY) {
- return {
- appId: process.env.PARSE_APP_ID.trim(),
- masterKey: process.env.PARSE_MASTER_KEY.trim(),
- serverURL: process.env.PARSE_SERVER_URL || DEFAULT_SERVER_URL,
- source: 'env',
- };
- }
- // 第 2 级:~/.fmode/config.json
- const homeCfg = readJsonMaybe(path.join(os.homedir(), '.fmode', 'config.json'));
- const fromHome = extractParseSection(homeCfg);
- if (fromHome) {
- return { ...fromHome, serverURL: fromHome.serverURL || DEFAULT_SERVER_URL, source: '~/.fmode/config.json' };
- }
- // 第 3 级:future-server config.nova.json(平台开发机标准位置)
- const candidates = [
- path.join(os.homedir(), 'git-repos', 'future-server', 'config', 'config.nova.json'),
- '/opt/data/git-repos/future-server/config/config.nova.json',
- process.env.FUTURE_SERVER_DIR ? path.join(process.env.FUTURE_SERVER_DIR, 'config', 'config.nova.json') : null,
- ].filter(Boolean);
- for (const p of candidates) {
- const cfg = readJsonMaybe(p);
- const sec = extractParseSection(cfg);
- if (sec) {
- return { ...sec, serverURL: sec.serverURL || DEFAULT_SERVER_URL, source: p };
- }
- }
- return null;
- }
- /** 未配置凭据时的统一报错指引(退出码 2) */
- export function failNoCredentials() {
- console.error('[task-progress] 未找到 Parse 凭据。按以下任一方式配置(零硬编码,凭据不入仓):');
- console.error(' 1. 环境变量: export PARSE_APP_ID=<appId> PARSE_MASTER_KEY=<masterKey>');
- console.error(' 2. ~/.fmode/config.json 写入 {"parse":{"appId":"...","masterKey":"..."}}');
- console.error(' 3. 平台开发机: future-server/config/config.nova.json 的 parse 段自动读取');
- process.exit(2);
- }
- /** Date 字段包装:裸字符串 → {"__type":"Date","iso":...}(防 schema 111) */
- export function wrapDate(value) {
- if (!value) return value;
- if (value && typeof value === 'object' && value.__type === 'Date' && value.iso) return value;
- const d = new Date(value);
- if (isNaN(d.getTime())) throw new Error(`无效日期: ${value}`);
- return { __type: 'Date', iso: d.toISOString() };
- }
- /** Pointer 包装 */
- export function wrapPointer(className, objectId) {
- return { __type: 'Pointer', className, objectId };
- }
- function headers(cred, extra) {
- const h = {
- 'X-Parse-Application-Id': cred.appId,
- 'X-Parse-Master-Key': cred.masterKey,
- 'Content-Type': 'application/json',
- ...extra,
- };
- return h;
- }
- async function parseFetch(url, options) {
- const res = await fetch(url, { ...options, signal: AbortSignal.timeout(30000) });
- const body = await res.json().catch(() => null);
- if (!res.ok) {
- const code = body && body.code ? body.code : res.status;
- const err = new Error(`Parse ${res.status} code=${code}: ${JSON.stringify(body && body.error ? body.error : body)}`);
- err.parseCode = code;
- throw err;
- }
- return body;
- }
- /** GET /classes/<Table>(filters: {where, limit, order, keys}) */
- export async function queryObjects(table, cred, filters = {}) {
- const qs = new URLSearchParams();
- if (filters.where) qs.set('where', JSON.stringify(filters.where));
- if (filters.limit) qs.set('limit', String(filters.limit));
- if (filters.order) qs.set('order', filters.order);
- if (filters.keys) qs.set('keys', filters.keys);
- const url = `${cred.serverURL}/classes/${table}${qs.toString() ? `?${qs}` : ''}`;
- const body = await parseFetch(url, { method: 'GET', headers: headers(cred) });
- return body.results || [];
- }
- /** POST /classes/<Table> 创建;data 中 Date 字段自动包装 */
- export async function createObject(table, cred, data) {
- const url = `${cred.serverURL}/classes/${table}`;
- return parseFetch(url, { method: 'POST', headers: headers(cred), body: JSON.stringify(data) });
- }
- /** PUT /classes/<Table>/<objectId> 更新;data 中 Date 字段自动包装 */
- export async function updateObject(table, cred, objectId, data) {
- const url = `${cred.serverURL}/classes/${table}/${objectId}`;
- return parseFetch(url, { method: 'PUT', headers: headers(cred), body: JSON.stringify(data) });
- }
- /** GET /schemas/<Table>(schema 校验用) */
- export async function getSchema(table, cred) {
- const url = `${cred.serverURL}/schemas/${table}`;
- try {
- return await parseFetch(url, { method: 'GET', headers: headers(cred) });
- } catch (e) {
- if (e.parseCode === 103 || (e.message && e.message.includes('class'))) return null; // 表不存在
- throw e;
- }
- }
- /** POST /schemas/<Table> 建表;已存在的表(code 105)回落 PUT 增量补列(幂等) */
- export async function createSchema(table, cred, fields) {
- const url = `${cred.serverURL}/schemas/${table}`;
- try {
- return await parseFetch(url, { method: 'POST', headers: headers(cred), body: JSON.stringify({ className: table, fields }) });
- } catch (e) {
- if (e.parseCode === 105 || (e.message && e.message.includes('already exist'))) {
- // 表已存在:PUT /schemas/<Class> 增量加缺失列(已存在的列 Parse 自动忽略)
- const body = await parseFetch(url, { method: 'PUT', headers: headers(cred), body: JSON.stringify({ fields }) });
- return { idempotent: true, ...body };
- }
- // 106 = 字段已存在(幂等场景视为成功)
- if (e.parseCode === 106 || (e.message && e.message.includes('already exist'))) {
- return { idempotent: true, parseCode: e.parseCode };
- }
- throw e;
- }
- }
- /** 幂等 upsert:按 where 查第一条,命中则覆盖更新,未命中则创建。返回 {objectId, created} */
- export async function upsertObject(table, cred, where, data) {
- const existing = await queryObjects(table, cred, { where, limit: 1 });
- if (existing.length > 0) {
- const objectId = existing[0].objectId;
- await updateObject(table, cred, objectId, data);
- return { objectId, created: false };
- }
- const created = await createObject(table, cred, data);
- return { objectId: created.objectId, created: true };
- }
|