backtrace-console 0.0.3 → 0.0.4
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 +23 -0
- package/bin/backtrace-server.js +19 -14
- package/bin/www +93 -0
- package/lib/BacktraceCodexTool.js +32 -0
- package/lib/backtrace/analysis.js +356 -0
- package/lib/backtrace/constants.js +27 -0
- package/lib/backtrace/options.js +278 -0
- package/lib/backtrace/query-download.js +117 -0
- package/lib/backtrace/query-session.js +229 -0
- package/lib/backtrace/query.js +506 -0
- package/lib/backtrace/repair-fingerprint.js +405 -0
- package/lib/backtrace/repair.js +530 -0
- package/lib/backtrace/tool.js +364 -0
- package/lib/backtrace/utils.js +297 -0
- package/lib/cli/args.js +177 -0
- package/lib/cli/run.js +191 -0
- package/lib/feishu.js +66 -0
- package/lib/scheduler.js +126 -0
- package/package.json +8 -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/index-page.js +504 -0
- package/public/index.html +138 -0
- package/public/stylesheets/style.css +186 -0
- 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 +10 -0
- package/routes/index.js +9 -0
- package/routes/users.js +9 -0
package/lib/cli/args.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// 解析 bin/backtrace-cli.js 使用的轻量命令行语法,
|
|
2
|
+
// 把 argv 数组转换成后续流程需要的标准化选项对象。
|
|
3
|
+
function printCliUsage() {
|
|
4
|
+
console.log([
|
|
5
|
+
"Usage:",
|
|
6
|
+
" backtrace <command> [options]",
|
|
7
|
+
"",
|
|
8
|
+
"Commands:",
|
|
9
|
+
" list",
|
|
10
|
+
" fingerprint",
|
|
11
|
+
" collect",
|
|
12
|
+
" summarize-errors",
|
|
13
|
+
" fix-plan",
|
|
14
|
+
" apply-fix",
|
|
15
|
+
"",
|
|
16
|
+
"Shared options:",
|
|
17
|
+
" --fingerprint <value[,value2,...]>",
|
|
18
|
+
" --from <unixTs> default: 1",
|
|
19
|
+
" --to <unixTs> default: now",
|
|
20
|
+
" --query-url <url>",
|
|
21
|
+
" --proxy <url>",
|
|
22
|
+
" --workdir <dir>",
|
|
23
|
+
" --storage-dir <dir>",
|
|
24
|
+
" --download-concurrency <n>",
|
|
25
|
+
"",
|
|
26
|
+
"Fix-plan options:",
|
|
27
|
+
" --fingerprint <value> required for backtrace fix-plan",
|
|
28
|
+
" --report-path <path> relative path under fingerprints/",
|
|
29
|
+
" --repair-plan-path <path> relative path of existing repair plan file",
|
|
30
|
+
" --repair-version <value>",
|
|
31
|
+
" --plan-file <path> load plan text from file",
|
|
32
|
+
" --plan-text <text> pass plan text directly",
|
|
33
|
+
"",
|
|
34
|
+
"Examples:",
|
|
35
|
+
" backtrace list",
|
|
36
|
+
" backtrace fingerprint",
|
|
37
|
+
" backtrace fingerprint db221f7a4f76...",
|
|
38
|
+
" backtrace collect --fingerprint db221f7",
|
|
39
|
+
" backtrace summarize-errors --fingerprint db221f7a4f76...",
|
|
40
|
+
" backtrace fix-plan --fingerprint db221f7a4f76...",
|
|
41
|
+
" backtrace apply-fix --fingerprint db221f7a4f76...",
|
|
42
|
+
].join("\n"));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 保持显式解析,这样每个支持的参数都只映射到一个确定字段,
|
|
46
|
+
// 调用方也能明确区分“显示帮助”和“执行命令”两种模式。
|
|
47
|
+
function parseCliArgs(argv) {
|
|
48
|
+
const args = Array.from(argv || []);
|
|
49
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
50
|
+
return { help: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const command = args[0];
|
|
54
|
+
let index = 1;
|
|
55
|
+
|
|
56
|
+
const options = {
|
|
57
|
+
command,
|
|
58
|
+
fingerprint: "",
|
|
59
|
+
objectId: "",
|
|
60
|
+
from: "",
|
|
61
|
+
to: "",
|
|
62
|
+
queryUrl: "",
|
|
63
|
+
proxy: "",
|
|
64
|
+
workdir: "",
|
|
65
|
+
storageDir: "",
|
|
66
|
+
downloadConcurrency: "",
|
|
67
|
+
limit: "",
|
|
68
|
+
offset: "",
|
|
69
|
+
select: "",
|
|
70
|
+
reportPath: "",
|
|
71
|
+
repairPlanPath: "",
|
|
72
|
+
repairVersion: "",
|
|
73
|
+
planFile: "",
|
|
74
|
+
planText: "",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// 按顺序解析参数,因为这里只支持简单的
|
|
78
|
+
// "--name value" 形式,以及一个可选的 fix-plan 子命令。
|
|
79
|
+
for (; index < args.length; index += 1) {
|
|
80
|
+
const arg = args[index];
|
|
81
|
+
if (!arg.startsWith("-") && command === "fingerprint" && !options.fingerprint) {
|
|
82
|
+
options.fingerprint = arg;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (arg === "--fingerprint") {
|
|
86
|
+
options.fingerprint = args[index + 1] || "";
|
|
87
|
+
index += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (arg === "--object-id") {
|
|
91
|
+
options.objectId = args[index + 1] || "";
|
|
92
|
+
index += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (arg === "--from") {
|
|
96
|
+
options.from = args[index + 1] || "";
|
|
97
|
+
index += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (arg === "--to") {
|
|
101
|
+
options.to = args[index + 1] || "";
|
|
102
|
+
index += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (arg === "--query-url") {
|
|
106
|
+
options.queryUrl = args[index + 1] || "";
|
|
107
|
+
index += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (arg === "--proxy") {
|
|
111
|
+
options.proxy = args[index + 1] || "";
|
|
112
|
+
index += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (arg === "--workdir") {
|
|
116
|
+
options.workdir = args[index + 1] || "";
|
|
117
|
+
index += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (arg === "--storage-dir") {
|
|
121
|
+
options.storageDir = args[index + 1] || "";
|
|
122
|
+
index += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (arg === "--download-concurrency") {
|
|
126
|
+
options.downloadConcurrency = args[index + 1] || "";
|
|
127
|
+
index += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (arg === "--limit") {
|
|
131
|
+
options.limit = args[index + 1] || "";
|
|
132
|
+
index += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (arg === "--offset") {
|
|
136
|
+
options.offset = args[index + 1] || "";
|
|
137
|
+
index += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (arg === "--select") {
|
|
141
|
+
options.select = args[index + 1] || "";
|
|
142
|
+
index += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (arg === "--report-path") {
|
|
146
|
+
options.reportPath = args[index + 1] || "";
|
|
147
|
+
index += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (arg === "--repair-plan-path") {
|
|
151
|
+
options.repairPlanPath = args[index + 1] || "";
|
|
152
|
+
index += 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (arg === "--repair-version") {
|
|
156
|
+
options.repairVersion = args[index + 1] || "";
|
|
157
|
+
index += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (arg === "--plan-file") {
|
|
161
|
+
options.planFile = args[index + 1] || "";
|
|
162
|
+
index += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (arg === "--plan-text") {
|
|
166
|
+
options.planText = args[index + 1] || "";
|
|
167
|
+
index += 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { help: false, options };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
printCliUsage,
|
|
176
|
+
parseCliArgs,
|
|
177
|
+
};
|
package/lib/cli/run.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const { createOptions, runBacktraceCommand, printList } = require("../BacktraceCodexTool");
|
|
3
|
+
const { applyFixFromFingerprint, generateRepairPlanFromFingerprint } = require("../backtrace/repair-fingerprint");
|
|
4
|
+
|
|
5
|
+
// 把 CLI 原始字符串参数转换成底层 Backtrace 工具共用的标准选项结构。
|
|
6
|
+
function buildBacktraceOverrides(rawOptions, command) {
|
|
7
|
+
const overrides = { command };
|
|
8
|
+
// 这里只复制用户显式传入的字段,避免把空字符串覆盖默认值。
|
|
9
|
+
if (rawOptions.fingerprint) {
|
|
10
|
+
overrides.fingerprint = rawOptions.fingerprint;
|
|
11
|
+
}
|
|
12
|
+
if (rawOptions.objectId) {
|
|
13
|
+
overrides.objectId = rawOptions.objectId;
|
|
14
|
+
}
|
|
15
|
+
if (rawOptions.from) {
|
|
16
|
+
overrides.from = rawOptions.from;
|
|
17
|
+
}
|
|
18
|
+
if (rawOptions.to) {
|
|
19
|
+
overrides.to = rawOptions.to;
|
|
20
|
+
}
|
|
21
|
+
if (rawOptions.queryUrl) {
|
|
22
|
+
overrides.queryUrl = rawOptions.queryUrl;
|
|
23
|
+
}
|
|
24
|
+
if (rawOptions.proxy) {
|
|
25
|
+
overrides.proxy = rawOptions.proxy;
|
|
26
|
+
}
|
|
27
|
+
if (rawOptions.workdir) {
|
|
28
|
+
overrides.workdir = rawOptions.workdir;
|
|
29
|
+
}
|
|
30
|
+
if (rawOptions.storageDir) {
|
|
31
|
+
overrides.storageDir = rawOptions.storageDir;
|
|
32
|
+
}
|
|
33
|
+
if (rawOptions.downloadConcurrency) {
|
|
34
|
+
overrides.downloadConcurrency = Number(rawOptions.downloadConcurrency);
|
|
35
|
+
}
|
|
36
|
+
if (rawOptions.limit) {
|
|
37
|
+
overrides.limit = Number(rawOptions.limit);
|
|
38
|
+
}
|
|
39
|
+
if (rawOptions.offset) {
|
|
40
|
+
overrides.offset = Number(rawOptions.offset);
|
|
41
|
+
}
|
|
42
|
+
if (rawOptions.select) {
|
|
43
|
+
overrides.select = rawOptions.select;
|
|
44
|
+
}
|
|
45
|
+
return createOptions(overrides);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// CLI 默认输出结构化 JSON,便于管道处理和人工查看。
|
|
49
|
+
function printJson(data) {
|
|
50
|
+
console.log(JSON.stringify(data, null, 2));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatUnixTimestamp(value) {
|
|
54
|
+
const numeric = Number(value);
|
|
55
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
56
|
+
return String(value || "");
|
|
57
|
+
}
|
|
58
|
+
return `${numeric} (${new Date(numeric * 1000).toISOString()})`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatObjectTimestamp(value) {
|
|
62
|
+
const numeric = Number(value);
|
|
63
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
64
|
+
return String(value || "");
|
|
65
|
+
}
|
|
66
|
+
const date = new Date(numeric * 1000);
|
|
67
|
+
const year = String(date.getFullYear());
|
|
68
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
69
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
70
|
+
const hour = String(date.getHours()).padStart(2, "0");
|
|
71
|
+
const minute = String(date.getMinutes()).padStart(2, "0");
|
|
72
|
+
const second = String(date.getSeconds()).padStart(2, "0");
|
|
73
|
+
return `${year}-${month}-${day} ${hour}:${minute}:${second} (${numeric})`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function truncateLine(value, maxLength = 200) {
|
|
77
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
78
|
+
if (text.length <= maxLength) {
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
return `${text.slice(0, maxLength)}...`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function printFingerprintDetail(result) {
|
|
85
|
+
console.log(`Fingerprint: ${result.fingerprint}`);
|
|
86
|
+
console.log(`First Seen: ${formatUnixTimestamp(result.firstSeen)}`);
|
|
87
|
+
console.log(`Issue State: ${result.issueState || ""}`);
|
|
88
|
+
console.log(`Classifiers: ${result.classifiers || ""}`);
|
|
89
|
+
console.log(`Object Count: ${result.objectCount}`);
|
|
90
|
+
console.log(`Query Total Rows: ${result.querySummary?.totalRows ?? result.objectCount}`);
|
|
91
|
+
console.log(`Fetched Objects: ${result.querySummary?.fetchedRows ?? result.objectCount}`);
|
|
92
|
+
console.log(`Page Limit: ${result.querySummary?.pageLimit ?? ""}`);
|
|
93
|
+
console.log(`Error Message: ${truncateLine(result.errorMessage, 240)}`);
|
|
94
|
+
console.log("");
|
|
95
|
+
console.log("Objects:");
|
|
96
|
+
|
|
97
|
+
if (!Array.isArray(result.objects) || result.objects.length === 0) {
|
|
98
|
+
console.log("No objects found.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
result.objects.forEach((item, index) => {
|
|
103
|
+
console.log(`${index + 1}. ${item.objectIdHex} ${formatObjectTimestamp(item.timestamp)}`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function printLocalFingerprintList(items) {
|
|
108
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
109
|
+
console.log("No local fingerprints found.");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
items.forEach((item, index) => {
|
|
114
|
+
console.log(`${index + 1}. ${item.fingerprint}`);
|
|
115
|
+
console.log(` State: ${item.issueState || ""}`);
|
|
116
|
+
console.log(` Error: ${truncateLine(item.errorMessage, 200)}`);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 根据解析后的命令分发到对应的 Backtrace 或修复流程。
|
|
121
|
+
async function runCliCommand(rawOptions) {
|
|
122
|
+
if (rawOptions.command === "list") {
|
|
123
|
+
const options = buildBacktraceOverrides(rawOptions, "list");
|
|
124
|
+
const result = await runBacktraceCommand("list", options);
|
|
125
|
+
printLocalFingerprintList(result.items);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (rawOptions.command === "fingerprint") {
|
|
130
|
+
// fingerprint 只查远端并打印指纹列表,不涉及本地分析和报告生成。
|
|
131
|
+
const options = buildBacktraceOverrides(rawOptions, "fingerprint");
|
|
132
|
+
const result = await runBacktraceCommand("fingerprint", options);
|
|
133
|
+
if (result.mode === "detail") {
|
|
134
|
+
printFingerprintDetail(result);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
printList(result.items, {
|
|
138
|
+
from: options.from,
|
|
139
|
+
to: options.to,
|
|
140
|
+
totalRows: result.querySummary.totalRows,
|
|
141
|
+
totalListed: result.querySummary.fetchedRows,
|
|
142
|
+
objectCount: result.querySummary.objectCount,
|
|
143
|
+
pageLimit: result.querySummary.pageLimit,
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (rawOptions.command === "collect") {
|
|
149
|
+
// collect 只做下载,不生成分析报告。
|
|
150
|
+
const options = buildBacktraceOverrides(rawOptions, "collect-all");
|
|
151
|
+
const result = await runBacktraceCommand("collect-all", options);
|
|
152
|
+
printJson({
|
|
153
|
+
command: "collect",
|
|
154
|
+
fingerprint: options.fingerprint || "",
|
|
155
|
+
downloadedCount: result.downloadedItems.length,
|
|
156
|
+
storageDir: options.storageDir,
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (rawOptions.command === "summarize-errors") {
|
|
162
|
+
const options = buildBacktraceOverrides(rawOptions, "summarize-fingerprint-errors");
|
|
163
|
+
const result = await runBacktraceCommand("summarize-fingerprint-errors", options);
|
|
164
|
+
printJson(result);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (rawOptions.command === "fix-plan") {
|
|
169
|
+
if (!rawOptions.fingerprint) {
|
|
170
|
+
throw new Error("--fingerprint is required for fix-plan");
|
|
171
|
+
}
|
|
172
|
+
const result = await generateRepairPlanFromFingerprint({ fingerprint: rawOptions.fingerprint }, rawOptions);
|
|
173
|
+
printJson(result);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (rawOptions.command === "apply-fix") {
|
|
178
|
+
if (!rawOptions.fingerprint) {
|
|
179
|
+
throw new Error("--fingerprint is required for apply-fix");
|
|
180
|
+
}
|
|
181
|
+
const result = await applyFixFromFingerprint({ fingerprint: rawOptions.fingerprint }, rawOptions);
|
|
182
|
+
printJson(result);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
throw new Error("Unsupported command. Use --help to see available commands.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
runCliCommand,
|
|
191
|
+
};
|
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,23 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backtrace-console",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "CLI server manager for backtrace-console — run/start/stop the Express server.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
7
|
-
"bin/backtrace-server.js"
|
|
7
|
+
"bin/backtrace-server.js",
|
|
8
|
+
"bin/www",
|
|
9
|
+
"app.js",
|
|
10
|
+
"lib",
|
|
11
|
+
"public",
|
|
12
|
+
"routes"
|
|
8
13
|
],
|
|
9
14
|
"bin": {
|
|
10
15
|
"backtrace-server": "bin/backtrace-server.js"
|
|
11
16
|
},
|
|
12
17
|
"scripts": {
|
|
13
18
|
"start": "node ./bin/www",
|
|
14
|
-
"backtrace": "node ./bin/backtrace-cli.js",
|
|
15
19
|
"server:run": "node ./bin/backtrace-server.js run",
|
|
16
20
|
"server:start": "node ./bin/backtrace-server.js start",
|
|
17
21
|
"server:stop": "node ./bin/backtrace-server.js stop",
|
|
18
22
|
"server:status": "node ./bin/backtrace-server.js status"
|
|
19
23
|
},
|
|
20
|
-
"
|
|
24
|
+
"dependencies": {
|
|
21
25
|
"@openai/codex-sdk": "^0.128.0",
|
|
22
26
|
"cookie-parser": "~1.4.4",
|
|
23
27
|
"debug": "~2.6.9",
|