lumencode 1.3.1 → 1.3.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/index.js +348 -343
- package/lib/aggregate.js +58 -3
- package/lib/config.js +21 -8
- package/lib/path-utils.js +18 -0
- package/lib/report.js +969 -53
- package/lib/scenario.js +29 -4
- package/lib/server.js +331 -316
- package/package.json +1 -1
- package/public/app.js +232 -17
- package/public/config.js +1 -0
- package/public/export.js +11 -7
- package/public/index.html +77 -16
- package/public/style.css +248 -1
- package/public/utils.js +218 -0
package/lib/server.js
CHANGED
|
@@ -3,16 +3,18 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
3
3
|
import { join, extname, resolve, sep } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { saveConfig } from './config.js';
|
|
6
|
-
import {
|
|
6
|
+
import { parseRepoPaths } from './path-utils.js';
|
|
7
|
+
import { generateWorkReport, generateFeishuCard, generateBossReport } from './report.js';
|
|
8
|
+
import { classifyRecord } from './scenario.js';
|
|
7
9
|
import { collectAllRecords, filterRecordsByPeriod, groupBySessions, computeUsageStats, computeTrendData, computePrevPeriodRange } from './aggregate.js';
|
|
8
10
|
import { normalizeProjectPath } from './aggregate.js';
|
|
9
11
|
import { invalidateFileCache } from './cache.js';
|
|
10
|
-
import { invalidateGitCache, getGitStatsForMultipleReposAsync, finalizeGitStats, computeAIContribution, computeCommitTypes, computeFileHotspots } from './git.js';
|
|
12
|
+
import { invalidateGitCache, getGitStatsForMultipleReposAsync, finalizeGitStats, computeAIContribution, computeCommitTypes, computeFileHotspots } from './git.js';
|
|
11
13
|
import { identifyBillingBlocks } from './blocks.js';
|
|
12
|
-
import { detectAvailableTools, parseAllEnabledTools } from './parsers/index.js';
|
|
13
|
-
import { isAssistantRecord, getInputTokens, getOutputTokens } from './record-utils.js';
|
|
14
|
-
import { StepTracker } from './step-tracker.js';
|
|
15
|
-
import { disableHooks, enableHooks, getHooksStatus, HOOK_TOOLS, initStepTracking } from './hooks-manager.js';
|
|
14
|
+
import { detectAvailableTools, parseAllEnabledTools } from './parsers/index.js';
|
|
15
|
+
import { isAssistantRecord, getInputTokens, getOutputTokens } from './record-utils.js';
|
|
16
|
+
import { StepTracker } from './step-tracker.js';
|
|
17
|
+
import { disableHooks, enableHooks, getHooksStatus, HOOK_TOOLS, initStepTracking } from './hooks-manager.js';
|
|
16
18
|
|
|
17
19
|
// basename 提取,兼容不同路径格式
|
|
18
20
|
function getProjectBaseName(p) {
|
|
@@ -39,42 +41,42 @@ const MIME = {
|
|
|
39
41
|
'.ico': 'image/x-icon',
|
|
40
42
|
};
|
|
41
43
|
|
|
42
|
-
export function startServer(config, effectiveIncludeProjects, buildReportData, configPath) {
|
|
43
|
-
function computeIncludeProjects(cfg) {
|
|
44
|
-
if (cfg.repos && cfg.repos.length > 0) {
|
|
45
|
-
return cfg.repos.map(r => normalizeProjectPath(r));
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getHookProjectRoots(cfg) {
|
|
51
|
-
if (!Array.isArray(cfg.repos)) return [];
|
|
52
|
-
return [...new Set(cfg.repos.map(r => normalizeProjectPath(String(r || '').trim())).filter(Boolean))];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function getConfiguredHooksStatus(cfg) {
|
|
56
|
-
const projectRoots = getHookProjectRoots(cfg);
|
|
57
|
-
const projects = projectRoots.map(root => getHooksStatus(root));
|
|
58
|
-
const total = projects.length;
|
|
59
|
-
const enabledCount = (tool) => projects.filter(p => p[tool]?.enabled).length;
|
|
60
|
-
const stepsReadyCount = projects.filter(p => p.stepsInitialized).length;
|
|
61
|
-
const toolStatus = (tool) => ({
|
|
62
|
-
enabled: total > 0 && enabledCount(tool) === total,
|
|
63
|
-
enabledCount: enabledCount(tool),
|
|
64
|
-
total,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
targetMode: 'configured-projects',
|
|
69
|
-
projectCount: total,
|
|
70
|
-
projects,
|
|
71
|
-
stepsInitialized: total > 0 && stepsReadyCount === total,
|
|
72
|
-
stepsReadyCount,
|
|
73
|
-
claude: toolStatus(HOOK_TOOLS.CLAUDE),
|
|
74
|
-
codex: toolStatus(HOOK_TOOLS.CODEX),
|
|
75
|
-
opencode: toolStatus(HOOK_TOOLS.OPENCODE),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
44
|
+
export function startServer(config, effectiveIncludeProjects, buildReportData, configPath) {
|
|
45
|
+
function computeIncludeProjects(cfg) {
|
|
46
|
+
if (cfg.repos && cfg.repos.length > 0) {
|
|
47
|
+
return cfg.repos.map(r => normalizeProjectPath(r));
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getHookProjectRoots(cfg) {
|
|
53
|
+
if (!Array.isArray(cfg.repos)) return [];
|
|
54
|
+
return [...new Set(cfg.repos.map(r => normalizeProjectPath(String(r || '').trim())).filter(Boolean))];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getConfiguredHooksStatus(cfg) {
|
|
58
|
+
const projectRoots = getHookProjectRoots(cfg);
|
|
59
|
+
const projects = projectRoots.map(root => getHooksStatus(root));
|
|
60
|
+
const total = projects.length;
|
|
61
|
+
const enabledCount = (tool) => projects.filter(p => p[tool]?.enabled).length;
|
|
62
|
+
const stepsReadyCount = projects.filter(p => p.stepsInitialized).length;
|
|
63
|
+
const toolStatus = (tool) => ({
|
|
64
|
+
enabled: total > 0 && enabledCount(tool) === total,
|
|
65
|
+
enabledCount: enabledCount(tool),
|
|
66
|
+
total,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
targetMode: 'configured-projects',
|
|
71
|
+
projectCount: total,
|
|
72
|
+
projects,
|
|
73
|
+
stepsInitialized: total > 0 && stepsReadyCount === total,
|
|
74
|
+
stepsReadyCount,
|
|
75
|
+
claude: toolStatus(HOOK_TOOLS.CLAUDE),
|
|
76
|
+
codex: toolStatus(HOOK_TOOLS.CODEX),
|
|
77
|
+
opencode: toolStatus(HOOK_TOOLS.OPENCODE),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
78
80
|
|
|
79
81
|
const PORT = process.env.LUMENCODE_PORT || 4567;
|
|
80
82
|
|
|
@@ -89,9 +91,9 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
89
91
|
const REPORT_CACHE_TTL = 30_000; // 30s
|
|
90
92
|
const REPORT_CACHE_MAX_SIZE = 50;
|
|
91
93
|
|
|
92
|
-
function getReportCacheKey(period, date, tool, customStart, customEnd) {
|
|
93
|
-
return `${period}|${date}|${tool || 'all'}|${customStart || ''}|${customEnd || ''}`;
|
|
94
|
-
}
|
|
94
|
+
function getReportCacheKey(period, date, tool, customStart, customEnd) {
|
|
95
|
+
return `${period}|${date}|${tool || 'all'}|${customStart || ''}|${customEnd || ''}`;
|
|
96
|
+
}
|
|
95
97
|
|
|
96
98
|
function getCachedReport(cacheKey) {
|
|
97
99
|
const cached = _reportCache.get(cacheKey);
|
|
@@ -109,55 +111,55 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
function invalidateReportCache() {
|
|
113
|
-
_reportCache.clear();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function writeJson(res, statusCode, data) {
|
|
117
|
-
res.writeHead(statusCode, {
|
|
118
|
-
'Content-Type': 'application/json',
|
|
119
|
-
'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
|
|
120
|
-
});
|
|
121
|
-
res.end(JSON.stringify(data));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function parseHookTools(value) {
|
|
125
|
-
if (!value) return [HOOK_TOOLS.CLAUDE, HOOK_TOOLS.CODEX, HOOK_TOOLS.OPENCODE];
|
|
126
|
-
const tools = [];
|
|
127
|
-
for (const raw of String(value).split(',')) {
|
|
128
|
-
const tool = raw.trim().toLowerCase();
|
|
129
|
-
if (!tool) continue;
|
|
130
|
-
if (tool === 'claude' || tool === 'claude-code') tools.push(HOOK_TOOLS.CLAUDE);
|
|
131
|
-
else if (tool === 'codex') tools.push(HOOK_TOOLS.CODEX);
|
|
132
|
-
else if (tool === 'opencode' || tool === 'open-code') tools.push(HOOK_TOOLS.OPENCODE);
|
|
133
|
-
}
|
|
134
|
-
return [...new Set(tools)];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function readJsonBody(req, res, callback) {
|
|
138
|
-
let body = '';
|
|
139
|
-
let bodySize = 0;
|
|
140
|
-
const MAX_BODY = 1024 * 1024; // 1MB
|
|
141
|
-
req.on('data', chunk => {
|
|
142
|
-
bodySize += chunk.length;
|
|
143
|
-
if (bodySize > MAX_BODY) { req.destroy(); return; }
|
|
144
|
-
body += chunk;
|
|
145
|
-
});
|
|
146
|
-
req.on('end', () => {
|
|
147
|
-
if (bodySize > MAX_BODY) {
|
|
148
|
-
writeJson(res, 413, { error: '请求体过大' });
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
try {
|
|
152
|
-
Promise.resolve(callback(body ? JSON.parse(body) : {})).catch(err => {
|
|
153
|
-
console.error('API error:', err.message);
|
|
154
|
-
writeJson(res, 500, { error: err.message || '服务器内部错误' });
|
|
155
|
-
});
|
|
156
|
-
} catch {
|
|
157
|
-
writeJson(res, 400, { error: 'JSON 解析失败' });
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
}
|
|
114
|
+
function invalidateReportCache() {
|
|
115
|
+
_reportCache.clear();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeJson(res, statusCode, data) {
|
|
119
|
+
res.writeHead(statusCode, {
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
|
|
122
|
+
});
|
|
123
|
+
res.end(JSON.stringify(data));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseHookTools(value) {
|
|
127
|
+
if (!value) return [HOOK_TOOLS.CLAUDE, HOOK_TOOLS.CODEX, HOOK_TOOLS.OPENCODE];
|
|
128
|
+
const tools = [];
|
|
129
|
+
for (const raw of String(value).split(',')) {
|
|
130
|
+
const tool = raw.trim().toLowerCase();
|
|
131
|
+
if (!tool) continue;
|
|
132
|
+
if (tool === 'claude' || tool === 'claude-code') tools.push(HOOK_TOOLS.CLAUDE);
|
|
133
|
+
else if (tool === 'codex') tools.push(HOOK_TOOLS.CODEX);
|
|
134
|
+
else if (tool === 'opencode' || tool === 'open-code') tools.push(HOOK_TOOLS.OPENCODE);
|
|
135
|
+
}
|
|
136
|
+
return [...new Set(tools)];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readJsonBody(req, res, callback) {
|
|
140
|
+
let body = '';
|
|
141
|
+
let bodySize = 0;
|
|
142
|
+
const MAX_BODY = 1024 * 1024; // 1MB
|
|
143
|
+
req.on('data', chunk => {
|
|
144
|
+
bodySize += chunk.length;
|
|
145
|
+
if (bodySize > MAX_BODY) { req.destroy(); return; }
|
|
146
|
+
body += chunk;
|
|
147
|
+
});
|
|
148
|
+
req.on('end', () => {
|
|
149
|
+
if (bodySize > MAX_BODY) {
|
|
150
|
+
writeJson(res, 413, { error: '请求体过大' });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
Promise.resolve(callback(body ? JSON.parse(body) : {})).catch(err => {
|
|
155
|
+
console.error('API error:', err.message);
|
|
156
|
+
writeJson(res, 500, { error: err.message || '服务器内部错误' });
|
|
157
|
+
});
|
|
158
|
+
} catch {
|
|
159
|
+
writeJson(res, 400, { error: 'JSON 解析失败' });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
161
163
|
|
|
162
164
|
function getCachedParse(config, includeProjects) {
|
|
163
165
|
const key = `${config.claudeDir}|${includeProjects?.join(',') || ''}`;
|
|
@@ -166,80 +168,80 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
166
168
|
return null;
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
async function getOrParse(config, includeProjects) {
|
|
170
|
-
const cached = getCachedParse(config, includeProjects);
|
|
171
|
-
if (cached) return cached;
|
|
172
|
-
const result = await parseAllEnabledTools(config, {
|
|
173
|
-
excludeProjects: config.excludeProjects,
|
|
171
|
+
async function getOrParse(config, includeProjects) {
|
|
172
|
+
const cached = getCachedParse(config, includeProjects);
|
|
173
|
+
if (cached) return cached;
|
|
174
|
+
const result = await parseAllEnabledTools(config, {
|
|
175
|
+
excludeProjects: config.excludeProjects,
|
|
174
176
|
includeProjects,
|
|
175
177
|
});
|
|
176
178
|
_parsedCache = result;
|
|
177
179
|
_parsedCacheKey = `${config.claudeDir}|${includeProjects?.join(',') || ''}`;
|
|
178
|
-
_parsedCacheExpire = Date.now() + PARSED_CACHE_TTL;
|
|
179
|
-
return result;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function deriveProjectGitStats(gitStats, project, start, end, attributionOptions) {
|
|
183
|
-
if (!gitStats?.commitList?.length || !project) return null;
|
|
184
|
-
const windowEnd = end + 'T23:59:59';
|
|
185
|
-
const commitList = gitStats.commitList.filter(c => {
|
|
186
|
-
return getProjectBaseName(c.repo) === project && (c.date || '') >= start && (c.date || '') <= windowEnd;
|
|
187
|
-
});
|
|
188
|
-
if (commitList.length === 0) return null;
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
commits: commitList.length,
|
|
192
|
-
filesChanged: new Set(commitList.flatMap(c => (c.files || []).map(f => f.path))).size,
|
|
193
|
-
linesAdded: commitList.reduce((s, c) => s + (c.linesAdded || 0), 0),
|
|
194
|
-
linesDeleted: commitList.reduce((s, c) => s + (c.linesDeleted || 0), 0),
|
|
195
|
-
commitList,
|
|
196
|
-
commitTypes: computeCommitTypes(commitList),
|
|
197
|
-
fileHotspots: computeFileHotspots(commitList, 10),
|
|
198
|
-
aiContribution: computeAIContribution(commitList, null, attributionOptions),
|
|
199
|
-
attributionSummary: gitStats.attributionSummary,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function prepareJsonReportData(baseData, tool) {
|
|
204
|
-
const data = { ...baseData };
|
|
205
|
-
|
|
206
|
-
if (tool !== 'all' && data.gitStats?.aiContributionByTool) {
|
|
207
|
-
const toolAi = data.gitStats.aiContributionByTool[tool];
|
|
208
|
-
if (toolAi) {
|
|
209
|
-
data.gitStats = { ...data.gitStats, aiContribution: toolAi };
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (data.usageStats?.models) {
|
|
214
|
-
const modelEntries = Object.entries(data.usageStats.models)
|
|
215
|
-
.sort((a, b) => b[1].cost - a[1].cost);
|
|
216
|
-
const totalCost = modelEntries.reduce((s, [, d]) => s + (d.cost || 0), 0);
|
|
217
|
-
const cacheRead = data.usageStats.cacheRead || 0;
|
|
218
|
-
const cacheCreate = data.usageStats.cacheCreate || 0;
|
|
219
|
-
let cacheSaving = 0;
|
|
220
|
-
if (totalCost > 0 && (cacheRead + cacheCreate) > 0) {
|
|
221
|
-
const totalInput = data.usageStats.inputTokens || 1;
|
|
222
|
-
const avgInputCostPerToken = totalCost / (totalInput + data.usageStats.outputTokens + cacheRead + cacheCreate || 1);
|
|
223
|
-
cacheSaving = Math.round(cacheRead * avgInputCostPerToken * 0.9 * 100) / 100;
|
|
224
|
-
}
|
|
225
|
-
data.costBreakdown = {
|
|
226
|
-
models: modelEntries.map(([name, d]) => ({
|
|
227
|
-
name,
|
|
228
|
-
cost: d.cost || 0,
|
|
229
|
-
mode: d.costMode || 'unknown',
|
|
230
|
-
requests: d.count,
|
|
231
|
-
inputTokens: d.inputTokens,
|
|
232
|
-
outputTokens: d.outputTokens,
|
|
233
|
-
})),
|
|
234
|
-
cacheSaving,
|
|
235
|
-
total: Math.round(totalCost * 100) / 100,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return data;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const server = createServer(async (req, res) => {
|
|
180
|
+
_parsedCacheExpire = Date.now() + PARSED_CACHE_TTL;
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function deriveProjectGitStats(gitStats, project, start, end, attributionOptions) {
|
|
185
|
+
if (!gitStats?.commitList?.length || !project) return null;
|
|
186
|
+
const windowEnd = end + 'T23:59:59';
|
|
187
|
+
const commitList = gitStats.commitList.filter(c => {
|
|
188
|
+
return getProjectBaseName(c.repo) === project && (c.date || '') >= start && (c.date || '') <= windowEnd;
|
|
189
|
+
});
|
|
190
|
+
if (commitList.length === 0) return null;
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
commits: commitList.length,
|
|
194
|
+
filesChanged: new Set(commitList.flatMap(c => (c.files || []).map(f => f.path))).size,
|
|
195
|
+
linesAdded: commitList.reduce((s, c) => s + (c.linesAdded || 0), 0),
|
|
196
|
+
linesDeleted: commitList.reduce((s, c) => s + (c.linesDeleted || 0), 0),
|
|
197
|
+
commitList,
|
|
198
|
+
commitTypes: computeCommitTypes(commitList),
|
|
199
|
+
fileHotspots: computeFileHotspots(commitList, 10),
|
|
200
|
+
aiContribution: computeAIContribution(commitList, null, attributionOptions),
|
|
201
|
+
attributionSummary: gitStats.attributionSummary,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function prepareJsonReportData(baseData, tool) {
|
|
206
|
+
const data = { ...baseData };
|
|
207
|
+
|
|
208
|
+
if (tool !== 'all' && data.gitStats?.aiContributionByTool) {
|
|
209
|
+
const toolAi = data.gitStats.aiContributionByTool[tool];
|
|
210
|
+
if (toolAi) {
|
|
211
|
+
data.gitStats = { ...data.gitStats, aiContribution: toolAi };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (data.usageStats?.models) {
|
|
216
|
+
const modelEntries = Object.entries(data.usageStats.models)
|
|
217
|
+
.sort((a, b) => b[1].cost - a[1].cost);
|
|
218
|
+
const totalCost = modelEntries.reduce((s, [, d]) => s + (d.cost || 0), 0);
|
|
219
|
+
const cacheRead = data.usageStats.cacheRead || 0;
|
|
220
|
+
const cacheCreate = data.usageStats.cacheCreate || 0;
|
|
221
|
+
let cacheSaving = 0;
|
|
222
|
+
if (totalCost > 0 && (cacheRead + cacheCreate) > 0) {
|
|
223
|
+
const totalInput = data.usageStats.inputTokens || 1;
|
|
224
|
+
const avgInputCostPerToken = totalCost / (totalInput + data.usageStats.outputTokens + cacheRead + cacheCreate || 1);
|
|
225
|
+
cacheSaving = Math.round(cacheRead * avgInputCostPerToken * 0.9 * 100) / 100;
|
|
226
|
+
}
|
|
227
|
+
data.costBreakdown = {
|
|
228
|
+
models: modelEntries.map(([name, d]) => ({
|
|
229
|
+
name,
|
|
230
|
+
cost: d.cost || 0,
|
|
231
|
+
mode: d.costMode || 'unknown',
|
|
232
|
+
requests: d.count,
|
|
233
|
+
inputTokens: d.inputTokens,
|
|
234
|
+
outputTokens: d.outputTokens,
|
|
235
|
+
})),
|
|
236
|
+
cacheSaving,
|
|
237
|
+
total: Math.round(totalCost * 100) / 100,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return data;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const server = createServer(async (req, res) => {
|
|
243
245
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
244
246
|
|
|
245
247
|
// 安全响应头
|
|
@@ -249,60 +251,60 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
249
251
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
250
252
|
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
251
253
|
|
|
252
|
-
// API endpoint
|
|
253
|
-
if (url.pathname === '/api/hooks') {
|
|
254
|
-
try {
|
|
255
|
-
if (req.method === 'GET') {
|
|
256
|
-
writeJson(res, 200, getConfiguredHooksStatus(config));
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (req.method === 'POST') {
|
|
261
|
-
readJsonBody(req, res, async (body) => {
|
|
262
|
-
const action = body.action || 'enable';
|
|
263
|
-
const tools = parseHookTools(body.tools || url.searchParams.get('tools'));
|
|
264
|
-
if (tools.length === 0) {
|
|
265
|
-
writeJson(res, 400, { error: '未选择支持的 hooks 工具' });
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
const projectRoots = getHookProjectRoots(config);
|
|
269
|
-
if (projectRoots.length === 0) {
|
|
270
|
-
writeJson(res, 400, { error: '请先在设置中添加项目路径,页面开启 hooks 只作用于设置内配置的项目。' });
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
const stepTracking = [];
|
|
274
|
-
const results = [];
|
|
275
|
-
for (const projectRoot of projectRoots) {
|
|
276
|
-
let projectStepTracking = null;
|
|
277
|
-
if (action !== 'disable') {
|
|
278
|
-
projectStepTracking = await initStepTracking(projectRoot);
|
|
279
|
-
stepTracking.push({ projectRoot, ...projectStepTracking });
|
|
280
|
-
}
|
|
281
|
-
const projectResults = action === 'disable'
|
|
282
|
-
? disableHooks(projectRoot, tools, { backup: true })
|
|
283
|
-
: enableHooks(projectRoot, tools, { backup: true });
|
|
284
|
-
results.push({ projectRoot, stepTracking: projectStepTracking, results: projectResults });
|
|
285
|
-
}
|
|
286
|
-
writeJson(res, 200, {
|
|
287
|
-
success: true,
|
|
288
|
-
action,
|
|
289
|
-
stepTracking,
|
|
290
|
-
results,
|
|
291
|
-
status: getConfiguredHooksStatus(config),
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
writeJson(res, 405, { error: 'Method not allowed' });
|
|
298
|
-
} catch (err) {
|
|
299
|
-
console.error('API error:', err.message);
|
|
300
|
-
writeJson(res, 500, { error: err.message || '服务器内部错误' });
|
|
301
|
-
}
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (url.pathname === '/api/tools') {
|
|
254
|
+
// API endpoint
|
|
255
|
+
if (url.pathname === '/api/hooks') {
|
|
256
|
+
try {
|
|
257
|
+
if (req.method === 'GET') {
|
|
258
|
+
writeJson(res, 200, getConfiguredHooksStatus(config));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (req.method === 'POST') {
|
|
263
|
+
readJsonBody(req, res, async (body) => {
|
|
264
|
+
const action = body.action || 'enable';
|
|
265
|
+
const tools = parseHookTools(body.tools || url.searchParams.get('tools'));
|
|
266
|
+
if (tools.length === 0) {
|
|
267
|
+
writeJson(res, 400, { error: '未选择支持的 hooks 工具' });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const projectRoots = getHookProjectRoots(config);
|
|
271
|
+
if (projectRoots.length === 0) {
|
|
272
|
+
writeJson(res, 400, { error: '请先在设置中添加项目路径,页面开启 hooks 只作用于设置内配置的项目。' });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const stepTracking = [];
|
|
276
|
+
const results = [];
|
|
277
|
+
for (const projectRoot of projectRoots) {
|
|
278
|
+
let projectStepTracking = null;
|
|
279
|
+
if (action !== 'disable') {
|
|
280
|
+
projectStepTracking = await initStepTracking(projectRoot);
|
|
281
|
+
stepTracking.push({ projectRoot, ...projectStepTracking });
|
|
282
|
+
}
|
|
283
|
+
const projectResults = action === 'disable'
|
|
284
|
+
? disableHooks(projectRoot, tools, { backup: true })
|
|
285
|
+
: enableHooks(projectRoot, tools, { backup: true });
|
|
286
|
+
results.push({ projectRoot, stepTracking: projectStepTracking, results: projectResults });
|
|
287
|
+
}
|
|
288
|
+
writeJson(res, 200, {
|
|
289
|
+
success: true,
|
|
290
|
+
action,
|
|
291
|
+
stepTracking,
|
|
292
|
+
results,
|
|
293
|
+
status: getConfiguredHooksStatus(config),
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
writeJson(res, 405, { error: 'Method not allowed' });
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error('API error:', err.message);
|
|
302
|
+
writeJson(res, 500, { error: err.message || '服务器内部错误' });
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (url.pathname === '/api/tools') {
|
|
306
308
|
try {
|
|
307
309
|
const tools = await detectAvailableTools(config);
|
|
308
310
|
const enabled = config.enabledTools || tools.filter(t => t.detected).map(t => t.name);
|
|
@@ -372,26 +374,26 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
372
374
|
|
|
373
375
|
try {
|
|
374
376
|
// 查询结果缓存:相同条件直接返回缓存
|
|
375
|
-
const reportCacheKey = getReportCacheKey(period, date, tool, customStart, customEnd);
|
|
376
|
-
let data = getCachedReport(reportCacheKey);
|
|
377
|
-
if (data && format !== 'work') {
|
|
378
|
-
const responseData = prepareJsonReportData(data, tool);
|
|
379
|
-
res.writeHead(200, {
|
|
380
|
-
'Content-Type': 'application/json',
|
|
381
|
-
'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
|
|
382
|
-
'X-Cache': 'HIT',
|
|
383
|
-
});
|
|
384
|
-
res.end(JSON.stringify(responseData));
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
let parsed = null;
|
|
389
|
-
if (!data) {
|
|
390
|
-
parsed = await getOrParse(config, includeProjects);
|
|
391
|
-
data = await buildReportData(period, date, config, includeProjects, tool, parsed, { customStart, customEnd });
|
|
392
|
-
if (data) setCachedReport(reportCacheKey, data);
|
|
393
|
-
}
|
|
394
|
-
if (!data) {
|
|
377
|
+
const reportCacheKey = getReportCacheKey(period, date, tool, customStart, customEnd);
|
|
378
|
+
let data = getCachedReport(reportCacheKey);
|
|
379
|
+
if (data && format !== 'work') {
|
|
380
|
+
const responseData = prepareJsonReportData(data, tool);
|
|
381
|
+
res.writeHead(200, {
|
|
382
|
+
'Content-Type': 'application/json',
|
|
383
|
+
'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
|
|
384
|
+
'X-Cache': 'HIT',
|
|
385
|
+
});
|
|
386
|
+
res.end(JSON.stringify(responseData));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let parsed = null;
|
|
391
|
+
if (!data) {
|
|
392
|
+
parsed = await getOrParse(config, includeProjects);
|
|
393
|
+
data = await buildReportData(period, date, config, includeProjects, tool, parsed, { customStart, customEnd });
|
|
394
|
+
if (data) setCachedReport(reportCacheKey, data);
|
|
395
|
+
}
|
|
396
|
+
if (!data) {
|
|
395
397
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
396
398
|
res.end(JSON.stringify({
|
|
397
399
|
error: '未找到数据',
|
|
@@ -417,11 +419,11 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
417
419
|
let projUsageStats = data.usageStats;
|
|
418
420
|
let projGitStats = data.gitStats;
|
|
419
421
|
let projPrevStats = data.prevStats;
|
|
420
|
-
let projectName = '';
|
|
421
|
-
|
|
422
|
-
if (project) {
|
|
423
|
-
if (!parsed) parsed = await getOrParse(config, includeProjects);
|
|
424
|
-
const { records: allRecords } = parsed;
|
|
422
|
+
let projectName = '';
|
|
423
|
+
|
|
424
|
+
if (project) {
|
|
425
|
+
if (!parsed) parsed = await getOrParse(config, includeProjects);
|
|
426
|
+
const { records: allRecords } = parsed;
|
|
425
427
|
const toolRecords = tool !== 'all' ? allRecords.filter(r => r.tool === tool) : allRecords;
|
|
426
428
|
// basename 匹配
|
|
427
429
|
const projRecords = toolRecords.filter(r => {
|
|
@@ -441,10 +443,15 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
441
443
|
projPrevStats = prevProjFiltered.length > 0 ? computeUsageStats(prevProjFiltered, config.scenarioKeywords, config.costMode) : null;
|
|
442
444
|
|
|
443
445
|
// 单项目 Git 统计
|
|
444
|
-
projGitStats = deriveProjectGitStats(data.gitStats, project, pStart, pEnd, config.aiAttribution);
|
|
445
|
-
}
|
|
446
|
+
projGitStats = deriveProjectGitStats(data.gitStats, project, pStart, pEnd, config.aiAttribution);
|
|
447
|
+
}
|
|
446
448
|
|
|
447
|
-
|
|
449
|
+
let markdown;
|
|
450
|
+
if (level === 'boss') {
|
|
451
|
+
markdown = generateBossReport(projUsageStats, projGitStats, period, data.start, data.end, projPrevStats, platform);
|
|
452
|
+
} else {
|
|
453
|
+
markdown = generateWorkReport(projUsageStats, projGitStats, period, data.start, data.end, projPrevStats, { level, platform, tool, projectName });
|
|
454
|
+
}
|
|
448
455
|
res.writeHead(200, {
|
|
449
456
|
'Content-Type': 'text/plain; charset=utf-8',
|
|
450
457
|
'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
|
|
@@ -453,14 +460,14 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
453
460
|
return;
|
|
454
461
|
}
|
|
455
462
|
|
|
456
|
-
const responseData = prepareJsonReportData(data, tool);
|
|
463
|
+
const responseData = prepareJsonReportData(data, tool);
|
|
457
464
|
|
|
458
|
-
res.writeHead(200, {
|
|
465
|
+
res.writeHead(200, {
|
|
459
466
|
'Content-Type': 'application/json',
|
|
460
467
|
'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
|
|
461
468
|
'X-Cache': 'MISS',
|
|
462
469
|
});
|
|
463
|
-
res.end(JSON.stringify(responseData));
|
|
470
|
+
res.end(JSON.stringify(responseData));
|
|
464
471
|
} catch (err) {
|
|
465
472
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
466
473
|
console.error('API error:', err.message);
|
|
@@ -491,10 +498,10 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
491
498
|
const extEnd = new Date(end);
|
|
492
499
|
extEnd.setDate(extEnd.getDate() + 2);
|
|
493
500
|
const gitStats = await getGitStatsForMultipleReposAsync(sessionRepos, start, extEnd.toISOString().slice(0, 10) + 'T23:59:59');
|
|
494
|
-
await finalizeGitStats(gitStats, sessions, {
|
|
495
|
-
attribution: config.aiAttribution,
|
|
496
|
-
stepTracking: config.stepTracking,
|
|
497
|
-
});
|
|
501
|
+
await finalizeGitStats(gitStats, sessions, {
|
|
502
|
+
attribution: config.aiAttribution,
|
|
503
|
+
stepTracking: config.stepTracking,
|
|
504
|
+
});
|
|
498
505
|
}
|
|
499
506
|
} catch (e) { console.warn("[server] error", e.message); }
|
|
500
507
|
}
|
|
@@ -556,21 +563,14 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
556
563
|
}
|
|
557
564
|
result = Object.values(dailyMap).sort((a, b) => a.date.localeCompare(b.date));
|
|
558
565
|
} else if (dimension === 'scenario') {
|
|
566
|
+
// 复用 classifyRecord 确保与统计逻辑一致
|
|
559
567
|
const matched = filtered.filter(r => {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
return type === 'user' && text;
|
|
568
|
+
const classified = classifyRecord(r, config.scenarioKeywords);
|
|
569
|
+
return !!classified[key];
|
|
563
570
|
});
|
|
564
571
|
for (const r of matched) {
|
|
565
572
|
const text = r.metadata?.text || r.text || '';
|
|
566
|
-
|
|
567
|
-
const keywords = config.scenarioKeywords?.[key] || [];
|
|
568
|
-
for (const kw of keywords) {
|
|
569
|
-
if (lower.includes(kw.toLowerCase())) {
|
|
570
|
-
result.push({ text: text.slice(0, 200), timestamp: r.timestamp, project: r.project });
|
|
571
|
-
break;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
573
|
+
result.push({ text: text.slice(0, 200), timestamp: r.timestamp, project: r.project });
|
|
574
574
|
if (result.length >= 10) break;
|
|
575
575
|
}
|
|
576
576
|
}
|
|
@@ -644,10 +644,25 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
644
644
|
if (newConfig.codexDir !== undefined) { if (!validatePath(newConfig.codexDir)) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'codexDir 格式无效' })); return; } config.codexDir = newConfig.codexDir; }
|
|
645
645
|
if (newConfig.opencodeDir !== undefined) { if (!validatePath(newConfig.opencodeDir)) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'opencodeDir 格式无效' })); return; } config.opencodeDir = newConfig.opencodeDir; }
|
|
646
646
|
if (newConfig.enabledTools !== undefined) config.enabledTools = newConfig.enabledTools;
|
|
647
|
-
if (newConfig.repos !== undefined)
|
|
648
|
-
if (newConfig.excludeProjects !== undefined) config.excludeProjects = newConfig.excludeProjects;
|
|
649
|
-
if (newConfig.scenarioKeywords !== undefined)
|
|
650
|
-
|
|
647
|
+
if (newConfig.repos !== undefined) config.repos = parseRepoPaths(newConfig.repos);
|
|
648
|
+
if (newConfig.excludeProjects !== undefined) config.excludeProjects = parseRepoPaths(newConfig.excludeProjects);
|
|
649
|
+
if (newConfig.scenarioKeywords !== undefined) {
|
|
650
|
+
// 服务端校验:限制关键词长度和数量
|
|
651
|
+
const sk = newConfig.scenarioKeywords;
|
|
652
|
+
if (typeof sk === 'object' && sk !== null) {
|
|
653
|
+
const sanitized = {};
|
|
654
|
+
for (const [scene, words] of Object.entries(sk)) {
|
|
655
|
+
if (!Array.isArray(words)) continue;
|
|
656
|
+
sanitized[scene] = words
|
|
657
|
+
.map(w => String(w).trim())
|
|
658
|
+
.filter(w => w.length > 0 && w.length <= 100)
|
|
659
|
+
.filter(w => !/[\x00-\x1f\x7f]/.test(w))
|
|
660
|
+
.slice(0, 50);
|
|
661
|
+
}
|
|
662
|
+
config.scenarioKeywords = sanitized;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (newConfig.stepTracking !== undefined) config.stepTracking = newConfig.stepTracking;
|
|
651
666
|
invalidateFileCache();
|
|
652
667
|
invalidateGitCache();
|
|
653
668
|
_parsedCache = null; // 配置变更后清除解析缓存
|
|
@@ -669,21 +684,21 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
669
684
|
}
|
|
670
685
|
|
|
671
686
|
// Step blame stats API
|
|
672
|
-
if (url.pathname === '/api/step-stats') {
|
|
673
|
-
let stepStats = { stepCount: 0, sessionCount: 0, available: false };
|
|
674
|
-
try {
|
|
675
|
-
if (config.stepTracking?.enabled !== false) {
|
|
676
|
-
for (const repo of config.repos || []) {
|
|
677
|
-
const tracker = new StepTracker(repo, { dbPath: config.stepTracking?.dbPath });
|
|
678
|
-
if (await tracker.isAvailableAsync()) {
|
|
679
|
-
await tracker.open();
|
|
680
|
-
stepStats = { ...tracker.getStats(), available: true };
|
|
681
|
-
tracker.close();
|
|
682
|
-
break;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
} catch { /* step tracking not available */ }
|
|
687
|
+
if (url.pathname === '/api/step-stats') {
|
|
688
|
+
let stepStats = { stepCount: 0, sessionCount: 0, available: false };
|
|
689
|
+
try {
|
|
690
|
+
if (config.stepTracking?.enabled !== false) {
|
|
691
|
+
for (const repo of config.repos || []) {
|
|
692
|
+
const tracker = new StepTracker(repo, { dbPath: config.stepTracking?.dbPath });
|
|
693
|
+
if (await tracker.isAvailableAsync()) {
|
|
694
|
+
await tracker.open();
|
|
695
|
+
stepStats = { ...tracker.getStats(), available: true };
|
|
696
|
+
tracker.close();
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} catch { /* step tracking not available */ }
|
|
687
702
|
res.writeHead(200, {
|
|
688
703
|
'Content-Type': 'application/json',
|
|
689
704
|
'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
|
|
@@ -738,9 +753,9 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
738
753
|
const cyan = '\x1b[96m';
|
|
739
754
|
const green = '\x1b[92m';
|
|
740
755
|
const yellow = '\x1b[93m';
|
|
741
|
-
const blue = '\x1b[94m';
|
|
742
|
-
const dim = '\x1b[2m';
|
|
743
|
-
const actualPort = server.address()?.port || PORT;
|
|
756
|
+
const blue = '\x1b[94m';
|
|
757
|
+
const dim = '\x1b[2m';
|
|
758
|
+
const actualPort = server.address()?.port || PORT;
|
|
744
759
|
|
|
745
760
|
const banner = [
|
|
746
761
|
'',
|
|
@@ -763,35 +778,35 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
763
778
|
if (configPath) {
|
|
764
779
|
process.stdout.write(` ${dim}●${R} ${B}Config${R} ${configPath}\n`);
|
|
765
780
|
}
|
|
766
|
-
const repoCount = config.repos?.length || 0;
|
|
767
|
-
if (repoCount > 0) {
|
|
768
|
-
process.stdout.write(` ${dim}●${R} ${B}Projects${R} ${repoCount} repo(s) detected\n`);
|
|
769
|
-
}
|
|
770
|
-
const hookStatus = getConfiguredHooksStatus(config);
|
|
771
|
-
const hookParts = [
|
|
772
|
-
`Claude ${hookStatus.claude.enabledCount}/${hookStatus.projectCount}`,
|
|
773
|
-
`Codex ${hookStatus.codex.enabledCount}/${hookStatus.projectCount}`,
|
|
774
|
-
`OpenCode ${hookStatus.opencode.enabledCount}/${hookStatus.projectCount}`,
|
|
775
|
-
`steps ${hookStatus.stepsReadyCount}/${hookStatus.projectCount}`,
|
|
776
|
-
];
|
|
777
|
-
process.stdout.write(` ${dim}●${R} ${B}Hooks${R} ${hookParts.join(' / ')}\n`);
|
|
778
|
-
if (hookStatus.projectCount === 0) {
|
|
779
|
-
process.stdout.write(` ${yellow}${B}!${R} 未配置项目路径:请先在页面设置中添加项目,页面开启 hooks 只作用于设置内项目。\n`);
|
|
780
|
-
} else if (!hookStatus.claude.enabled || !hookStatus.codex.enabled || !hookStatus.opencode.enabled || !hookStatus.stepsInitialized) {
|
|
781
|
-
process.stdout.write(` ${yellow}${B}!${R} 行级归因未完整开启:在页面中开启,或进入项目目录运行 ${B}npx lumencode hooks enable${R}。\n`);
|
|
782
|
-
}
|
|
783
|
-
process.stdout.write('\n');
|
|
784
|
-
process.stdout.write(` ${green}${B}✓${R} Server ready at ${blue}${B}http://localhost:${actualPort}${R}\n`);
|
|
781
|
+
const repoCount = config.repos?.length || 0;
|
|
782
|
+
if (repoCount > 0) {
|
|
783
|
+
process.stdout.write(` ${dim}●${R} ${B}Projects${R} ${repoCount} repo(s) detected\n`);
|
|
784
|
+
}
|
|
785
|
+
const hookStatus = getConfiguredHooksStatus(config);
|
|
786
|
+
const hookParts = [
|
|
787
|
+
`Claude ${hookStatus.claude.enabledCount}/${hookStatus.projectCount}`,
|
|
788
|
+
`Codex ${hookStatus.codex.enabledCount}/${hookStatus.projectCount}`,
|
|
789
|
+
`OpenCode ${hookStatus.opencode.enabledCount}/${hookStatus.projectCount}`,
|
|
790
|
+
`steps ${hookStatus.stepsReadyCount}/${hookStatus.projectCount}`,
|
|
791
|
+
];
|
|
792
|
+
process.stdout.write(` ${dim}●${R} ${B}Hooks${R} ${hookParts.join(' / ')}\n`);
|
|
793
|
+
if (hookStatus.projectCount === 0) {
|
|
794
|
+
process.stdout.write(` ${yellow}${B}!${R} 未配置项目路径:请先在页面设置中添加项目,页面开启 hooks 只作用于设置内项目。\n`);
|
|
795
|
+
} else if (!hookStatus.claude.enabled || !hookStatus.codex.enabled || !hookStatus.opencode.enabled || !hookStatus.stepsInitialized) {
|
|
796
|
+
process.stdout.write(` ${yellow}${B}!${R} 行级归因未完整开启:在页面中开启,或进入项目目录运行 ${B}npx lumencode hooks enable${R}。\n`);
|
|
797
|
+
}
|
|
798
|
+
process.stdout.write('\n');
|
|
799
|
+
process.stdout.write(` ${green}${B}✓${R} Server ready at ${blue}${B}http://localhost:${actualPort}${R}\n`);
|
|
785
800
|
process.stdout.write('\n');
|
|
786
801
|
|
|
787
|
-
// Auto-open browser
|
|
788
|
-
if (process.env.LUMENCODE_NO_OPEN !== '1') {
|
|
789
|
-
const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
790
|
-
import('child_process').then(({ exec }) => {
|
|
791
|
-
exec(`${openCmd} http://localhost:${actualPort}`, () => {});
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
return server;
|
|
797
|
-
}
|
|
802
|
+
// Auto-open browser
|
|
803
|
+
if (process.env.LUMENCODE_NO_OPEN !== '1') {
|
|
804
|
+
const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
805
|
+
import('child_process').then(({ exec }) => {
|
|
806
|
+
exec(`${openCmd} http://localhost:${actualPort}`, () => {});
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
return server;
|
|
812
|
+
}
|