qwenproxy-cli 1.0.0 → 1.0.2
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.
- package/LICENSE +14 -14
- package/README.md +906 -906
- package/bin/qwenproxy.js +5 -1
- package/package.json +77 -78
- package/src/api/error-classifier.ts +159 -159
- package/src/api/error-helpers.ts +118 -118
- package/src/api/models.ts +261 -261
- package/src/api/server.ts +860 -859
- package/src/cache/memory-cache.ts +385 -385
- package/src/clean-cache.ts +204 -204
- package/src/core/account-concurrency.ts +671 -671
- package/src/core/account-manager.ts +301 -297
- package/src/core/account-priority.ts +163 -163
- package/src/core/accounts.ts +186 -186
- package/src/core/config.ts +383 -383
- package/src/core/crypto-utils.ts +79 -79
- package/src/core/database.ts +276 -276
- package/src/core/errors.ts +118 -118
- package/src/core/logger.ts +269 -269
- package/src/core/memory-usage.ts +84 -84
- package/src/core/metrics.ts +291 -291
- package/src/core/model-alias.ts +77 -77
- package/src/core/model-registry.ts +544 -544
- package/src/core/mutex.ts +119 -119
- package/src/core/paths.ts +199 -199
- package/src/core/prompt-limits.ts +214 -214
- package/src/core/reasoning-effort.ts +102 -102
- package/src/core/stream-registry.ts +96 -96
- package/src/core/waf-isolation.ts +117 -117
- package/src/core/watchdog.ts +195 -195
- package/src/delete-chats.ts +23 -23
- package/src/index.ts +65 -64
- package/src/login.ts +147 -147
- package/src/reset-cooldowns.ts +11 -11
- package/src/routes/anthropic/index.ts +355 -355
- package/src/routes/anthropic/translate.ts +522 -522
- package/src/routes/anthropic/types.ts +154 -154
- package/src/routes/anthropic/validation.ts +144 -144
- package/src/routes/chat/account.ts +1817 -1817
- package/src/routes/chat/context.ts +241 -241
- package/src/routes/chat/errors.ts +85 -85
- package/src/routes/chat/helpers.ts +268 -268
- package/src/routes/chat/index.ts +618 -618
- package/src/routes/chat/media.ts +285 -285
- package/src/routes/chat/retry-policy.ts +754 -754
- package/src/routes/chat/stop.ts +98 -98
- package/src/routes/chat/streaming.ts +2710 -2710
- package/src/routes/chat/validation.ts +526 -526
- package/src/routes/chat.ts +2 -2
- package/src/routes/completions.ts +290 -290
- package/src/routes/images.ts +139 -139
- package/src/routes/responses/adapter.ts +503 -503
- package/src/routes/responses/index.ts +405 -405
- package/src/routes/responses/state.ts +230 -230
- package/src/routes/responses/streaming.ts +528 -528
- package/src/routes/responses/types.ts +285 -285
- package/src/routes/responses/validation.ts +202 -202
- package/src/routes/upload.ts +731 -731
- package/src/routes/videos.ts +214 -214
- package/src/services/auth-playwright.ts +173 -173
- package/src/services/captcha-coordinator.ts +161 -161
- package/src/services/captcha-solver.ts +553 -553
- package/src/services/chat-cleanup.ts +80 -80
- package/src/services/context-meter.ts +317 -317
- package/src/services/fingerprint.ts +242 -242
- package/src/services/human-behavior.ts +173 -173
- package/src/services/media-generation.ts +1748 -1748
- package/src/services/playwright.ts +2878 -2800
- package/src/services/qwen-chat-pool.ts +345 -345
- package/src/services/qwen-errors.ts +133 -133
- package/src/services/qwen-headers.ts +79 -79
- package/src/services/qwen-thread-state.ts +393 -393
- package/src/services/qwen-url.ts +19 -19
- package/src/services/qwen.ts +3126 -3126
- package/src/services/session-keeper.ts +88 -88
- package/src/services/token-estimation-metrics.ts +118 -118
- package/src/sync/claude-code.ts +75 -75
- package/src/sync/codex.ts +123 -123
- package/src/sync/index.ts +362 -362
- package/src/sync/omp.ts +105 -105
- package/src/sync/opencode.ts +214 -214
- package/src/sync/types.ts +53 -53
- package/src/sync/utils.ts +27 -27
- package/src/sync-clients.ts +189 -189
- package/src/tools/instructions.ts +137 -137
- package/src/tools/manifest.ts +81 -81
- package/src/tools/parser.ts +2989 -2989
- package/src/tools/toolcall-tags.ts +142 -142
- package/src/tui/app.ts +259 -264
- package/src/tui/index.ts +61 -61
- package/src/tui/markdown.ts +258 -258
- package/src/tui/proxy-client.ts +331 -326
- package/src/tui/screen.ts +294 -278
- package/src/tui/server-manager.ts +270 -270
- package/src/tui/theme.ts +432 -432
- package/src/tui/types.ts +33 -33
- package/src/tui/views/accounts-view.ts +656 -656
- package/src/tui/views/chat-view.ts +1018 -823
- package/src/tui/views/logs-view.ts +479 -413
- package/src/tui/views/status-view.ts +204 -204
- package/src/tui/views/storage-view.ts +304 -291
- package/src/tui/views/sync-view.ts +409 -409
- package/src/types/ali-oss.d.ts +32 -32
- package/src/update-cli.ts +121 -0
- package/src/utils/context-truncation.ts +84 -84
- package/src/utils/json.ts +380 -380
- package/src/utils/session-id.ts +37 -37
- package/src/utils/tool-call-guard.ts +84 -84
- package/src/utils/types.ts +109 -109
package/src/core/logger.ts
CHANGED
|
@@ -1,269 +1,269 @@
|
|
|
1
|
-
import "dotenv/config";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Mask an email address for safe logging.
|
|
5
|
-
* "user@example.com" → "user@***"
|
|
6
|
-
*/
|
|
7
|
-
export function maskEmail(email: string | undefined | null): string {
|
|
8
|
-
if (!email) return "<unknown>";
|
|
9
|
-
const atIndex = email.indexOf("@");
|
|
10
|
-
if (atIndex <= 0) return "<invalid>";
|
|
11
|
-
return email.substring(0, atIndex);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// ─── Log sanitization ───────────────────────────────────────────────────────────
|
|
15
|
-
// Log entries carry raw upstream payloads (headers, cookies, JWT-backed
|
|
16
|
-
// sessions, malformed tool-call dumps). Redact known credential keys and any
|
|
17
|
-
// loose JWT/API-key shaped string before the entry hits the console, even when
|
|
18
|
-
// the value appears nested inside a bigger object or a quoted JSON dump.
|
|
19
|
-
|
|
20
|
-
const SENSITIVE_KEY_PATTERN =
|
|
21
|
-
/^(authorization|auth|cookie|cookies|set-cookie|api[_-]?key|apikey|access[_-]?token|refresh[_-]?token|token|password|passwd|secret|x5sec|x5secdata|bx-ua|bx-v|bx-umidtoken)$/i;
|
|
22
|
-
|
|
23
|
-
/** WAF cookie names come in many variants: x5sec, x5sec_v3, x5sec-cn, bx-temp... */
|
|
24
|
-
const WAF_KEY_PREFIX_PATTERN = /^(x5sec|bx[-_])/i;
|
|
25
|
-
|
|
26
|
-
/** JWT (3 base64url segments) or OpenAI-style `sk-` API key. */
|
|
27
|
-
const LOOSE_SECRET_PATTERN =
|
|
28
|
-
/(eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}|sk-[A-Za-z0-9_-]{20,})/g;
|
|
29
|
-
|
|
30
|
-
/** Credentials embedded in free-form strings: header lines, cookie jars, URLs. */
|
|
31
|
-
const EMBEDDED_SECRET_PATTERN =
|
|
32
|
-
/((?:x5sec(?:data|_[a-z0-9]+|-[a-z0-9]+)?|bx[-_][a-z0-9_-]+)\s*=\s*[^;\s"']+)|(\bBearer\s+[A-Za-z0-9._~+\/=-]+)/gi;
|
|
33
|
-
|
|
34
|
-
function isPassthroughObject(value: object): boolean {
|
|
35
|
-
return (
|
|
36
|
-
value instanceof Date ||
|
|
37
|
-
value instanceof RegExp ||
|
|
38
|
-
value instanceof Error ||
|
|
39
|
-
value instanceof Map ||
|
|
40
|
-
value instanceof Set ||
|
|
41
|
-
value instanceof ArrayBuffer ||
|
|
42
|
-
ArrayBuffer.isView(value)
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function redactLogValue(value: unknown): unknown {
|
|
47
|
-
if (typeof value === "string") {
|
|
48
|
-
return value
|
|
49
|
-
.replace(LOOSE_SECRET_PATTERN, "[REDACTED]")
|
|
50
|
-
.replace(EMBEDDED_SECRET_PATTERN, "[REDACTED]");
|
|
51
|
-
}
|
|
52
|
-
if (Array.isArray(value)) {
|
|
53
|
-
return value.map((item) => redactLogValue(item));
|
|
54
|
-
}
|
|
55
|
-
if (value !== null && typeof value === "object" && !isPassthroughObject(value)) {
|
|
56
|
-
// Recurse into own enumerable props of BOTH plain objects and class
|
|
57
|
-
// instances (sessions, response wrappers) — JSON.stringify serializes
|
|
58
|
-
// those the same way, so redacting them loses nothing.
|
|
59
|
-
const out: Record<string, unknown> = {};
|
|
60
|
-
for (const [key, val] of Object.entries(value)) {
|
|
61
|
-
out[key] =
|
|
62
|
-
SENSITIVE_KEY_PATTERN.test(key) || WAF_KEY_PREFIX_PATTERN.test(key)
|
|
63
|
-
? "[REDACTED]"
|
|
64
|
-
: redactLogValue(val);
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function redactLogMessage(message: string): string {
|
|
72
|
-
return message
|
|
73
|
-
.replace(LOOSE_SECRET_PATTERN, "[REDACTED]")
|
|
74
|
-
.replace(EMBEDDED_SECRET_PATTERN, "[REDACTED]");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
78
|
-
|
|
79
|
-
const LEVEL_RANK: Record<LogLevel, number> = {
|
|
80
|
-
debug: 0,
|
|
81
|
-
info: 1,
|
|
82
|
-
warn: 2,
|
|
83
|
-
error: 3,
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Data/hora BR local (sem ms) usado APENAS no campo `until=` das linhas de
|
|
88
|
-
* cooldown/quota — informação de negócio, não um carimbo de log.
|
|
89
|
-
*/
|
|
90
|
-
export function formatCooldownUntil(date: Date): string {
|
|
91
|
-
const pad = (n: number): string => String(n).padStart(2, "0");
|
|
92
|
-
return `${pad(date.getDate())}/${pad(date.getMonth() + 1)}/${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* TOOLCALL_DEBUG levels:
|
|
97
|
-
* "0" or undefined = disabled
|
|
98
|
-
* "1" = full debug (all toolcall logs)
|
|
99
|
-
* "errors" = only on errors (log toolcall details when parser/execution fails)
|
|
100
|
-
*
|
|
101
|
-
* UPSTREAM_DEBUG:
|
|
102
|
-
* "true" = log raw SSE chunks received from Qwen
|
|
103
|
-
*/
|
|
104
|
-
export type ToolcallDebugLevel = "0" | "1" | "errors";
|
|
105
|
-
|
|
106
|
-
export interface LogEntry {
|
|
107
|
-
timestamp: Date;
|
|
108
|
-
level: LogLevel;
|
|
109
|
-
message: string;
|
|
110
|
-
context?: string;
|
|
111
|
-
data?: Record<string, unknown>;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export class Logger {
|
|
115
|
-
private minLevel: LogLevel;
|
|
116
|
-
private context?: string;
|
|
117
|
-
|
|
118
|
-
constructor(level: LogLevel = "warn", context?: string) {
|
|
119
|
-
this.minLevel = level;
|
|
120
|
-
this.context = context;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private shouldLog(level: LogLevel): boolean {
|
|
124
|
-
return LEVEL_RANK[level] >= LEVEL_RANK[this.minLevel];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Cheap level check so callers can skip building expensive log payloads. */
|
|
128
|
-
isLevelEnabled(level: LogLevel): boolean {
|
|
129
|
-
return this.shouldLog(level);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private formatEntry(entry: LogEntry): string {
|
|
133
|
-
const pad = (str: string): string => str.padStart(5, " ");
|
|
134
|
-
const colorCode =
|
|
135
|
-
entry.level === "error"
|
|
136
|
-
? "\x1b[31m"
|
|
137
|
-
: entry.level === "warn"
|
|
138
|
-
? "\x1b[33m"
|
|
139
|
-
: entry.level === "debug"
|
|
140
|
-
? "\x1b[36m"
|
|
141
|
-
: "";
|
|
142
|
-
const reset = "\x1b[0m";
|
|
143
|
-
|
|
144
|
-
const coloredLevel = colorCode + pad(entry.level.toUpperCase()) + reset;
|
|
145
|
-
const contextPart = entry.context ? ` [${entry.context}]` : "";
|
|
146
|
-
|
|
147
|
-
// Redact credentials from BOTH the message (payload previews often embed
|
|
148
|
-
// cookies/JWTs) and the structured data.
|
|
149
|
-
const safeMessage = redactLogMessage(entry.message);
|
|
150
|
-
const safeData = entry.data ? redactLogValue(entry.data) : undefined;
|
|
151
|
-
|
|
152
|
-
let output = `${coloredLevel}${contextPart} ${safeMessage}`;
|
|
153
|
-
|
|
154
|
-
if (safeData !== undefined) {
|
|
155
|
-
output += "\n" + JSON.stringify(safeData, null, 2);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return output;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
debug(message: string, data?: Record<string, unknown>): void {
|
|
162
|
-
if (this.shouldLog("debug")) {
|
|
163
|
-
console.log(
|
|
164
|
-
this.formatEntry({
|
|
165
|
-
timestamp: new Date(),
|
|
166
|
-
level: "debug",
|
|
167
|
-
message: this.context ? `[${this.context}] ${message}` : message,
|
|
168
|
-
data,
|
|
169
|
-
}),
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
info(message: string, data?: Record<string, unknown>): void {
|
|
175
|
-
if (this.shouldLog("info")) {
|
|
176
|
-
console.log(
|
|
177
|
-
this.formatEntry({
|
|
178
|
-
timestamp: new Date(),
|
|
179
|
-
level: "info",
|
|
180
|
-
message: this.context ? `[${this.context}] ${message}` : message,
|
|
181
|
-
data,
|
|
182
|
-
}),
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
warn(message: string, data?: Record<string, unknown>): void {
|
|
188
|
-
if (this.shouldLog("warn")) {
|
|
189
|
-
console.warn(
|
|
190
|
-
this.formatEntry({
|
|
191
|
-
timestamp: new Date(),
|
|
192
|
-
level: "warn",
|
|
193
|
-
message: this.context ? `[${this.context}] ${message}` : message,
|
|
194
|
-
data,
|
|
195
|
-
}),
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
error(message: string, data?: Record<string, unknown>): void {
|
|
201
|
-
if (this.shouldLog("error")) {
|
|
202
|
-
console.error(
|
|
203
|
-
this.formatEntry({
|
|
204
|
-
timestamp: new Date(),
|
|
205
|
-
level: "error",
|
|
206
|
-
message: this.context ? `[${this.context}] ${message}` : message,
|
|
207
|
-
data,
|
|
208
|
-
}),
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Determine initial log level from environment
|
|
215
|
-
const envLevel = process.env.LOG_LEVEL as LogLevel | undefined;
|
|
216
|
-
const toolcallDebugEnv = process.env.TOOLCALL_DEBUG || "errors";
|
|
217
|
-
|
|
218
|
-
export const toolcallDebugLevel: ToolcallDebugLevel =
|
|
219
|
-
toolcallDebugEnv === "1"
|
|
220
|
-
? "1"
|
|
221
|
-
: toolcallDebugEnv === "errors"
|
|
222
|
-
? "errors"
|
|
223
|
-
: "0";
|
|
224
|
-
|
|
225
|
-
const initialLevel: LogLevel =
|
|
226
|
-
toolcallDebugLevel === "1"
|
|
227
|
-
? "debug"
|
|
228
|
-
: envLevel && ["debug", "info", "warn", "error"].includes(envLevel)
|
|
229
|
-
? envLevel
|
|
230
|
-
// Default for general users: quiet terminal with only warnings/errors
|
|
231
|
-
// (+ the always-on request/account console lines). Debugging opt-in via
|
|
232
|
-
// LOG_LEVEL=debug / TOOLCALL_DEBUG=1.
|
|
233
|
-
: "warn";
|
|
234
|
-
|
|
235
|
-
export const logger = new Logger(initialLevel);
|
|
236
|
-
|
|
237
|
-
export function isDebugEnabled(): boolean {
|
|
238
|
-
return logger.isLevelEnabled("debug");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Helper to check if toolcall debug is enabled
|
|
242
|
-
export function isToolcallDebugEnabled(): boolean {
|
|
243
|
-
return toolcallDebugLevel === "1";
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function isToolcallErrorDebugEnabled(): boolean {
|
|
247
|
-
return toolcallDebugLevel === "1" || toolcallDebugLevel === "errors";
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export const upstreamDebugEnabled = process.env.UPSTREAM_DEBUG === "true";
|
|
251
|
-
|
|
252
|
-
// Confirm debug mode on startup (only log if explicitly set)
|
|
253
|
-
if (process.env.TOOLCALL_DEBUG) {
|
|
254
|
-
if (toolcallDebugLevel === "1") {
|
|
255
|
-
console.log("🔍 [Logger] TOOLCALL_DEBUG=1 - full debug logs active");
|
|
256
|
-
} else if (toolcallDebugLevel === "errors") {
|
|
257
|
-
console.log(
|
|
258
|
-
"[Logger] TOOLCALL_DEBUG=errors - toolcall logs on errors only",
|
|
259
|
-
);
|
|
260
|
-
} else {
|
|
261
|
-
console.log("🔇 [Logger] TOOLCALL_DEBUG=0 - toolcall logs disabled");
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (upstreamDebugEnabled) {
|
|
266
|
-
console.log(
|
|
267
|
-
"[Logger] UPSTREAM_DEBUG=true - raw upstream chunks logging active",
|
|
268
|
-
);
|
|
269
|
-
}
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mask an email address for safe logging.
|
|
5
|
+
* "user@example.com" → "user@***"
|
|
6
|
+
*/
|
|
7
|
+
export function maskEmail(email: string | undefined | null): string {
|
|
8
|
+
if (!email) return "<unknown>";
|
|
9
|
+
const atIndex = email.indexOf("@");
|
|
10
|
+
if (atIndex <= 0) return "<invalid>";
|
|
11
|
+
return email.substring(0, atIndex);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ─── Log sanitization ───────────────────────────────────────────────────────────
|
|
15
|
+
// Log entries carry raw upstream payloads (headers, cookies, JWT-backed
|
|
16
|
+
// sessions, malformed tool-call dumps). Redact known credential keys and any
|
|
17
|
+
// loose JWT/API-key shaped string before the entry hits the console, even when
|
|
18
|
+
// the value appears nested inside a bigger object or a quoted JSON dump.
|
|
19
|
+
|
|
20
|
+
const SENSITIVE_KEY_PATTERN =
|
|
21
|
+
/^(authorization|auth|cookie|cookies|set-cookie|api[_-]?key|apikey|access[_-]?token|refresh[_-]?token|token|password|passwd|secret|x5sec|x5secdata|bx-ua|bx-v|bx-umidtoken)$/i;
|
|
22
|
+
|
|
23
|
+
/** WAF cookie names come in many variants: x5sec, x5sec_v3, x5sec-cn, bx-temp... */
|
|
24
|
+
const WAF_KEY_PREFIX_PATTERN = /^(x5sec|bx[-_])/i;
|
|
25
|
+
|
|
26
|
+
/** JWT (3 base64url segments) or OpenAI-style `sk-` API key. */
|
|
27
|
+
const LOOSE_SECRET_PATTERN =
|
|
28
|
+
/(eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}|sk-[A-Za-z0-9_-]{20,})/g;
|
|
29
|
+
|
|
30
|
+
/** Credentials embedded in free-form strings: header lines, cookie jars, URLs. */
|
|
31
|
+
const EMBEDDED_SECRET_PATTERN =
|
|
32
|
+
/((?:x5sec(?:data|_[a-z0-9]+|-[a-z0-9]+)?|bx[-_][a-z0-9_-]+)\s*=\s*[^;\s"']+)|(\bBearer\s+[A-Za-z0-9._~+\/=-]+)/gi;
|
|
33
|
+
|
|
34
|
+
function isPassthroughObject(value: object): boolean {
|
|
35
|
+
return (
|
|
36
|
+
value instanceof Date ||
|
|
37
|
+
value instanceof RegExp ||
|
|
38
|
+
value instanceof Error ||
|
|
39
|
+
value instanceof Map ||
|
|
40
|
+
value instanceof Set ||
|
|
41
|
+
value instanceof ArrayBuffer ||
|
|
42
|
+
ArrayBuffer.isView(value)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function redactLogValue(value: unknown): unknown {
|
|
47
|
+
if (typeof value === "string") {
|
|
48
|
+
return value
|
|
49
|
+
.replace(LOOSE_SECRET_PATTERN, "[REDACTED]")
|
|
50
|
+
.replace(EMBEDDED_SECRET_PATTERN, "[REDACTED]");
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value.map((item) => redactLogValue(item));
|
|
54
|
+
}
|
|
55
|
+
if (value !== null && typeof value === "object" && !isPassthroughObject(value)) {
|
|
56
|
+
// Recurse into own enumerable props of BOTH plain objects and class
|
|
57
|
+
// instances (sessions, response wrappers) — JSON.stringify serializes
|
|
58
|
+
// those the same way, so redacting them loses nothing.
|
|
59
|
+
const out: Record<string, unknown> = {};
|
|
60
|
+
for (const [key, val] of Object.entries(value)) {
|
|
61
|
+
out[key] =
|
|
62
|
+
SENSITIVE_KEY_PATTERN.test(key) || WAF_KEY_PREFIX_PATTERN.test(key)
|
|
63
|
+
? "[REDACTED]"
|
|
64
|
+
: redactLogValue(val);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function redactLogMessage(message: string): string {
|
|
72
|
+
return message
|
|
73
|
+
.replace(LOOSE_SECRET_PATTERN, "[REDACTED]")
|
|
74
|
+
.replace(EMBEDDED_SECRET_PATTERN, "[REDACTED]");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
78
|
+
|
|
79
|
+
const LEVEL_RANK: Record<LogLevel, number> = {
|
|
80
|
+
debug: 0,
|
|
81
|
+
info: 1,
|
|
82
|
+
warn: 2,
|
|
83
|
+
error: 3,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Data/hora BR local (sem ms) usado APENAS no campo `until=` das linhas de
|
|
88
|
+
* cooldown/quota — informação de negócio, não um carimbo de log.
|
|
89
|
+
*/
|
|
90
|
+
export function formatCooldownUntil(date: Date): string {
|
|
91
|
+
const pad = (n: number): string => String(n).padStart(2, "0");
|
|
92
|
+
return `${pad(date.getDate())}/${pad(date.getMonth() + 1)}/${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* TOOLCALL_DEBUG levels:
|
|
97
|
+
* "0" or undefined = disabled
|
|
98
|
+
* "1" = full debug (all toolcall logs)
|
|
99
|
+
* "errors" = only on errors (log toolcall details when parser/execution fails)
|
|
100
|
+
*
|
|
101
|
+
* UPSTREAM_DEBUG:
|
|
102
|
+
* "true" = log raw SSE chunks received from Qwen
|
|
103
|
+
*/
|
|
104
|
+
export type ToolcallDebugLevel = "0" | "1" | "errors";
|
|
105
|
+
|
|
106
|
+
export interface LogEntry {
|
|
107
|
+
timestamp: Date;
|
|
108
|
+
level: LogLevel;
|
|
109
|
+
message: string;
|
|
110
|
+
context?: string;
|
|
111
|
+
data?: Record<string, unknown>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class Logger {
|
|
115
|
+
private minLevel: LogLevel;
|
|
116
|
+
private context?: string;
|
|
117
|
+
|
|
118
|
+
constructor(level: LogLevel = "warn", context?: string) {
|
|
119
|
+
this.minLevel = level;
|
|
120
|
+
this.context = context;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private shouldLog(level: LogLevel): boolean {
|
|
124
|
+
return LEVEL_RANK[level] >= LEVEL_RANK[this.minLevel];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Cheap level check so callers can skip building expensive log payloads. */
|
|
128
|
+
isLevelEnabled(level: LogLevel): boolean {
|
|
129
|
+
return this.shouldLog(level);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private formatEntry(entry: LogEntry): string {
|
|
133
|
+
const pad = (str: string): string => str.padStart(5, " ");
|
|
134
|
+
const colorCode =
|
|
135
|
+
entry.level === "error"
|
|
136
|
+
? "\x1b[31m"
|
|
137
|
+
: entry.level === "warn"
|
|
138
|
+
? "\x1b[33m"
|
|
139
|
+
: entry.level === "debug"
|
|
140
|
+
? "\x1b[36m"
|
|
141
|
+
: "";
|
|
142
|
+
const reset = "\x1b[0m";
|
|
143
|
+
|
|
144
|
+
const coloredLevel = colorCode + pad(entry.level.toUpperCase()) + reset;
|
|
145
|
+
const contextPart = entry.context ? ` [${entry.context}]` : "";
|
|
146
|
+
|
|
147
|
+
// Redact credentials from BOTH the message (payload previews often embed
|
|
148
|
+
// cookies/JWTs) and the structured data.
|
|
149
|
+
const safeMessage = redactLogMessage(entry.message);
|
|
150
|
+
const safeData = entry.data ? redactLogValue(entry.data) : undefined;
|
|
151
|
+
|
|
152
|
+
let output = `${coloredLevel}${contextPart} ${safeMessage}`;
|
|
153
|
+
|
|
154
|
+
if (safeData !== undefined) {
|
|
155
|
+
output += "\n" + JSON.stringify(safeData, null, 2);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return output;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
debug(message: string, data?: Record<string, unknown>): void {
|
|
162
|
+
if (this.shouldLog("debug")) {
|
|
163
|
+
console.log(
|
|
164
|
+
this.formatEntry({
|
|
165
|
+
timestamp: new Date(),
|
|
166
|
+
level: "debug",
|
|
167
|
+
message: this.context ? `[${this.context}] ${message}` : message,
|
|
168
|
+
data,
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
info(message: string, data?: Record<string, unknown>): void {
|
|
175
|
+
if (this.shouldLog("info")) {
|
|
176
|
+
console.log(
|
|
177
|
+
this.formatEntry({
|
|
178
|
+
timestamp: new Date(),
|
|
179
|
+
level: "info",
|
|
180
|
+
message: this.context ? `[${this.context}] ${message}` : message,
|
|
181
|
+
data,
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
warn(message: string, data?: Record<string, unknown>): void {
|
|
188
|
+
if (this.shouldLog("warn")) {
|
|
189
|
+
console.warn(
|
|
190
|
+
this.formatEntry({
|
|
191
|
+
timestamp: new Date(),
|
|
192
|
+
level: "warn",
|
|
193
|
+
message: this.context ? `[${this.context}] ${message}` : message,
|
|
194
|
+
data,
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
error(message: string, data?: Record<string, unknown>): void {
|
|
201
|
+
if (this.shouldLog("error")) {
|
|
202
|
+
console.error(
|
|
203
|
+
this.formatEntry({
|
|
204
|
+
timestamp: new Date(),
|
|
205
|
+
level: "error",
|
|
206
|
+
message: this.context ? `[${this.context}] ${message}` : message,
|
|
207
|
+
data,
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Determine initial log level from environment
|
|
215
|
+
const envLevel = process.env.LOG_LEVEL as LogLevel | undefined;
|
|
216
|
+
const toolcallDebugEnv = process.env.TOOLCALL_DEBUG || "errors";
|
|
217
|
+
|
|
218
|
+
export const toolcallDebugLevel: ToolcallDebugLevel =
|
|
219
|
+
toolcallDebugEnv === "1"
|
|
220
|
+
? "1"
|
|
221
|
+
: toolcallDebugEnv === "errors"
|
|
222
|
+
? "errors"
|
|
223
|
+
: "0";
|
|
224
|
+
|
|
225
|
+
const initialLevel: LogLevel =
|
|
226
|
+
toolcallDebugLevel === "1"
|
|
227
|
+
? "debug"
|
|
228
|
+
: envLevel && ["debug", "info", "warn", "error"].includes(envLevel)
|
|
229
|
+
? envLevel
|
|
230
|
+
// Default for general users: quiet terminal with only warnings/errors
|
|
231
|
+
// (+ the always-on request/account console lines). Debugging opt-in via
|
|
232
|
+
// LOG_LEVEL=debug / TOOLCALL_DEBUG=1.
|
|
233
|
+
: "warn";
|
|
234
|
+
|
|
235
|
+
export const logger = new Logger(initialLevel);
|
|
236
|
+
|
|
237
|
+
export function isDebugEnabled(): boolean {
|
|
238
|
+
return logger.isLevelEnabled("debug");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Helper to check if toolcall debug is enabled
|
|
242
|
+
export function isToolcallDebugEnabled(): boolean {
|
|
243
|
+
return toolcallDebugLevel === "1";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function isToolcallErrorDebugEnabled(): boolean {
|
|
247
|
+
return toolcallDebugLevel === "1" || toolcallDebugLevel === "errors";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export const upstreamDebugEnabled = process.env.UPSTREAM_DEBUG === "true";
|
|
251
|
+
|
|
252
|
+
// Confirm debug mode on startup (only log if explicitly set)
|
|
253
|
+
if (process.env.TOOLCALL_DEBUG) {
|
|
254
|
+
if (toolcallDebugLevel === "1") {
|
|
255
|
+
console.log("🔍 [Logger] TOOLCALL_DEBUG=1 - full debug logs active");
|
|
256
|
+
} else if (toolcallDebugLevel === "errors") {
|
|
257
|
+
console.log(
|
|
258
|
+
"[Logger] TOOLCALL_DEBUG=errors - toolcall logs on errors only",
|
|
259
|
+
);
|
|
260
|
+
} else {
|
|
261
|
+
console.log("🔇 [Logger] TOOLCALL_DEBUG=0 - toolcall logs disabled");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (upstreamDebugEnabled) {
|
|
266
|
+
console.log(
|
|
267
|
+
"[Logger] UPSTREAM_DEBUG=true - raw upstream chunks logging active",
|
|
268
|
+
);
|
|
269
|
+
}
|