zen-gitsync 2.11.39 → 2.12.3

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.
Files changed (58) hide show
  1. package/LICENSE +190 -21
  2. package/README.md +695 -695
  3. package/index.js +25 -11
  4. package/package.json +2 -2
  5. package/scripts/convert-colors-to-vars.cjs +286 -272
  6. package/scripts/convert-fontsize-to-vars.cjs +221 -207
  7. package/scripts/convert-spacing-to-vars.cjs +256 -242
  8. package/scripts/convert-to-standard-vars.cjs +282 -268
  9. package/scripts/release.js +599 -585
  10. package/src/config.js +350 -336
  11. package/src/gitCommit.js +455 -440
  12. package/src/ui/public/assets/EditorView-CbqSI9nw.css +1 -0
  13. package/src/ui/public/assets/EditorView-GS5cmh99.js +21 -0
  14. package/src/ui/public/assets/SourceMapView-DyMK80hS.css +1 -0
  15. package/src/ui/public/assets/SourceMapView-_YRtzmZZ.js +3 -0
  16. package/src/ui/public/assets/index-ML5Y-5lO.css +1 -0
  17. package/src/ui/public/assets/index-yky0Sd13.js +73 -0
  18. package/src/ui/public/assets/{ts.worker-Dth06zuC.js → ts.worker-METxwbDZ.js} +1 -16
  19. package/src/ui/public/assets/{vendor-B1T2uxYO.js → vendor-DITsiaGj.js} +294 -287
  20. package/src/ui/public/assets/vendor-q83wvJns.css +1 -0
  21. package/src/ui/public/index.html +4 -4
  22. package/src/ui/server/.claude/codediff.txt +6 -0
  23. package/src/ui/server/index.js +410 -396
  24. package/src/ui/server/middleware/requestLogger.js +51 -37
  25. package/src/ui/server/routes/branchStatus.js +101 -87
  26. package/src/ui/server/routes/code.js +110 -96
  27. package/src/ui/server/routes/codeAnalysis.js +995 -981
  28. package/src/ui/server/routes/config.js +1172 -1158
  29. package/src/ui/server/routes/exec.js +272 -258
  30. package/src/ui/server/routes/fileOpen.js +279 -265
  31. package/src/ui/server/routes/fs.js +701 -699
  32. package/src/ui/server/routes/git/diff.js +352 -338
  33. package/src/ui/server/routes/git/diffUtils.js +128 -114
  34. package/src/ui/server/routes/git/stash.js +552 -538
  35. package/src/ui/server/routes/git/tags.js +172 -158
  36. package/src/ui/server/routes/git.js +190 -176
  37. package/src/ui/server/routes/gitOps.js +1179 -1165
  38. package/src/ui/server/routes/instances.js +38 -24
  39. package/src/ui/server/routes/npm.js +1023 -1009
  40. package/src/ui/server/routes/process.js +82 -68
  41. package/src/ui/server/routes/status.js +67 -53
  42. package/src/ui/server/routes/terminal.js +319 -305
  43. package/src/ui/server/socket/registerUiSocketHandlers.js +226 -212
  44. package/src/ui/server/utils/createSavePortToFile.js +46 -32
  45. package/src/ui/server/utils/instanceRegistry.js +270 -256
  46. package/src/ui/server/utils/pathGuard.js +155 -0
  47. package/src/ui/server/utils/pathGuard.test.js +138 -0
  48. package/src/ui/server/utils/randomStartPort.js +51 -37
  49. package/src/ui/server/utils/startServerOnAvailablePort.js +101 -87
  50. package/src/utils/index.js +1058 -1044
  51. package/src/ui/public/assets/devopicons-QN4QXivI.woff2 +0 -0
  52. package/src/ui/public/assets/file-icons-C0jOugUK.woff2 +0 -0
  53. package/src/ui/public/assets/fontawesome-B-jkhYfk.woff2 +0 -0
  54. package/src/ui/public/assets/index-BvVl-092.js +0 -95
  55. package/src/ui/public/assets/index-DXO3Lvqi.css +0 -1
  56. package/src/ui/public/assets/mfixx-CpAhKOZz.woff2 +0 -0
  57. package/src/ui/public/assets/octicons-CaZ_fok2.woff2 +0 -0
  58. package/src/ui/public/assets/vendor-hOO_r_AU.css +0 -1
@@ -1,1158 +1,1172 @@
1
- import express from 'express';
2
- import path from 'path';
3
- import os from 'os';
4
- import fs from 'fs/promises';
5
- import open from 'open';
6
-
7
- // 跳过的产物/资源/lock 文件(用 stat 一行带过,不打 patch)
8
- const SKIP_FILE_PATTERNS = [
9
- /(^|[\\/])dist[\\/]/,
10
- /(^|[\\/])build[\\/]/,
11
- /(^|[\\/])\.next[\\/]/,
12
- /(^|[\\/])coverage[\\/]/,
13
- /(^|[\\/])node_modules[\\/]/,
14
- /\.lock$/,
15
- /package-lock\.json$/,
16
- /yarn\.lock$/,
17
- /pnpm-lock\.yaml$/,
18
- /min\.js$/,
19
- /min\.css$/,
20
- /\.bundle\.js$/,
21
- /\.map$/,
22
- /\.(png|jpe?g|gif|webp|svg|ico|bmp|tiff)$/,
23
- /\.(mp4|mov|avi|mkv|webm)$/,
24
- /\.(mp3|wav|ogg|flac)$/,
25
- /\.(woff2?|ttf|otf|eot)$/,
26
- /\.(pdf|zip|tar|gz|7z|rar)$/
27
- ]
28
-
29
- // 文件优先级:数字越大越重要
30
- function filePriority(p) {
31
- if (/\.(test|spec)\.(js|ts|jsx|tsx|vue)$/i.test(p)) return 1 // 测试文件
32
- if (/^docs?\//i.test(p) || /\.md$/i.test(p)) return 2 // 文档
33
- if (/\.(json|ya?ml|toml|env|ini|cfg|conf)$/i.test(p)) return 3 // 配置
34
- if (/\.(css|scss|sass|less|html|vue|jsx|tsx)$/i.test(p)) return 4 // 前端
35
- if (/\.(ts|js|mjs|cjs)$/i.test(p)) return 5 // 后端/脚本
36
- return 3 // 其它(资源、未知)
37
- }
38
-
39
- function isSkippedFile(path) {
40
- return SKIP_FILE_PATTERNS.some(re => re.test(path))
41
- }
42
-
43
- // 从 git diff 文本解析出 per-file 块
44
- // git diff 输出格式: "diff --git a/path b/path\n...一系列 header...\n--- a/path\n+++ b/path\n@@ ...\n<patch>"
45
- function parseDiffByFile(diffText) {
46
- if (!diffText) return []
47
- const files = []
48
- // "diff --git " 切分,首段可能为空(文本以这个开头则第一段空)
49
- const parts = diffText.split(/^diff --git /m).filter(Boolean)
50
- for (const part of parts) {
51
- const headerEnd = part.indexOf('\n')
52
- const headerLine = (headerEnd >= 0 ? part.slice(0, headerEnd) : part).trim()
53
- // 从 header 里抓路径: "a/path b/path" -> 优先 b
54
- const m = headerLine.match(/^a\/(.+?)\s+b\/(.+)$/)
55
- if (!m) continue
56
- const bPath = m[2]
57
- const patch = part.slice(headerEnd + 1)
58
- // 统计 +/- 行数
59
- let added = 0, removed = 0
60
- for (const line of patch.split('\n')) {
61
- if (line.startsWith('+') && !line.startsWith('+++')) added++
62
- else if (line.startsWith('-') && !line.startsWith('---')) removed++
63
- }
64
- files.push({ path: bPath, patch, added, removed })
65
- }
66
- return files
67
- }
68
-
69
- // 收集 AI 生成提交信息所需的 diff 和文件列表
70
- // 关键: untracked 文件默认不会出现在 git diff --staged 输出里,
71
- // 所以先对所有 untracked 文件做 git add -N(intent to add),
72
- // 这样它们会以 "new file" 形式出现在 diff 里
73
- async function collectDiffForAi({ execGitCommand, getCurrentProjectPath }) {
74
- if (typeof execGitCommand !== 'function') {
75
- return { diff: '', fileList: [] }
76
- }
77
- const projectPath = typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : ''
78
- const cwdOpts = projectPath ? { cwd: projectPath, log: false } : { log: false }
79
-
80
- let fileList = []
81
- try {
82
- // 1. 拿到工作区状态,识别 untracked 文件
83
- const { stdout: statusOut } = await execGitCommand('git status --porcelain=1 --untracked-files=all', cwdOpts)
84
- const untracked = []
85
- const trackedChanges = []
86
- for (const line of (statusOut || '').split('\n')) {
87
- if (!line || line.length < 3) continue
88
- // porcelain=1 格式: "XY path" X=index状态, Y=worktree状态
89
- const x = line[0]
90
- const y = line[1]
91
- const path = line.slice(3)
92
- if (x === '?' && y === '?') {
93
- untracked.push(path)
94
- } else if (x !== ' ' || y !== ' ') {
95
- // 有改动(暂存或工作区)
96
- trackedChanges.push(`${x !== ' ' ? x : ' '} ${path}`.trim())
97
- }
98
- }
99
- fileList = [...trackedChanges, ...untracked.map(p => `? ${p}`)]
100
-
101
- // 2. untracked 文件做 intent to add(只在内存中,不会真的暂存内容)
102
- if (untracked.length > 0) {
103
- // 加上 --force 以防某些文件已经在 index 中
104
- // 分批处理,避免命令行过长
105
- const batchSize = 20
106
- for (let i = 0; i < untracked.length; i += batchSize) {
107
- const batch = untracked.slice(i, i + batchSize)
108
- const quoted = batch.map(p => `"${p.replace(/"/g, '\\"')}"`).join(' ')
109
- try {
110
- await execGitCommand(`git add -N --force ${quoted}`, cwdOpts)
111
- } catch (e) {
112
- // 单批失败不影响整体
113
- console.warn('[generate-commit] git add -N failed for batch:', e?.message)
114
- }
115
- }
116
- }
117
-
118
- // 3. 合并 staged + unstaged diff
119
- // 用 --no-color 避免 ANSI 干扰, --no-ext-diff 避免外接 diff 工具
120
- const [stagedRes, unstagedRes] = await Promise.all([
121
- execGitCommand('git diff --staged --no-color --no-ext-diff', cwdOpts).catch(() => ({ stdout: '' })),
122
- execGitCommand('git diff --no-color --no-ext-diff', cwdOpts).catch(() => ({ stdout: '' }))
123
- ])
124
- let combined = ''
125
- if (stagedRes?.stdout) combined += stagedRes.stdout.trim() + '\n'
126
- if (unstagedRes?.stdout) combined += unstagedRes.stdout.trim() + '\n'
127
-
128
- return { diff: combined.trim(), fileList }
129
- } catch (error) {
130
- console.error('[generate-commit] collectDiffForAi error:', error?.message)
131
- return { diff: '', fileList }
132
- }
133
- }
134
-
135
- // diff 压缩成给 LLM 的紧凑文本
136
- // 策略: 跳过产物/资源文件(用一行 stat 带过),源码按优先级排序,每个文件 patch 1500 字,总预算 6000 字
137
- function prepareDiffForPrompt(diffText, fileList) {
138
- const safeFileList = Array.isArray(fileList) ? fileList : []
139
- let files = parseDiffByFile(diffText)
140
-
141
- // 如果客户端明确指定了文件列表,则只保留与所选文件匹配的 diff 块
142
- if (safeFileList.length > 0 && files.length > 0) {
143
- const selectedPaths = new Set(
144
- safeFileList
145
- .map(s => {
146
- if (typeof s !== 'string') return ''
147
- // fileList 形如 "M src/foo.ts" 或 "? new-file.ts",取最后的路径部分
148
- const m = s.match(/^[A-Z?\s]+\s+(.+)$/)
149
- return (m ? m[1] : s).replace(/\\/g, '/')
150
- })
151
- .filter(Boolean)
152
- )
153
- if (selectedPaths.size > 0) {
154
- files = files.filter(f => selectedPaths.has(f.path.replace(/\\/g, '/')))
155
- }
156
- }
157
-
158
- // 如果 parse 出来是空的(diff 可能是空或非标准格式),退回到 fileList 推断
159
- if (files.length === 0) {
160
- const list = safeFileList.slice(0, 30).map(s => {
161
- // fileList 形如 "M src/foo.ts" 或 "? new-file.ts"
162
- const m = s.match(/^[A-Z?\s]+\s+(.+)$/)
163
- return m ? m[1] : s
164
- })
165
- if (list.length === 0) return ''
166
- return list.map(p => {
167
- if (isSkippedFile(p)) return `${p} [产物/资源,已跳过]`
168
- return p
169
- }).join('\n')
170
- }
171
-
172
- // 拆分: 跳过的 vs 保留的
173
- const skipped = []
174
- const kept = []
175
- for (const f of files) {
176
- if (isSkippedFile(f.path)) {
177
- skipped.push(f)
178
- } else {
179
- kept.push(f)
180
- }
181
- }
182
-
183
- // 保留的文件按优先级降序,同优先级按 +/- 总数降序(改得多的优先)
184
- kept.sort((a, b) => {
185
- const dp = filePriority(b.path) - filePriority(a.path)
186
- if (dp !== 0) return dp
187
- return (b.added + b.removed) - (a.added + a.removed)
188
- })
189
-
190
- const TOTAL_BUDGET = 6000
191
- const PER_FILE_LIMIT = 1500
192
- const lines = []
193
- let budget = TOTAL_BUDGET
194
-
195
- // 先把跳过的文件用一行 stat 总结
196
- for (const f of skipped) {
197
- if (budget < 80) break
198
- lines.push(`${f.path} [+${f.added}/-${f.removed}, 已跳过产物/资源]`)
199
- budget -= 80
200
- }
201
-
202
- // 再装源码,带预算控制
203
- for (const f of kept) {
204
- if (budget <= 50) break
205
- let patch = f.patch
206
- let truncated = false
207
- if (patch.length > PER_FILE_LIMIT) {
208
- patch = patch.slice(0, PER_FILE_LIMIT) + '\n... (diff 已截断)'
209
- truncated = true
210
- }
211
- const block = `--- ${f.path} (+${f.added}/-${f.removed})${truncated ? ' [截断]' : ''}\n${patch}`
212
- if (block.length > budget) {
213
- // 单个文件塞不下了,截到能塞下为止
214
- const sliceLen = Math.max(0, budget - 80)
215
- if (sliceLen < 100) break
216
- lines.push(`--- ${f.path} (+${f.added}/-${f.removed}) [预算耗尽,已截断]\n${patch.slice(0, sliceLen)}`)
217
- budget = 0
218
- break
219
- }
220
- lines.push(block)
221
- budget -= block.length
222
- }
223
-
224
- if (lines.length === 0) return ''
225
- return lines.join('\n\n')
226
- }
227
-
228
- export { prepareDiffForPrompt, parseDiffByFile, isSkippedFile, filePriority, collectDiffForAi }
229
-
230
- export function registerConfigRoutes({
231
- app,
232
- configManager,
233
- execGitCommand,
234
- getCurrentProjectPath
235
- }) {
236
- // 保存最近访问的目录
237
- app.post('/api/save_recent_directory', async (req, res) => {
238
- try {
239
- const { path } = req.body;
240
-
241
- if (!path) {
242
- return res.status(400).json({
243
- success: false,
244
- error: '目录路径不能为空'
245
- });
246
- }
247
-
248
- // 保存到配置
249
- await configManager.saveRecentDirectory(path);
250
-
251
- res.json({
252
- success: true
253
- });
254
- } catch (error) {
255
- res.status(500).json({
256
- success: false,
257
- error: error.message
258
- });
259
- }
260
- });
261
-
262
- // 删除最近访问的目录
263
- app.post('/api/remove_recent_directory', async (req, res) => {
264
- try {
265
- const { path: dirPath } = req.body;
266
- if (!dirPath) {
267
- return res.status(400).json({ success: false, error: '目录路径不能为空' });
268
- }
269
- const list = await configManager.removeRecentDirectory(dirPath);
270
- res.json({ success: true, directories: list });
271
- } catch (error) {
272
- res.status(500).json({ success: false, error: error.message });
273
- }
274
- });
275
-
276
- // 获取配置
277
- app.get('/api/config/getConfig', async (req, res) => {
278
- try {
279
- // console.log('获取配置中。。。')
280
- const config = await configManager.loadConfig()
281
-
282
- // 兼容旧数据:补齐自定义命令 id,避免前端编辑/删除/编排引用异常
283
- if (Array.isArray(config.customCommands)) {
284
- let changed = false
285
- config.customCommands = config.customCommands.map((cmd) => {
286
- if (cmd && typeof cmd === 'object' && !cmd.id) {
287
- changed = true
288
- return {
289
- ...cmd,
290
- id: `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
291
- }
292
- }
293
- return cmd
294
- })
295
- if (changed) {
296
- await configManager.saveConfig(config)
297
- }
298
- }
299
-
300
- // 初始化命令模板(首次安装/旧配置兼容)
301
- if (!Array.isArray(config.commandTemplates)) {
302
- config.commandTemplates = []
303
- }
304
-
305
- if (config.commandTemplates.length === 0) {
306
- config.commandTemplates = [
307
- 'echo "{{cmd}}"',
308
- 'npm run dev',
309
- 'npm run build',
310
- 'git status',
311
- 'git add .',
312
- 'git commit -m "{{message}}" --no-verify',
313
- 'git push',
314
- ]
315
- await configManager.saveConfig(config)
316
- }
317
-
318
- // console.log('获取配置成功')
319
- res.json(config)
320
- } catch (error) {
321
- // console.log('获取配置失败')
322
- const configPath = path.join(os.homedir(), '.git-commit-tool.json')
323
- res.status(500).json({
324
- success: false,
325
- code: 'CONFIG_LOAD_FAILED',
326
- error: error?.message || String(error),
327
- configPath
328
- })
329
- }
330
- })
331
-
332
- // 保存默认提交信息
333
- app.post('/api/config/saveDefaultMessage', express.json(), async (req, res) => {
334
- try {
335
- const { defaultCommitMessage } = req.body
336
-
337
- if (!defaultCommitMessage) {
338
- return res.status(400).json({ success: false, error: '缺少必要参数' })
339
- }
340
-
341
- const config = await configManager.loadConfig()
342
-
343
- // 更新默认提交信息
344
- config.defaultCommitMessage = defaultCommitMessage
345
- await configManager.saveConfig(config)
346
-
347
- res.json({ success: true })
348
- } catch (error) {
349
- res.status(500).json({ success: false, error: error.message })
350
- }
351
- })
352
-
353
- // 保存所有配置
354
- app.post('/api/config/saveAll', express.json(), async (req, res) => {
355
- try {
356
- const { config } = req.body
357
-
358
- if (!config) {
359
- return res.status(400).json({ success: false, error: '缺少必要参数' })
360
- }
361
-
362
- await configManager.saveConfig(config)
363
-
364
- res.json({ success: true })
365
- } catch (error) {
366
- res.status(500).json({ success: false, error: error.message })
367
- }
368
- })
369
-
370
- // 检查系统配置文件格式
371
- app.get('/api/config/check-file-format', async (req, res) => {
372
- try {
373
- const configPath = path.join(os.homedir(), '.git-commit-tool.json');
374
-
375
- try {
376
- const data = await fs.readFile(configPath, 'utf-8');
377
- try {
378
- JSON.parse(data);
379
- res.json({ success: true, isValidJson: true, exists: true });
380
- } catch (parseError) {
381
- res.json({
382
- success: true,
383
- isValidJson: false,
384
- exists: true,
385
- error: `JSON解析失败: ${parseError.message}`
386
- });
387
- }
388
- } catch (fileError) {
389
- if (fileError.code === 'ENOENT') {
390
- res.json({ success: true, isValidJson: true, exists: false });
391
- } else {
392
- res.json({
393
- success: true,
394
- isValidJson: false,
395
- exists: true,
396
- error: `文件读取失败: ${fileError.message}`
397
- });
398
- }
399
- }
400
- } catch (error) {
401
- res.status(500).json({ success: false, error: error.message });
402
- }
403
- })
404
-
405
- // 使用系统默认程序打开配置文件 ~/.git-commit-tool.json
406
- app.post('/api/config/open-file', async (req, res) => {
407
- try {
408
- const filePath = path.join(os.homedir(), '.git-commit-tool.json');
409
- try {
410
- // 检查文件是否存在,不存在也尝试让系统创建(可能会打开空文件)
411
- await fs.access(filePath);
412
- } catch (_) {
413
- // 如果文件不存在,先创建一个最小结构,避免某些系统无法打开不存在的路径
414
- try {
415
- await fs.writeFile(filePath, '{}', 'utf-8');
416
- } catch (e) {
417
- // 创建失败不阻断打开尝试
418
- console.warn('创建配置文件失败(可忽略):', e?.message || e);
419
- }
420
- }
421
-
422
- await open(filePath, { wait: false });
423
- res.json({ success: true })
424
- } catch (error) {
425
- res.status(400).json({ success: false, error: `无法打开配置文件: ${error.message}` })
426
- }
427
- })
428
-
429
- // 保存模板
430
- app.post('/api/config/save-template', express.json(), async (req, res) => {
431
- try {
432
- const { template, type } = req.body
433
-
434
- if (!template || !type) {
435
- return res.status(400).json({ success: false, error: '缺少必要参数' })
436
- }
437
-
438
- const config = await configManager.loadConfig()
439
-
440
- if (type === 'description') {
441
- // 确保描述模板数组存在
442
- if (!config.descriptionTemplates) {
443
- config.descriptionTemplates = []
444
- }
445
-
446
- // 检查是否已存在相同模板
447
- if (!config.descriptionTemplates.includes(template)) {
448
- config.descriptionTemplates.push(template)
449
- await configManager.saveConfig(config)
450
- }
451
- } else if (type === 'scope') {
452
- // 确保作用域模板数组存在
453
- if (!config.scopeTemplates) {
454
- config.scopeTemplates = []
455
- }
456
-
457
- // 检查是否已存在相同模板
458
- if (!config.scopeTemplates.includes(template)) {
459
- config.scopeTemplates.push(template)
460
- await configManager.saveConfig(config)
461
- }
462
- } else if (type === 'message') {
463
- // 确保提交信息模板数组存在
464
- if (!config.messageTemplates) {
465
- config.messageTemplates = []
466
- }
467
-
468
- // 检查是否已存在相同模板
469
- if (!config.messageTemplates.includes(template)) {
470
- config.messageTemplates.push(template)
471
- await configManager.saveConfig(config)
472
- }
473
- } else if (type === 'command') {
474
- if (!config.commandTemplates) {
475
- config.commandTemplates = []
476
- }
477
- if (!config.commandTemplates.includes(template)) {
478
- config.commandTemplates.push(template)
479
- await configManager.saveConfig(config)
480
- }
481
- } else {
482
- return res.status(400).json({ success: false, error: '不支持的模板类型' })
483
- }
484
-
485
- res.json({ success: true })
486
- } catch (error) {
487
- res.status(500).json({ success: false, error: error.message })
488
- }
489
- })
490
-
491
- // 删除模板
492
- app.post('/api/config/delete-template', express.json(), async (req, res) => {
493
- try {
494
- const { template, type } = req.body
495
-
496
- if (!template || !type) {
497
- return res.status(400).json({ success: false, error: '缺少必要参数' })
498
- }
499
-
500
- const config = await configManager.loadConfig()
501
-
502
- if (type === 'description') {
503
- // 确保描述模板数组存在
504
- if (config.descriptionTemplates) {
505
- const index = config.descriptionTemplates.indexOf(template)
506
- if (index !== -1) {
507
- config.descriptionTemplates.splice(index, 1)
508
- await configManager.saveConfig(config)
509
- }
510
- }
511
- } else if (type === 'scope') {
512
- // 确保作用域模板数组存在
513
- if (config.scopeTemplates) {
514
- const index = config.scopeTemplates.indexOf(template)
515
- if (index !== -1) {
516
- config.scopeTemplates.splice(index, 1)
517
- await configManager.saveConfig(config)
518
- }
519
- }
520
- } else if (type === 'message') {
521
- // 确保提交信息模板数组存在
522
- if (config.messageTemplates) {
523
- const index = config.messageTemplates.indexOf(template)
524
- if (index !== -1) {
525
- config.messageTemplates.splice(index, 1)
526
- await configManager.saveConfig(config)
527
- }
528
- }
529
- } else if (type === 'command') {
530
- if (config.commandTemplates) {
531
- const index = config.commandTemplates.indexOf(template)
532
- if (index !== -1) {
533
- config.commandTemplates.splice(index, 1)
534
- await configManager.saveConfig(config)
535
- }
536
- }
537
- } else {
538
- return res.status(400).json({ success: false, error: '不支持的模板类型' })
539
- }
540
-
541
- res.json({ success: true })
542
- } catch (error) {
543
- res.status(500).json({ success: false, error: error.message })
544
- }
545
- })
546
-
547
- // 更新模板
548
- app.post('/api/config/update-template', express.json(), async (req, res) => {
549
- try {
550
- const { oldTemplate, newTemplate, type } = req.body
551
-
552
- if (!oldTemplate || !newTemplate || !type) {
553
- return res.status(400).json({ success: false, error: '缺少必要参数' })
554
- }
555
-
556
- const config = await configManager.loadConfig()
557
-
558
- if (type === 'description') {
559
- // 确保描述模板数组存在
560
- if (config.descriptionTemplates) {
561
- const index = config.descriptionTemplates.indexOf(oldTemplate)
562
- if (index !== -1) {
563
- config.descriptionTemplates[index] = newTemplate
564
- await configManager.saveConfig(config)
565
- } else {
566
- return res.status(404).json({ success: false, error: '未找到原模板' })
567
- }
568
- } else {
569
- return res.status(404).json({ success: false, error: '模板列表不存在' })
570
- }
571
- } else if (type === 'scope') {
572
- // 确保作用域模板数组存在
573
- if (config.scopeTemplates) {
574
- const index = config.scopeTemplates.indexOf(oldTemplate)
575
- if (index !== -1) {
576
- config.scopeTemplates[index] = newTemplate
577
- await configManager.saveConfig(config)
578
- } else {
579
- return res.status(404).json({ success: false, error: '未找到原模板' })
580
- }
581
- } else {
582
- return res.status(404).json({ success: false, error: '模板列表不存在' })
583
- }
584
- } else if (type === 'message') {
585
- // 确保提交信息模板数组存在
586
- if (config.messageTemplates) {
587
- const index = config.messageTemplates.indexOf(oldTemplate)
588
- if (index !== -1) {
589
- config.messageTemplates[index] = newTemplate
590
- await configManager.saveConfig(config)
591
- } else {
592
- return res.status(404).json({ success: false, error: '未找到原模板' })
593
- }
594
- } else {
595
- return res.status(404).json({ success: false, error: '模板列表不存在' })
596
- }
597
- } else if (type === 'command') {
598
- if (config.commandTemplates) {
599
- const index = config.commandTemplates.indexOf(oldTemplate)
600
- if (index !== -1) {
601
- config.commandTemplates[index] = newTemplate
602
- await configManager.saveConfig(config)
603
- } else {
604
- return res.status(404).json({ success: false, error: '未找到原模板' })
605
- }
606
- } else {
607
- return res.status(404).json({ success: false, error: '模板列表不存在' })
608
- }
609
- } else {
610
- return res.status(400).json({ success: false, error: '不支持的模板类型' })
611
- }
612
-
613
- res.json({ success: true })
614
- } catch (error) {
615
- res.status(500).json({ success: false, error: error.message })
616
- }
617
- })
618
-
619
- // 置顶模板
620
- app.post('/api/config/pin-template', express.json(), async (req, res) => {
621
- try {
622
- const { template, type } = req.body
623
-
624
- if (!template || !type) {
625
- return res.status(400).json({ success: false, error: '缺少必要参数' })
626
- }
627
-
628
- const config = await configManager.loadConfig()
629
-
630
- if (type === 'description') {
631
- if (config.descriptionTemplates) {
632
- // 删除原位置的模板
633
- config.descriptionTemplates = config.descriptionTemplates.filter(t => t !== template)
634
- // 添加到第一位
635
- config.descriptionTemplates.unshift(template)
636
- await configManager.saveConfig(config)
637
- } else {
638
- return res.status(404).json({ success: false, error: '模板列表不存在' })
639
- }
640
- } else if (type === 'scope') {
641
- if (config.scopeTemplates) {
642
- config.scopeTemplates = config.scopeTemplates.filter(t => t !== template)
643
- config.scopeTemplates.unshift(template)
644
- await configManager.saveConfig(config)
645
- } else {
646
- return res.status(404).json({ success: false, error: '模板列表不存在' })
647
- }
648
- } else if (type === 'message') {
649
- if (config.messageTemplates) {
650
- config.messageTemplates = config.messageTemplates.filter(t => t !== template)
651
- config.messageTemplates.unshift(template)
652
- await configManager.saveConfig(config)
653
- } else {
654
- return res.status(404).json({ success: false, error: '模板列表不存在' })
655
- }
656
- } else if (type === 'command') {
657
- if (config.commandTemplates) {
658
- config.commandTemplates = config.commandTemplates.filter(t => t !== template)
659
- config.commandTemplates.unshift(template)
660
- await configManager.saveConfig(config)
661
- } else {
662
- return res.status(404).json({ success: false, error: '模板列表不存在' })
663
- }
664
- } else {
665
- return res.status(400).json({ success: false, error: '不支持的模板类型' })
666
- }
667
-
668
- res.json({ success: true })
669
- } catch (error) {
670
- res.status(500).json({ success: false, error: error.message })
671
- }
672
- })
673
-
674
- // 保存自定义命令
675
- app.post('/api/config/save-custom-command', express.json(), async (req, res) => {
676
- try {
677
- const { command } = req.body
678
-
679
- if (!command || !command.name || !command.command) {
680
- return res.status(400).json({ success: false, error: '缺少必要参数' })
681
- }
682
-
683
- const config = await configManager.loadConfig()
684
-
685
- // 确保自定义命令数组存在
686
- if (!Array.isArray(config.customCommands)) {
687
- config.customCommands = []
688
- }
689
-
690
- // 生成唯一ID
691
- const id = `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
692
- const newCommand = {
693
- id,
694
- name: command.name,
695
- description: command.description || '',
696
- directory: command.directory || '',
697
- command: command.command,
698
- params: Array.isArray(command.params) ? command.params : []
699
- }
700
-
701
- config.customCommands.push(newCommand)
702
- await configManager.saveConfig(config)
703
-
704
- res.json({ success: true, command: newCommand })
705
- } catch (error) {
706
- res.status(500).json({ success: false, error: error.message })
707
- }
708
- })
709
-
710
- // 删除自定义命令
711
- app.post('/api/config/delete-custom-command', express.json(), async (req, res) => {
712
- try {
713
- const { id } = req.body
714
-
715
- if (!id) {
716
- return res.status(400).json({ success: false, error: '缺少命令ID参数' })
717
- }
718
-
719
- const config = await configManager.loadConfig()
720
-
721
- if (Array.isArray(config.customCommands)) {
722
- const index = config.customCommands.findIndex(cmd => cmd.id === id)
723
- if (index !== -1) {
724
- config.customCommands.splice(index, 1)
725
- await configManager.saveConfig(config)
726
- }
727
- }
728
-
729
- res.json({ success: true })
730
- } catch (error) {
731
- res.status(500).json({ success: false, error: error.message })
732
- }
733
- })
734
-
735
- // 置顶自定义命令(移到数组首位)
736
- app.post('/api/config/pin-custom-command', express.json(), async (req, res) => {
737
- try {
738
- const { id } = req.body
739
-
740
- if (!id) {
741
- return res.status(400).json({ success: false, error: '缺少命令ID参数' })
742
- }
743
-
744
- const config = await configManager.loadConfig()
745
-
746
- if (Array.isArray(config.customCommands)) {
747
- const index = config.customCommands.findIndex(cmd => cmd.id === id)
748
- if (index > 0) {
749
- const [cmd] = config.customCommands.splice(index, 1)
750
- config.customCommands.unshift(cmd)
751
- await configManager.saveConfig(config)
752
- }
753
- }
754
-
755
- res.json({ success: true })
756
- } catch (error) {
757
- res.status(500).json({ success: false, error: error.message })
758
- }
759
- })
760
-
761
- // 更新自定义命令
762
- app.post('/api/config/update-custom-command', express.json(), async (req, res) => {
763
- try {
764
- const { id, command } = req.body
765
-
766
- if (!id || !command || !command.name || !command.command) {
767
- return res.status(400).json({ success: false, error: '缺少必要参数' })
768
- }
769
-
770
- const config = await configManager.loadConfig()
771
-
772
- if (Array.isArray(config.customCommands)) {
773
- const index = config.customCommands.findIndex(cmd => cmd.id === id)
774
- if (index !== -1) {
775
- config.customCommands[index] = {
776
- id,
777
- name: command.name,
778
- description: command.description || '',
779
- directory: command.directory || '',
780
- command: command.command,
781
- params: Array.isArray(command.params) ? command.params : []
782
- }
783
- await configManager.saveConfig(config)
784
- } else {
785
- return res.status(404).json({ success: false, error: '未找到指定命令' })
786
- }
787
- } else {
788
- return res.status(404).json({ success: false, error: '命令列表不存在' })
789
- }
790
-
791
- res.json({ success: true })
792
- } catch (error) {
793
- res.status(500).json({ success: false, error: error.message })
794
- }
795
- })
796
-
797
- // 保存指令编排
798
- app.post('/api/config/save-orchestration', express.json(), async (req, res) => {
799
- try {
800
- const { orchestration } = req.body
801
-
802
- if (!orchestration || !orchestration.name) {
803
- return res.status(400).json({ success: false, error: '缺少必要参数' })
804
- }
805
-
806
- const config = await configManager.loadConfig()
807
-
808
- // 确保编排数组存在
809
- if (!Array.isArray(config.orchestrations)) {
810
- config.orchestrations = []
811
- }
812
-
813
- // 生成唯一ID
814
- const id = `orch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
815
- const newOrchestration = {
816
- id,
817
- name: orchestration.name,
818
- description: orchestration.description || '',
819
- flowData: orchestration.flowData || null
820
- }
821
-
822
- config.orchestrations.push(newOrchestration)
823
- await configManager.saveConfig(config)
824
-
825
- res.json({ success: true, orchestration: newOrchestration })
826
- } catch (error) {
827
- res.status(500).json({ success: false, error: error.message })
828
- }
829
- })
830
-
831
- // 删除指令编排
832
- app.post('/api/config/delete-orchestration', express.json(), async (req, res) => {
833
- try {
834
- const { id } = req.body
835
-
836
- if (!id) {
837
- return res.status(400).json({ success: false, error: '缺少编排ID参数' })
838
- }
839
-
840
- const config = await configManager.loadConfig()
841
-
842
- if (Array.isArray(config.orchestrations)) {
843
- const index = config.orchestrations.findIndex(orch => orch.id === id)
844
- if (index !== -1) {
845
- config.orchestrations.splice(index, 1)
846
- await configManager.saveConfig(config)
847
- }
848
- }
849
-
850
- res.json({ success: true })
851
- } catch (error) {
852
- res.status(500).json({ success: false, error: error.message })
853
- }
854
- })
855
-
856
- // 更新指令编排
857
- app.post('/api/config/update-orchestration', express.json(), async (req, res) => {
858
- try {
859
- const { id, orchestration } = req.body
860
-
861
- if (!id || !orchestration || !orchestration.name) {
862
- return res.status(400).json({ success: false, error: '缺少必要参数' })
863
- }
864
-
865
- const config = await configManager.loadConfig()
866
-
867
- if (Array.isArray(config.orchestrations)) {
868
- const index = config.orchestrations.findIndex(orch => orch.id === id)
869
- if (index !== -1) {
870
- config.orchestrations[index] = {
871
- id,
872
- name: orchestration.name,
873
- description: orchestration.description || '',
874
- flowData: orchestration.flowData || null
875
- }
876
- await configManager.saveConfig(config)
877
- } else {
878
- return res.status(404).json({ success: false, error: '未找到指定编排' })
879
- }
880
- } else {
881
- return res.status(404).json({ success: false, error: '编排列表不存在' })
882
- }
883
-
884
- res.json({ success: true })
885
- } catch (error) {
886
- res.status(500).json({ success: false, error: error.message })
887
- }
888
- })
889
-
890
- // 保存项目启动项
891
- app.post('/api/config/save-startup-items', express.json(), async (req, res) => {
892
- try {
893
- const { startupItems, startupAutoRun } = req.body
894
-
895
- if (!Array.isArray(startupItems)) {
896
- return res.status(400).json({ success: false, error: '启动项必须是数组' })
897
- }
898
-
899
- const config = await configManager.loadConfig()
900
- config.startupItems = startupItems
901
- if (typeof startupAutoRun === 'boolean') {
902
- config.startupAutoRun = startupAutoRun
903
- }
904
- await configManager.saveConfig(config)
905
-
906
- res.json({ success: true })
907
- } catch (error) {
908
- res.status(500).json({ success: false, error: error.message })
909
- }
910
- })
911
-
912
- // 保存提交设置
913
- app.post('/api/config/save-commit-settings', express.json(), async (req, res) => {
914
- try {
915
- const { isStandardCommit, skipHooks, autoQuickPushOnEnter, autoSetDefaultMessage, autoClosePushModal, pullBeforePush } = req.body
916
- const config = await configManager.loadConfig()
917
- if (isStandardCommit !== undefined) config.isStandardCommit = Boolean(isStandardCommit)
918
- if (skipHooks !== undefined) config.skipHooks = Boolean(skipHooks)
919
- if (autoQuickPushOnEnter !== undefined) config.autoQuickPushOnEnter = Boolean(autoQuickPushOnEnter)
920
- if (autoSetDefaultMessage !== undefined) config.autoSetDefaultMessage = Boolean(autoSetDefaultMessage)
921
- if (autoClosePushModal !== undefined) config.autoClosePushModal = Boolean(autoClosePushModal)
922
- if (pullBeforePush !== undefined) config.pullBeforePush = Boolean(pullBeforePush)
923
- await configManager.saveConfig(config)
924
- res.json({ success: true })
925
- } catch (error) {
926
- res.status(500).json({ success: false, error: error.message })
927
- }
928
- })
929
-
930
- // 保存"一键推送成功后启动项"
931
- app.post('/api/config/save-after-quick-push-action', express.json(), async (req, res) => {
932
- try {
933
- const { afterQuickPushAction } = req.body
934
-
935
- if (!afterQuickPushAction || typeof afterQuickPushAction !== 'object') {
936
- return res.status(400).json({ success: false, error: '缺少必要参数' })
937
- }
938
-
939
- const enabled = Boolean(afterQuickPushAction.enabled)
940
- const type = afterQuickPushAction.type === 'workflow' ? 'workflow' : 'command'
941
- const refId = String(afterQuickPushAction.refId || '').trim()
942
-
943
- const config = await configManager.loadConfig()
944
- config.afterQuickPushAction = {
945
- enabled,
946
- type,
947
- refId
948
- }
949
- await configManager.saveConfig(config)
950
-
951
- res.json({ success: true })
952
- } catch (error) {
953
- res.status(500).json({ success: false, error: error.message })
954
- }
955
- })
956
-
957
- // 保存通用设置(主题和语言)
958
- app.post('/api/config/save-general-settings', express.json(), async (req, res) => {
959
- try {
960
- const { theme, locale } = req.body
961
-
962
- // 读取原始配置以保留项目设置
963
- const rawConfig = await configManager.readRawConfigFile()
964
-
965
- // 更新全局设置
966
- if (theme && ['light', 'dark', 'auto'].includes(theme)) {
967
- rawConfig.theme = theme
968
- }
969
- if (locale && ['zh-CN', 'en-US'].includes(locale)) {
970
- rawConfig.locale = locale
971
- }
972
-
973
- // 直接写入原始配置,避免覆盖项目设置
974
- await configManager.writeRawConfigFile(rawConfig)
975
- res.json({ success: true })
976
- } catch (error) {
977
- res.status(500).json({ success: false, error: error.message })
978
- }
979
- })
980
-
981
- // 保存 UI 状态(视图模式/分割比例/控制台状态/布局比例/编辑器自动保存等)
982
- // 接受 partial body,浅合并到顶层 ui 对象。例:{ layout: {...} } / { commandConsole: {...} } / { fileListViewMode: 'tree' }
983
- app.post('/api/config/save-ui-settings', express.json(), async (req, res) => {
984
- try {
985
- const partial = req.body && typeof req.body === 'object' ? req.body : {}
986
-
987
- const rawConfig = await configManager.readRawConfigFile()
988
-
989
- // 确保 ui 容器存在
990
- if (!rawConfig.ui || typeof rawConfig.ui !== 'object' || Array.isArray(rawConfig.ui)) {
991
- rawConfig.ui = {}
992
- }
993
-
994
- // 浅合并顶层 ui 字段(支持嵌套对象整体替换,如 commandConsole)
995
- for (const key of Object.keys(partial)) {
996
- rawConfig.ui[key] = partial[key]
997
- }
998
-
999
- await configManager.writeRawConfigFile(rawConfig)
1000
- res.json({ success: true })
1001
- } catch (error) {
1002
- res.status(500).json({ success: false, error: error.message })
1003
- }
1004
- })
1005
-
1006
- // 保存 AI 模型配置(models 是全局配置,存在配置文件顶层,跨项目共享)
1007
- app.post('/api/config/save-models', express.json(), async (req, res) => {
1008
- try {
1009
- const { models } = req.body
1010
- if (!Array.isArray(models)) {
1011
- return res.status(400).json({ success: false, error: '缺少 models 参数' })
1012
- }
1013
- const rawConfig = await configManager.readRawConfigFile()
1014
- rawConfig.models = models
1015
- await configManager.writeRawConfigFile(rawConfig)
1016
- res.json({ success: true })
1017
- } catch (error) {
1018
- res.status(500).json({ success: false, error: error.message })
1019
- }
1020
- })
1021
-
1022
- // 测试 AI 模型是否可用
1023
- app.post('/api/config/test-model', express.json(), async (req, res) => {
1024
- const { baseURL, model, apiKey } = req.body || {}
1025
- if (!baseURL || !model) {
1026
- return res.status(400).json({ success: false, error: '缺少 baseURL 或 model 参数' })
1027
- }
1028
- try {
1029
- const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }))
1030
- const url = `${baseURL.replace(/\/$/, '')}/chat/completions`
1031
- const headers = { 'Content-Type': 'application/json' }
1032
- if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
1033
- const body = JSON.stringify({
1034
- model,
1035
- messages: [{ role: 'user', content: 'Hello, reply with just "ok".' }],
1036
- max_tokens: 16,
1037
- stream: false
1038
- })
1039
- const controller = new AbortController()
1040
- const timer = setTimeout(() => controller.abort(), 15000)
1041
- let response
1042
- try {
1043
- response = await fetch(url, { method: 'POST', headers, body, signal: controller.signal })
1044
- } finally {
1045
- clearTimeout(timer)
1046
- }
1047
- const data = await response.json().catch(() => ({}))
1048
- if (!response.ok) {
1049
- const msg = data?.error?.message || data?.message || `HTTP ${response.status}`
1050
- return res.json({ success: false, error: msg })
1051
- }
1052
- const reply = data?.choices?.[0]?.message?.content || data?.choices?.[0]?.text || '✅'
1053
- res.json({ success: true, reply: reply.trim().slice(0, 100) })
1054
- } catch (error) {
1055
- const msg = error.name === 'AbortError' ? '请求超时(15s)' : error.message
1056
- res.json({ success: false, error: msg })
1057
- }
1058
- })
1059
-
1060
- // AI 生成提交信息
1061
- app.post('/api/config/generate-commit-message', express.json(), async (req, res) => {
1062
- const { fileList } = req.body || {}
1063
- try {
1064
- const rawConfig = await configManager.readRawConfigFile()
1065
- const models = Array.isArray(rawConfig.models) ? rawConfig.models : []
1066
- const defaultModel = models.find(m => m.isDefault) || models[0]
1067
- if (!defaultModel) {
1068
- return res.json({ success: false, error: '未配置 AI 模型,请先在通用设置中添加模型' })
1069
- }
1070
-
1071
- // 后端自己收集 diff,确保 untracked 文件也能进 prompt
1072
- const { diff: rawDiff, fileList: serverFileList } = await collectDiffForAi({ execGitCommand, getCurrentProjectPath })
1073
- const safeFileList = Array.isArray(fileList) && fileList.length > 0 ? fileList : serverFileList
1074
- const diffText = prepareDiffForPrompt(rawDiff, safeFileList)
1075
- const filesText = safeFileList.slice(0, 30).join('\n')
1076
- const prompt = `你是一个 Git 提交信息生成助手。根据以下 git diff 信息,生成一条符合 Conventional Commits 规范的提交信息。
1077
-
1078
- 要求:
1079
- 1. type 只能是:feat/fix/docs/style/refactor/test/chore 之一
1080
- 2. scope 可选,表示影响范围,简短英文或中文,如果改动范围明确就填
1081
- 3. description 用中文简短描述本次变更(不超过50字)
1082
- 4. 只返回 JSON,格式:{"type":"feat","scope":"","description":"xxx"}
1083
-
1084
- 变更文件:
1085
- ${filesText}
1086
-
1087
- git diff --staged:
1088
- ${diffText || '(无 staged 内容,请根据文件列表推断)'}`
1089
-
1090
- console.log('[generate-commit] ===== PROMPT START (length: ' + prompt.length + ') =====')
1091
- console.log(prompt)
1092
- console.log('[generate-commit] ===== PROMPT END =====')
1093
-
1094
- const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }))
1095
- const url = `${defaultModel.baseURL.replace(/\/$/, '')}/chat/completions`
1096
- const headers = { 'Content-Type': 'application/json' }
1097
- if (defaultModel.apiKey) headers['Authorization'] = `Bearer ${defaultModel.apiKey}`
1098
- const body = JSON.stringify({
1099
- model: defaultModel.model,
1100
- messages: [{ role: 'user', content: prompt }],
1101
- max_tokens: 1024,
1102
- temperature: 0.3,
1103
- stream: false
1104
- })
1105
- const controller = new AbortController()
1106
- const timer = setTimeout(() => controller.abort(), 30000)
1107
- let response
1108
- try {
1109
- response = await fetch(url, { method: 'POST', headers, body, signal: controller.signal })
1110
- } finally {
1111
- clearTimeout(timer)
1112
- }
1113
- const data = await response.json().catch(() => ({}))
1114
- if (!response.ok) {
1115
- const msg = data?.error?.message || data?.message || `HTTP ${response.status}`
1116
- return res.json({ success: false, error: msg })
1117
- }
1118
- const content = data?.choices?.[0]?.message?.content || ''
1119
- console.log('[generate-commit] raw content length:', content.length, JSON.stringify(content).slice(0, 600))
1120
-
1121
- const codeBlockMatch = content.match(/```(?:json)?\s*(\{[^`]*?\})\s*```/)
1122
- const jsonMatch = codeBlockMatch
1123
- ? [codeBlockMatch[1]]
1124
- : [...content.matchAll(/\{[^{}]*\}/g)].at(-1)
1125
-
1126
- if (!jsonMatch) {
1127
- console.error('[generate-commit] no JSON found, full content:', content)
1128
- return res.json({ success: false, error: 'model returned no valid JSON' })
1129
- }
1130
- let parsed
1131
- try {
1132
- parsed = JSON.parse(jsonMatch[0])
1133
- } catch {
1134
- const typeM = jsonMatch[0].match(/"type"\s*:\s*"([^"]+)"/)
1135
- const scopeM = jsonMatch[0].match(/"scope"\s*:\s*"([^"]*)"/)
1136
- const descM = jsonMatch[0].match(/"description"\s*:\s*"([^"]+)"/)
1137
- if (typeM || descM) {
1138
- return res.json({
1139
- success: true,
1140
- type: (typeM?.[1] || 'feat').trim(),
1141
- scope: (scopeM?.[1] || '').trim(),
1142
- description: (descM?.[1] || '').trim()
1143
- })
1144
- }
1145
- return res.json({ success: false, error: 'JSON parse failed' })
1146
- }
1147
- res.json({
1148
- success: true,
1149
- type: String(parsed.type || 'feat').trim(),
1150
- scope: String(parsed.scope || '').trim(),
1151
- description: String(parsed.description || '').trim()
1152
- })
1153
- } catch (error) {
1154
- const msg = error.name === 'AbortError' ? 'timeout 30s' : error.message
1155
- res.json({ success: false, error: msg })
1156
- }
1157
- })
1158
- }
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ import express from 'express';
16
+ import path from 'path';
17
+ import os from 'os';
18
+ import fs from 'fs/promises';
19
+ import open from 'open';
20
+
21
+ // 跳过的产物/资源/lock 文件(用 stat 一行带过,不打 patch)
22
+ const SKIP_FILE_PATTERNS = [
23
+ /(^|[\\/])dist[\\/]/,
24
+ /(^|[\\/])build[\\/]/,
25
+ /(^|[\\/])\.next[\\/]/,
26
+ /(^|[\\/])coverage[\\/]/,
27
+ /(^|[\\/])node_modules[\\/]/,
28
+ /\.lock$/,
29
+ /package-lock\.json$/,
30
+ /yarn\.lock$/,
31
+ /pnpm-lock\.yaml$/,
32
+ /min\.js$/,
33
+ /min\.css$/,
34
+ /\.bundle\.js$/,
35
+ /\.map$/,
36
+ /\.(png|jpe?g|gif|webp|svg|ico|bmp|tiff)$/,
37
+ /\.(mp4|mov|avi|mkv|webm)$/,
38
+ /\.(mp3|wav|ogg|flac)$/,
39
+ /\.(woff2?|ttf|otf|eot)$/,
40
+ /\.(pdf|zip|tar|gz|7z|rar)$/
41
+ ]
42
+
43
+ // 文件优先级:数字越大越重要
44
+ function filePriority(p) {
45
+ if (/\.(test|spec)\.(js|ts|jsx|tsx|vue)$/i.test(p)) return 1 // 测试文件
46
+ if (/^docs?\//i.test(p) || /\.md$/i.test(p)) return 2 // 文档
47
+ if (/\.(json|ya?ml|toml|env|ini|cfg|conf)$/i.test(p)) return 3 // 配置
48
+ if (/\.(css|scss|sass|less|html|vue|jsx|tsx)$/i.test(p)) return 4 // 前端
49
+ if (/\.(ts|js|mjs|cjs)$/i.test(p)) return 5 // 后端/脚本
50
+ return 3 // 其它(资源、未知)
51
+ }
52
+
53
+ function isSkippedFile(path) {
54
+ return SKIP_FILE_PATTERNS.some(re => re.test(path))
55
+ }
56
+
57
+ // git diff 文本解析出 per-file 块
58
+ // git diff 输出格式: "diff --git a/path b/path\n...一系列 header...\n--- a/path\n+++ b/path\n@@ ...\n<patch>"
59
+ function parseDiffByFile(diffText) {
60
+ if (!diffText) return []
61
+ const files = []
62
+ // "diff --git " 切分,首段可能为空(文本以这个开头则第一段空)
63
+ const parts = diffText.split(/^diff --git /m).filter(Boolean)
64
+ for (const part of parts) {
65
+ const headerEnd = part.indexOf('\n')
66
+ const headerLine = (headerEnd >= 0 ? part.slice(0, headerEnd) : part).trim()
67
+ // 从 header 里抓路径: "a/path b/path" -> 优先 b
68
+ const m = headerLine.match(/^a\/(.+?)\s+b\/(.+)$/)
69
+ if (!m) continue
70
+ const bPath = m[2]
71
+ const patch = part.slice(headerEnd + 1)
72
+ // 统计 +/- 行数
73
+ let added = 0, removed = 0
74
+ for (const line of patch.split('\n')) {
75
+ if (line.startsWith('+') && !line.startsWith('+++')) added++
76
+ else if (line.startsWith('-') && !line.startsWith('---')) removed++
77
+ }
78
+ files.push({ path: bPath, patch, added, removed })
79
+ }
80
+ return files
81
+ }
82
+
83
+ // 收集 AI 生成提交信息所需的 diff 和文件列表
84
+ // 关键: untracked 文件默认不会出现在 git diff --staged 输出里,
85
+ // 所以先对所有 untracked 文件做 git add -N(intent to add),
86
+ // 这样它们会以 "new file" 形式出现在 diff
87
+ async function collectDiffForAi({ execGitCommand, getCurrentProjectPath }) {
88
+ if (typeof execGitCommand !== 'function') {
89
+ return { diff: '', fileList: [] }
90
+ }
91
+ const projectPath = typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : ''
92
+ const cwdOpts = projectPath ? { cwd: projectPath, log: false } : { log: false }
93
+
94
+ let fileList = []
95
+ try {
96
+ // 1. 拿到工作区状态,识别 untracked 文件
97
+ const { stdout: statusOut } = await execGitCommand('git status --porcelain=1 --untracked-files=all', cwdOpts)
98
+ const untracked = []
99
+ const trackedChanges = []
100
+ for (const line of (statusOut || '').split('\n')) {
101
+ if (!line || line.length < 3) continue
102
+ // porcelain=1 格式: "XY path" X=index状态, Y=worktree状态
103
+ const x = line[0]
104
+ const y = line[1]
105
+ const path = line.slice(3)
106
+ if (x === '?' && y === '?') {
107
+ untracked.push(path)
108
+ } else if (x !== ' ' || y !== ' ') {
109
+ // 有改动(暂存或工作区)
110
+ trackedChanges.push(`${x !== ' ' ? x : ' '} ${path}`.trim())
111
+ }
112
+ }
113
+ fileList = [...trackedChanges, ...untracked.map(p => `? ${p}`)]
114
+
115
+ // 2. 对 untracked 文件做 intent to add(只在内存中,不会真的暂存内容)
116
+ if (untracked.length > 0) {
117
+ // 加上 --force 以防某些文件已经在 index 中
118
+ // 分批处理,避免命令行过长
119
+ const batchSize = 20
120
+ for (let i = 0; i < untracked.length; i += batchSize) {
121
+ const batch = untracked.slice(i, i + batchSize)
122
+ const quoted = batch.map(p => `"${p.replace(/"/g, '\\"')}"`).join(' ')
123
+ try {
124
+ await execGitCommand(`git add -N --force ${quoted}`, cwdOpts)
125
+ } catch (e) {
126
+ // 单批失败不影响整体
127
+ console.warn('[generate-commit] git add -N failed for batch:', e?.message)
128
+ }
129
+ }
130
+ }
131
+
132
+ // 3. 合并 staged + unstaged diff
133
+ // 用 --no-color 避免 ANSI 干扰, --no-ext-diff 避免外接 diff 工具
134
+ const [stagedRes, unstagedRes] = await Promise.all([
135
+ execGitCommand('git diff --staged --no-color --no-ext-diff', cwdOpts).catch(() => ({ stdout: '' })),
136
+ execGitCommand('git diff --no-color --no-ext-diff', cwdOpts).catch(() => ({ stdout: '' }))
137
+ ])
138
+ let combined = ''
139
+ if (stagedRes?.stdout) combined += stagedRes.stdout.trim() + '\n'
140
+ if (unstagedRes?.stdout) combined += unstagedRes.stdout.trim() + '\n'
141
+
142
+ return { diff: combined.trim(), fileList }
143
+ } catch (error) {
144
+ console.error('[generate-commit] collectDiffForAi error:', error?.message)
145
+ return { diff: '', fileList }
146
+ }
147
+ }
148
+
149
+ // diff 压缩成给 LLM 的紧凑文本
150
+ // 策略: 跳过产物/资源文件(用一行 stat 带过),源码按优先级排序,每个文件 patch 限 1500 字,总预算 6000 字
151
+ function prepareDiffForPrompt(diffText, fileList) {
152
+ const safeFileList = Array.isArray(fileList) ? fileList : []
153
+ let files = parseDiffByFile(diffText)
154
+
155
+ // 如果客户端明确指定了文件列表,则只保留与所选文件匹配的 diff 块
156
+ if (safeFileList.length > 0 && files.length > 0) {
157
+ const selectedPaths = new Set(
158
+ safeFileList
159
+ .map(s => {
160
+ if (typeof s !== 'string') return ''
161
+ // fileList 形如 "M src/foo.ts" 或 "? new-file.ts",取最后的路径部分
162
+ const m = s.match(/^[A-Z?\s]+\s+(.+)$/)
163
+ return (m ? m[1] : s).replace(/\\/g, '/')
164
+ })
165
+ .filter(Boolean)
166
+ )
167
+ if (selectedPaths.size > 0) {
168
+ files = files.filter(f => selectedPaths.has(f.path.replace(/\\/g, '/')))
169
+ }
170
+ }
171
+
172
+ // 如果 parse 出来是空的(diff 可能是空或非标准格式),退回到 fileList 推断
173
+ if (files.length === 0) {
174
+ const list = safeFileList.slice(0, 30).map(s => {
175
+ // fileList 形如 "M src/foo.ts" 或 "? new-file.ts"
176
+ const m = s.match(/^[A-Z?\s]+\s+(.+)$/)
177
+ return m ? m[1] : s
178
+ })
179
+ if (list.length === 0) return ''
180
+ return list.map(p => {
181
+ if (isSkippedFile(p)) return `${p} [产物/资源,已跳过]`
182
+ return p
183
+ }).join('\n')
184
+ }
185
+
186
+ // 拆分: 跳过的 vs 保留的
187
+ const skipped = []
188
+ const kept = []
189
+ for (const f of files) {
190
+ if (isSkippedFile(f.path)) {
191
+ skipped.push(f)
192
+ } else {
193
+ kept.push(f)
194
+ }
195
+ }
196
+
197
+ // 保留的文件按优先级降序,同优先级按 +/- 总数降序(改得多的优先)
198
+ kept.sort((a, b) => {
199
+ const dp = filePriority(b.path) - filePriority(a.path)
200
+ if (dp !== 0) return dp
201
+ return (b.added + b.removed) - (a.added + a.removed)
202
+ })
203
+
204
+ const TOTAL_BUDGET = 6000
205
+ const PER_FILE_LIMIT = 1500
206
+ const lines = []
207
+ let budget = TOTAL_BUDGET
208
+
209
+ // 先把跳过的文件用一行 stat 总结
210
+ for (const f of skipped) {
211
+ if (budget < 80) break
212
+ lines.push(`${f.path} [+${f.added}/-${f.removed}, 已跳过产物/资源]`)
213
+ budget -= 80
214
+ }
215
+
216
+ // 再装源码,带预算控制
217
+ for (const f of kept) {
218
+ if (budget <= 50) break
219
+ let patch = f.patch
220
+ let truncated = false
221
+ if (patch.length > PER_FILE_LIMIT) {
222
+ patch = patch.slice(0, PER_FILE_LIMIT) + '\n... (diff 已截断)'
223
+ truncated = true
224
+ }
225
+ const block = `--- ${f.path} (+${f.added}/-${f.removed})${truncated ? ' [截断]' : ''}\n${patch}`
226
+ if (block.length > budget) {
227
+ // 单个文件塞不下了,截到能塞下为止
228
+ const sliceLen = Math.max(0, budget - 80)
229
+ if (sliceLen < 100) break
230
+ lines.push(`--- ${f.path} (+${f.added}/-${f.removed}) [预算耗尽,已截断]\n${patch.slice(0, sliceLen)}`)
231
+ budget = 0
232
+ break
233
+ }
234
+ lines.push(block)
235
+ budget -= block.length
236
+ }
237
+
238
+ if (lines.length === 0) return ''
239
+ return lines.join('\n\n')
240
+ }
241
+
242
+ export { prepareDiffForPrompt, parseDiffByFile, isSkippedFile, filePriority, collectDiffForAi }
243
+
244
+ export function registerConfigRoutes({
245
+ app,
246
+ configManager,
247
+ execGitCommand,
248
+ getCurrentProjectPath
249
+ }) {
250
+ // 保存最近访问的目录
251
+ app.post('/api/save_recent_directory', async (req, res) => {
252
+ try {
253
+ const { path } = req.body;
254
+
255
+ if (!path) {
256
+ return res.status(400).json({
257
+ success: false,
258
+ error: '目录路径不能为空'
259
+ });
260
+ }
261
+
262
+ // 保存到配置
263
+ await configManager.saveRecentDirectory(path);
264
+
265
+ res.json({
266
+ success: true
267
+ });
268
+ } catch (error) {
269
+ res.status(500).json({
270
+ success: false,
271
+ error: error.message
272
+ });
273
+ }
274
+ });
275
+
276
+ // 删除最近访问的目录
277
+ app.post('/api/remove_recent_directory', async (req, res) => {
278
+ try {
279
+ const { path: dirPath } = req.body;
280
+ if (!dirPath) {
281
+ return res.status(400).json({ success: false, error: '目录路径不能为空' });
282
+ }
283
+ const list = await configManager.removeRecentDirectory(dirPath);
284
+ res.json({ success: true, directories: list });
285
+ } catch (error) {
286
+ res.status(500).json({ success: false, error: error.message });
287
+ }
288
+ });
289
+
290
+ // 获取配置
291
+ app.get('/api/config/getConfig', async (req, res) => {
292
+ try {
293
+ // console.log('获取配置中。。。')
294
+ const config = await configManager.loadConfig()
295
+
296
+ // 兼容旧数据:补齐自定义命令 id,避免前端编辑/删除/编排引用异常
297
+ if (Array.isArray(config.customCommands)) {
298
+ let changed = false
299
+ config.customCommands = config.customCommands.map((cmd) => {
300
+ if (cmd && typeof cmd === 'object' && !cmd.id) {
301
+ changed = true
302
+ return {
303
+ ...cmd,
304
+ id: `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
305
+ }
306
+ }
307
+ return cmd
308
+ })
309
+ if (changed) {
310
+ await configManager.saveConfig(config)
311
+ }
312
+ }
313
+
314
+ // 初始化命令模板(首次安装/旧配置兼容)
315
+ if (!Array.isArray(config.commandTemplates)) {
316
+ config.commandTemplates = []
317
+ }
318
+
319
+ if (config.commandTemplates.length === 0) {
320
+ config.commandTemplates = [
321
+ 'echo "{{cmd}}"',
322
+ 'npm run dev',
323
+ 'npm run build',
324
+ 'git status',
325
+ 'git add .',
326
+ 'git commit -m "{{message}}" --no-verify',
327
+ 'git push',
328
+ ]
329
+ await configManager.saveConfig(config)
330
+ }
331
+
332
+ // console.log('获取配置成功')
333
+ res.json(config)
334
+ } catch (error) {
335
+ // console.log('获取配置失败')
336
+ const configPath = path.join(os.homedir(), '.git-commit-tool.json')
337
+ res.status(500).json({
338
+ success: false,
339
+ code: 'CONFIG_LOAD_FAILED',
340
+ error: error?.message || String(error),
341
+ configPath
342
+ })
343
+ }
344
+ })
345
+
346
+ // 保存默认提交信息
347
+ app.post('/api/config/saveDefaultMessage', express.json(), async (req, res) => {
348
+ try {
349
+ const { defaultCommitMessage } = req.body
350
+
351
+ if (!defaultCommitMessage) {
352
+ return res.status(400).json({ success: false, error: '缺少必要参数' })
353
+ }
354
+
355
+ const config = await configManager.loadConfig()
356
+
357
+ // 更新默认提交信息
358
+ config.defaultCommitMessage = defaultCommitMessage
359
+ await configManager.saveConfig(config)
360
+
361
+ res.json({ success: true })
362
+ } catch (error) {
363
+ res.status(500).json({ success: false, error: error.message })
364
+ }
365
+ })
366
+
367
+ // 保存所有配置
368
+ app.post('/api/config/saveAll', express.json(), async (req, res) => {
369
+ try {
370
+ const { config } = req.body
371
+
372
+ if (!config) {
373
+ return res.status(400).json({ success: false, error: '缺少必要参数' })
374
+ }
375
+
376
+ await configManager.saveConfig(config)
377
+
378
+ res.json({ success: true })
379
+ } catch (error) {
380
+ res.status(500).json({ success: false, error: error.message })
381
+ }
382
+ })
383
+
384
+ // 检查系统配置文件格式
385
+ app.get('/api/config/check-file-format', async (req, res) => {
386
+ try {
387
+ const configPath = path.join(os.homedir(), '.git-commit-tool.json');
388
+
389
+ try {
390
+ const data = await fs.readFile(configPath, 'utf-8');
391
+ try {
392
+ JSON.parse(data);
393
+ res.json({ success: true, isValidJson: true, exists: true });
394
+ } catch (parseError) {
395
+ res.json({
396
+ success: true,
397
+ isValidJson: false,
398
+ exists: true,
399
+ error: `JSON解析失败: ${parseError.message}`
400
+ });
401
+ }
402
+ } catch (fileError) {
403
+ if (fileError.code === 'ENOENT') {
404
+ res.json({ success: true, isValidJson: true, exists: false });
405
+ } else {
406
+ res.json({
407
+ success: true,
408
+ isValidJson: false,
409
+ exists: true,
410
+ error: `文件读取失败: ${fileError.message}`
411
+ });
412
+ }
413
+ }
414
+ } catch (error) {
415
+ res.status(500).json({ success: false, error: error.message });
416
+ }
417
+ })
418
+
419
+ // 使用系统默认程序打开配置文件 ~/.git-commit-tool.json
420
+ app.post('/api/config/open-file', async (req, res) => {
421
+ try {
422
+ const filePath = path.join(os.homedir(), '.git-commit-tool.json');
423
+ try {
424
+ // 检查文件是否存在,不存在也尝试让系统创建(可能会打开空文件)
425
+ await fs.access(filePath);
426
+ } catch (_) {
427
+ // 如果文件不存在,先创建一个最小结构,避免某些系统无法打开不存在的路径
428
+ try {
429
+ await fs.writeFile(filePath, '{}', 'utf-8');
430
+ } catch (e) {
431
+ // 创建失败不阻断打开尝试
432
+ console.warn('创建配置文件失败(可忽略):', e?.message || e);
433
+ }
434
+ }
435
+
436
+ await open(filePath, { wait: false });
437
+ res.json({ success: true })
438
+ } catch (error) {
439
+ res.status(400).json({ success: false, error: `无法打开配置文件: ${error.message}` })
440
+ }
441
+ })
442
+
443
+ // 保存模板
444
+ app.post('/api/config/save-template', express.json(), async (req, res) => {
445
+ try {
446
+ const { template, type } = req.body
447
+
448
+ if (!template || !type) {
449
+ return res.status(400).json({ success: false, error: '缺少必要参数' })
450
+ }
451
+
452
+ const config = await configManager.loadConfig()
453
+
454
+ if (type === 'description') {
455
+ // 确保描述模板数组存在
456
+ if (!config.descriptionTemplates) {
457
+ config.descriptionTemplates = []
458
+ }
459
+
460
+ // 检查是否已存在相同模板
461
+ if (!config.descriptionTemplates.includes(template)) {
462
+ config.descriptionTemplates.push(template)
463
+ await configManager.saveConfig(config)
464
+ }
465
+ } else if (type === 'scope') {
466
+ // 确保作用域模板数组存在
467
+ if (!config.scopeTemplates) {
468
+ config.scopeTemplates = []
469
+ }
470
+
471
+ // 检查是否已存在相同模板
472
+ if (!config.scopeTemplates.includes(template)) {
473
+ config.scopeTemplates.push(template)
474
+ await configManager.saveConfig(config)
475
+ }
476
+ } else if (type === 'message') {
477
+ // 确保提交信息模板数组存在
478
+ if (!config.messageTemplates) {
479
+ config.messageTemplates = []
480
+ }
481
+
482
+ // 检查是否已存在相同模板
483
+ if (!config.messageTemplates.includes(template)) {
484
+ config.messageTemplates.push(template)
485
+ await configManager.saveConfig(config)
486
+ }
487
+ } else if (type === 'command') {
488
+ if (!config.commandTemplates) {
489
+ config.commandTemplates = []
490
+ }
491
+ if (!config.commandTemplates.includes(template)) {
492
+ config.commandTemplates.push(template)
493
+ await configManager.saveConfig(config)
494
+ }
495
+ } else {
496
+ return res.status(400).json({ success: false, error: '不支持的模板类型' })
497
+ }
498
+
499
+ res.json({ success: true })
500
+ } catch (error) {
501
+ res.status(500).json({ success: false, error: error.message })
502
+ }
503
+ })
504
+
505
+ // 删除模板
506
+ app.post('/api/config/delete-template', express.json(), async (req, res) => {
507
+ try {
508
+ const { template, type } = req.body
509
+
510
+ if (!template || !type) {
511
+ return res.status(400).json({ success: false, error: '缺少必要参数' })
512
+ }
513
+
514
+ const config = await configManager.loadConfig()
515
+
516
+ if (type === 'description') {
517
+ // 确保描述模板数组存在
518
+ if (config.descriptionTemplates) {
519
+ const index = config.descriptionTemplates.indexOf(template)
520
+ if (index !== -1) {
521
+ config.descriptionTemplates.splice(index, 1)
522
+ await configManager.saveConfig(config)
523
+ }
524
+ }
525
+ } else if (type === 'scope') {
526
+ // 确保作用域模板数组存在
527
+ if (config.scopeTemplates) {
528
+ const index = config.scopeTemplates.indexOf(template)
529
+ if (index !== -1) {
530
+ config.scopeTemplates.splice(index, 1)
531
+ await configManager.saveConfig(config)
532
+ }
533
+ }
534
+ } else if (type === 'message') {
535
+ // 确保提交信息模板数组存在
536
+ if (config.messageTemplates) {
537
+ const index = config.messageTemplates.indexOf(template)
538
+ if (index !== -1) {
539
+ config.messageTemplates.splice(index, 1)
540
+ await configManager.saveConfig(config)
541
+ }
542
+ }
543
+ } else if (type === 'command') {
544
+ if (config.commandTemplates) {
545
+ const index = config.commandTemplates.indexOf(template)
546
+ if (index !== -1) {
547
+ config.commandTemplates.splice(index, 1)
548
+ await configManager.saveConfig(config)
549
+ }
550
+ }
551
+ } else {
552
+ return res.status(400).json({ success: false, error: '不支持的模板类型' })
553
+ }
554
+
555
+ res.json({ success: true })
556
+ } catch (error) {
557
+ res.status(500).json({ success: false, error: error.message })
558
+ }
559
+ })
560
+
561
+ // 更新模板
562
+ app.post('/api/config/update-template', express.json(), async (req, res) => {
563
+ try {
564
+ const { oldTemplate, newTemplate, type } = req.body
565
+
566
+ if (!oldTemplate || !newTemplate || !type) {
567
+ return res.status(400).json({ success: false, error: '缺少必要参数' })
568
+ }
569
+
570
+ const config = await configManager.loadConfig()
571
+
572
+ if (type === 'description') {
573
+ // 确保描述模板数组存在
574
+ if (config.descriptionTemplates) {
575
+ const index = config.descriptionTemplates.indexOf(oldTemplate)
576
+ if (index !== -1) {
577
+ config.descriptionTemplates[index] = newTemplate
578
+ await configManager.saveConfig(config)
579
+ } else {
580
+ return res.status(404).json({ success: false, error: '未找到原模板' })
581
+ }
582
+ } else {
583
+ return res.status(404).json({ success: false, error: '模板列表不存在' })
584
+ }
585
+ } else if (type === 'scope') {
586
+ // 确保作用域模板数组存在
587
+ if (config.scopeTemplates) {
588
+ const index = config.scopeTemplates.indexOf(oldTemplate)
589
+ if (index !== -1) {
590
+ config.scopeTemplates[index] = newTemplate
591
+ await configManager.saveConfig(config)
592
+ } else {
593
+ return res.status(404).json({ success: false, error: '未找到原模板' })
594
+ }
595
+ } else {
596
+ return res.status(404).json({ success: false, error: '模板列表不存在' })
597
+ }
598
+ } else if (type === 'message') {
599
+ // 确保提交信息模板数组存在
600
+ if (config.messageTemplates) {
601
+ const index = config.messageTemplates.indexOf(oldTemplate)
602
+ if (index !== -1) {
603
+ config.messageTemplates[index] = newTemplate
604
+ await configManager.saveConfig(config)
605
+ } else {
606
+ return res.status(404).json({ success: false, error: '未找到原模板' })
607
+ }
608
+ } else {
609
+ return res.status(404).json({ success: false, error: '模板列表不存在' })
610
+ }
611
+ } else if (type === 'command') {
612
+ if (config.commandTemplates) {
613
+ const index = config.commandTemplates.indexOf(oldTemplate)
614
+ if (index !== -1) {
615
+ config.commandTemplates[index] = newTemplate
616
+ await configManager.saveConfig(config)
617
+ } else {
618
+ return res.status(404).json({ success: false, error: '未找到原模板' })
619
+ }
620
+ } else {
621
+ return res.status(404).json({ success: false, error: '模板列表不存在' })
622
+ }
623
+ } else {
624
+ return res.status(400).json({ success: false, error: '不支持的模板类型' })
625
+ }
626
+
627
+ res.json({ success: true })
628
+ } catch (error) {
629
+ res.status(500).json({ success: false, error: error.message })
630
+ }
631
+ })
632
+
633
+ // 置顶模板
634
+ app.post('/api/config/pin-template', express.json(), async (req, res) => {
635
+ try {
636
+ const { template, type } = req.body
637
+
638
+ if (!template || !type) {
639
+ return res.status(400).json({ success: false, error: '缺少必要参数' })
640
+ }
641
+
642
+ const config = await configManager.loadConfig()
643
+
644
+ if (type === 'description') {
645
+ if (config.descriptionTemplates) {
646
+ // 删除原位置的模板
647
+ config.descriptionTemplates = config.descriptionTemplates.filter(t => t !== template)
648
+ // 添加到第一位
649
+ config.descriptionTemplates.unshift(template)
650
+ await configManager.saveConfig(config)
651
+ } else {
652
+ return res.status(404).json({ success: false, error: '模板列表不存在' })
653
+ }
654
+ } else if (type === 'scope') {
655
+ if (config.scopeTemplates) {
656
+ config.scopeTemplates = config.scopeTemplates.filter(t => t !== template)
657
+ config.scopeTemplates.unshift(template)
658
+ await configManager.saveConfig(config)
659
+ } else {
660
+ return res.status(404).json({ success: false, error: '模板列表不存在' })
661
+ }
662
+ } else if (type === 'message') {
663
+ if (config.messageTemplates) {
664
+ config.messageTemplates = config.messageTemplates.filter(t => t !== template)
665
+ config.messageTemplates.unshift(template)
666
+ await configManager.saveConfig(config)
667
+ } else {
668
+ return res.status(404).json({ success: false, error: '模板列表不存在' })
669
+ }
670
+ } else if (type === 'command') {
671
+ if (config.commandTemplates) {
672
+ config.commandTemplates = config.commandTemplates.filter(t => t !== template)
673
+ config.commandTemplates.unshift(template)
674
+ await configManager.saveConfig(config)
675
+ } else {
676
+ return res.status(404).json({ success: false, error: '模板列表不存在' })
677
+ }
678
+ } else {
679
+ return res.status(400).json({ success: false, error: '不支持的模板类型' })
680
+ }
681
+
682
+ res.json({ success: true })
683
+ } catch (error) {
684
+ res.status(500).json({ success: false, error: error.message })
685
+ }
686
+ })
687
+
688
+ // 保存自定义命令
689
+ app.post('/api/config/save-custom-command', express.json(), async (req, res) => {
690
+ try {
691
+ const { command } = req.body
692
+
693
+ if (!command || !command.name || !command.command) {
694
+ return res.status(400).json({ success: false, error: '缺少必要参数' })
695
+ }
696
+
697
+ const config = await configManager.loadConfig()
698
+
699
+ // 确保自定义命令数组存在
700
+ if (!Array.isArray(config.customCommands)) {
701
+ config.customCommands = []
702
+ }
703
+
704
+ // 生成唯一ID
705
+ const id = `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
706
+ const newCommand = {
707
+ id,
708
+ name: command.name,
709
+ description: command.description || '',
710
+ directory: command.directory || '',
711
+ command: command.command,
712
+ params: Array.isArray(command.params) ? command.params : []
713
+ }
714
+
715
+ config.customCommands.push(newCommand)
716
+ await configManager.saveConfig(config)
717
+
718
+ res.json({ success: true, command: newCommand })
719
+ } catch (error) {
720
+ res.status(500).json({ success: false, error: error.message })
721
+ }
722
+ })
723
+
724
+ // 删除自定义命令
725
+ app.post('/api/config/delete-custom-command', express.json(), async (req, res) => {
726
+ try {
727
+ const { id } = req.body
728
+
729
+ if (!id) {
730
+ return res.status(400).json({ success: false, error: '缺少命令ID参数' })
731
+ }
732
+
733
+ const config = await configManager.loadConfig()
734
+
735
+ if (Array.isArray(config.customCommands)) {
736
+ const index = config.customCommands.findIndex(cmd => cmd.id === id)
737
+ if (index !== -1) {
738
+ config.customCommands.splice(index, 1)
739
+ await configManager.saveConfig(config)
740
+ }
741
+ }
742
+
743
+ res.json({ success: true })
744
+ } catch (error) {
745
+ res.status(500).json({ success: false, error: error.message })
746
+ }
747
+ })
748
+
749
+ // 置顶自定义命令(移到数组首位)
750
+ app.post('/api/config/pin-custom-command', express.json(), async (req, res) => {
751
+ try {
752
+ const { id } = req.body
753
+
754
+ if (!id) {
755
+ return res.status(400).json({ success: false, error: '缺少命令ID参数' })
756
+ }
757
+
758
+ const config = await configManager.loadConfig()
759
+
760
+ if (Array.isArray(config.customCommands)) {
761
+ const index = config.customCommands.findIndex(cmd => cmd.id === id)
762
+ if (index > 0) {
763
+ const [cmd] = config.customCommands.splice(index, 1)
764
+ config.customCommands.unshift(cmd)
765
+ await configManager.saveConfig(config)
766
+ }
767
+ }
768
+
769
+ res.json({ success: true })
770
+ } catch (error) {
771
+ res.status(500).json({ success: false, error: error.message })
772
+ }
773
+ })
774
+
775
+ // 更新自定义命令
776
+ app.post('/api/config/update-custom-command', express.json(), async (req, res) => {
777
+ try {
778
+ const { id, command } = req.body
779
+
780
+ if (!id || !command || !command.name || !command.command) {
781
+ return res.status(400).json({ success: false, error: '缺少必要参数' })
782
+ }
783
+
784
+ const config = await configManager.loadConfig()
785
+
786
+ if (Array.isArray(config.customCommands)) {
787
+ const index = config.customCommands.findIndex(cmd => cmd.id === id)
788
+ if (index !== -1) {
789
+ config.customCommands[index] = {
790
+ id,
791
+ name: command.name,
792
+ description: command.description || '',
793
+ directory: command.directory || '',
794
+ command: command.command,
795
+ params: Array.isArray(command.params) ? command.params : []
796
+ }
797
+ await configManager.saveConfig(config)
798
+ } else {
799
+ return res.status(404).json({ success: false, error: '未找到指定命令' })
800
+ }
801
+ } else {
802
+ return res.status(404).json({ success: false, error: '命令列表不存在' })
803
+ }
804
+
805
+ res.json({ success: true })
806
+ } catch (error) {
807
+ res.status(500).json({ success: false, error: error.message })
808
+ }
809
+ })
810
+
811
+ // 保存指令编排
812
+ app.post('/api/config/save-orchestration', express.json(), async (req, res) => {
813
+ try {
814
+ const { orchestration } = req.body
815
+
816
+ if (!orchestration || !orchestration.name) {
817
+ return res.status(400).json({ success: false, error: '缺少必要参数' })
818
+ }
819
+
820
+ const config = await configManager.loadConfig()
821
+
822
+ // 确保编排数组存在
823
+ if (!Array.isArray(config.orchestrations)) {
824
+ config.orchestrations = []
825
+ }
826
+
827
+ // 生成唯一ID
828
+ const id = `orch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
829
+ const newOrchestration = {
830
+ id,
831
+ name: orchestration.name,
832
+ description: orchestration.description || '',
833
+ flowData: orchestration.flowData || null
834
+ }
835
+
836
+ config.orchestrations.push(newOrchestration)
837
+ await configManager.saveConfig(config)
838
+
839
+ res.json({ success: true, orchestration: newOrchestration })
840
+ } catch (error) {
841
+ res.status(500).json({ success: false, error: error.message })
842
+ }
843
+ })
844
+
845
+ // 删除指令编排
846
+ app.post('/api/config/delete-orchestration', express.json(), async (req, res) => {
847
+ try {
848
+ const { id } = req.body
849
+
850
+ if (!id) {
851
+ return res.status(400).json({ success: false, error: '缺少编排ID参数' })
852
+ }
853
+
854
+ const config = await configManager.loadConfig()
855
+
856
+ if (Array.isArray(config.orchestrations)) {
857
+ const index = config.orchestrations.findIndex(orch => orch.id === id)
858
+ if (index !== -1) {
859
+ config.orchestrations.splice(index, 1)
860
+ await configManager.saveConfig(config)
861
+ }
862
+ }
863
+
864
+ res.json({ success: true })
865
+ } catch (error) {
866
+ res.status(500).json({ success: false, error: error.message })
867
+ }
868
+ })
869
+
870
+ // 更新指令编排
871
+ app.post('/api/config/update-orchestration', express.json(), async (req, res) => {
872
+ try {
873
+ const { id, orchestration } = req.body
874
+
875
+ if (!id || !orchestration || !orchestration.name) {
876
+ return res.status(400).json({ success: false, error: '缺少必要参数' })
877
+ }
878
+
879
+ const config = await configManager.loadConfig()
880
+
881
+ if (Array.isArray(config.orchestrations)) {
882
+ const index = config.orchestrations.findIndex(orch => orch.id === id)
883
+ if (index !== -1) {
884
+ config.orchestrations[index] = {
885
+ id,
886
+ name: orchestration.name,
887
+ description: orchestration.description || '',
888
+ flowData: orchestration.flowData || null
889
+ }
890
+ await configManager.saveConfig(config)
891
+ } else {
892
+ return res.status(404).json({ success: false, error: '未找到指定编排' })
893
+ }
894
+ } else {
895
+ return res.status(404).json({ success: false, error: '编排列表不存在' })
896
+ }
897
+
898
+ res.json({ success: true })
899
+ } catch (error) {
900
+ res.status(500).json({ success: false, error: error.message })
901
+ }
902
+ })
903
+
904
+ // 保存项目启动项
905
+ app.post('/api/config/save-startup-items', express.json(), async (req, res) => {
906
+ try {
907
+ const { startupItems, startupAutoRun } = req.body
908
+
909
+ if (!Array.isArray(startupItems)) {
910
+ return res.status(400).json({ success: false, error: '启动项必须是数组' })
911
+ }
912
+
913
+ const config = await configManager.loadConfig()
914
+ config.startupItems = startupItems
915
+ if (typeof startupAutoRun === 'boolean') {
916
+ config.startupAutoRun = startupAutoRun
917
+ }
918
+ await configManager.saveConfig(config)
919
+
920
+ res.json({ success: true })
921
+ } catch (error) {
922
+ res.status(500).json({ success: false, error: error.message })
923
+ }
924
+ })
925
+
926
+ // 保存提交设置
927
+ app.post('/api/config/save-commit-settings', express.json(), async (req, res) => {
928
+ try {
929
+ const { isStandardCommit, skipHooks, autoQuickPushOnEnter, autoSetDefaultMessage, autoClosePushModal, pullBeforePush } = req.body
930
+ const config = await configManager.loadConfig()
931
+ if (isStandardCommit !== undefined) config.isStandardCommit = Boolean(isStandardCommit)
932
+ if (skipHooks !== undefined) config.skipHooks = Boolean(skipHooks)
933
+ if (autoQuickPushOnEnter !== undefined) config.autoQuickPushOnEnter = Boolean(autoQuickPushOnEnter)
934
+ if (autoSetDefaultMessage !== undefined) config.autoSetDefaultMessage = Boolean(autoSetDefaultMessage)
935
+ if (autoClosePushModal !== undefined) config.autoClosePushModal = Boolean(autoClosePushModal)
936
+ if (pullBeforePush !== undefined) config.pullBeforePush = Boolean(pullBeforePush)
937
+ await configManager.saveConfig(config)
938
+ res.json({ success: true })
939
+ } catch (error) {
940
+ res.status(500).json({ success: false, error: error.message })
941
+ }
942
+ })
943
+
944
+ // 保存"一键推送成功后启动项"
945
+ app.post('/api/config/save-after-quick-push-action', express.json(), async (req, res) => {
946
+ try {
947
+ const { afterQuickPushAction } = req.body
948
+
949
+ if (!afterQuickPushAction || typeof afterQuickPushAction !== 'object') {
950
+ return res.status(400).json({ success: false, error: '缺少必要参数' })
951
+ }
952
+
953
+ const enabled = Boolean(afterQuickPushAction.enabled)
954
+ const type = afterQuickPushAction.type === 'workflow' ? 'workflow' : 'command'
955
+ const refId = String(afterQuickPushAction.refId || '').trim()
956
+
957
+ const config = await configManager.loadConfig()
958
+ config.afterQuickPushAction = {
959
+ enabled,
960
+ type,
961
+ refId
962
+ }
963
+ await configManager.saveConfig(config)
964
+
965
+ res.json({ success: true })
966
+ } catch (error) {
967
+ res.status(500).json({ success: false, error: error.message })
968
+ }
969
+ })
970
+
971
+ // 保存通用设置(主题和语言)
972
+ app.post('/api/config/save-general-settings', express.json(), async (req, res) => {
973
+ try {
974
+ const { theme, locale } = req.body
975
+
976
+ // 读取原始配置以保留项目设置
977
+ const rawConfig = await configManager.readRawConfigFile()
978
+
979
+ // 更新全局设置
980
+ if (theme && ['light', 'dark', 'auto'].includes(theme)) {
981
+ rawConfig.theme = theme
982
+ }
983
+ if (locale && ['zh-CN', 'en-US'].includes(locale)) {
984
+ rawConfig.locale = locale
985
+ }
986
+
987
+ // 直接写入原始配置,避免覆盖项目设置
988
+ await configManager.writeRawConfigFile(rawConfig)
989
+ res.json({ success: true })
990
+ } catch (error) {
991
+ res.status(500).json({ success: false, error: error.message })
992
+ }
993
+ })
994
+
995
+ // 保存 UI 状态(视图模式/分割比例/控制台状态/布局比例/编辑器自动保存等)
996
+ // 接受 partial body,浅合并到顶层 ui 对象。例:{ layout: {...} } / { commandConsole: {...} } / { fileListViewMode: 'tree' }
997
+ app.post('/api/config/save-ui-settings', express.json(), async (req, res) => {
998
+ try {
999
+ const partial = req.body && typeof req.body === 'object' ? req.body : {}
1000
+
1001
+ const rawConfig = await configManager.readRawConfigFile()
1002
+
1003
+ // 确保 ui 容器存在
1004
+ if (!rawConfig.ui || typeof rawConfig.ui !== 'object' || Array.isArray(rawConfig.ui)) {
1005
+ rawConfig.ui = {}
1006
+ }
1007
+
1008
+ // 浅合并顶层 ui 字段(支持嵌套对象整体替换,如 commandConsole)
1009
+ for (const key of Object.keys(partial)) {
1010
+ rawConfig.ui[key] = partial[key]
1011
+ }
1012
+
1013
+ await configManager.writeRawConfigFile(rawConfig)
1014
+ res.json({ success: true })
1015
+ } catch (error) {
1016
+ res.status(500).json({ success: false, error: error.message })
1017
+ }
1018
+ })
1019
+
1020
+ // 保存 AI 模型配置(models 是全局配置,存在配置文件顶层,跨项目共享)
1021
+ app.post('/api/config/save-models', express.json(), async (req, res) => {
1022
+ try {
1023
+ const { models } = req.body
1024
+ if (!Array.isArray(models)) {
1025
+ return res.status(400).json({ success: false, error: '缺少 models 参数' })
1026
+ }
1027
+ const rawConfig = await configManager.readRawConfigFile()
1028
+ rawConfig.models = models
1029
+ await configManager.writeRawConfigFile(rawConfig)
1030
+ res.json({ success: true })
1031
+ } catch (error) {
1032
+ res.status(500).json({ success: false, error: error.message })
1033
+ }
1034
+ })
1035
+
1036
+ // 测试 AI 模型是否可用
1037
+ app.post('/api/config/test-model', express.json(), async (req, res) => {
1038
+ const { baseURL, model, apiKey } = req.body || {}
1039
+ if (!baseURL || !model) {
1040
+ return res.status(400).json({ success: false, error: '缺少 baseURL 或 model 参数' })
1041
+ }
1042
+ try {
1043
+ const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }))
1044
+ const url = `${baseURL.replace(/\/$/, '')}/chat/completions`
1045
+ const headers = { 'Content-Type': 'application/json' }
1046
+ if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
1047
+ const body = JSON.stringify({
1048
+ model,
1049
+ messages: [{ role: 'user', content: 'Hello, reply with just "ok".' }],
1050
+ max_tokens: 16,
1051
+ stream: false
1052
+ })
1053
+ const controller = new AbortController()
1054
+ const timer = setTimeout(() => controller.abort(), 15000)
1055
+ let response
1056
+ try {
1057
+ response = await fetch(url, { method: 'POST', headers, body, signal: controller.signal })
1058
+ } finally {
1059
+ clearTimeout(timer)
1060
+ }
1061
+ const data = await response.json().catch(() => ({}))
1062
+ if (!response.ok) {
1063
+ const msg = data?.error?.message || data?.message || `HTTP ${response.status}`
1064
+ return res.json({ success: false, error: msg })
1065
+ }
1066
+ const reply = data?.choices?.[0]?.message?.content || data?.choices?.[0]?.text || '✅'
1067
+ res.json({ success: true, reply: reply.trim().slice(0, 100) })
1068
+ } catch (error) {
1069
+ const msg = error.name === 'AbortError' ? '请求超时(15s)' : error.message
1070
+ res.json({ success: false, error: msg })
1071
+ }
1072
+ })
1073
+
1074
+ // AI 生成提交信息
1075
+ app.post('/api/config/generate-commit-message', express.json(), async (req, res) => {
1076
+ const { fileList } = req.body || {}
1077
+ try {
1078
+ const rawConfig = await configManager.readRawConfigFile()
1079
+ const models = Array.isArray(rawConfig.models) ? rawConfig.models : []
1080
+ const defaultModel = models.find(m => m.isDefault) || models[0]
1081
+ if (!defaultModel) {
1082
+ return res.json({ success: false, error: '未配置 AI 模型,请先在通用设置中添加模型' })
1083
+ }
1084
+
1085
+ // 后端自己收集 diff,确保 untracked 文件也能进 prompt
1086
+ const { diff: rawDiff, fileList: serverFileList } = await collectDiffForAi({ execGitCommand, getCurrentProjectPath })
1087
+ const safeFileList = Array.isArray(fileList) && fileList.length > 0 ? fileList : serverFileList
1088
+ const diffText = prepareDiffForPrompt(rawDiff, safeFileList)
1089
+ const filesText = safeFileList.slice(0, 30).join('\n')
1090
+ const prompt = `你是一个 Git 提交信息生成助手。根据以下 git diff 信息,生成一条符合 Conventional Commits 规范的提交信息。
1091
+
1092
+ 要求:
1093
+ 1. type 只能是:feat/fix/docs/style/refactor/test/chore 之一
1094
+ 2. scope 可选,表示影响范围,简短英文或中文,如果改动范围明确就填
1095
+ 3. description 用中文简短描述本次变更(不超过50字)
1096
+ 4. 只返回 JSON,格式:{"type":"feat","scope":"","description":"xxx"}
1097
+
1098
+ 变更文件:
1099
+ ${filesText}
1100
+
1101
+ git diff --staged:
1102
+ ${diffText || '(无 staged 内容,请根据文件列表推断)'}`
1103
+
1104
+ console.log('[generate-commit] ===== PROMPT START (length: ' + prompt.length + ') =====')
1105
+ console.log(prompt)
1106
+ console.log('[generate-commit] ===== PROMPT END =====')
1107
+
1108
+ const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }))
1109
+ const url = `${defaultModel.baseURL.replace(/\/$/, '')}/chat/completions`
1110
+ const headers = { 'Content-Type': 'application/json' }
1111
+ if (defaultModel.apiKey) headers['Authorization'] = `Bearer ${defaultModel.apiKey}`
1112
+ const body = JSON.stringify({
1113
+ model: defaultModel.model,
1114
+ messages: [{ role: 'user', content: prompt }],
1115
+ max_tokens: 1024,
1116
+ temperature: 0.3,
1117
+ stream: false
1118
+ })
1119
+ const controller = new AbortController()
1120
+ const timer = setTimeout(() => controller.abort(), 30000)
1121
+ let response
1122
+ try {
1123
+ response = await fetch(url, { method: 'POST', headers, body, signal: controller.signal })
1124
+ } finally {
1125
+ clearTimeout(timer)
1126
+ }
1127
+ const data = await response.json().catch(() => ({}))
1128
+ if (!response.ok) {
1129
+ const msg = data?.error?.message || data?.message || `HTTP ${response.status}`
1130
+ return res.json({ success: false, error: msg })
1131
+ }
1132
+ const content = data?.choices?.[0]?.message?.content || ''
1133
+ console.log('[generate-commit] raw content length:', content.length, JSON.stringify(content).slice(0, 600))
1134
+
1135
+ const codeBlockMatch = content.match(/```(?:json)?\s*(\{[^`]*?\})\s*```/)
1136
+ const jsonMatch = codeBlockMatch
1137
+ ? [codeBlockMatch[1]]
1138
+ : [...content.matchAll(/\{[^{}]*\}/g)].at(-1)
1139
+
1140
+ if (!jsonMatch) {
1141
+ console.error('[generate-commit] no JSON found, full content:', content)
1142
+ return res.json({ success: false, error: 'model returned no valid JSON' })
1143
+ }
1144
+ let parsed
1145
+ try {
1146
+ parsed = JSON.parse(jsonMatch[0])
1147
+ } catch {
1148
+ const typeM = jsonMatch[0].match(/"type"\s*:\s*"([^"]+)"/)
1149
+ const scopeM = jsonMatch[0].match(/"scope"\s*:\s*"([^"]*)"/)
1150
+ const descM = jsonMatch[0].match(/"description"\s*:\s*"([^"]+)"/)
1151
+ if (typeM || descM) {
1152
+ return res.json({
1153
+ success: true,
1154
+ type: (typeM?.[1] || 'feat').trim(),
1155
+ scope: (scopeM?.[1] || '').trim(),
1156
+ description: (descM?.[1] || '').trim()
1157
+ })
1158
+ }
1159
+ return res.json({ success: false, error: 'JSON parse failed' })
1160
+ }
1161
+ res.json({
1162
+ success: true,
1163
+ type: String(parsed.type || 'feat').trim(),
1164
+ scope: String(parsed.scope || '').trim(),
1165
+ description: String(parsed.description || '').trim()
1166
+ })
1167
+ } catch (error) {
1168
+ const msg = error.name === 'AbortError' ? 'timeout 30s' : error.message
1169
+ res.json({ success: false, error: msg })
1170
+ }
1171
+ })
1172
+ }