backtrace-console 0.0.2 → 0.0.3
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/README.md +26 -9
- package/bin/backtrace-server.js +30 -26
- package/package.json +5 -10
- package/app.js +0 -23
- package/bin/backtrace-cli.js +0 -22
- package/bin/www +0 -93
- package/lib/BacktraceCodexTool.js +0 -32
- package/lib/backtrace/analysis.js +0 -356
- package/lib/backtrace/constants.js +0 -27
- package/lib/backtrace/options.js +0 -278
- package/lib/backtrace/query-download.js +0 -117
- package/lib/backtrace/query-session.js +0 -229
- package/lib/backtrace/query.js +0 -506
- package/lib/backtrace/repair-fingerprint.js +0 -405
- package/lib/backtrace/repair.js +0 -530
- package/lib/backtrace/tool.js +0 -364
- package/lib/backtrace/utils.js +0 -297
- package/lib/cli/args.js +0 -177
- package/lib/cli/run.js +0 -191
- package/lib/feishu.js +0 -66
- package/lib/scheduler.js +0 -126
- package/public/chat-components.css +0 -569
- package/public/chat-core.js +0 -635
- package/public/chat-layout.css +0 -290
- package/public/chat-render.js +0 -308
- package/public/chat-send.js +0 -230
- package/public/chat.html +0 -69
- package/public/index-page.js +0 -504
- package/public/index.html +0 -138
- package/public/stylesheets/style.css +0 -186
- package/routes/backtrace-chat.js +0 -389
- package/routes/backtrace-files.js +0 -88
- package/routes/backtrace-fix-plan.js +0 -53
- package/routes/backtrace-run.js +0 -128
- package/routes/backtrace-shared.js +0 -202
- package/routes/backtrace.js +0 -10
- package/routes/index.js +0 -9
- package/routes/users.js +0 -9
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
const fs = require("node:fs/promises");
|
|
2
|
-
const fsNative = require("node:fs");
|
|
3
|
-
const path = require("node:path");
|
|
4
|
-
const { logStep } = require("./utils");
|
|
5
|
-
|
|
6
|
-
function formatBytes(bytes) {
|
|
7
|
-
const numeric = Number(bytes || 0);
|
|
8
|
-
if (!Number.isFinite(numeric) || numeric <= 0) return "0 B";
|
|
9
|
-
const units = ["B", "KB", "MB", "GB"];
|
|
10
|
-
let value = numeric;
|
|
11
|
-
let unitIndex = 0;
|
|
12
|
-
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
13
|
-
value /= 1024;
|
|
14
|
-
unitIndex += 1;
|
|
15
|
-
}
|
|
16
|
-
return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function formatAttachmentSpeed(bytes, elapsedMs) {
|
|
20
|
-
const seconds = Math.max(0.001, Number(elapsedMs || 0) / 1000);
|
|
21
|
-
return `${formatBytes(Number(bytes || 0) / seconds)}/s`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function formatAttachmentProgress(downloadedBytes, totalBytes) {
|
|
25
|
-
if (!totalBytes) return formatBytes(downloadedBytes);
|
|
26
|
-
const percent = Math.min(100, Math.max(0, (downloadedBytes / totalBytes) * 100));
|
|
27
|
-
return `${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)} (${percent.toFixed(1)}%)`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function formatAttachmentStatus(downloadedBytes, totalBytes, elapsedMs) {
|
|
31
|
-
const progress = formatAttachmentProgress(downloadedBytes, totalBytes);
|
|
32
|
-
const transferred = `${formatBytes(downloadedBytes)} transferred`;
|
|
33
|
-
if (!elapsedMs) return `${progress}, ${transferred}`;
|
|
34
|
-
return [progress, transferred, formatAttachmentSpeed(downloadedBytes, elapsedMs)].join(", ");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function downloadAttachment(session, queryUrl, objectHexId, targetDir, attachment, buildSiblingUrl) {
|
|
38
|
-
const savedPath = path.join(targetDir, attachment.name);
|
|
39
|
-
const existing = await fs.stat(savedPath).catch(() => null);
|
|
40
|
-
if (existing && existing.isFile()) {
|
|
41
|
-
logStep("download", "attachment already exists, skipping", savedPath);
|
|
42
|
-
return { ...attachment, savedPath, skipped: true, sizeBytes: existing.size || 0 };
|
|
43
|
-
}
|
|
44
|
-
const source = new URL(queryUrl);
|
|
45
|
-
const url = buildSiblingUrl(queryUrl, "/api/get");
|
|
46
|
-
const project = source.searchParams.get("project");
|
|
47
|
-
if (project) url.searchParams.set("project", project);
|
|
48
|
-
url.searchParams.set("object", objectHexId);
|
|
49
|
-
url.searchParams.set("attachment_id", attachment.id);
|
|
50
|
-
url.searchParams.set("attachment_inline", "false");
|
|
51
|
-
logStep("download", "downloading attachment", { objectId: objectHexId, attachment: attachment.name });
|
|
52
|
-
const response = await session.requestStream(url.toString(), {}, `download ${objectHexId}/${attachment.name}`);
|
|
53
|
-
const totalBytes = Number(response.headers.get("content-length") || attachment.size || 0);
|
|
54
|
-
const fileStream = fsNative.createWriteStream(savedPath);
|
|
55
|
-
const startedAt = Date.now();
|
|
56
|
-
let downloadedBytes = 0;
|
|
57
|
-
let progressTimer = null;
|
|
58
|
-
const printProgress = () => {
|
|
59
|
-
const elapsedMs = Math.max(1, Date.now() - startedAt);
|
|
60
|
-
logStep("download", `file ${formatAttachmentStatus(downloadedBytes, totalBytes, elapsedMs)}`, {
|
|
61
|
-
objectId: objectHexId,
|
|
62
|
-
attachment: attachment.name,
|
|
63
|
-
});
|
|
64
|
-
};
|
|
65
|
-
try {
|
|
66
|
-
printProgress();
|
|
67
|
-
progressTimer = setInterval(printProgress, 1000);
|
|
68
|
-
for await (const chunk of response.body) {
|
|
69
|
-
downloadedBytes += chunk.length;
|
|
70
|
-
if (!fileStream.write(chunk)) {
|
|
71
|
-
await new Promise((resolve) => fileStream.once("drain", resolve));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
await new Promise((resolve, reject) => {
|
|
75
|
-
fileStream.end((error) => (error ? reject(error) : resolve()));
|
|
76
|
-
});
|
|
77
|
-
} catch (error) {
|
|
78
|
-
if (progressTimer) clearInterval(progressTimer);
|
|
79
|
-
fileStream.destroy();
|
|
80
|
-
await fs.unlink(savedPath).catch(() => {});
|
|
81
|
-
throw error;
|
|
82
|
-
}
|
|
83
|
-
if (progressTimer) clearInterval(progressTimer);
|
|
84
|
-
printProgress();
|
|
85
|
-
logStep("download", "attachment saved", savedPath);
|
|
86
|
-
return { ...attachment, savedPath, sizeBytes: downloadedBytes };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function downloadAttachmentWithRetry(session, queryUrl, objectHexId, targetDir, attachment, buildSiblingUrl, maxAttempts = 5) {
|
|
90
|
-
let lastError = null;
|
|
91
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
92
|
-
try {
|
|
93
|
-
return await downloadAttachment(session, queryUrl, objectHexId, targetDir, attachment, buildSiblingUrl);
|
|
94
|
-
} catch (error) {
|
|
95
|
-
lastError = error;
|
|
96
|
-
logStep("download", "attachment download failed", {
|
|
97
|
-
objectId: objectHexId,
|
|
98
|
-
attachment: attachment.name,
|
|
99
|
-
attempt,
|
|
100
|
-
maxAttempts,
|
|
101
|
-
message: error instanceof Error ? error.message : String(error),
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
logStep("download", "attachment skipped after retries", {
|
|
106
|
-
objectId: objectHexId,
|
|
107
|
-
attachment: attachment.name,
|
|
108
|
-
maxAttempts,
|
|
109
|
-
message: lastError instanceof Error ? lastError.message : String(lastError || ""),
|
|
110
|
-
});
|
|
111
|
-
return { ...attachment, savedPath: "", skipped: true, failed: true };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
module.exports = {
|
|
115
|
-
downloadAttachment: downloadAttachment,
|
|
116
|
-
downloadAttachmentWithRetry: downloadAttachmentWithRetry,
|
|
117
|
-
};
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
const { logStep, retry } = require("./utils");
|
|
2
|
-
|
|
3
|
-
// 进程级 token 缓存,key = loginUrl,有效期 1 小时
|
|
4
|
-
const TOKEN_TTL_MS = 60 * 60 * 1000;
|
|
5
|
-
const tokenCache = new Map(); // loginUrl -> { token, expiresAt }
|
|
6
|
-
|
|
7
|
-
function getCachedToken(loginUrl) {
|
|
8
|
-
const entry = tokenCache.get(loginUrl);
|
|
9
|
-
if (!entry) return null;
|
|
10
|
-
if (Date.now() >= entry.expiresAt) {
|
|
11
|
-
tokenCache.delete(loginUrl);
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
return entry.token;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function setCachedToken(loginUrl, token) {
|
|
18
|
-
tokenCache.set(loginUrl, { token, expiresAt: Date.now() + TOKEN_TTL_MS });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function clearCachedToken(loginUrl) {
|
|
22
|
-
tokenCache.delete(loginUrl);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let undiciModule = null;
|
|
26
|
-
|
|
27
|
-
function getUndici() {
|
|
28
|
-
if (!undiciModule) {
|
|
29
|
-
undiciModule = require("undici");
|
|
30
|
-
}
|
|
31
|
-
return undiciModule;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function createProxyDispatcher(proxyUrl) {
|
|
35
|
-
const normalizedProxy = String(proxyUrl || "").trim();
|
|
36
|
-
if (!normalizedProxy) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
try {
|
|
40
|
-
const { ProxyAgent } = getUndici();
|
|
41
|
-
return new ProxyAgent(normalizedProxy);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
throw new Error(`Proxy support requires dependency "undici": ${error instanceof Error ? error.message : String(error)}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function withDispatcher(options, dispatcher) {
|
|
48
|
-
if (!dispatcher) {
|
|
49
|
-
return options || {};
|
|
50
|
-
}
|
|
51
|
-
return { ...(options || {}), dispatcher };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function buildSiblingUrl(sourceUrl, pathname) {
|
|
55
|
-
const url = new URL(sourceUrl);
|
|
56
|
-
url.pathname = pathname;
|
|
57
|
-
url.search = "";
|
|
58
|
-
return url;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function buildLoginUrl(queryUrl) {
|
|
62
|
-
return buildSiblingUrl(queryUrl, "/api/login").toString();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function withSessionToken(urlString, sessionToken) {
|
|
66
|
-
const url = new URL(urlString);
|
|
67
|
-
url.searchParams.set("token", sessionToken);
|
|
68
|
-
return url;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
class AuthenticationError extends Error {
|
|
72
|
-
constructor(message) {
|
|
73
|
-
super(message);
|
|
74
|
-
this.name = "AuthenticationError";
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
class BacktraceSession {
|
|
79
|
-
constructor(options) {
|
|
80
|
-
this.username = options.username;
|
|
81
|
-
this.password = options.password;
|
|
82
|
-
this.retries = options.retries;
|
|
83
|
-
this.proxy = String(options.proxy || "").trim();
|
|
84
|
-
this.dispatcher = createProxyDispatcher(this.proxy);
|
|
85
|
-
this.token = "";
|
|
86
|
-
this.loginUrl = buildLoginUrl(options.queryUrl);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async login() {
|
|
90
|
-
const cached = getCachedToken(this.loginUrl);
|
|
91
|
-
if (cached) {
|
|
92
|
-
this.token = cached;
|
|
93
|
-
logStep("auth", "using cached token");
|
|
94
|
-
return this.token;
|
|
95
|
-
}
|
|
96
|
-
const form = new URLSearchParams();
|
|
97
|
-
form.set("username", this.username);
|
|
98
|
-
form.set("password", this.password);
|
|
99
|
-
logStep("auth", "logging in", { loginUrl: this.loginUrl, username: this.username });
|
|
100
|
-
const payload = await retry("backtrace login", this.retries, async () => {
|
|
101
|
-
const response = await fetch(this.loginUrl, {
|
|
102
|
-
method: "POST",
|
|
103
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
104
|
-
body: form.toString(),
|
|
105
|
-
...(this.dispatcher ? { dispatcher: this.dispatcher } : {}),
|
|
106
|
-
});
|
|
107
|
-
const text = await response.text();
|
|
108
|
-
let data = null;
|
|
109
|
-
try {
|
|
110
|
-
data = text ? JSON.parse(text) : null;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
data = null;
|
|
113
|
-
}
|
|
114
|
-
if (!response.ok) {
|
|
115
|
-
throw new Error(`Login failed: ${response.status} ${response.statusText}`);
|
|
116
|
-
}
|
|
117
|
-
if (!data || !data.token) {
|
|
118
|
-
throw new Error("Login response did not contain a token");
|
|
119
|
-
}
|
|
120
|
-
return data;
|
|
121
|
-
});
|
|
122
|
-
this.token = payload.token;
|
|
123
|
-
setCachedToken(this.loginUrl, this.token);
|
|
124
|
-
logStep("auth", "login succeeded");
|
|
125
|
-
return this.token;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async getToken() {
|
|
129
|
-
if (!this.token) {
|
|
130
|
-
await this.login();
|
|
131
|
-
}
|
|
132
|
-
return this.token;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async refreshToken() {
|
|
136
|
-
logStep("auth", "refreshing session token");
|
|
137
|
-
clearCachedToken(this.loginUrl);
|
|
138
|
-
this.token = "";
|
|
139
|
-
return this.login();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
buildHeaders(extraHeaders, token) {
|
|
143
|
-
return { ...(extraHeaders || {}), Cookie: `token=${token}` };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
parseResponseBody(text) {
|
|
147
|
-
try {
|
|
148
|
-
return text ? JSON.parse(text) : null;
|
|
149
|
-
} catch (error) {
|
|
150
|
-
return text;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
isAuthFailure(response, parsedBody) {
|
|
155
|
-
const normalizedBody = typeof parsedBody === "string" ? parsedBody.toLowerCase() : JSON.stringify(parsedBody || {}).toLowerCase();
|
|
156
|
-
return response.status === 401
|
|
157
|
-
|| response.status === 403
|
|
158
|
-
|| normalizedBody.includes("invalid token")
|
|
159
|
-
|| normalizedBody.includes("authentication")
|
|
160
|
-
|| normalizedBody.includes("not authorized");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async request(urlString, options, label, mode) {
|
|
164
|
-
return retry(label, this.retries, async () => {
|
|
165
|
-
let token = await this.getToken();
|
|
166
|
-
let authAttempt = 0;
|
|
167
|
-
while (authAttempt < 2) {
|
|
168
|
-
const url = withSessionToken(urlString, token);
|
|
169
|
-
const response = await fetch(url.toString(), withDispatcher({ ...options, headers: this.buildHeaders(options?.headers, token) }, this.dispatcher));
|
|
170
|
-
if (mode === "buffer" && response.ok) {
|
|
171
|
-
return Buffer.from(await response.arrayBuffer());
|
|
172
|
-
}
|
|
173
|
-
const text = await response.text();
|
|
174
|
-
const parsed = this.parseResponseBody(text);
|
|
175
|
-
if (response.ok) {
|
|
176
|
-
return parsed;
|
|
177
|
-
}
|
|
178
|
-
if (this.isAuthFailure(response, parsed) && authAttempt === 0) {
|
|
179
|
-
await this.refreshToken();
|
|
180
|
-
token = await this.getToken();
|
|
181
|
-
authAttempt += 1;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
|
|
185
|
-
}
|
|
186
|
-
throw new AuthenticationError("Authentication failed after token refresh");
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async requestJson(urlString, options, label) {
|
|
191
|
-
return this.request(urlString, options, label, "json");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async requestBuffer(urlString, options, label) {
|
|
195
|
-
return this.request(urlString, options, label, "buffer");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async requestStream(urlString, options, label) {
|
|
199
|
-
return retry(label, this.retries, async () => {
|
|
200
|
-
let token = await this.getToken();
|
|
201
|
-
let authAttempt = 0;
|
|
202
|
-
while (authAttempt < 2) {
|
|
203
|
-
const url = withSessionToken(urlString, token);
|
|
204
|
-
const response = await fetch(url.toString(), withDispatcher({ ...options, headers: this.buildHeaders(options?.headers, token) }, this.dispatcher));
|
|
205
|
-
if (response.ok) {
|
|
206
|
-
return response;
|
|
207
|
-
}
|
|
208
|
-
const text = await response.text();
|
|
209
|
-
const parsed = this.parseResponseBody(text);
|
|
210
|
-
if (this.isAuthFailure(response, parsed) && authAttempt === 0) {
|
|
211
|
-
await this.refreshToken();
|
|
212
|
-
token = await this.getToken();
|
|
213
|
-
authAttempt += 1;
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
|
|
217
|
-
}
|
|
218
|
-
throw new AuthenticationError("Authentication failed after token refresh");
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
module.exports = {
|
|
224
|
-
AuthenticationError,
|
|
225
|
-
BacktraceSession,
|
|
226
|
-
buildSiblingUrl,
|
|
227
|
-
buildLoginUrl,
|
|
228
|
-
withSessionToken,
|
|
229
|
-
};
|