const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const os = require('os'); const DEFAULT_API_BASE = 'https://server.fmode.cn/api/qiwei'; const CREDENTIALS_FILE = path.join(os.homedir(), '.claude', 'qiwei-credentials.json'); function readJsonMaybe(filePath) { try { if (!filePath || !fs.existsSync(filePath)) return {}; return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '')); } catch { return {}; } } function firstNonEmpty(values) { return values.find(value => typeof value === 'string' && value.trim()) || ''; } function readEnvFileMaybe(filePath) { try { if (!filePath || !fs.existsSync(filePath)) return {}; const env = {}; const content = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''); for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith('#')) continue; const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/); if (!match) continue; let value = match[2].trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } env[match[1]] = value; } return env; } catch { return {}; } } function envCandidates() { const candidates = []; let current = process.cwd(); while (current) { candidates.push(path.join(current, '.env.local')); candidates.push(path.join(current, '.env')); const parent = path.dirname(current); if (parent === current) break; current = parent; } candidates.push(path.resolve(__dirname, '..', '..', '..', '.env.local')); candidates.push(path.resolve(__dirname, '..', '..', '..', '.env')); return [...new Set(candidates)]; } function readEnvFiles() { return envCandidates().reduce((merged, filePath) => { const next = readEnvFileMaybe(filePath); for (const [key, value] of Object.entries(next)) { if (!merged[key]) merged[key] = value; } return merged; }, {}); } function readClaudeSettingsEnv() { const files = [ path.join(os.homedir(), '.claude', 'settings.json'), path.join(os.homedir(), '.claude', 'settings.local.json'), path.join(process.cwd(), '.claude', 'settings.json'), path.join(process.cwd(), '.claude', 'settings.local.json') ]; const merged = {}; for (const filePath of files) { const json = readJsonMaybe(filePath); const env = json && typeof json.env === 'object' && json.env ? json.env : null; if (!env) continue; for (const [key, value] of Object.entries(env)) { if (!merged[key] && typeof value === 'string' && value.trim()) merged[key] = value; } } return merged; } function readFmodeConfig() { const files = [ path.join(os.homedir(), '.fmode', 'config.json'), path.join(process.cwd(), '.fmode', 'config.json') ]; const merged = {}; for (const filePath of files) { const json = readJsonMaybe(filePath); for (const [key, value] of Object.entries(json || {})) { if (merged[key] === undefined) merged[key] = value; } } return merged; } function pickFmodeAnthropicToken(env) { const token = env && typeof env.ANTHROPIC_AUTH_TOKEN === 'string' ? env.ANTHROPIC_AUTH_TOKEN.trim() : ''; if (!token || !/^sk-/i.test(token) || /^sk-ant-/i.test(token)) return ''; const base = String((env && (env.ANTHROPIC_BASE_URL || env.ANTHROPIC_API_BASE)) || '').toLowerCase(); if (base && !base.includes('fmode')) return ''; return token; } function normalizeToken(value) { return String(value || '').trim().replace(/^Bearer\s+/i, ''); } function readQiweiAuthToken(input = {}) { const fileEnv = readEnvFiles(); const claudeEnv = readClaudeSettingsEnv(); const fmodeConfig = readFmodeConfig(); const token = firstNonEmpty([ input.authToken, input.fmodeApiKey, input.fmodeApiToken, input.sessionToken, input.apiToken, input.token, fileEnv.QIWEI_AUTH_TOKEN, fileEnv.QIWE_AUTH_TOKEN, fileEnv.FMODE_API_KEY, fileEnv.FMODE_API_TOKEN, fileEnv.NEWAPI_TOKEN, fileEnv.VOC_TOKEN, fileEnv.VOC_SOCIAL_TOKEN, process.env.QIWEI_AUTH_TOKEN, process.env.QIWE_AUTH_TOKEN, process.env.FMODE_API_KEY, process.env.FMODE_API_TOKEN, process.env.NEWAPI_TOKEN, process.env.VOC_TOKEN, process.env.VOC_SOCIAL_TOKEN, fmodeConfig.newapiToken, fmodeConfig.newApiToken, fmodeConfig.fmodeApiKey, fmodeConfig.fmodeApiToken, claudeEnv.FMODE_API_KEY, claudeEnv.NEWAPI_TOKEN, pickFmodeAnthropicToken(process.env), pickFmodeAnthropicToken(claudeEnv) ]); return normalizeToken(token); } function readCredentialsFile() { return readJsonMaybe(CREDENTIALS_FILE); } function readQiweiUid(input = {}) { const fileEnv = readEnvFiles(); const creds = readCredentialsFile(); return firstNonEmpty([ input.uid, input.qiweiUid, fileEnv.QIWEI_UID, fileEnv.QIWE_UID, process.env.QIWEI_UID, process.env.QIWE_UID, creds.uid ]); } function readQiweiApiBase(input = {}) { const fileEnv = readEnvFiles(); const creds = readCredentialsFile(); return String(firstNonEmpty([ input.apiBase, input.baseUrl, fileEnv.QIWEI_API_BASE, fileEnv.QIWEI_RELAY_BASE_URL, fileEnv.QIWE_API_BASE, fileEnv.QIWE_RELAY_BASE_URL, process.env.QIWEI_API_BASE, process.env.QIWEI_RELAY_BASE_URL, process.env.QIWE_API_BASE, process.env.QIWE_RELAY_BASE_URL, creds.apiBase, DEFAULT_API_BASE ]) || DEFAULT_API_BASE).replace(/\/$/, ''); } function saveQiweiClientConfig({ uid, apiBase } = {}) { const saved = []; try { const dir = path.dirname(CREDENTIALS_FILE); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const current = readCredentialsFile(); const next = { ...current }; if (uid) next.uid = uid; if (apiBase) next.apiBase = apiBase; next.updatedAt = new Date().toISOString(); fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(next, null, 2), 'utf8'); saved.push(CREDENTIALS_FILE); } catch { // ignore, fall through to env file } try { const envPath = path.join(process.cwd(), '.env.local'); const pairs = []; if (uid) pairs.push(['QIWEI_UID', uid]); if (apiBase) pairs.push(['QIWEI_API_BASE', apiBase]); if (pairs.length) { let content = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : ''; for (const [key, value] of pairs) { const line = `${key}=${value}`; const re = new RegExp(`^${key}\\s*=.*$`, 'm'); content = re.test(content) ? content.replace(re, line) : `${content.replace(/\n*$/, '')}${content ? '\n' : ''}${line}\n`; } fs.writeFileSync(envPath, content, 'utf8'); saved.push(envPath); } } catch { // ignore } return saved; } function ensureQiweiUid(input = {}) { const existing = readQiweiUid(input); if (existing) return existing; const uid = `qiwei-${crypto.randomUUID()}`; saveQiweiClientConfig({ uid, apiBase: readQiweiApiBase(input) }); return uid; } function isConfigured(input = {}) { return Boolean(readQiweiAuthToken(input) && readQiweiUid(input)); } module.exports = { DEFAULT_API_BASE, CREDENTIALS_FILE, readQiweiAuthToken, readQiweiUid, ensureQiweiUid, readQiweiApiBase, saveQiweiClientConfig, isConfigured, readEnvFiles, readClaudeSettingsEnv, readFmodeConfig, pickFmodeAnthropicToken };