credentials.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. const crypto = require('crypto');
  2. const fs = require('fs');
  3. const path = require('path');
  4. const os = require('os');
  5. const DEFAULT_API_BASE = 'https://server.fmode.cn/api/qiwei';
  6. const CREDENTIALS_FILE = path.join(os.homedir(), '.claude', 'qiwei-credentials.json');
  7. let activeQiweiContext = {};
  8. function setActiveQiweiContext(input = {}) {
  9. activeQiweiContext = {
  10. uid: String(input.uid || input.qiweiUid || '').trim(),
  11. guid: String(input.guid || input.qiweiGuid || input.deviceGuid || '').trim(),
  12. apiBase: String(input.apiBase || input.baseUrl || '').trim().replace(/\/$/, ''),
  13. };
  14. return { ...activeQiweiContext };
  15. }
  16. function getActiveQiweiContext() {
  17. return { ...activeQiweiContext };
  18. }
  19. function readJsonMaybe(filePath) {
  20. try {
  21. if (!filePath || !fs.existsSync(filePath)) return {};
  22. return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
  23. } catch {
  24. return {};
  25. }
  26. }
  27. function firstNonEmpty(values) {
  28. return values.find(value => typeof value === 'string' && value.trim()) || '';
  29. }
  30. function readEnvFileMaybe(filePath) {
  31. try {
  32. if (!filePath || !fs.existsSync(filePath)) return {};
  33. const env = {};
  34. const content = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
  35. for (const rawLine of content.split(/\r?\n/)) {
  36. const line = rawLine.trim();
  37. if (!line || line.startsWith('#')) continue;
  38. const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
  39. if (!match) continue;
  40. let value = match[2].trim();
  41. if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
  42. value = value.slice(1, -1);
  43. }
  44. env[match[1]] = value;
  45. }
  46. return env;
  47. } catch {
  48. return {};
  49. }
  50. }
  51. function envCandidates() {
  52. const candidates = [];
  53. let current = process.cwd();
  54. while (current) {
  55. candidates.push(path.join(current, '.env.local'));
  56. candidates.push(path.join(current, '.env'));
  57. const parent = path.dirname(current);
  58. if (parent === current) break;
  59. current = parent;
  60. }
  61. candidates.push(path.resolve(__dirname, '..', '..', '..', '.env.local'));
  62. candidates.push(path.resolve(__dirname, '..', '..', '..', '.env'));
  63. return [...new Set(candidates)];
  64. }
  65. function readEnvFiles() {
  66. return envCandidates().reduce((merged, filePath) => {
  67. const next = readEnvFileMaybe(filePath);
  68. for (const [key, value] of Object.entries(next)) {
  69. if (!merged[key]) merged[key] = value;
  70. }
  71. return merged;
  72. }, {});
  73. }
  74. function readClaudeSettingsEnv() {
  75. const files = [
  76. path.join(os.homedir(), '.claude', 'settings.json'),
  77. path.join(os.homedir(), '.claude', 'settings.local.json'),
  78. path.join(process.cwd(), '.claude', 'settings.json'),
  79. path.join(process.cwd(), '.claude', 'settings.local.json')
  80. ];
  81. const merged = {};
  82. for (const filePath of files) {
  83. const json = readJsonMaybe(filePath);
  84. const env = json && typeof json.env === 'object' && json.env ? json.env : null;
  85. if (!env) continue;
  86. for (const [key, value] of Object.entries(env)) {
  87. if (!merged[key] && typeof value === 'string' && value.trim()) merged[key] = value;
  88. }
  89. }
  90. return merged;
  91. }
  92. function readFmodeConfig() {
  93. const files = [
  94. path.join(os.homedir(), '.fmode', 'config.json'),
  95. path.join(process.cwd(), '.fmode', 'config.json')
  96. ];
  97. const merged = {};
  98. for (const filePath of files) {
  99. const json = readJsonMaybe(filePath);
  100. for (const [key, value] of Object.entries(json || {})) {
  101. if (merged[key] === undefined) merged[key] = value;
  102. }
  103. }
  104. return merged;
  105. }
  106. function pickFmodeAnthropicToken(env) {
  107. const token = env && typeof env.ANTHROPIC_AUTH_TOKEN === 'string' ? env.ANTHROPIC_AUTH_TOKEN.trim() : '';
  108. if (!token || !/^sk-/i.test(token) || /^sk-ant-/i.test(token)) return '';
  109. const base = String((env && (env.ANTHROPIC_BASE_URL || env.ANTHROPIC_API_BASE)) || '').toLowerCase();
  110. if (base && !base.includes('fmode')) return '';
  111. return token;
  112. }
  113. function normalizeToken(value) {
  114. return String(value || '').trim().replace(/^Bearer\s+/i, '');
  115. }
  116. function readQiweiAuthToken(input = {}) {
  117. const fileEnv = readEnvFiles();
  118. const claudeEnv = readClaudeSettingsEnv();
  119. const fmodeConfig = readFmodeConfig();
  120. const token = firstNonEmpty([
  121. input.authToken,
  122. input.fmodeApiKey,
  123. input.fmodeApiToken,
  124. input.sessionToken,
  125. input.apiToken,
  126. input.token,
  127. process.env.QIWEI_AUTH_TOKEN,
  128. process.env.QIWE_AUTH_TOKEN,
  129. process.env.FMODE_API_KEY,
  130. process.env.FMODE_API_TOKEN,
  131. process.env.NEWAPI_TOKEN,
  132. pickFmodeAnthropicToken(process.env),
  133. fileEnv.QIWEI_AUTH_TOKEN,
  134. fileEnv.QIWE_AUTH_TOKEN,
  135. fileEnv.FMODE_API_KEY,
  136. fileEnv.FMODE_API_TOKEN,
  137. fileEnv.NEWAPI_TOKEN,
  138. fmodeConfig.newapiToken,
  139. fmodeConfig.newApiToken,
  140. fmodeConfig.fmodeApiKey,
  141. fmodeConfig.fmodeApiToken,
  142. claudeEnv.FMODE_API_KEY,
  143. claudeEnv.NEWAPI_TOKEN,
  144. pickFmodeAnthropicToken(claudeEnv),
  145. fileEnv.VOC_TOKEN,
  146. fileEnv.VOC_SOCIAL_TOKEN,
  147. process.env.VOC_TOKEN,
  148. process.env.VOC_SOCIAL_TOKEN
  149. ]);
  150. return normalizeToken(token);
  151. }
  152. function readCredentialsFile() {
  153. return readJsonMaybe(CREDENTIALS_FILE);
  154. }
  155. function readQiweiUid(input = {}) {
  156. const fileEnv = readEnvFiles();
  157. const creds = readCredentialsFile();
  158. return firstNonEmpty([
  159. input.uid,
  160. input.qiweiUid,
  161. activeQiweiContext.uid,
  162. process.env.QIWEI_UID,
  163. process.env.QIWE_UID,
  164. fileEnv.QIWEI_UID,
  165. fileEnv.QIWE_UID,
  166. creds.uid
  167. ]);
  168. }
  169. function readQiweiApiBase(input = {}) {
  170. const fileEnv = readEnvFiles();
  171. const creds = readCredentialsFile();
  172. return String(firstNonEmpty([
  173. input.apiBase,
  174. input.baseUrl,
  175. activeQiweiContext.apiBase,
  176. process.env.QIWEI_API_BASE,
  177. process.env.QIWEI_RELAY_BASE_URL,
  178. process.env.QIWE_API_BASE,
  179. process.env.QIWE_RELAY_BASE_URL,
  180. fileEnv.QIWEI_API_BASE,
  181. fileEnv.QIWEI_RELAY_BASE_URL,
  182. fileEnv.QIWE_API_BASE,
  183. fileEnv.QIWE_RELAY_BASE_URL,
  184. creds.apiBase,
  185. DEFAULT_API_BASE
  186. ]) || DEFAULT_API_BASE).replace(/\/$/, '');
  187. }
  188. function saveQiweiClientConfig({ uid, apiBase, guid, authToken } = {}) {
  189. const saved = [];
  190. try {
  191. const dir = path.dirname(CREDENTIALS_FILE);
  192. if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  193. const current = readCredentialsFile();
  194. const next = { ...current };
  195. if (uid) next.uid = uid;
  196. if (apiBase) next.apiBase = apiBase;
  197. if (guid) next.guid = guid;
  198. next.updatedAt = new Date().toISOString();
  199. fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(next, null, 2), 'utf8');
  200. saved.push(CREDENTIALS_FILE);
  201. } catch {
  202. // ignore, fall through to env file
  203. }
  204. try {
  205. const envPath = path.join(process.cwd(), '.env.local');
  206. const pairs = [];
  207. if (uid) pairs.push(['QIWEI_UID', uid]);
  208. if (apiBase) pairs.push(['QIWEI_API_BASE', apiBase]);
  209. if (guid) pairs.push(['QIWEI_GUID', guid]);
  210. if (authToken) pairs.push(['QIWEI_AUTH_TOKEN', normalizeToken(authToken)]);
  211. if (pairs.length) {
  212. let content = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
  213. for (const [key, value] of pairs) {
  214. const line = `${key}=${value}`;
  215. const re = new RegExp(`^${key}\\s*=.*$`, 'm');
  216. content = re.test(content)
  217. ? content.replace(re, line)
  218. : `${content.replace(/\n*$/, '')}${content ? '\n' : ''}${line}\n`;
  219. }
  220. fs.writeFileSync(envPath, content, 'utf8');
  221. saved.push(envPath);
  222. }
  223. } catch {
  224. // ignore
  225. }
  226. return saved;
  227. }
  228. function readQiweiGuid(input = {}) {
  229. const fileEnv = readEnvFiles();
  230. const creds = readCredentialsFile();
  231. return firstNonEmpty([
  232. input.guid,
  233. input.qiweiGuid,
  234. input.deviceGuid,
  235. activeQiweiContext.guid,
  236. process.env.QIWEI_GUID,
  237. process.env.QIWE_GUID,
  238. fileEnv.QIWEI_GUID,
  239. fileEnv.QIWE_GUID,
  240. creds.guid
  241. ]);
  242. }
  243. function ensureQiweiUid(input = {}) {
  244. const existing = readQiweiUid(input);
  245. if (existing) return existing;
  246. const uid = `qiwei-${crypto.randomUUID()}`;
  247. saveQiweiClientConfig({ uid, apiBase: readQiweiApiBase(input) });
  248. return uid;
  249. }
  250. function isConfigured(input = {}) {
  251. return Boolean(readQiweiAuthToken(input) && readQiweiUid(input));
  252. }
  253. module.exports = {
  254. DEFAULT_API_BASE,
  255. CREDENTIALS_FILE,
  256. readQiweiAuthToken,
  257. readQiweiUid,
  258. readQiweiGuid,
  259. ensureQiweiUid,
  260. readQiweiApiBase,
  261. saveQiweiClientConfig,
  262. setActiveQiweiContext,
  263. getActiveQiweiContext,
  264. isConfigured,
  265. readEnvFiles,
  266. readClaudeSettingsEnv,
  267. readFmodeConfig,
  268. pickFmodeAnthropicToken
  269. };