const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const os = require('os'); const { AsyncLocalStorage } = require('async_hooks'); const DEFAULT_API_BASE = 'https://server.fmode.cn/api/qiwei'; const CREDENTIALS_FILE = path.join(os.homedir(), '.claude', 'qiwei-credentials.json'); let activeQiweiContext = {}; const qiweiContextStorage = new AsyncLocalStorage(); function safeAccountKey(value, fallback = 'legacy-default') { return String(value || fallback).trim().replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 100) || fallback; } function setActiveQiweiContext(input = {}) { activeQiweiContext = { uid: String(input.uid || input.qiweiUid || '').trim(), guid: String(input.guid || input.qiweiGuid || input.deviceGuid || '').trim(), userId: String(input.userId || input.wecomUserId || '').trim(), nickname: String(input.nickname || '').trim(), corpName: String(input.corpName || '').trim(), apiBase: String(input.apiBase || input.baseUrl || '').trim().replace(/\/$/, ''), }; return { ...activeQiweiContext }; } function getActiveQiweiContext() { return { ...activeQiweiContext, ...(qiweiContextStorage.getStore() || {}) }; } function runWithQiweiContext(input = {}, callback) { if (typeof callback !== 'function') throw new TypeError('callback must be a function'); const parent = getActiveQiweiContext(); const context = { ...parent, uid: String(input.uid || input.qiweiUid || parent.uid || '').trim(), guid: String(input.guid || input.qiweiGuid || input.deviceGuid || parent.guid || '').trim(), userId: String(input.userId || input.wecomUserId || parent.userId || '').trim(), nickname: String(input.nickname || parent.nickname || '').trim(), corpName: String(input.corpName || parent.corpName || '').trim(), apiBase: String(input.apiBase || input.baseUrl || parent.apiBase || '').trim().replace(/\/$/, ''), }; return qiweiContextStorage.run(context, callback); } 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 (!Object.prototype.hasOwnProperty.call(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() : ''; const base = String((env && (env.ANTHROPIC_BASE_URL || env.ANTHROPIC_API_BASE)) || '').toLowerCase(); return pickFmodeApiToken(token, base); } function normalizeToken(value) { return String(value || '').trim().replace(/^Bearer\s+/i, ''); } function pickFmodeApiToken(value, apiBase = '') { const token = normalizeToken(value); if (!/^sk-/i.test(token) || /^sk-ant-/i.test(token)) return ''; const base = String(apiBase || '').trim().toLowerCase(); if (base && !base.includes('fmode')) return ''; return token; } function readFmodeVoiceToken(input = {}, sourceOverrides = {}) { const processEnv = sourceOverrides.processEnv || process.env; const fileEnv = sourceOverrides.fileEnv || readEnvFiles(); const claudeEnv = sourceOverrides.claudeEnv || readClaudeSettingsEnv(); const fmodeConfig = sourceOverrides.fmodeConfig || readFmodeConfig(); const voiceBase = firstNonEmpty([ input.voiceApiBase, input.endpoint, processEnv.QIWEI_VOICE_ENDPOINT, fileEnv.QIWEI_VOICE_ENDPOINT, 'https://server.fmode.cn/api/voice/indextts2' ]); const fmodeBase = firstNonEmpty([ input.fmodeApiBase, processEnv.FMODE_API_BASE, fileEnv.FMODE_API_BASE, fmodeConfig.apiBase, fmodeConfig.llmBaseUrl, 'https://api.fmode.cn' ]); return firstNonEmpty([ pickFmodeApiToken(input.voiceAuthToken || input.authToken, voiceBase), pickFmodeApiToken(processEnv.QIWEI_VOICE_AUTH_TOKEN, voiceBase), pickFmodeApiToken(fileEnv.QIWEI_VOICE_AUTH_TOKEN, voiceBase), pickFmodeApiToken(input.fmodeApiToken || input.fmodeApiKey || input.newapiToken, fmodeBase), pickFmodeApiToken(processEnv.FMODE_API_TOKEN, fmodeBase), pickFmodeApiToken(processEnv.FMODE_API_KEY, fmodeBase), pickFmodeApiToken(processEnv.NEWAPI_TOKEN, fmodeBase), pickFmodeApiToken(fileEnv.FMODE_API_TOKEN, fmodeBase), pickFmodeApiToken(fileEnv.FMODE_API_KEY, fmodeBase), pickFmodeApiToken(fileEnv.NEWAPI_TOKEN, fmodeBase), pickFmodeApiToken(fmodeConfig.fmodeApiToken, fmodeBase), pickFmodeApiToken(fmodeConfig.fmodeApiKey, fmodeBase), pickFmodeApiToken(fmodeConfig.newapiToken, fmodeBase), pickFmodeApiToken(fmodeConfig.newApiToken, fmodeBase), pickFmodeApiToken(claudeEnv.FMODE_API_TOKEN, fmodeBase), pickFmodeApiToken(claudeEnv.FMODE_API_KEY, fmodeBase), pickFmodeApiToken(claudeEnv.NEWAPI_TOKEN, fmodeBase), pickFmodeAnthropicToken(processEnv), pickFmodeAnthropicToken(claudeEnv) ]); } 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, process.env.QIWEI_AUTH_TOKEN, process.env.QIWE_AUTH_TOKEN, process.env.FMODE_API_KEY, process.env.FMODE_API_TOKEN, process.env.NEWAPI_TOKEN, pickFmodeAnthropicToken(process.env), fileEnv.QIWEI_AUTH_TOKEN, fileEnv.QIWE_AUTH_TOKEN, fileEnv.FMODE_API_KEY, fileEnv.FMODE_API_TOKEN, fileEnv.NEWAPI_TOKEN, fmodeConfig.newapiToken, fmodeConfig.newApiToken, fmodeConfig.fmodeApiKey, fmodeConfig.fmodeApiToken, claudeEnv.FMODE_API_KEY, claudeEnv.NEWAPI_TOKEN, pickFmodeAnthropicToken(claudeEnv), fileEnv.VOC_TOKEN, fileEnv.VOC_SOCIAL_TOKEN, process.env.VOC_TOKEN, process.env.VOC_SOCIAL_TOKEN ]); return normalizeToken(token); } function readQiweiUpstreamToken(input = {}) { const fileEnv = readEnvFiles(); return normalizeToken(firstNonEmpty([ input.upstreamToken, input.qiweiToken, process.env.QIWEI_UPSTREAM_TOKEN, process.env.QIWEI_TOKEN, fileEnv.QIWEI_UPSTREAM_TOKEN, fileEnv.QIWEI_TOKEN ])); } function readQiweiUpstreamApiBase(input = {}) { const fileEnv = readEnvFiles(); return String(firstNonEmpty([ input.upstreamApiBase, input.qiweiApiBase, process.env.QIWEI_UPSTREAM_API_BASE, fileEnv.QIWEI_UPSTREAM_API_BASE, 'https://manager.qiweapi.com/qiwe' ])).replace(/\/$/, ''); } function readQiweiTransportMode(input = {}) { return String(firstNonEmpty([ input.transportMode, process.env.QIWEI_TRANSPORT_MODE, readEnvFiles().QIWEI_TRANSPORT_MODE, 'fmode' ])).trim().toLowerCase(); } function readCredentialsFile() { return readJsonMaybe(CREDENTIALS_FILE); } function readQiweiUid(input = {}) { const fileEnv = readEnvFiles(); const creds = readCredentialsFile(); return firstNonEmpty([ input.uid, input.qiweiUid, activeQiweiContext.uid, process.env.QIWEI_UID, process.env.QIWE_UID, fileEnv.QIWEI_UID, fileEnv.QIWE_UID, creds.uid ]); } function readQiweiApiBase(input = {}) { const fileEnv = readEnvFiles(); const creds = readCredentialsFile(); return String(firstNonEmpty([ input.apiBase, input.baseUrl, activeQiweiContext.apiBase, process.env.QIWEI_API_BASE, process.env.QIWEI_RELAY_BASE_URL, process.env.QIWE_API_BASE, process.env.QIWE_RELAY_BASE_URL, fileEnv.QIWEI_API_BASE, fileEnv.QIWEI_RELAY_BASE_URL, fileEnv.QIWE_API_BASE, fileEnv.QIWE_RELAY_BASE_URL, creds.apiBase, DEFAULT_API_BASE ]) || DEFAULT_API_BASE).replace(/\/$/, ''); } function resolveQiweiEnvPath(envRoot) { return path.join(envRoot ? path.resolve(envRoot) : process.cwd(), '.env.local'); } function saveQiweiClientConfig(input = {}) { const { uid, apiBase, guid, authToken, envRoot, userId, nickname, corpName } = input; const hasGuid = Object.prototype.hasOwnProperty.call(input, 'guid'); 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; if (hasGuid) { if (guid) next.guid = guid; else delete next.guid; } for (const [key, value] of Object.entries({ userId, nickname, corpName })) { if (Object.prototype.hasOwnProperty.call(input, key)) next[key] = String(value || '').trim(); } 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 = resolveQiweiEnvPath(envRoot); const pairs = []; if (uid) pairs.push(['QIWEI_UID', uid]); if (apiBase) pairs.push(['QIWEI_API_BASE', apiBase]); if (hasGuid) pairs.push(['QIWEI_GUID', String(guid || '').trim()]); if (authToken) pairs.push(['QIWEI_AUTH_TOKEN', normalizeToken(authToken)]); 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 } if (uid) process.env.QIWEI_UID = String(uid).trim(); if (apiBase) process.env.QIWEI_API_BASE = String(apiBase).trim().replace(/\/$/, ''); if (hasGuid) { if (guid) process.env.QIWEI_GUID = String(guid).trim(); else process.env.QIWEI_GUID = ''; } return saved; } function readQiweiAccountMetadata(input = {}) { const creds = readCredentialsFile(); const fileEnv = readEnvFiles(); return { userId: firstNonEmpty([input.userId, input.wecomUserId, activeQiweiContext.userId, creds.userId]), nickname: firstNonEmpty([input.nickname, activeQiweiContext.nickname, creds.nickname]), corpName: firstNonEmpty([input.corpName, activeQiweiContext.corpName, creds.corpName]), legacyAccountKey: safeAccountKey(firstNonEmpty([ input.legacyAccountKey, process.env.QIWEI_LEGACY_ACCOUNT_KEY, fileEnv.QIWEI_LEGACY_ACCOUNT_KEY, 'legacy-default' ])) }; } function qiweiAccountKey(input = {}) { const metadata = readQiweiAccountMetadata(input); if (!metadata.userId) return metadata.legacyAccountKey; return crypto.createHash('sha256').update(metadata.userId).digest('hex').slice(0, 16); } function readQiweiGuid(input = {}) { const fileEnv = readEnvFiles(); const creds = readCredentialsFile(); const processGuid = Object.prototype.hasOwnProperty.call(process.env, 'QIWEI_GUID') ? process.env.QIWEI_GUID : process.env.QIWE_GUID; const fileGuid = Object.prototype.hasOwnProperty.call(fileEnv, 'QIWEI_GUID') ? fileEnv.QIWEI_GUID : fileEnv.QIWE_GUID; const requestedUid = firstNonEmpty([input.uid, input.qiweiUid]); const storedUid = firstNonEmpty([ process.env.QIWEI_UID, process.env.QIWE_UID, fileEnv.QIWEI_UID, fileEnv.QIWE_UID, creds.uid, activeQiweiContext.uid ]); const contextMatchesUid = !requestedUid || !storedUid || requestedUid === storedUid; return firstNonEmpty([ input.guid, input.qiweiGuid, input.deviceGuid, contextMatchesUid ? activeQiweiContext.guid : '', contextMatchesUid ? processGuid : '', contextMatchesUid ? fileGuid : '', contextMatchesUid ? creds.guid : '' ]); } function readFmodeApiKey(input = {}) { const fmodeConfig = readFmodeConfig(); return firstNonEmpty([ input.fmodeApiKey, input.newapiToken, input.fmodeApiToken, process.env.FMODE_API_KEY, process.env.FMODE_API_TOKEN, process.env.NEWAPI_TOKEN, fmodeConfig.newapiToken, fmodeConfig.fmodeApiToken, fmodeConfig.newApiToken ]); } function readFmodeLlmBase(input = {}) { const fmodeConfig = readFmodeConfig(); return String(firstNonEmpty([ input.fmodeLlmBase, input.llmBaseUrl, process.env.FMODE_LLM_BASE_URL, process.env.FMODE_API_BASE, fmodeConfig.llmBaseUrl, fmodeConfig.apiBase, 'https://api.fmode.cn' ]) || 'https://api.fmode.cn').replace(/\/$/, ''); } 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, readQiweiUpstreamToken, readQiweiUpstreamApiBase, readQiweiTransportMode, readFmodeVoiceToken, readQiweiUid, readQiweiGuid, readFmodeApiKey, readFmodeLlmBase, ensureQiweiUid, readQiweiApiBase, resolveQiweiEnvPath, saveQiweiClientConfig, readQiweiAccountMetadata, qiweiAccountKey, safeAccountKey, setActiveQiweiContext, getActiveQiweiContext, runWithQiweiContext, isConfigured, readEnvFiles, readClaudeSettingsEnv, readFmodeConfig, pickFmodeAnthropicToken, pickFmodeApiToken };