| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- // lib/config.mjs — 身份与凭据解析
- // 优先级:FEME_* 环境变量 > /opt/data/fmode-identity.json > 兼容旧名 FMODE_*
- //
- // 铁律:任何情况下都不打印、不落盘、不提交明文 token。
- // 本模块只提供「取值」与「脱敏显示」,脱敏一律走 mask()。
- import { readFileSync, existsSync } from 'node:fs';
- import { homedir } from 'node:os';
- import { join } from 'node:path';
- /** 身份文件候选路径(按序探测,取第一个存在的) */
- export const IDENTITY_PATHS = [
- process.env.FEME_IDENTITY_FILE,
- process.env.FMODE_IDENTITY_FILE,
- '/opt/data/fmode-identity.json',
- join(homedir(), '.fmode', 'identity.json'),
- ].filter(Boolean);
- /** 网关基础地址候选环境变量(FEME_* 优先) */
- const BASE_ENV_KEYS = [
- 'FEME_API_BASE_URL',
- 'FEME_API_BASE',
- 'FMODE_API_BASE_URL',
- 'FMODE_API_BASE',
- ];
- /** token 候选环境变量(FEME_* 优先) */
- const TOKEN_ENV_KEYS = [
- 'FEME_API_TOKEN',
- 'FEME_API_KEY',
- 'FEME_TOKEN',
- 'FMODE_API_TOKEN',
- 'FMODE_API_KEY',
- 'FMODE_NEWAPI_TOKEN',
- 'OPENAI_API_KEY',
- ];
- const DEFAULT_BASE = 'https://api.fmode.cn/v1';
- /** 读取身份文件(不存在则返回 null,绝不抛错) */
- function readIdentityFile() {
- for (const p of IDENTITY_PATHS) {
- try {
- if (p && existsSync(p)) {
- return { path: p, data: JSON.parse(readFileSync(p, 'utf8')) };
- }
- } catch {
- /* 忽略损坏的身份文件,继续找下一个 */
- }
- }
- return { path: null, data: {} };
- }
- /** 从环境变量取第一个非空值 */
- function firstEnv(keys) {
- for (const k of keys) {
- const v = process.env[k];
- if (v && String(v).trim() && !String(v).includes('***')) return String(v).trim();
- }
- return null;
- }
- /** 把 base url 归一化成以 /v1 结尾、无尾斜杠 */
- export function normalizeBaseUrl(url) {
- let u = String(url || DEFAULT_BASE).trim().replace(/\/+$/, '');
- if (!/\/v\d+$/.test(u)) u += '/v1';
- return u;
- }
- let _cache = null;
- /**
- * 解析运行配置。结果会被缓存(同一进程内身份不会变)。
- * @returns {{baseUrl:string, rootUrl:string, token:string|null, tokenSource:string,
- * identityPath:string|null, identity:object}}
- */
- export function loadConfig({ refresh = false } = {}) {
- if (_cache && !refresh) return _cache;
- const ident = readIdentityFile();
- const id = ident.data || {};
- // token:FEME_* > 身份文件 > FMODE_*
- let token = firstEnv(['FEME_API_TOKEN', 'FEME_API_KEY', 'FEME_TOKEN']);
- let tokenSource = token ? 'env:FEME_*' : null;
- if (!token) {
- for (const k of ['newapi_token', 'api_key', 'apiKey', 'token']) {
- if (id[k] && String(id[k]).trim()) {
- token = String(id[k]).trim();
- tokenSource = `identity:${k}`;
- break;
- }
- }
- }
- if (!token) {
- token = firstEnv(['FMODE_API_TOKEN', 'FMODE_API_KEY', 'FMODE_NEWAPI_TOKEN', 'OPENAI_API_KEY']);
- tokenSource = token ? 'env:FMODE_*' : null;
- }
- // base url:FEME_* > 身份文件 > FMODE_* > 默认
- let baseUrl = firstEnv(['FEME_API_BASE_URL', 'FEME_API_BASE']);
- if (!baseUrl && id.api_base_url) baseUrl = String(id.api_base_url);
- if (!baseUrl) baseUrl = firstEnv(['FMODE_API_BASE_URL', 'FMODE_API_BASE']);
- if (!baseUrl) baseUrl = DEFAULT_BASE;
- const normalized = normalizeBaseUrl(baseUrl);
- const rootUrl = normalized.replace(/\/v\d+$/, '');
- _cache = {
- baseUrl: normalized,
- rootUrl,
- token,
- tokenSource,
- identityPath: ident.path,
- identity: id,
- };
- return _cache;
- }
- /** 脱敏显示,用于日志/错误信息。永远不要把未脱敏 token 拼进字符串。 */
- export function mask(secret) {
- const s = String(secret || '');
- if (!s) return '<empty>';
- if (s.length <= 8) return s[0] + '*'.repeat(Math.max(1, s.length - 1));
- return `${s.slice(0, 3)}${'*'.repeat(6)}${s.slice(-4)}`;
- }
- /** 取 token,缺失则抛出可操作的错误 */
- export function requireToken() {
- const { token } = loadConfig();
- if (!token) {
- throw new Error(
- '未找到 API 凭据。请任选其一:\n' +
- ' 1) 设置环境变量 FEME_API_TOKEN=<你的token>\n' +
- ' 2) 写入 /opt/data/fmode-identity.json 的 newapi_token 字段\n' +
- ' 3) 兼容旧变量 FMODE_API_KEY\n' +
- '(本技能不会把 token 写入任何文件或日志)'
- );
- }
- return token;
- }
- /**
- * 生成到子进程的安全环境变量(供 ffmpeg 等外部命令使用),
- * 不包含任何 token。
- */
- export function safeEnv() {
- const env = { ...process.env };
- for (const k of [...TOKEN_ENV_KEYS, ...BASE_ENV_KEYS]) delete env[k];
- return env;
- }
- /** 打印当前配置摘要(token 已脱敏) */
- export function describeConfig() {
- const c = loadConfig();
- return [
- `baseUrl : ${c.baseUrl}`,
- `rootUrl : ${c.rootUrl}`,
- `token : ${mask(c.token)} (来源: ${c.tokenSource || '未配置'})`,
- `identityFile : ${c.identityPath || '未找到'}`,
- ].join('\n');
- }
- export { BASE_ENV_KEYS, TOKEN_ENV_KEYS, DEFAULT_BASE };
|