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.
@@ -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,
@@ -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
- const meta = {
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 };
@@ -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.1",
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.118.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
  }