| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- 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
- };
|