creditdaddy 0.9.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,147 +1,147 @@
1
- /**
2
- * 本机 Qoder 检测 — 安装探测 + 凭据扫描。
3
- *
4
- * Qoder IDE 是 VS Code 系(Electron),凭据散落在 userData 目录;
5
- * 另扫 ~/.qoder(CLI)。扫描纯本地进行,候选 token 只保留在内存,
6
- * 面板按 candidateId 导入,token 不在日志中输出。
7
- */
8
-
9
- import fs from 'node:fs';
10
- import fsp from 'node:fs/promises';
11
- import path from 'node:path';
12
- import os from 'node:os';
13
- import crypto from 'node:crypto';
14
-
15
- // 候选内存表:candidateId → { token, file?, provider?, user?, refreshToken?, expiresAt?, at }(10 分钟过期)
16
- const candidates = new Map();
17
- const CAND_TTL_MS = 10 * 60 * 1000;
18
-
19
- const TOKEN_RE = /\b(?:dt|pt)-[A-Za-z0-9_-]{16,}\b/g;
20
-
21
- /** 纯函数:提取文本中的 Qoder token(去重)——供测试 */
22
- export function extractTokens(text) {
23
- const found = new Set();
24
- let m;
25
- TOKEN_RE.lastIndex = 0;
26
- while ((m = TOKEN_RE.exec(String(text))) !== null) found.add(m[0]);
27
- return [...found];
28
- }
29
-
30
- function mask(token) {
31
- return token.slice(0, 5) + '...' + token.slice(-4);
32
- }
33
-
34
- function isWindows() { return process.platform === 'win32'; }
35
- function isMac() { return process.platform === 'darwin'; }
36
-
37
- /** 安装探测:IDE 可执行文件 + CLI */
38
- export function detectInstalls() {
39
- const home = os.homedir();
40
- const appdata = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
41
- const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
42
- const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
43
-
44
- const idePaths = isMac()
45
- ? ['/Applications/Qoder.app']
46
- : isWindows()
47
- ? [
48
- path.join(localAppData, 'Programs', 'Qoder', 'Qoder.exe'),
49
- path.join(programFiles, 'Qoder', 'Qoder.exe'),
50
- ]
51
- : ['/usr/bin/qoder', '/opt/Qoder/qoder', '/usr/share/qoder/qoder'];
52
-
53
- const ideDataDirs = isMac()
54
- ? [path.join(home, 'Library', 'Application Support', 'Qoder')]
55
- : isWindows()
56
- ? [path.join(appdata, 'Qoder')]
57
- : [path.join(home, '.config', 'Qoder')];
58
-
59
- const exists = (p) => { try { return fs.existsSync(p); } catch { return false; } };
60
-
61
- return {
62
- platform: process.platform,
63
- ide: idePaths.map((p) => ({ path: p, exists: exists(p) })),
64
- ideDataDirs: ideDataDirs.map((p) => ({ path: p, exists: exists(p) })),
65
- cliDir: { path: path.join(home, '.qoder'), exists: exists(path.join(home, '.qoder')) },
66
- installed: idePaths.some((p) => exists(p)),
67
- };
68
- }
69
-
70
- // 扫描时跳过的目录(缓存/噪音)
71
- const SKIP_DIRS = new Set([
72
- 'node_modules', '.git', 'Cache', 'CachedData', 'CacheStorage', 'Code Cache',
73
- 'GPUCache', 'logs', 'Crashes', 'crashpad', 'blob_storage', 'Service Worker',
74
- 'History', 'DawnGraphiteCache', 'DawnWebGPUCache', 'SharedDictionary',
75
- 'index', 'tmp', 'temp', 'workspaceStorage',
76
- ]);
77
-
78
- const MAX_FILE_BYTES = 1.5 * 1024 * 1024;
79
- const MAX_FILES = 4000;
80
- const MAX_MS = 6000;
81
-
82
- /** 扫描目录集合,产出候选 token(含少量上下文以辅助辨认) */
83
- export async function scanLocalTokens(dirs) {
84
- const deadline = Date.now() + MAX_MS;
85
- const found = new Map(); // token → { file, count }
86
- let scanned = 0;
87
-
88
- async function walk(dir, depth) {
89
- if (depth > 5 || scanned >= MAX_FILES || Date.now() > deadline) return;
90
- let entries;
91
- try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch { return; }
92
- for (const e of entries) {
93
- if (scanned >= MAX_FILES || Date.now() > deadline) return;
94
- const p = path.join(dir, e.name);
95
- if (e.isDirectory()) {
96
- if (!SKIP_DIRS.has(e.name) && !e.name.startsWith('.')) await walk(p, depth + 1);
97
- continue;
98
- }
99
- let st;
100
- try { st = await fsp.stat(p); } catch { continue; }
101
- if (!st.isFile() || st.size === 0 || st.size > MAX_FILE_BYTES) continue;
102
- scanned++;
103
- let text;
104
- try { text = await fsp.readFile(p, 'utf8'); } catch { continue; }
105
- for (const token of extractTokens(text)) {
106
- const prev = found.get(token);
107
- if (prev) prev.count++;
108
- else found.set(token, { file: p, count: 1 });
109
- }
110
- }
111
- }
112
-
113
- for (const d of dirs) {
114
- try { if (fs.existsSync(d) && fs.statSync(d).isDirectory()) await walk(d, 0); } catch {}
115
- }
116
-
117
- const list = [];
118
- for (const [token, meta] of found) {
119
- const id = putCandidate({ token, file: meta.file });
120
- list.push({
121
- id,
122
- tokenMasked: mask(token),
123
- kind: token.startsWith('pt-') ? 'PAT' : 'device',
124
- file: meta.file,
125
- hits: meta.count,
126
- });
127
- }
128
- list.sort((a, b) => b.hits - a.hits);
129
- return { scanned, candidates: list.slice(0, 50) };
130
- }
131
-
132
- /** 放入候选表(带 TTL),返回 candidateId。c 至少含 token,可带 provider / user / refreshToken / expiresAt */
133
- export function putCandidate(c) {
134
- const now = Date.now();
135
- for (const [k, v] of candidates) if (v.at < now - CAND_TTL_MS) candidates.delete(k);
136
- const id = crypto.createHash('sha256').update(c.token).digest('hex').slice(0, 16);
137
- candidates.set(id, { ...c, at: now });
138
- return id;
139
- }
140
-
141
- /** 按 candidateId 读取候选(不会消费掉,导入前可反复查询);过期返回 null */
142
- export function peekCandidate(id) {
143
- const c = candidates.get(id);
144
- if (!c) return null;
145
- if (c.at < Date.now() - CAND_TTL_MS) { candidates.delete(id); return null; }
146
- return c;
147
- }
1
+ /**
2
+ * 本机 Qoder 检测 — 安装探测 + 凭据扫描。
3
+ *
4
+ * Qoder IDE 是 VS Code 系(Electron),凭据散落在 userData 目录;
5
+ * 另扫 ~/.qoder(CLI)。扫描纯本地进行,候选 token 只保留在内存,
6
+ * 面板按 candidateId 导入,token 不在日志中输出。
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import fsp from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import os from 'node:os';
13
+ import crypto from 'node:crypto';
14
+
15
+ // 候选内存表:candidateId → { token, file?, provider?, user?, refreshToken?, expiresAt?, at }(10 分钟过期)
16
+ const candidates = new Map();
17
+ const CAND_TTL_MS = 10 * 60 * 1000;
18
+
19
+ const TOKEN_RE = /\b(?:dt|pt)-[A-Za-z0-9_-]{16,}\b/g;
20
+
21
+ /** 纯函数:提取文本中的 Qoder token(去重)——供测试 */
22
+ export function extractTokens(text) {
23
+ const found = new Set();
24
+ let m;
25
+ TOKEN_RE.lastIndex = 0;
26
+ while ((m = TOKEN_RE.exec(String(text))) !== null) found.add(m[0]);
27
+ return [...found];
28
+ }
29
+
30
+ function mask(token) {
31
+ return token.slice(0, 5) + '...' + token.slice(-4);
32
+ }
33
+
34
+ function isWindows() { return process.platform === 'win32'; }
35
+ function isMac() { return process.platform === 'darwin'; }
36
+
37
+ /** 安装探测:IDE 可执行文件 + CLI */
38
+ export function detectInstalls() {
39
+ const home = os.homedir();
40
+ const appdata = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
41
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
42
+ const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
43
+
44
+ const idePaths = isMac()
45
+ ? ['/Applications/Qoder.app']
46
+ : isWindows()
47
+ ? [
48
+ path.join(localAppData, 'Programs', 'Qoder', 'Qoder.exe'),
49
+ path.join(programFiles, 'Qoder', 'Qoder.exe'),
50
+ ]
51
+ : ['/usr/bin/qoder', '/opt/Qoder/qoder', '/usr/share/qoder/qoder'];
52
+
53
+ const ideDataDirs = isMac()
54
+ ? [path.join(home, 'Library', 'Application Support', 'Qoder')]
55
+ : isWindows()
56
+ ? [path.join(appdata, 'Qoder')]
57
+ : [path.join(home, '.config', 'Qoder')];
58
+
59
+ const exists = (p) => { try { return fs.existsSync(p); } catch { return false; } };
60
+
61
+ return {
62
+ platform: process.platform,
63
+ ide: idePaths.map((p) => ({ path: p, exists: exists(p) })),
64
+ ideDataDirs: ideDataDirs.map((p) => ({ path: p, exists: exists(p) })),
65
+ cliDir: { path: path.join(home, '.qoder'), exists: exists(path.join(home, '.qoder')) },
66
+ installed: idePaths.some((p) => exists(p)),
67
+ };
68
+ }
69
+
70
+ // 扫描时跳过的目录(缓存/噪音)
71
+ const SKIP_DIRS = new Set([
72
+ 'node_modules', '.git', 'Cache', 'CachedData', 'CacheStorage', 'Code Cache',
73
+ 'GPUCache', 'logs', 'Crashes', 'crashpad', 'blob_storage', 'Service Worker',
74
+ 'History', 'DawnGraphiteCache', 'DawnWebGPUCache', 'SharedDictionary',
75
+ 'index', 'tmp', 'temp', 'workspaceStorage',
76
+ ]);
77
+
78
+ const MAX_FILE_BYTES = 1.5 * 1024 * 1024;
79
+ const MAX_FILES = 4000;
80
+ const MAX_MS = 6000;
81
+
82
+ /** 扫描目录集合,产出候选 token(含少量上下文以辅助辨认) */
83
+ export async function scanLocalTokens(dirs) {
84
+ const deadline = Date.now() + MAX_MS;
85
+ const found = new Map(); // token → { file, count }
86
+ let scanned = 0;
87
+
88
+ async function walk(dir, depth) {
89
+ if (depth > 5 || scanned >= MAX_FILES || Date.now() > deadline) return;
90
+ let entries;
91
+ try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch { return; }
92
+ for (const e of entries) {
93
+ if (scanned >= MAX_FILES || Date.now() > deadline) return;
94
+ const p = path.join(dir, e.name);
95
+ if (e.isDirectory()) {
96
+ if (!SKIP_DIRS.has(e.name) && !e.name.startsWith('.')) await walk(p, depth + 1);
97
+ continue;
98
+ }
99
+ let st;
100
+ try { st = await fsp.stat(p); } catch { continue; }
101
+ if (!st.isFile() || st.size === 0 || st.size > MAX_FILE_BYTES) continue;
102
+ scanned++;
103
+ let text;
104
+ try { text = await fsp.readFile(p, 'utf8'); } catch { continue; }
105
+ for (const token of extractTokens(text)) {
106
+ const prev = found.get(token);
107
+ if (prev) prev.count++;
108
+ else found.set(token, { file: p, count: 1 });
109
+ }
110
+ }
111
+ }
112
+
113
+ for (const d of dirs) {
114
+ try { if (fs.existsSync(d) && fs.statSync(d).isDirectory()) await walk(d, 0); } catch {}
115
+ }
116
+
117
+ const list = [];
118
+ for (const [token, meta] of found) {
119
+ const id = putCandidate({ token, file: meta.file });
120
+ list.push({
121
+ id,
122
+ tokenMasked: mask(token),
123
+ kind: token.startsWith('pt-') ? 'PAT' : 'device',
124
+ file: meta.file,
125
+ hits: meta.count,
126
+ });
127
+ }
128
+ list.sort((a, b) => b.hits - a.hits);
129
+ return { scanned, candidates: list.slice(0, 50) };
130
+ }
131
+
132
+ /** 放入候选表(带 TTL),返回 candidateId。c 至少含 token,可带 provider / user / refreshToken / expiresAt */
133
+ export function putCandidate(c) {
134
+ const now = Date.now();
135
+ for (const [k, v] of candidates) if (v.at < now - CAND_TTL_MS) candidates.delete(k);
136
+ const id = crypto.createHash('sha256').update(c.token).digest('hex').slice(0, 16);
137
+ candidates.set(id, { ...c, at: now });
138
+ return id;
139
+ }
140
+
141
+ /** 按 candidateId 读取候选(不会消费掉,导入前可反复查询);过期返回 null */
142
+ export function peekCandidate(id) {
143
+ const c = candidates.get(id);
144
+ if (!c) return null;
145
+ if (c.at < Date.now() - CAND_TTL_MS) { candidates.delete(id); return null; }
146
+ return c;
147
+ }
package/src/logger.js CHANGED
@@ -1,8 +1,38 @@
1
- /** 轻量日志:环形缓冲(供面板查看)+ 控制台输出 */
1
+ /** 轻量日志:环形缓冲(供面板查看)+ 控制台输出 + 按日期归档落盘 */
2
2
 
3
3
  const RING_SIZE = 300;
4
4
  const ring = [];
5
5
 
6
+ // ─── 按日期归档:logs/daemon-YYYY-MM-DD.log(跨天自动换文件) ───
7
+ import fsSync from 'node:fs';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+
11
+ let archiveDir = null;
12
+ try {
13
+ const explicit = process.env.CREDITDADDY_HOME || process.env.QODERDADDY_HOME;
14
+ const base = explicit || path.join(os.homedir(), '.creditdaddy');
15
+ archiveDir = path.join(base, 'logs');
16
+ fsSync.mkdirSync(archiveDir, { recursive: true });
17
+ } catch { archiveDir = null; }
18
+
19
+ let archiveDay = null;
20
+ let archiveStream = null;
21
+
22
+ function archiveLine(line) {
23
+ if (!archiveDir) return;
24
+ try {
25
+ const day = line.at.slice(0, 10);
26
+ if (archiveDay !== day || !archiveStream) {
27
+ if (archiveStream) archiveStream.end();
28
+ archiveStream = fsSync.createWriteStream(path.join(archiveDir, `daemon-${day}.log`), { flags: 'a' });
29
+ archiveDay = day;
30
+ }
31
+ archiveStream.write('[' + line.at + '] ' + line.level.toUpperCase() + ' [' + line.tag + '] ' + line.msg + '\n');
32
+ } catch { // 落盘失败不拖累主流程
33
+ }
34
+ }
35
+
6
36
  function ts() {
7
37
  const d = new Date();
8
38
  const pad = (n) => String(n).padStart(2, '0');
@@ -14,6 +44,7 @@ export function log(level, tag, msg) {
14
44
  const line = { at: ts(), level, tag, msg: String(msg) };
15
45
  ring.push(line);
16
46
  if (ring.length > RING_SIZE) ring.shift();
47
+ archiveLine(line);
17
48
  const prefix = level === 'error' ? '✗' : level === 'warn' ? '!' : '·';
18
49
  console.error(`[${line.at}] ${prefix} [${tag}] ${line.msg}`);
19
50
  }
@@ -28,3 +59,10 @@ export const logger = {
28
59
  export function getLogs(limit = 100) {
29
60
  return ring.slice(-limit);
30
61
  }
62
+
63
+ /** 测试收尾用:归档流的句柄不关,测试进程的临时目录在 Windows 上删不掉 */
64
+ export function closeArchiveStream() {
65
+ try { archiveStream?.end(); } catch {}
66
+ archiveStream = null;
67
+ archiveDay = null;
68
+ }