backtrace-console 0.0.4 → 0.0.6

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/lib/feishu.js CHANGED
@@ -2,7 +2,8 @@ const https = require('node:https');
2
2
  const http = require('node:http');
3
3
 
4
4
  const TEMPLATE_ID = 'AAqtY8l8Sr3hO';
5
- const TEMPLATE_VERSION = '1.0.3';
5
+ const TEMPLATE_VERSION = '1.0.5';
6
+ const BASE_URL = (process.env.BACKTRACE_CONSOLE_URL || 'http://127.0.0.1:3000').replace(/\/$/, '');
6
7
 
7
8
  function postJson(url, body) {
8
9
  return new Promise(function(resolve, reject) {
@@ -41,7 +42,9 @@ async function sendFeishuCard(webhookUrl, fingerprintData) {
41
42
  fingerprint: fingerprintData.fingerprint || '',
42
43
  errormessage: fingerprintData.errorMessage || '',
43
44
  classifiers: fingerprintData.classifiers || '',
44
- jumpurl: fingerprintData.jumpUrl || ('http://127.0.0.1:3000/chat.html?fingerprint=' + (fingerprintData.fingerprint || '')),
45
+ jumpurl: fingerprintData.jumpUrl || (BASE_URL + '/chat.html?fingerprint=' + (fingerprintData.fingerprint || '')),
46
+ claude_jumpurl: fingerprintData.claude_jumpUrl || (BASE_URL + '/chat-claude.html?fingerprint=' + (fingerprintData.fingerprint || '')),
47
+
45
48
  };
46
49
 
47
50
  var cardContent = {
package/lib/p4ops.js ADDED
@@ -0,0 +1,72 @@
1
+ var { exec } = require('node:child_process');
2
+
3
+ var P4PORT = process.env.P4PORT || '';
4
+ var P4USER = process.env.P4USER || '';
5
+ var P4CLIENT = process.env.P4CLIENT || '';
6
+ var P4_WORKDIR = process.env.BACKTRACE_WORKDIR || process.cwd();
7
+
8
+ function checkConfig() {
9
+ return Boolean(P4PORT && P4USER && P4CLIENT);
10
+ }
11
+
12
+ function buildArgs(cmdParts) {
13
+ var args = [];
14
+ if (P4PORT) { args.push('-p'); args.push(P4PORT); }
15
+ if (P4USER) { args.push('-u'); args.push(P4USER); }
16
+ if (P4CLIENT) { args.push('-c'); args.push(P4CLIENT); }
17
+ return args.concat(cmdParts);
18
+ }
19
+
20
+ function p4exec(args) {
21
+ return new Promise(function(resolve) {
22
+ var childEnv = Object.assign({}, process.env);
23
+ if (process.env.P4PASSWD) childEnv.P4PASSWD = process.env.P4PASSWD;
24
+ var cmd = 'p4 ' + args.join(' ');
25
+ exec(cmd, { cwd: P4_WORKDIR, timeout: 120000, env: childEnv }, function(err, stdout, stderr) {
26
+ if (err) {
27
+ resolve({ ok: false, error: err.message, output: (stderr || '').slice(0, 1000) });
28
+ } else {
29
+ resolve({ ok: true, output: (stdout || '') });
30
+ }
31
+ });
32
+ });
33
+ }
34
+
35
+ // 对指定文件列表运行 reconcile -e,使其在 P4 中标记为 opened for edit
36
+ function p4ReconcileFiles(files) {
37
+ if (!checkConfig()) return Promise.resolve({ ok: true, skipped: true, output: '' });
38
+ if (!files || !files.length) return Promise.resolve({ ok: true, output: '' });
39
+ // Quote each path and pass as separate args
40
+ var quotedFiles = files.map(function(f) { return '"' + String(f).replace(/"/g, "'") + '"'; });
41
+ console.log('[p4ops] reconcile -e ' + files.length + ' file(s)');
42
+ return p4exec(buildArgs(['reconcile', '-e'].concat(quotedFiles)));
43
+ }
44
+
45
+ // files: 本地文件路径数组,仅提交这些文件(p4 submit -d "desc" file1 file2 ...)
46
+ function p4Submit(description, files) {
47
+ if (!checkConfig()) return Promise.resolve({ ok: false, error: '未配置 P4PORT/P4USER/P4CLIENT' });
48
+ if (!description) return Promise.resolve({ ok: false, error: 'description is required' });
49
+ var safeDesc = String(description)
50
+ .replace(/[&|<>^]/g, '')
51
+ .replace(/"/g, "'")
52
+ .replace(/[\r\n]+/g, ' - ')
53
+ .trim()
54
+ .slice(0, 200);
55
+ var cmdParts = ['submit', '-d', '"' + safeDesc + '"'];
56
+ if (files && files.length) {
57
+ var quotedFiles = files.map(function(f) { return '"' + String(f).replace(/"/g, "'") + '"'; });
58
+ cmdParts = cmdParts.concat(quotedFiles);
59
+ console.log('[p4ops] submit -d "' + safeDesc + '" (' + files.length + ' 个文件)');
60
+ } else {
61
+ console.log('[p4ops] submit -d "' + safeDesc + '" (全部 opened)');
62
+ }
63
+ return p4exec(buildArgs(cmdParts)).then(function(result) {
64
+ if (result.ok) {
65
+ var m = (result.output || '').match(/Change\s+(\d+)\s+submitted/i);
66
+ result.changelistId = m ? m[1] : null;
67
+ }
68
+ return result;
69
+ });
70
+ }
71
+
72
+ module.exports = { p4ReconcileFiles, p4Submit, checkConfig };
package/lib/p4sync.js ADDED
@@ -0,0 +1,35 @@
1
+ var { exec } = require('node:child_process');
2
+
3
+ var P4PORT = process.env.P4PORT || '';
4
+ var P4USER = process.env.P4USER || '';
5
+ var P4CLIENT = process.env.P4CLIENT || '';
6
+ var P4_WORKDIR = process.env.BACKTRACE_WORKDIR || process.cwd();
7
+
8
+ async function runP4Sync() {
9
+ if (!P4PORT || !P4USER || !P4CLIENT) {
10
+ console.log('[p4sync] 未配置 P4PORT/P4USER/P4CLIENT,跳过同步');
11
+ return { ok: true, skipped: true };
12
+ }
13
+
14
+ var args = ['-p', P4PORT, '-u', P4USER, '-c', P4CLIENT, 'sync', '...'];
15
+ var cmd = 'p4 ' + args.join(' ');
16
+ console.log('[p4sync] 执行: ' + cmd + ' (cwd=' + P4_WORKDIR + ')');
17
+
18
+ return new Promise(function(resolve) {
19
+ var childEnv = Object.assign({}, process.env);
20
+ if (process.env.P4PASSWD) childEnv.P4PASSWD = process.env.P4PASSWD;
21
+ exec(cmd, { cwd: P4_WORKDIR, timeout: 120000, env: childEnv }, function(err, stdout, stderr) {
22
+ if (err) {
23
+ console.error('[p4sync] 同步失败:', err.message);
24
+ if (stderr) console.error('[p4sync] stderr:', stderr.slice(0, 500));
25
+ resolve({ ok: false, error: err.message });
26
+ } else {
27
+ console.log('[p4sync] 同步成功');
28
+ if (stdout) console.log('[p4sync] 输出:', stdout.slice(0, 500));
29
+ resolve({ ok: true });
30
+ }
31
+ });
32
+ });
33
+ }
34
+
35
+ module.exports = { runP4Sync };
package/lib/scheduler.js CHANGED
@@ -1,11 +1,28 @@
1
1
  var fs = require('node:fs/promises');
2
+ var path = require('node:path');
2
3
  var cron = require('node-cron');
3
4
  var { BacktraceCodexTool } = require('./backtrace/tool');
4
5
  var { sendFeishuCard } = require('./feishu');
5
6
  var { readFingerprintMeta, writeFingerprintMeta, FINGERPRINTS_ROOT } = require('../routes/backtrace-shared');
7
+ var { runP4Sync } = require('./p4sync');
6
8
 
7
9
  var SCHEDULER_CRON = process.env.SCHEDULER_CRON || '*/5 * * * *';
8
10
  var FEISHU_WEBHOOK_URL = process.env.FEISHU_WEBHOOK_URL || '';
11
+ var STATE_FILE = path.join(FINGERPRINTS_ROOT, '.scheduler-state.json');
12
+
13
+ async function readSchedulerState() {
14
+ try {
15
+ var raw = await fs.readFile(STATE_FILE, 'utf8');
16
+ return JSON.parse(raw) || {};
17
+ } catch (e) {
18
+ return {};
19
+ }
20
+ }
21
+
22
+ async function writeSchedulerState(state) {
23
+ await fs.mkdir(FINGERPRINTS_ROOT, { recursive: true }).catch(function() {});
24
+ await fs.writeFile(STATE_FILE, JSON.stringify(state, null, 2), 'utf8');
25
+ }
9
26
 
10
27
  // 从 cron 表达式推算间隔秒数,用于首次执行时确定查询起点。
11
28
  // 支持常见格式:*/N(每N分钟)、单个数字(每小时/天固定时刻)。
@@ -34,82 +51,111 @@ function cronToIntervalSeconds(cronExpr) {
34
51
  return 300;
35
52
  }
36
53
 
54
+ async function listLocalFingerprintNames() {
55
+ var entries = await fs.readdir(FINGERPRINTS_ROOT, { withFileTypes: true }).catch(function() { return []; });
56
+ return entries
57
+ .filter(function(e) { return e.isDirectory() && !e.name.startsWith('.'); })
58
+ .map(function(e) { return e.name; });
59
+ }
60
+
37
61
  async function runScheduledTask() {
38
62
  var now = Math.floor(Date.now() / 1000);
39
63
  var intervalSeconds = cronToIntervalSeconds(SCHEDULER_CRON);
40
- var from = String(now - intervalSeconds);
41
64
 
42
- console.log('[scheduler] 开始执行,时间范围 from=' + from + ' to=' + now);
65
+ // 读取上次成功查询的截止时间,用于失败恢复
66
+ // - 上次成功:from = lastSuccessTo(连续覆盖)
67
+ // - 上次失败 / 首次:from = now - intervalSeconds(默认窗口)
68
+ var state = await readSchedulerState();
69
+ var defaultFrom = now - intervalSeconds;
70
+ var from = (typeof state.lastSuccessTo === 'number' && state.lastSuccessTo > 0)
71
+ ? state.lastSuccessTo
72
+ : defaultFrom;
73
+
74
+ // 防御性下限:不允许窗口跨度超过 24 小时,避免长时间故障后一次拉取过量数据
75
+ var MAX_LOOKBACK = 24 * 3600;
76
+ if (now - from > MAX_LOOKBACK) {
77
+ console.warn('[scheduler] 上次成功时间过早 (' + (now - from) + 's),截断到 ' + MAX_LOOKBACK + 's');
78
+ from = now - MAX_LOOKBACK;
79
+ }
43
80
 
44
- var tool = new BacktraceCodexTool();
81
+ console.log('[scheduler] 开始执行,时间范围 from=' + from + ' to=' + now + '(跨度 ' + (now - from) + 's)');
82
+
83
+ // 0. P4 同步:拉取最新代码,保证后续分析基于最新源码
84
+ await runP4Sync().catch(function(err) {
85
+ console.error('[scheduler] p4sync 异常:', err.message);
86
+ });
45
87
 
46
- // 1. 查询远端新增 fingerprint
47
- var remoteFingerprints = [];
88
+ // 1. 拉取远端前,先记录当前本地已有的 fingerprint 目录(用于区分"远端新增"与"本地补推")
89
+ var existingLocal = new Set(await listLocalFingerprintNames());
90
+
91
+ // 2. 查询远端 fingerprint(tool.fingerprint 内部会把新结果写到本地目录)
92
+ var tool = new BacktraceCodexTool();
93
+ var remoteOk = false;
48
94
  try {
49
95
  var result = await tool.fingerprint({ from: String(from), to: String(now) });
50
- remoteFingerprints = (result.items || []).map(function(item) {
96
+ var remoteFingerprints = (result.items || []).map(function(item) {
51
97
  return item.fingerprint || (item.values && item.values.fingerprint) || '';
52
98
  }).filter(Boolean);
53
99
  console.log('[scheduler] 远端查询到 ' + remoteFingerprints.length + ' 个 fingerprint');
100
+ remoteOk = true;
54
101
  } catch (err) {
55
- console.error('[scheduler] 查询 fingerprint 失败:', err.message);
102
+ console.error('[scheduler] 查询 fingerprint 失败:', err.message + '(下次将从 from=' + from + ' 继续补查)');
103
+ // 远端失败不影响本地补推,继续扫描;不更新 lastSuccessTo
56
104
  }
57
105
 
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)));
106
+ // 远端查询成功才推进 lastSuccessTo,下次窗口从 now 起;失败则保留旧值
107
+ if (remoteOk) {
108
+ state.lastSuccessTo = now;
109
+ state.lastSuccessAt = new Date().toISOString();
110
+ await writeSchedulerState(state).catch(function(err) {
111
+ console.error('[scheduler] 写入 scheduler-state 失败:', err.message);
112
+ });
113
+ }
66
114
 
115
+ // 3. 扫描本地全部目录,找出尚未推送过(meta.feishuNotifiedAt 缺失)的
116
+ var allLocal = await listLocalFingerprintNames();
67
117
  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 });
118
+ for (var i = 0; i < allLocal.length; i++) {
119
+ var fpName = allLocal[i];
120
+ var localMeta = await readFingerprintMeta(fpName).catch(function() { return null; });
121
+ if (!localMeta || localMeta.feishuNotifiedAt) continue;
122
+ pendingItems.push({
123
+ fp: fpName,
124
+ meta: localMeta,
125
+ isNewRemote: !existingLocal.has(fpName),
126
+ });
73
127
  }
74
128
 
75
- console.log('[scheduler] 待推送 fingerprint: ' + pendingItems.length + ' 个');
129
+ var newCount = pendingItems.filter(function(it) { return it.isNewRemote; }).length;
130
+ var localCount = pendingItems.length - newCount;
131
+ console.log('[scheduler] 待推送 ' + pendingItems.length + ' 个(远端新增 ' + newCount + ',本地补推 ' + localCount + ')');
76
132
 
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
- }
133
+ if (pendingItems.length === 0) return;
134
+
135
+ if (!FEISHU_WEBHOOK_URL) {
136
+ console.warn('[scheduler] FEISHU_WEBHOOK_URL 未配置,跳过推送');
137
+ return;
102
138
  }
103
139
 
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);
140
+ for (var j = 0; j < pendingItems.length; j++) {
141
+ var entry = pendingItems[j];
142
+ var sendResult = await sendFeishuCard(FEISHU_WEBHOOK_URL, {
143
+ fingerprint: entry.fp,
144
+ errorMessage: entry.meta.errorMessage || '',
145
+ classifiers: entry.meta.classifiers || '',
146
+ }).catch(function(err) {
147
+ return { ok: false, error: err.message };
148
+ });
149
+
150
+ var tag = entry.isNewRemote ? '[远端新增]' : '[本地补推]';
151
+ if (sendResult.ok) {
152
+ var updated = Object.assign({}, entry.meta, { feishuNotifiedAt: new Date().toISOString() });
153
+ await writeFingerprintMeta(entry.fp, updated).catch(function(err) {
154
+ console.error('[scheduler] 写入 meta.json 失败 ' + entry.fp + ':', err.message);
155
+ });
156
+ console.log('[scheduler] ' + tag + ' 推送成功: ' + entry.fp);
157
+ } else {
158
+ console.error('[scheduler] ' + tag + ' 推送失败: ' + entry.fp, sendResult.body || sendResult.error);
113
159
  }
114
160
  }
115
161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtrace-console",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "CLI server manager for backtrace-console — run/start/stop the Express server.",
5
5
  "private": false,
6
6
  "files": [
@@ -22,6 +22,7 @@
22
22
  "server:status": "node ./bin/backtrace-server.js status"
23
23
  },
24
24
  "dependencies": {
25
+ "@anthropic-ai/claude-agent-sdk": "^0.2.133",
25
26
  "@openai/codex-sdk": "^0.128.0",
26
27
  "cookie-parser": "~1.4.4",
27
28
  "debug": "~2.6.9",