credentials.js 7.3 KB

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