lumencode 1.3.2 → 1.3.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/README.md +12 -0
- package/lib/attribution.js +8 -0
- package/lib/git.js +195 -192
- package/lib/report.js +5 -1
- package/lib/server.js +361 -29
- package/lib/smart-report-store.js +98 -0
- package/lib/smart-report.js +339 -0
- package/package.json +1 -1
- package/public/api.js +25 -0
- package/public/app.js +1483 -1167
- package/public/config.js +4 -0
- package/public/index.html +86 -3
- package/public/style.css +202 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
const AGENTS = {
|
|
4
|
+
claude: {
|
|
5
|
+
name: 'claude',
|
|
6
|
+
displayName: 'Claude Code',
|
|
7
|
+
command: 'claude',
|
|
8
|
+
args: ['--print'],
|
|
9
|
+
},
|
|
10
|
+
codex: {
|
|
11
|
+
name: 'codex',
|
|
12
|
+
displayName: 'Codex',
|
|
13
|
+
command: 'codex',
|
|
14
|
+
args: ['exec', '-'],
|
|
15
|
+
},
|
|
16
|
+
opencode: {
|
|
17
|
+
name: 'opencode',
|
|
18
|
+
displayName: 'OpenCode',
|
|
19
|
+
command: 'opencode',
|
|
20
|
+
args: ['run', '-'],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const RUN_TIMEOUT_MS = 10 * 60_000;
|
|
25
|
+
const MAX_OUTPUT_BYTES = 512 * 1024;
|
|
26
|
+
const MAX_MARKDOWN_CHARS = 60_000;
|
|
27
|
+
const MAX_SOURCE_REPORT_CHARS = 35_000;
|
|
28
|
+
export const SMART_REPORT_PROMPT_MARKER = 'LUMENCODE_SMART_REPORT_INTERNAL_TASK';
|
|
29
|
+
|
|
30
|
+
function quoteCmdArg(value) {
|
|
31
|
+
return `"${String(value).replace(/"/g, '\\"')}"`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildAgentSpawnInvocation(definition, args, platform = process.platform) {
|
|
35
|
+
if (platform === 'win32') {
|
|
36
|
+
return {
|
|
37
|
+
command: 'cmd.exe',
|
|
38
|
+
args: ['/d', '/s', '/c', [definition.command, ...args.map(quoteCmdArg)].join(' ')],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { command: definition.command, args };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildAgentLookupInvocation(definition, platform = process.platform) {
|
|
45
|
+
if (platform === 'win32') {
|
|
46
|
+
return { command: 'where.exe', args: [definition.command] };
|
|
47
|
+
}
|
|
48
|
+
return { command: 'sh', args: ['-lc', `command -v ${definition.command}`] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pickObject(source, keys) {
|
|
52
|
+
const out = {};
|
|
53
|
+
if (!source || typeof source !== 'object') return out;
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
if (source[key] !== undefined) out[key] = source[key];
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mapValues(source, mapper, limit = 50) {
|
|
61
|
+
const out = {};
|
|
62
|
+
if (!source || typeof source !== 'object') return out;
|
|
63
|
+
for (const [key, value] of Object.entries(source).slice(0, limit)) {
|
|
64
|
+
out[key] = mapper(value);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function compactCommit(commit) {
|
|
70
|
+
return pickObject(commit, [
|
|
71
|
+
'subject',
|
|
72
|
+
'type',
|
|
73
|
+
'scope',
|
|
74
|
+
'repo',
|
|
75
|
+
'date',
|
|
76
|
+
'isAI',
|
|
77
|
+
'aiConfidence',
|
|
78
|
+
'attributionType',
|
|
79
|
+
'linesAdded',
|
|
80
|
+
'linesDeleted',
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function compactSourceReports(sourceReports) {
|
|
85
|
+
const out = {};
|
|
86
|
+
if (!sourceReports || typeof sourceReports !== 'object') return out;
|
|
87
|
+
for (const key of ['detailedMarkdown', 'briefMarkdown', 'bossMarkdown']) {
|
|
88
|
+
if (sourceReports[key]) out[key] = String(sourceReports[key]).slice(0, MAX_SOURCE_REPORT_CHARS);
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getAgentDefinition(agent) {
|
|
94
|
+
return AGENTS[String(agent || '').toLowerCase()] || null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function listSmartReportAgents() {
|
|
98
|
+
return Object.values(AGENTS).map(({ name, displayName }) => ({ name, displayName }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function detectSmartReportAgents(checker = checkAgentAvailable) {
|
|
102
|
+
const results = [];
|
|
103
|
+
for (const definition of Object.values(AGENTS)) {
|
|
104
|
+
const status = await checker(definition);
|
|
105
|
+
results.push({
|
|
106
|
+
name: definition.name,
|
|
107
|
+
displayName: definition.displayName,
|
|
108
|
+
command: definition.command,
|
|
109
|
+
detected: status.detected,
|
|
110
|
+
version: status.version || '',
|
|
111
|
+
error: status.error || '',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function checkAgentAvailable(definition) {
|
|
118
|
+
return new Promise(resolve => {
|
|
119
|
+
const invocation = buildAgentLookupInvocation(definition);
|
|
120
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
121
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
shell: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
let stdout = '';
|
|
127
|
+
let stderr = '';
|
|
128
|
+
const timer = setTimeout(() => {
|
|
129
|
+
child.kill('SIGTERM');
|
|
130
|
+
resolve({ detected: false, error: 'version check timeout' });
|
|
131
|
+
}, 5_000);
|
|
132
|
+
|
|
133
|
+
child.stdout.on('data', chunk => { stdout += chunk.toString('utf8'); });
|
|
134
|
+
child.stderr.on('data', chunk => { stderr += chunk.toString('utf8'); });
|
|
135
|
+
child.on('error', err => {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
resolve({ detected: false, error: err.message });
|
|
138
|
+
});
|
|
139
|
+
child.on('close', code => {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
if (code === 0) {
|
|
142
|
+
resolve({ detected: true, version: stdout.trim().split('\n')[0] || '' });
|
|
143
|
+
} else {
|
|
144
|
+
resolve({ detected: false, error: stderr.trim() || `exit ${code}` });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function buildSmartReportContext(reportData, workMarkdown, options = {}) {
|
|
151
|
+
const usage = reportData?.usageStats || {};
|
|
152
|
+
const git = reportData?.gitStats || {};
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
meta: {
|
|
156
|
+
period: options.period || '',
|
|
157
|
+
date: options.date || '',
|
|
158
|
+
tool: options.tool || 'all',
|
|
159
|
+
project: options.project || '',
|
|
160
|
+
level: options.level || 'detailed',
|
|
161
|
+
style: options.style || 'default',
|
|
162
|
+
platform: options.platform || 'default',
|
|
163
|
+
start: reportData?.start || '',
|
|
164
|
+
end: reportData?.end || '',
|
|
165
|
+
},
|
|
166
|
+
usage: {
|
|
167
|
+
...pickObject(usage, [
|
|
168
|
+
'requestCount',
|
|
169
|
+
'sessionCount',
|
|
170
|
+
'userMessageCount',
|
|
171
|
+
'activeDays',
|
|
172
|
+
'totalTokens',
|
|
173
|
+
'inputTokens',
|
|
174
|
+
'outputTokens',
|
|
175
|
+
'cacheRead',
|
|
176
|
+
'cacheCreate',
|
|
177
|
+
'estimatedCost',
|
|
178
|
+
'subagentTokens',
|
|
179
|
+
]),
|
|
180
|
+
projects: mapValues(usage.projects, value => pickObject(value, ['requests', 'sessions', 'totalTokens', 'cost'])),
|
|
181
|
+
models: mapValues(usage.models, value => pickObject(value, ['count', 'inputTokens', 'outputTokens', 'cost', 'costMode'])),
|
|
182
|
+
scenarios: pickObject(usage.scenarios, Object.keys(usage.scenarios || {})),
|
|
183
|
+
tools: mapValues(usage.tools, value => pickObject(value, ['calls', 'uses'])),
|
|
184
|
+
skills: mapValues(usage.skills, value => pickObject(value, ['calls', 'uses'])),
|
|
185
|
+
mcpTools: mapValues(usage.mcpTools, value => pickObject(value, ['calls', 'uses'])),
|
|
186
|
+
},
|
|
187
|
+
git: {
|
|
188
|
+
...pickObject(git, [
|
|
189
|
+
'commits',
|
|
190
|
+
'filesChanged',
|
|
191
|
+
'linesAdded',
|
|
192
|
+
'linesDeleted',
|
|
193
|
+
'commitTypes',
|
|
194
|
+
'fileHotspots',
|
|
195
|
+
'aiContribution',
|
|
196
|
+
'attributionSummary',
|
|
197
|
+
]),
|
|
198
|
+
commitList: Array.isArray(git.commitList) ? git.commitList.slice(0, 30).map(compactCommit) : [],
|
|
199
|
+
},
|
|
200
|
+
trend: {
|
|
201
|
+
dailyStats: reportData?.trendData?.dailyStats || {},
|
|
202
|
+
},
|
|
203
|
+
costBreakdown: reportData?.costBreakdown || null,
|
|
204
|
+
workReportMarkdown: String(workMarkdown || '').slice(0, MAX_MARKDOWN_CHARS),
|
|
205
|
+
sourceReports: compactSourceReports(options.sourceReports),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function buildSmartReportPrompt(context) {
|
|
210
|
+
const level = context?.meta?.level || 'detailed';
|
|
211
|
+
const style = context?.meta?.style || 'default';
|
|
212
|
+
const levelInstruction = {
|
|
213
|
+
brief: [
|
|
214
|
+
'当前生成模式:AI 简报。',
|
|
215
|
+
'- 必须同时参考 detailedMarkdown 和 briefMarkdown;briefMarkdown 用于把握原简报口径,detailedMarkdown 用于补充关键依据。',
|
|
216
|
+
'- 输出应简短、聚焦,保留“数据摘要 / 工作亮点分析 / 关键洞察 / 风险与建议”。',
|
|
217
|
+
'- 不要展开成长篇详细报告。',
|
|
218
|
+
].join('\n'),
|
|
219
|
+
detailed: [
|
|
220
|
+
'当前生成模式:AI 详细报告。',
|
|
221
|
+
'- 以 detailedMarkdown 和结构化统计数据为主要依据。',
|
|
222
|
+
'- 输出可以包含完整章节,但结论必须能被输入数据支撑。',
|
|
223
|
+
].join('\n'),
|
|
224
|
+
}[level] || '当前生成模式:AI 详细报告。';
|
|
225
|
+
const styleInstruction = style === 'workhorse'
|
|
226
|
+
? [
|
|
227
|
+
'当前生成风格:牛马。',
|
|
228
|
+
'- 采用面向领导汇报的表达倾向,突出“做了什么、产出价值、工作投入、风险兜底、下一步计划”。',
|
|
229
|
+
'- 必须结合 detailedMarkdown 与 bossMarkdown;bossMarkdown 只作为领导汇报口径参考,不得突破数据边界。',
|
|
230
|
+
'- 可以让语言更适合向上汇报,但不得编造业务收益、ROI、节省时长或人员评价。',
|
|
231
|
+
].join('\n')
|
|
232
|
+
: [
|
|
233
|
+
'当前生成风格:默认风格。',
|
|
234
|
+
'- 采用专业、克制的分析报告口径,重点解释数据变化、工作亮点、风险和建议。',
|
|
235
|
+
].join('\n');
|
|
236
|
+
const sections = level === 'brief'
|
|
237
|
+
? '## 数据摘要\n## 工作亮点分析\n## 关键洞察\n## 风险与建议'
|
|
238
|
+
: '## 数据摘要\n## 工作亮点分析\n## 关键洞察\n## 异常与风险\n## 管理建议\n## 下一步关注点';
|
|
239
|
+
|
|
240
|
+
return [
|
|
241
|
+
SMART_REPORT_PROMPT_MARKER,
|
|
242
|
+
'',
|
|
243
|
+
'你是 LumenCode 的智能报告分析器。只能基于下面提供的数据和确定性工作汇报进行分析。',
|
|
244
|
+
'',
|
|
245
|
+
'边界要求:',
|
|
246
|
+
'- 只能引用输入数据中存在的统计事实,不得编造业务成果、节省时长、ROI 或人员评价。',
|
|
247
|
+
'- 不得读取源码、不得读取本地文件、不得执行命令、不得联网、不得请求更多上下文。',
|
|
248
|
+
'- 如果数据无法支持结论,必须明确写“数据不足”。',
|
|
249
|
+
'- 可以做趋势解释、异常识别、成本/Token/活跃度/项目分布/AI 贡献率分析。',
|
|
250
|
+
'- 输出 Markdown,语言为简体中文,面向管理者和项目负责人。',
|
|
251
|
+
'',
|
|
252
|
+
levelInstruction,
|
|
253
|
+
'',
|
|
254
|
+
styleInstruction,
|
|
255
|
+
'',
|
|
256
|
+
'建议章节:',
|
|
257
|
+
sections,
|
|
258
|
+
'',
|
|
259
|
+
'上下文 JSON:',
|
|
260
|
+
JSON.stringify(context, null, 2),
|
|
261
|
+
].join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function runSmartReportAgent(definition, prompt, options = {}) {
|
|
265
|
+
return new Promise((resolve, reject) => {
|
|
266
|
+
const timeoutMs = options.timeoutMs || RUN_TIMEOUT_MS;
|
|
267
|
+
const invocation = buildAgentSpawnInvocation(definition, definition.args);
|
|
268
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
269
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
270
|
+
windowsHide: true,
|
|
271
|
+
shell: false,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
let stdout = '';
|
|
275
|
+
let stderr = '';
|
|
276
|
+
let finished = false;
|
|
277
|
+
const timer = setTimeout(() => {
|
|
278
|
+
if (finished) return;
|
|
279
|
+
finished = true;
|
|
280
|
+
child.kill('SIGTERM');
|
|
281
|
+
reject(new Error(`${definition.displayName} 生成超时`));
|
|
282
|
+
}, timeoutMs);
|
|
283
|
+
|
|
284
|
+
child.stdout.on('data', chunk => {
|
|
285
|
+
stdout += chunk.toString('utf8');
|
|
286
|
+
if (Buffer.byteLength(stdout, 'utf8') > MAX_OUTPUT_BYTES) {
|
|
287
|
+
child.kill('SIGTERM');
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
child.stderr.on('data', chunk => {
|
|
292
|
+
stderr += chunk.toString('utf8');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
child.on('error', err => {
|
|
296
|
+
if (finished) return;
|
|
297
|
+
finished = true;
|
|
298
|
+
clearTimeout(timer);
|
|
299
|
+
reject(new Error(`无法启动 ${definition.displayName}: ${err.message}`));
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
child.on('close', code => {
|
|
303
|
+
if (finished) return;
|
|
304
|
+
finished = true;
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
if (code !== 0) {
|
|
307
|
+
reject(new Error(`${definition.displayName} 生成失败: ${stderr.trim() || `exit ${code}`}`));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
resolve(stdout.trim());
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
child.stdin.end(prompt);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function createSmartReport({
|
|
318
|
+
agent,
|
|
319
|
+
reportData,
|
|
320
|
+
workMarkdown,
|
|
321
|
+
options = {},
|
|
322
|
+
runner = runSmartReportAgent,
|
|
323
|
+
requireAvailable = false,
|
|
324
|
+
availabilityChecker = checkAgentAvailable,
|
|
325
|
+
}) {
|
|
326
|
+
const definition = getAgentDefinition(agent);
|
|
327
|
+
if (!definition) throw new Error('Unsupported smart report agent');
|
|
328
|
+
|
|
329
|
+
if (requireAvailable) {
|
|
330
|
+
const status = await availabilityChecker(definition);
|
|
331
|
+
if (!status.detected) {
|
|
332
|
+
throw new Error(`${definition.displayName} 未检测到,请确认已安装并且命令 ${definition.command} 在当前终端 PATH 中可用。${status.error ? `详情: ${status.error}` : ''}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const context = buildSmartReportContext(reportData, workMarkdown, options);
|
|
337
|
+
const prompt = buildSmartReportPrompt(context);
|
|
338
|
+
return runner(definition, prompt);
|
|
339
|
+
}
|
package/package.json
CHANGED
package/public/api.js
CHANGED
|
@@ -128,6 +128,31 @@ export async function updateHooks(action, tools = ['claude', 'codex', 'opencode'
|
|
|
128
128
|
return data;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
export async function fetchSmartReportTools() {
|
|
132
|
+
const res = await fetch(API.SMART_REPORT_TOOLS);
|
|
133
|
+
if (!res.ok) throw new Error('获取智能体工具失败');
|
|
134
|
+
return res.json();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function fetchSmartReportRecord(params) {
|
|
138
|
+
const qs = new URLSearchParams(params).toString();
|
|
139
|
+
const res = await fetch(`${API.SMART_REPORT}?${qs}`);
|
|
140
|
+
const data = await res.json().catch(() => ({}));
|
|
141
|
+
if (!res.ok) throw new Error(data.error || '获取智能报告记录失败');
|
|
142
|
+
return data;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function generateSmartReport(payload) {
|
|
146
|
+
const res = await fetch(API.SMART_REPORT, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: JSON.stringify(payload),
|
|
150
|
+
});
|
|
151
|
+
const data = await res.json().catch(() => ({}));
|
|
152
|
+
if (!res.ok) throw new Error(data.error || '智能报告生成失败');
|
|
153
|
+
return data;
|
|
154
|
+
}
|
|
155
|
+
|
|
131
156
|
export async function fetchWorkReport(params) {
|
|
132
157
|
const qs = new URLSearchParams(params).toString();
|
|
133
158
|
const res = await fetch(`${API.REPORT}?${qs}&format=work`);
|