backtrace-console 0.0.1 → 0.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/app.js +3 -2
- package/bin/backtrace-server.js +101 -0
- package/bin/www +3 -0
- package/lib/backtrace/constants.js +4 -0
- package/lib/backtrace/query-download.js +117 -0
- package/lib/backtrace/query-session.js +229 -0
- package/lib/backtrace/query.js +11 -445
- package/lib/backtrace/repair.js +35 -0
- package/lib/backtrace/tool.js +34 -3
- package/lib/feishu.js +66 -0
- package/lib/scheduler.js +126 -0
- package/package.json +10 -4
- package/public/chat-components.css +569 -0
- package/public/chat-core.js +635 -0
- package/public/chat-layout.css +290 -0
- package/public/chat-render.js +308 -0
- package/public/chat-send.js +230 -0
- package/public/chat.html +69 -0
- package/public/{__inline_check__.js → index-page.js} +107 -54
- package/public/index.html +1 -505
- package/routes/backtrace-chat.js +389 -0
- package/routes/backtrace-files.js +88 -0
- package/routes/backtrace-fix-plan.js +53 -0
- package/routes/backtrace-run.js +128 -0
- package/routes/backtrace-shared.js +202 -0
- package/routes/backtrace.js +7 -861
package/lib/backtrace/repair.js
CHANGED
|
@@ -485,7 +485,42 @@ async function readRepairPlanText(input, overrides = {}) {
|
|
|
485
485
|
return file.content;
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
|
|
489
|
+
async function runChatTurn(input, overrides = {}) {
|
|
490
|
+
const context = createRepairContext(overrides);
|
|
491
|
+
const prompt = String(input.prompt || "").trim();
|
|
492
|
+
const threadId = input.threadId;
|
|
493
|
+
|
|
494
|
+
if (!prompt) {
|
|
495
|
+
throw new Error("prompt is required");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const codex = await getCodex(context.proxy);
|
|
499
|
+
let thread;
|
|
500
|
+
|
|
501
|
+
if (threadId) {
|
|
502
|
+
thread = codex.resumeThread(threadId);
|
|
503
|
+
} else {
|
|
504
|
+
thread = codex.startThread({
|
|
505
|
+
workingDirectory: context.workdir,
|
|
506
|
+
skipGitRepoCheck: true,
|
|
507
|
+
sandboxMode: "read-only",
|
|
508
|
+
approvalPolicy: "never",
|
|
509
|
+
modelReasoningEffort: "low"
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const turn = await runCodexTurnWithStreaming(thread, prompt, "[backtrace-chat]", "run-chat", { threadId: thread.id });
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
ok: true,
|
|
517
|
+
reply: turn.finalResponse,
|
|
518
|
+
threadId: thread.id
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
488
522
|
module.exports = {
|
|
523
|
+
runChatTurn,
|
|
489
524
|
createRepairContext,
|
|
490
525
|
formatRepairVersion,
|
|
491
526
|
resolveCodexProxy,
|
package/lib/backtrace/tool.js
CHANGED
|
@@ -27,14 +27,29 @@ async function writeFingerprintMetadata(items, options) {
|
|
|
27
27
|
const fingerprint = normalizeFingerprint(item.fingerprint || item.values.fingerprint);
|
|
28
28
|
const fingerprintDir = path.join(options.storageDir, fingerprint);
|
|
29
29
|
const metaPath = path.join(fingerprintDir, "meta.json");
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
const incoming = {
|
|
31
32
|
fingerprint,
|
|
32
33
|
firstSeen: item.values["fingerprint;first_seen"] || "",
|
|
33
34
|
issueState: item.values["fingerprint;issues;state"] || "",
|
|
34
|
-
errorMessage: item.values["error.message"] || "",
|
|
35
35
|
classifiers: item.values.classifiers || "",
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
// 保留已有字段(如 feishuNotifiedAt),只更新远端字段
|
|
39
|
+
let existing = {};
|
|
40
|
+
const raw = await fs.readFile(metaPath, "utf8").catch(() => "");
|
|
41
|
+
if (raw) {
|
|
42
|
+
try { existing = JSON.parse(raw); } catch (_) { existing = {}; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// errorMessage 只在首次写入,后续不覆盖(Backtrace 每次可能返回不同 object 的值)
|
|
46
|
+
const errorMessage = item.values["error.message"] || "";
|
|
47
|
+
if (!existing.errorMessage && errorMessage) {
|
|
48
|
+
incoming.errorMessage = errorMessage;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const meta = { ...existing, ...incoming };
|
|
52
|
+
|
|
38
53
|
await ensureDirectory(fingerprintDir);
|
|
39
54
|
await fs.writeFile(metaPath, JSON.stringify(meta, null, 2), "utf8");
|
|
40
55
|
written.push({ fingerprint, metaPath });
|
|
@@ -183,6 +198,22 @@ class BacktraceCodexTool {
|
|
|
183
198
|
if (command === "fingerprint" && getRequestedFingerprints(options).length === 1) {
|
|
184
199
|
const detail = await queryFingerprintDetail(session, options);
|
|
185
200
|
const writtenMeta = await writeFingerprintMetadata([detail.summary], options);
|
|
201
|
+
|
|
202
|
+
// 为每个 object 创建本地目录并缓存元数据,避免重复请求远端
|
|
203
|
+
const logsRoot = path.join(options.storageDir, normalizeFingerprint(detail.fingerprint), "logs");
|
|
204
|
+
await ensureDirectory(logsRoot);
|
|
205
|
+
for (const obj of detail.objects) {
|
|
206
|
+
/* eslint-disable no-await-in-loop */
|
|
207
|
+
const objDir = path.join(logsRoot, obj.objectIdHex);
|
|
208
|
+
await ensureDirectory(objDir);
|
|
209
|
+
const objMetaPath = path.join(objDir, "object-meta.json");
|
|
210
|
+
const objMetaExists = await fs.stat(objMetaPath).catch(() => null);
|
|
211
|
+
if (!objMetaExists) {
|
|
212
|
+
await fs.writeFile(objMetaPath, JSON.stringify({ objectIdHex: obj.objectIdHex, timestamp: obj.timestamp }, null, 2), "utf8");
|
|
213
|
+
}
|
|
214
|
+
/* eslint-enable no-await-in-loop */
|
|
215
|
+
}
|
|
216
|
+
|
|
186
217
|
return {
|
|
187
218
|
command,
|
|
188
219
|
mode: "detail",
|
|
@@ -330,4 +361,4 @@ async function runBacktraceCommand(command, overrides = {}) {
|
|
|
330
361
|
module.exports = {
|
|
331
362
|
BacktraceCodexTool,
|
|
332
363
|
runBacktraceCommand,
|
|
333
|
-
};
|
|
364
|
+
};
|
package/lib/feishu.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const https = require('node:https');
|
|
2
|
+
const http = require('node:http');
|
|
3
|
+
|
|
4
|
+
const TEMPLATE_ID = 'AAqtY8l8Sr3hO';
|
|
5
|
+
const TEMPLATE_VERSION = '1.0.3';
|
|
6
|
+
|
|
7
|
+
function postJson(url, body) {
|
|
8
|
+
return new Promise(function(resolve, reject) {
|
|
9
|
+
var parsed = new URL(url);
|
|
10
|
+
var isHttps = parsed.protocol === 'https:';
|
|
11
|
+
var payload = JSON.stringify(body);
|
|
12
|
+
var options = {
|
|
13
|
+
hostname: parsed.hostname,
|
|
14
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
15
|
+
path: parsed.pathname + (parsed.search || ''),
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
var transport = isHttps ? https : http;
|
|
23
|
+
var req = transport.request(options, function(res) {
|
|
24
|
+
var chunks = [];
|
|
25
|
+
res.on('data', function(chunk) { chunks.push(chunk); });
|
|
26
|
+
res.on('end', function() {
|
|
27
|
+
var text = Buffer.concat(chunks).toString('utf8');
|
|
28
|
+
var data = null;
|
|
29
|
+
try { data = JSON.parse(text); } catch (_) { data = text; }
|
|
30
|
+
resolve({ statusCode: res.statusCode, body: data });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
req.on('error', reject);
|
|
34
|
+
req.write(payload);
|
|
35
|
+
req.end();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function sendFeishuCard(webhookUrl, fingerprintData) {
|
|
40
|
+
var templateVariable = {
|
|
41
|
+
fingerprint: fingerprintData.fingerprint || '',
|
|
42
|
+
errormessage: fingerprintData.errorMessage || '',
|
|
43
|
+
classifiers: fingerprintData.classifiers || '',
|
|
44
|
+
jumpurl: fingerprintData.jumpUrl || ('http://127.0.0.1:3000/chat.html?fingerprint=' + (fingerprintData.fingerprint || '')),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
var cardContent = {
|
|
48
|
+
type: 'template',
|
|
49
|
+
data: {
|
|
50
|
+
template_id: TEMPLATE_ID,
|
|
51
|
+
template_version_name: TEMPLATE_VERSION,
|
|
52
|
+
template_variable: templateVariable,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
var body = {
|
|
57
|
+
msg_type: 'interactive',
|
|
58
|
+
card: JSON.stringify(cardContent),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
var result = await postJson(webhookUrl, body);
|
|
62
|
+
var ok = result.statusCode === 200 && result.body && result.body.code === 0;
|
|
63
|
+
return { ok: ok, statusCode: result.statusCode, body: result.body };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { sendFeishuCard };
|
package/lib/scheduler.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
var fs = require('node:fs/promises');
|
|
2
|
+
var cron = require('node-cron');
|
|
3
|
+
var { BacktraceCodexTool } = require('./backtrace/tool');
|
|
4
|
+
var { sendFeishuCard } = require('./feishu');
|
|
5
|
+
var { readFingerprintMeta, writeFingerprintMeta, FINGERPRINTS_ROOT } = require('../routes/backtrace-shared');
|
|
6
|
+
|
|
7
|
+
var SCHEDULER_CRON = process.env.SCHEDULER_CRON || '*/5 * * * *';
|
|
8
|
+
var FEISHU_WEBHOOK_URL = process.env.FEISHU_WEBHOOK_URL || '';
|
|
9
|
+
|
|
10
|
+
// 从 cron 表达式推算间隔秒数,用于首次执行时确定查询起点。
|
|
11
|
+
// 支持常见格式:*/N(每N分钟)、单个数字(每小时/天固定时刻)。
|
|
12
|
+
function cronToIntervalSeconds(cronExpr) {
|
|
13
|
+
var parts = String(cronExpr || '').trim().split(/\s+/);
|
|
14
|
+
if (parts.length < 5) return 300;
|
|
15
|
+
|
|
16
|
+
var minute = parts[0];
|
|
17
|
+
var hour = parts[1];
|
|
18
|
+
var dayOfMonth = parts[2];
|
|
19
|
+
|
|
20
|
+
// */N 分钟
|
|
21
|
+
var everyNMin = minute.match(/^\*\/(\d+)$/);
|
|
22
|
+
if (everyNMin) return parseInt(everyNMin[1], 10) * 60;
|
|
23
|
+
|
|
24
|
+
// */N 小时
|
|
25
|
+
var everyNHour = hour.match(/^\*\/(\d+)$/);
|
|
26
|
+
if (everyNHour) return parseInt(everyNHour[1], 10) * 3600;
|
|
27
|
+
|
|
28
|
+
// 固定分钟(如 "0 * * * *" = 每小时)
|
|
29
|
+
if (/^\d+$/.test(minute) && hour === '*') return 3600;
|
|
30
|
+
|
|
31
|
+
// 固定小时(如 "0 9 * * *" = 每天)
|
|
32
|
+
if (/^\d+$/.test(minute) && /^\d+$/.test(hour) && dayOfMonth === '*') return 86400;
|
|
33
|
+
|
|
34
|
+
return 300;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function runScheduledTask() {
|
|
38
|
+
var now = Math.floor(Date.now() / 1000);
|
|
39
|
+
var intervalSeconds = cronToIntervalSeconds(SCHEDULER_CRON);
|
|
40
|
+
var from = String(now - intervalSeconds);
|
|
41
|
+
|
|
42
|
+
console.log('[scheduler] 开始执行,时间范围 from=' + from + ' to=' + now);
|
|
43
|
+
|
|
44
|
+
var tool = new BacktraceCodexTool();
|
|
45
|
+
|
|
46
|
+
// 1. 查询远端新增 fingerprint
|
|
47
|
+
var remoteFingerprints = [];
|
|
48
|
+
try {
|
|
49
|
+
var result = await tool.fingerprint({ from: String(from), to: String(now) });
|
|
50
|
+
remoteFingerprints = (result.items || []).map(function(item) {
|
|
51
|
+
return item.fingerprint || (item.values && item.values.fingerprint) || '';
|
|
52
|
+
}).filter(Boolean);
|
|
53
|
+
console.log('[scheduler] 远端查询到 ' + remoteFingerprints.length + ' 个 fingerprint');
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error('[scheduler] 查询 fingerprint 失败:', err.message);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. 扫描本地目录,找出所有没有 feishuNotifiedAt 的 fingerprint
|
|
59
|
+
var localDirs = await fs.readdir(FINGERPRINTS_ROOT, { withFileTypes: true }).catch(function() { return []; });
|
|
60
|
+
var localFingerprints = localDirs
|
|
61
|
+
.filter(function(e) { return e.isDirectory() && !e.name.startsWith('.'); })
|
|
62
|
+
.map(function(e) { return e.name; });
|
|
63
|
+
|
|
64
|
+
// 3. 合并去重:远端新增 + 本地未推送,统一过一遍 meta 检查
|
|
65
|
+
var allFingerprints = Array.from(new Set(remoteFingerprints.concat(localFingerprints)));
|
|
66
|
+
|
|
67
|
+
var pendingItems = [];
|
|
68
|
+
for (var i = 0; i < allFingerprints.length; i++) {
|
|
69
|
+
var fp = allFingerprints[i];
|
|
70
|
+
var meta = await readFingerprintMeta(fp).catch(function() { return null; });
|
|
71
|
+
if (!meta || meta.feishuNotifiedAt) continue;
|
|
72
|
+
pendingItems.push({ fp: fp, meta: meta });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log('[scheduler] 待推送 fingerprint: ' + pendingItems.length + ' 个');
|
|
76
|
+
|
|
77
|
+
if (pendingItems.length > 0) {
|
|
78
|
+
if (!FEISHU_WEBHOOK_URL) {
|
|
79
|
+
console.warn('[scheduler] FEISHU_WEBHOOK_URL 未配置,跳过推送');
|
|
80
|
+
} else {
|
|
81
|
+
for (var j = 0; j < pendingItems.length; j++) {
|
|
82
|
+
var entry = pendingItems[j];
|
|
83
|
+
var sendResult = await sendFeishuCard(FEISHU_WEBHOOK_URL, {
|
|
84
|
+
fingerprint: entry.fp,
|
|
85
|
+
errorMessage: entry.meta.errorMessage || '',
|
|
86
|
+
classifiers: entry.meta.classifiers || '',
|
|
87
|
+
}).catch(function(err) {
|
|
88
|
+
return { ok: false, error: err.message };
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (sendResult.ok) {
|
|
92
|
+
var updated = Object.assign({}, entry.meta, { feishuNotifiedAt: new Date().toISOString() });
|
|
93
|
+
await writeFingerprintMeta(entry.fp, updated).catch(function(err) {
|
|
94
|
+
console.error('[scheduler] 写入 meta.json 失败 ' + entry.fp + ':', err.message);
|
|
95
|
+
});
|
|
96
|
+
console.log('[scheduler] 推送成功: ' + entry.fp);
|
|
97
|
+
} else {
|
|
98
|
+
console.error('[scheduler] 推送失败: ' + entry.fp, sendResult.body || sendResult.error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. 同步本地已有 fingerprint 的 object 列表(不触发推送)
|
|
105
|
+
console.log('[scheduler] 同步本地 fingerprint 的 object 列表,共 ' + localFingerprints.length + ' 个');
|
|
106
|
+
for (var k = 0; k < localFingerprints.length; k++) {
|
|
107
|
+
var syncFp = localFingerprints[k];
|
|
108
|
+
try {
|
|
109
|
+
await tool.fingerprint({ fingerprint: syncFp, from: '1', to: String(now) });
|
|
110
|
+
console.log('[scheduler] 同步完成: ' + syncFp);
|
|
111
|
+
} catch (syncErr) {
|
|
112
|
+
console.error('[scheduler] 同步失败: ' + syncFp, syncErr.message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function start() {
|
|
118
|
+
console.log('[scheduler] 启动定时任务,cron=' + SCHEDULER_CRON);
|
|
119
|
+
cron.schedule(SCHEDULER_CRON, function() {
|
|
120
|
+
runScheduledTask().catch(function(err) {
|
|
121
|
+
console.error('[scheduler] 任务异常:', err.message);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { start };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backtrace-console",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "CLI for querying Backtrace fingerprints, collecting artifacts, and generating repair plans.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -11,19 +11,25 @@
|
|
|
11
11
|
"routes"
|
|
12
12
|
],
|
|
13
13
|
"bin": {
|
|
14
|
-
"backtrace": "bin/backtrace-cli.js"
|
|
14
|
+
"backtrace": "bin/backtrace-cli.js",
|
|
15
|
+
"backtrace-server": "bin/backtrace-server.js"
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
17
18
|
"start": "node ./bin/www",
|
|
18
|
-
"backtrace": "node ./bin/backtrace-cli.js"
|
|
19
|
+
"backtrace": "node ./bin/backtrace-cli.js",
|
|
20
|
+
"server:run": "node ./bin/backtrace-server.js run",
|
|
21
|
+
"server:start": "node ./bin/backtrace-server.js start",
|
|
22
|
+
"server:stop": "node ./bin/backtrace-server.js stop",
|
|
23
|
+
"server:status": "node ./bin/backtrace-server.js status"
|
|
19
24
|
},
|
|
20
25
|
"dependencies": {
|
|
21
|
-
"@openai/codex-sdk": "^0.
|
|
26
|
+
"@openai/codex-sdk": "^0.128.0",
|
|
22
27
|
"cookie-parser": "~1.4.4",
|
|
23
28
|
"debug": "~2.6.9",
|
|
24
29
|
"dotenv": "^17.4.1",
|
|
25
30
|
"express": "~4.16.1",
|
|
26
31
|
"morgan": "~1.9.1",
|
|
32
|
+
"node-cron": "^4.2.1",
|
|
27
33
|
"undici": "^7.24.8"
|
|
28
34
|
}
|
|
29
35
|
}
|