@hileeon/mcc 0.1.3 → 0.1.5

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.
Files changed (51) hide show
  1. package/dist/dashboard-server.js +1 -1
  2. package/dist/dashboard-server.js.map +1 -1
  3. package/dist/mcc.js +1 -1
  4. package/dist/ui/assets/index-B16lhKZ6.js +40 -0
  5. package/dist/ui/assets/index-jEfiB6-h.css +1 -0
  6. package/{ui → dist/ui}/index.html +3 -2
  7. package/package.json +8 -2
  8. package/.claude/CLAUDE.md +0 -204
  9. package/.claude/agents/.gitkeep +0 -0
  10. package/.claude/settings.json +0 -9
  11. package/.claude/skills/.gitkeep +0 -0
  12. package/docs/decisions.md +0 -33
  13. package/docs/lessons.md +0 -8
  14. package/docs/product.md +0 -37
  15. package/src/accounts/instance-manager.ts +0 -58
  16. package/src/accounts/shared-manager.ts +0 -154
  17. package/src/accounts/store.ts +0 -111
  18. package/src/core/model-router.ts +0 -82
  19. package/src/dashboard-server.ts +0 -427
  20. package/src/mcc.ts +0 -482
  21. package/src/mcp/external-registry.ts +0 -73
  22. package/src/mcp/installer.ts +0 -258
  23. package/src/mcp/mcp-config.ts +0 -168
  24. package/src/mcp/registry.ts +0 -89
  25. package/src/proxy/proxy-daemon.ts +0 -184
  26. package/src/proxy/proxy-entry.ts +0 -63
  27. package/src/proxy/proxy-paths.ts +0 -97
  28. package/src/proxy/proxy-server.ts +0 -278
  29. package/src/proxy/upstream-url.ts +0 -38
  30. package/src/shared/logger.ts +0 -140
  31. package/src/shared/provider-preset-catalog.ts +0 -340
  32. package/tsconfig.json +0 -33
  33. package/ui/.prettierrc +0 -9
  34. package/ui/package.json +0 -33
  35. package/ui/postcss.config.js +0 -6
  36. package/ui/src/App.tsx +0 -753
  37. package/ui/src/components/ui/button.tsx +0 -48
  38. package/ui/src/components/ui/card.tsx +0 -50
  39. package/ui/src/components/ui/input.tsx +0 -21
  40. package/ui/src/components/ui/label.tsx +0 -20
  41. package/ui/src/components/ui/select.tsx +0 -80
  42. package/ui/src/components/ui/switch.tsx +0 -26
  43. package/ui/src/components/ui/tabs.tsx +0 -52
  44. package/ui/src/index.css +0 -33
  45. package/ui/src/lib/api.ts +0 -185
  46. package/ui/src/lib/utils.ts +0 -6
  47. package/ui/src/main.tsx +0 -10
  48. package/ui/src/vite-env.d.ts +0 -1
  49. package/ui/tailwind.config.js +0 -49
  50. package/ui/tsconfig.json +0 -25
  51. package/ui/vite.config.ts +0 -20
@@ -1,63 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Proxy Daemon Entrypoint
4
- *
5
- * This file is spawned as a detached child process by proxy-daemon.ts.
6
- * It parses CLI args and starts the proxy server.
7
- *
8
- * Ported from CCS proxy/proxy-daemon-entry.js (simplified for single-profile MCC).
9
- */
10
-
11
- import { startProxyServer, type ProxyServerOptions } from './proxy-server';
12
-
13
- function parseArgs(argv: string[]): ProxyServerOptions {
14
- let port = 43456;
15
- let host = '127.0.0.1';
16
- let baseUrl = '';
17
- let apiKey = '';
18
- let model = '';
19
- let authToken = '';
20
-
21
- for (let i = 0; i < argv.length; i++) {
22
- const arg = argv[i];
23
- if (arg === '--port' && argv[i + 1]) { port = Number.parseInt(argv[++i] || '', 10) || port; continue; }
24
- if (arg === '--host' && argv[i + 1]) { host = argv[++i] || host; continue; }
25
- if (arg === '--base-url' && argv[i + 1]) { baseUrl = argv[++i] || ''; continue; }
26
- if (arg === '--api-key' && argv[i + 1]) { apiKey = argv[++i] || ''; continue; }
27
- if (arg === '--model' && argv[i + 1]) { model = argv[++i] || ''; continue; }
28
- if (arg === '--auth-token' && argv[i + 1]) { authToken = argv[++i] || ''; continue; }
29
- }
30
-
31
- if (!authToken.trim()) {
32
- throw new Error('Missing local proxy auth token');
33
- }
34
- if (!baseUrl.trim()) {
35
- throw new Error('Missing upstream base URL');
36
- }
37
- if (!apiKey.trim()) {
38
- throw new Error('Missing upstream API key');
39
- }
40
-
41
- return { port, host, baseUrl, apiKey, model: model || undefined, authToken };
42
- }
43
-
44
- function main(): void {
45
- const options = parseArgs(process.argv.slice(2));
46
- const server = startProxyServer(options);
47
-
48
- server.once('error', (error) => {
49
- console.error(`[MCC Proxy] Server error: ${error.message}`);
50
- process.exit(1);
51
- });
52
-
53
- console.log(`[MCC Proxy] Listening on http://${options.host}:${options.port}`);
54
-
55
- const shutdown = () => {
56
- console.log('[MCC Proxy] Shutting down...');
57
- server.close();
58
- };
59
- process.on('SIGTERM', shutdown);
60
- process.on('SIGINT', shutdown);
61
- }
62
-
63
- main();
@@ -1,97 +0,0 @@
1
- /**
2
- * Proxy path constants and session/PID file management
3
- * Ported from CCS proxy-daemon-paths.js + proxy-daemon-state.js
4
- */
5
-
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
-
9
- function getMccHome(): string {
10
- return process.env.MCC_HOME ?? path.join(process.env.HOME ?? process.env.USERPROFILE ?? '~', '.mcc');
11
- }
12
-
13
- export const PROXY_PORT_START = 43456;
14
- export const PROXY_PORT_END = 43555;
15
- export const PROXY_SERVICE_NAME = 'mcc-openai-compat-proxy';
16
-
17
- export function getProxyDir(): string {
18
- return path.join(getMccHome(), 'proxy');
19
- }
20
-
21
- export function getProxyPidPath(profileName: string): string {
22
- return path.join(getProxyDir(), `${encodeURIComponent(profileName)}.daemon.pid`);
23
- }
24
-
25
- export function getProxySessionPath(profileName: string): string {
26
- return path.join(getProxyDir(), `${encodeURIComponent(profileName)}.session.json`);
27
- }
28
-
29
- export interface ProxySession {
30
- profileName: string;
31
- port: number;
32
- host: string;
33
- authToken: string;
34
- baseUrl: string;
35
- startedAt: string;
36
- }
37
-
38
- function ensureProxyDir(): void {
39
- fs.mkdirSync(getProxyDir(), { recursive: true, mode: 0o700 });
40
- }
41
-
42
- export function readProxyPid(profileName: string): number | null {
43
- try {
44
- const raw = fs.readFileSync(getProxyPidPath(profileName), 'utf8').trim();
45
- const pid = Number.parseInt(raw, 10);
46
- return Number.isInteger(pid) ? pid : null;
47
- } catch {
48
- return null;
49
- }
50
- }
51
-
52
- export function writeProxyPid(profileName: string, pid: number): void {
53
- ensureProxyDir();
54
- fs.writeFileSync(getProxyPidPath(profileName), String(pid), 'utf8');
55
- }
56
-
57
- export function removeProxyPid(profileName: string): void {
58
- try {
59
- fs.unlinkSync(getProxyPidPath(profileName));
60
- } catch {
61
- // Best-effort cleanup
62
- }
63
- }
64
-
65
- export function readProxySession(profileName: string): ProxySession | null {
66
- try {
67
- return JSON.parse(fs.readFileSync(getProxySessionPath(profileName), 'utf8'));
68
- } catch {
69
- return null;
70
- }
71
- }
72
-
73
- export function writeProxySession(session: ProxySession): void {
74
- ensureProxyDir();
75
- fs.writeFileSync(getProxySessionPath(session.profileName), JSON.stringify(session, null, 2) + '\n', 'utf8');
76
- }
77
-
78
- export function removeProxySession(profileName: string): void {
79
- try {
80
- fs.unlinkSync(getProxySessionPath(profileName));
81
- } catch {
82
- // Best-effort cleanup
83
- }
84
- }
85
-
86
- export function hashProfileName(profileName: string): number {
87
- let hash = 0;
88
- for (const char of profileName.trim()) {
89
- hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
90
- }
91
- return hash;
92
- }
93
-
94
- export function resolveAdaptivePort(profileName: string): number {
95
- const rangeSize = PROXY_PORT_END - PROXY_PORT_START + 1;
96
- return PROXY_PORT_START + (hashProfileName(profileName) % rangeSize);
97
- }
@@ -1,278 +0,0 @@
1
- /**
2
- * Proxy Server - Simplified translation proxy for MCC
3
- *
4
- * Accepts Anthropic /v1/messages requests, translates to OpenAI chat/completions,
5
- * forwards to upstream provider, translates response back.
6
- *
7
- * Ported from CCS proxy/server/proxy-server.js + messages-route.js + http-helpers.js
8
- * Simplified: single-profile, no multi-profile routing, no CCS logging, native fetch.
9
- */
10
-
11
- import * as http from 'http';
12
- import * as stream from 'stream';
13
- import { createRequire } from 'module';
14
- import * as path from 'path';
15
- import { resolveOpenAIChatCompletionsUrl } from './upstream-url';
16
- import { PROXY_SERVICE_NAME } from './proxy-paths';
17
-
18
- // Load compiled JS modules from CCS (copied to lib/proxy/)
19
- const libProxyDir = path.resolve(__dirname, '..', '..', 'lib', 'proxy');
20
- const requireFromLib = createRequire(path.join(libProxyDir, 'noop.js'));
21
- const { ProxyRequestTransformer } = requireFromLib('./transformers/request-transformer');
22
- const { ProxySseStreamTransformer } = requireFromLib('./transformers/sse-stream-transformer');
23
-
24
- const REQUEST_TIMEOUT_MS = 600_000; // 10 minutes
25
-
26
- export interface ProxyServerOptions {
27
- host: string;
28
- port: number;
29
- authToken: string;
30
- baseUrl: string;
31
- apiKey: string;
32
- model?: string;
33
- }
34
-
35
- // --- HTTP Helpers (from CCS http-helpers.js) ---
36
-
37
- function writeJson(res: http.ServerResponse, statusCode: number, payload: unknown): void {
38
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
39
- res.end(JSON.stringify(payload));
40
- }
41
-
42
- const MAX_BODY_SIZE = 10 * 1024 * 1024;
43
-
44
- function readJsonBody(req: http.IncomingMessage): Promise<Record<string, unknown>> {
45
- return new Promise((resolve, reject) => {
46
- const chunks: Buffer[] = [];
47
- let total = 0;
48
- let settled = false;
49
-
50
- const resolveOnce = (payload: Record<string, unknown>) => {
51
- if (!settled) { settled = true; resolve(payload); }
52
- };
53
- const rejectOnce = (error: Error) => {
54
- if (!settled) { settled = true; reject(error); }
55
- };
56
-
57
- req.on('data', (chunk: Buffer) => {
58
- total += chunk.length;
59
- if (total > MAX_BODY_SIZE) {
60
- req.pause();
61
- rejectOnce(new Error('Request body too large (max 10MB)'));
62
- return;
63
- }
64
- chunks.push(chunk);
65
- });
66
-
67
- req.on('end', () => {
68
- const raw = Buffer.concat(chunks).toString('utf8').trim();
69
- if (!raw) { resolveOnce({}); return; }
70
- try { resolveOnce(JSON.parse(raw)); }
71
- catch { rejectOnce(new Error('Invalid JSON in request body')); }
72
- });
73
-
74
- req.on('error', (error: Error) => { rejectOnce(error); });
75
- });
76
- }
77
-
78
- async function pipeWebResponseToNode(response: Response, res: http.ServerResponse): Promise<void> {
79
- res.statusCode = response.status;
80
- response.headers.forEach((value, key) => { res.setHeader(key, value); });
81
- if (!response.body) { res.end(); return; }
82
- const nodeStream = stream.Readable.fromWeb(response.body as any);
83
- await new Promise<void>((resolve, reject) => {
84
- nodeStream.on('error', reject);
85
- nodeStream.on('end', resolve);
86
- nodeStream.pipe(res);
87
- });
88
- }
89
-
90
- // --- Auth ---
91
-
92
- function extractIncomingToken(headers: http.IncomingHttpHeaders): string | null {
93
- const xApiKey = headers['x-api-key'];
94
- if (typeof xApiKey === 'string' && xApiKey.trim().length > 0) return xApiKey.trim();
95
-
96
- const anthropicApiKey = headers['anthropic-api-key'];
97
- if (typeof anthropicApiKey === 'string' && anthropicApiKey.trim().length > 0) return anthropicApiKey.trim();
98
-
99
- const authHeader = headers.authorization;
100
- if (typeof authHeader === 'string' && authHeader.trim().length > 0) {
101
- const trimmed = authHeader.trim();
102
- const bearerPrefix = 'Bearer ';
103
- return trimmed.startsWith(bearerPrefix) ? trimmed.slice(bearerPrefix.length).trim() : trimmed;
104
- }
105
- return null;
106
- }
107
-
108
- function validateAuth(headers: http.IncomingHttpHeaders, expectedToken: string): boolean {
109
- return extractIncomingToken(headers) === expectedToken;
110
- }
111
-
112
- // --- Request Translation ---
113
-
114
- function buildUpstreamRequest(rawBody: Record<string, unknown>, options: ProxyServerOptions): string {
115
- const transformer = new ProxyRequestTransformer();
116
- const transformed = transformer.transform(rawBody);
117
- const body = {
118
- ...transformed,
119
- model: (transformed as any).model || options.model || 'gpt-4',
120
- stream: (transformed as any).stream === true,
121
- };
122
- return JSON.stringify(body);
123
- }
124
-
125
- // --- Server ---
126
-
127
- export function startProxyServer(options: ProxyServerOptions): http.Server {
128
- const server = http.createServer(async (req, res) => {
129
- const method = req.method || 'GET';
130
- const requestUrl = req.url || '/';
131
- const parsedUrl = new URL(requestUrl, 'http://127.0.0.1');
132
- const pathname = parsedUrl.pathname.length > 1
133
- ? parsedUrl.pathname.replace(/\/+$/, '')
134
- : parsedUrl.pathname;
135
-
136
- try {
137
- await handleRequest(req, res, method, pathname, options);
138
- } catch (err) {
139
- const message = err instanceof Error ? err.message : 'Unknown error';
140
- if (!res.headersSent) {
141
- writeJson(res, 500, { type: 'error', error: { type: 'api_error', message } });
142
- }
143
- }
144
- });
145
-
146
- server.listen(options.port, options.host);
147
- return server;
148
- }
149
-
150
- async function handleRequest(
151
- req: http.IncomingMessage,
152
- res: http.ServerResponse,
153
- method: string,
154
- pathname: string,
155
- options: ProxyServerOptions,
156
- ): Promise<void> {
157
- // Health check
158
- if ((method === 'GET' || method === 'HEAD') && pathname === '/health') {
159
- if (method === 'HEAD') {
160
- res.writeHead(200, { 'Content-Type': 'application/json' });
161
- res.end();
162
- } else {
163
- writeJson(res, 200, {
164
- ok: true,
165
- service: PROXY_SERVICE_NAME,
166
- host: options.host,
167
- port: options.port,
168
- });
169
- }
170
- return;
171
- }
172
-
173
- // Root info
174
- if ((method === 'GET' || method === 'HEAD') && pathname === '/') {
175
- if (method === 'HEAD') {
176
- res.writeHead(200, { 'Content-Type': 'application/json' });
177
- res.end();
178
- } else {
179
- writeJson(res, 200, {
180
- ok: true,
181
- service: PROXY_SERVICE_NAME,
182
- bind: { host: options.host, port: options.port },
183
- endpoints: ['/health', '/v1/messages', '/v1/models'],
184
- });
185
- }
186
- return;
187
- }
188
-
189
- // Models endpoint
190
- if (method === 'GET' && pathname === '/v1/models') {
191
- if (!validateAuth(req.headers, options.authToken)) {
192
- writeJson(res, 401, {
193
- type: 'error',
194
- error: { type: 'authentication_error', message: 'Missing or invalid local proxy token' },
195
- });
196
- return;
197
- }
198
- const models = [options.model].filter(Boolean).map((id) => ({
199
- id,
200
- object: 'model',
201
- created: 0,
202
- owned_by: 'mcc-proxy',
203
- }));
204
- writeJson(res, 200, { object: 'list', data: models });
205
- return;
206
- }
207
-
208
- // Messages endpoint (main translation)
209
- if (method === 'POST' && pathname === '/v1/messages') {
210
- await handleMessages(req, res, options);
211
- return;
212
- }
213
-
214
- writeJson(res, 404, { error: 'Not found' });
215
- }
216
-
217
- async function handleMessages(
218
- req: http.IncomingMessage,
219
- res: http.ServerResponse,
220
- options: ProxyServerOptions,
221
- ): Promise<void> {
222
- const transformer = new ProxySseStreamTransformer();
223
-
224
- if (!validateAuth(req.headers, options.authToken)) {
225
- await pipeWebResponseToNode(
226
- transformer.error(401, 'authentication_error', 'Missing or invalid local proxy token'),
227
- res,
228
- );
229
- return;
230
- }
231
-
232
- let timeoutMs = REQUEST_TIMEOUT_MS;
233
- try {
234
- const rawBody = await readJsonBody(req);
235
- const upstreamBody = buildUpstreamRequest(rawBody, options);
236
- const upstreamUrl = resolveOpenAIChatCompletionsUrl(options.baseUrl);
237
-
238
- const controller = new AbortController();
239
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
240
-
241
- // Client disconnect detection
242
- const abortOnDisconnect = (_source: string) => {
243
- if (!controller.signal.aborted && !res.writableEnded) {
244
- controller.abort();
245
- }
246
- };
247
- req.on('aborted', () => abortOnDisconnect('req.aborted'));
248
- req.socket?.on('close', () => abortOnDisconnect('socket.close'));
249
-
250
- try {
251
- const upstreamResponse = await fetch(upstreamUrl, {
252
- method: 'POST',
253
- headers: {
254
- 'Content-Type': 'application/json',
255
- Authorization: `Bearer ${options.apiKey}`,
256
- 'User-Agent': 'MCC-OpenAI-Compat-Proxy/1.0',
257
- },
258
- body: upstreamBody,
259
- signal: controller.signal,
260
- });
261
-
262
- const response = await transformer.transform(upstreamResponse);
263
- await pipeWebResponseToNode(response, res);
264
- } finally {
265
- clearTimeout(timeout);
266
- }
267
- } catch (error) {
268
- const message = error instanceof Error ? error.message : 'Unknown proxy error';
269
- const isAbort = error instanceof Error && error.name === 'AbortError';
270
- const status = isAbort ? 502 : message.includes('Request body too large') ? 413 : message.includes('Invalid JSON') ? 400 : 502;
271
- const type = status >= 500 ? 'api_error' : 'invalid_request_error';
272
- const errorMessage = isAbort
273
- ? `The upstream provider did not respond within ${timeoutMs / 1000} seconds`
274
- : message;
275
-
276
- await pipeWebResponseToNode(transformer.error(status, type, errorMessage), res);
277
- }
278
- }
@@ -1,38 +0,0 @@
1
- /**
2
- * Upstream URL resolution
3
- * Ported from CCS proxy/upstream-url.js
4
- */
5
-
6
- function normalizePathname(pathname: string): string {
7
- const trimmed = pathname.replace(/\/+$/, '');
8
- return trimmed || '';
9
- }
10
-
11
- function ensureSupportedProtocol(parsed: URL): void {
12
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
13
- throw new Error(`Unsupported upstream protocol: ${parsed.protocol}`);
14
- }
15
- }
16
-
17
- function buildResolvedUrl(baseUrl: string, suffix: string): string {
18
- const parsed = new URL(baseUrl);
19
- ensureSupportedProtocol(parsed);
20
- const pathname = normalizePathname(parsed.pathname);
21
- if (pathname.endsWith(suffix)) {
22
- return parsed.toString();
23
- }
24
- if (pathname.endsWith('/v1') || pathname.endsWith('/api')) {
25
- parsed.pathname = `${pathname}${suffix.startsWith('/') ? suffix : `/${suffix}`}`;
26
- return parsed.toString();
27
- }
28
- parsed.pathname = pathname ? `${pathname}/v1${suffix}` : `/v1${suffix}`;
29
- return parsed.toString();
30
- }
31
-
32
- export function resolveOpenAIChatCompletionsUrl(baseUrl: string): string {
33
- return buildResolvedUrl(baseUrl, '/chat/completions');
34
- }
35
-
36
- export function resolveOpenAIModelsUrl(baseUrl: string): string {
37
- return buildResolvedUrl(baseUrl, '/models');
38
- }
@@ -1,140 +0,0 @@
1
- /**
2
- * MCC Logger (TypeScript)
3
- *
4
- * lib/shared/logger.cjs 的 TS 版本,编译到 dist/shared/logger.js
5
- * 逻辑完全一致,只是加了类型。
6
- */
7
-
8
- import * as fs from 'fs';
9
- import * as path from 'path';
10
- import * as os from 'os';
11
-
12
- const LEVELS: Record<string, number> = { error: 0, warn: 1, info: 2, debug: 3 };
13
-
14
- function env(key: string, fallback: string): string {
15
- return process.env[key] ?? fallback;
16
- }
17
-
18
- function levelValue(level: string): number {
19
- return LEVELS[level?.toLowerCase()] ?? LEVELS.info;
20
- }
21
-
22
- let _sessionId = '';
23
- let _logDir = '';
24
- let _currentFile = '';
25
- let _currentSize = 0;
26
- let _maxSize = 5 * 1024 * 1024;
27
- let _maxFiles = 3;
28
- let _minLevel = LEVELS.info;
29
-
30
- function getLogDir(): string {
31
- if (_logDir) return _logDir;
32
- const logDirEnv = env('MCC_LOG_DIR', '');
33
- if (logDirEnv) return logDirEnv;
34
- const mccHome = env('MCC_HOME', path.join(os.homedir(), '.mcc'));
35
- const profile = env('MCC_CURRENT_PROFILE', 'default');
36
- return path.join(mccHome, 'logs', profile, _sessionId || 'nosession');
37
- }
38
-
39
- function getLogPath(): string {
40
- return path.join(getLogDir(), 'mcc.log');
41
- }
42
-
43
- function rotate(): void {
44
- const logPath = getLogPath();
45
- if (!fs.existsSync(logPath)) return;
46
- try { fs.unlinkSync(`${logPath}.${_maxFiles}`); } catch {}
47
- for (let i = _maxFiles - 1; i >= 1; i--) {
48
- const from = `${logPath}.${i}`;
49
- const to = `${logPath}.${i + 1}`;
50
- try { if (fs.existsSync(from)) fs.renameSync(from, to); } catch {}
51
- }
52
- try { fs.renameSync(logPath, `${logPath}.1`); } catch {}
53
- _currentSize = 0;
54
- _currentFile = '';
55
- }
56
-
57
- function ensureDir(): string {
58
- const dir = getLogDir();
59
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
60
- return dir;
61
- }
62
-
63
- function writeFile(line: string): void {
64
- if (!_sessionId) return;
65
- ensureDir();
66
- const logPath = getLogPath();
67
- const lineBytes = Buffer.byteLength(line, 'utf8') + 1;
68
- if (_currentFile !== logPath || _currentSize + lineBytes > _maxSize) {
69
- rotate();
70
- }
71
- try {
72
- fs.appendFileSync(logPath, line + '\n', { mode: 0o644 });
73
- _currentFile = logPath;
74
- _currentSize += lineBytes;
75
- } catch (err) {
76
- process.stderr.write(`[logger] write failed: ${(err as Error).message}\n`);
77
- }
78
- }
79
-
80
- function timestamp(): string {
81
- const now = new Date();
82
- const p = (n: number, l = 2) => String(n).padStart(l, '0');
83
- return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())} ` +
84
- `${p(now.getHours())}:${p(now.getMinutes())}:${p(now.getSeconds())}.${p(now.getMilliseconds(), 3)}`;
85
- }
86
-
87
- function format(level: string, message: string, component: string): string {
88
- const sid = _sessionId ? `[${_sessionId}]` : '';
89
- const comp = component ? `[${component}]` : '';
90
- return `${timestamp()} ${(level || 'info').toUpperCase().padEnd(5)} ${sid}${comp} ${message}`;
91
- }
92
-
93
- function write(level: string, component: string, message: string): void {
94
- if (levelValue(level) > _minLevel) return;
95
- const line = format(level, message, component);
96
- process.stderr.write(line + '\n');
97
- writeFile(line);
98
- }
99
-
100
- export function init(sessionId: string, logDir?: string): string {
101
- _sessionId = sessionId;
102
- _logDir = logDir || '';
103
- _currentFile = '';
104
- _currentSize = 0;
105
- _minLevel = levelValue(env('MCC_LOG_LEVEL', 'info'));
106
- _maxSize = parseInt(env('MCC_LOG_MAX_SIZE', ''), 10) || 5 * 1024 * 1024;
107
- _maxFiles = parseInt(env('MCC_LOG_MAX_FILES', ''), 10) || 3;
108
- const dir = ensureDir();
109
- try { fs.writeFileSync(path.join(dir, '.session'), `${sessionId}\n${Date.now()}\n`, { mode: 0o644 }); } catch {}
110
- return dir;
111
- }
112
-
113
- export function initFromEnv(): void {
114
- const sid = env('MCC_LOG_SESSION_ID', '');
115
- const dir = env('MCC_LOG_DIR', '');
116
- if (sid) init(sid, dir);
117
- }
118
-
119
- // --- 统一 API:log.info(component, message) ---
120
- const log = {
121
- error: (c: string, m: string) => write('error', m, c),
122
- warn: (c: string, m: string) => write('warn', m, c),
123
- info: (c: string, m: string) => write('info', m, c),
124
- debug: (c: string, m: string) => write('debug', m, c),
125
- init,
126
- initFromEnv,
127
- getSessionId: () => _sessionId,
128
- getLogDir,
129
- };
130
- export { log };
131
-
132
- export function isDebugEnabled(): boolean {
133
- return _minLevel >= LEVELS.debug;
134
- }
135
-
136
- export function makeSessionId(): string {
137
- const now = new Date();
138
- const p = (n: number, l = 2) => String(n).padStart(l, '0');
139
- return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}_${p(now.getHours())}-${p(now.getMinutes())}-${p(now.getSeconds())}`;
140
- }