@muyichengshayu/promptx 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/apps/server/src/gitDiff.js +302 -2
- package/apps/server/src/index.js +2 -0
- package/apps/server/src/taskRoutes.js +52 -0
- package/apps/web/dist/assets/{CodexSessionManagerDialog-CCBswCOE.js → CodexSessionManagerDialog-BojVd2nD.js} +3 -3
- package/apps/web/dist/assets/TaskDiffReviewDialog-GOruDINI.js +5 -0
- package/apps/web/dist/assets/TaskDiffReviewDialog-NsLGEda4.css +1 -0
- package/apps/web/dist/assets/WorkbenchSettingsDialog-BYYiMjvq.js +1 -0
- package/apps/web/dist/assets/WorkbenchView-BqHMDZ1g.js +60 -0
- package/apps/web/dist/assets/{WorkbenchView-Cf8q7kQb.css → WorkbenchView-DlttkjeG.css} +1 -1
- package/apps/web/dist/assets/index-7F-RPTS_.js +2 -0
- package/apps/web/dist/assets/{index-DhKg39i4.css → index-CpVsZfVv.css} +1 -1
- package/apps/web/dist/assets/{vendor-ui-BwEQCho1.js → vendor-ui-BglsaDbv.js} +67 -52
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/sounds/notify-didi.wav +0 -0
- package/package.json +1 -1
- package/apps/web/dist/assets/TaskDiffReviewDialog-PZ63ypA4.js +0 -4
- package/apps/web/dist/assets/TaskDiffReviewDialog-hxUGDcmF.css +0 -1
- package/apps/web/dist/assets/WorkbenchSettingsDialog-sRvpVVLk.js +0 -1
- package/apps/web/dist/assets/WorkbenchView-EAjf4pvb.js +0 -60
- package/apps/web/dist/assets/index-DpCCivzJ.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.6
|
|
4
|
+
|
|
5
|
+
- 修复切换任务时中栏消息偶发没有自动滚到底部的问题:任务切换后会进入一段短暂的强制跟底窗口,在 run 历史刷新、异步渲染和内容补齐期间持续补滚;如果用户主动手势滚动,则立即退出自动跟随,避免抢控制。
|
|
6
|
+
- 代码变更弹窗新增二进制文件预览:图片支持变更前 / 变更后双栏预览并可点击查看大图,音频 / 视频 / PDF 支持内嵌预览,其它二进制文件会展示大小、类型与 hash 摘要,并提供受限范围内的文件下载入口。
|
|
7
|
+
- 收紧二进制 diff 预览安全边界:新增的 blob 接口现在仅允许读取当前 diff 范围内的文件,不再可借由任务级 diff 预览访问仓库中的任意文件;同时补齐大文件限制、历史快照缺失时的失败兜底与相关服务端测试。
|
|
8
|
+
- 优化二进制 diff 预览细节:文件列表会直接标记“二进制”并显示 MIME 类型;切换 `workspace / task / run` 或不同 run 时会正确重置预览状态,不再残留上一轮的图片或错误状态;图片预览层级也已提升,避免被代码变更弹窗遮挡。
|
|
9
|
+
|
|
10
|
+
## 0.2.5
|
|
11
|
+
|
|
12
|
+
- 工作台新增任务级未读提醒:当非当前聚焦任务的一次 agent turn 进入终态后,任务卡片会显示未读红点,浏览器页签标题也会切换为“你有新消息”,帮助在多任务并行时更快发现新进展。
|
|
13
|
+
- 新增通知音能力与通用设置开关:通用设置里可单独控制是否播放任务未读提示音,默认关闭;同时补齐音频兼容、首次交互解锁与缓存刷新逻辑,减少浏览器不响或误触发的情况。
|
|
14
|
+
- 收敛未读状态细节与稳定性:当前任务前台聚焦时不会误记未读;切换任务后中栏 Agent 过滤会自动回到“全部”;未读去重缓存改为可回收并设置上限,避免工作台长时间打开后内存状态持续膨胀。
|
|
15
|
+
- 补齐未读通知相关回归测试,并修正页签标题国际化与主题色细节,确保中英文环境和不同主题下的体验更一致。
|
|
16
|
+
|
|
3
17
|
## 0.2.4
|
|
4
18
|
|
|
5
19
|
- 重构远程命令安全模型:把远程 `shell` 命令放行从旧的 Relay 高权限开关收口到统一的 `remoteCommandSecurity` 配置,明确区分 `disabled / relay / trusted-proxy` 三种模式;本机 loopback 访问继续默认允许,远程入口则必须显式匹配对应安全模式。
|
|
@@ -18,6 +18,25 @@ const DIFF_REVIEW_CACHE_TTL_MS = 4000
|
|
|
18
18
|
const DIFF_REVIEW_CACHE_MAX_ENTRIES = 80
|
|
19
19
|
const FILE_DIFF_CACHE_TTL_MS = 8000
|
|
20
20
|
const FILE_DIFF_CACHE_MAX_ENTRIES = 400
|
|
21
|
+
const MAX_BINARY_DIFF_BLOB_BYTES = Math.max(64 * 1024, Number(process.env.PROMPTX_GIT_DIFF_MAX_BINARY_BLOB_BYTES) || 8 * 1024 * 1024)
|
|
22
|
+
|
|
23
|
+
const BINARY_PREVIEW_MIME_TYPES = new Map([
|
|
24
|
+
['.avif', 'image/avif'],
|
|
25
|
+
['.bmp', 'image/bmp'],
|
|
26
|
+
['.gif', 'image/gif'],
|
|
27
|
+
['.ico', 'image/x-icon'],
|
|
28
|
+
['.jpeg', 'image/jpeg'],
|
|
29
|
+
['.jpg', 'image/jpeg'],
|
|
30
|
+
['.mp3', 'audio/mpeg'],
|
|
31
|
+
['.mp4', 'video/mp4'],
|
|
32
|
+
['.ogg', 'audio/ogg'],
|
|
33
|
+
['.ogv', 'video/ogg'],
|
|
34
|
+
['.pdf', 'application/pdf'],
|
|
35
|
+
['.png', 'image/png'],
|
|
36
|
+
['.wav', 'audio/wav'],
|
|
37
|
+
['.webm', 'video/webm'],
|
|
38
|
+
['.webp', 'image/webp'],
|
|
39
|
+
])
|
|
21
40
|
|
|
22
41
|
const diffReviewCache = new Map()
|
|
23
42
|
const fileDiffCache = new Map()
|
|
@@ -103,6 +122,38 @@ function createHash(value) {
|
|
|
103
122
|
return crypto.createHash('sha1').update(value).digest('hex')
|
|
104
123
|
}
|
|
105
124
|
|
|
125
|
+
function inferBinaryMimeType(filePath = '') {
|
|
126
|
+
return BINARY_PREVIEW_MIME_TYPES.get(path.extname(String(filePath || '').trim()).toLowerCase()) || 'application/octet-stream'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function inferBinaryPreviewKind(mimeType = '') {
|
|
130
|
+
const normalizedMimeType = String(mimeType || '').trim().toLowerCase()
|
|
131
|
+
if (normalizedMimeType.startsWith('image/')) {
|
|
132
|
+
return 'image'
|
|
133
|
+
}
|
|
134
|
+
if (normalizedMimeType.startsWith('audio/')) {
|
|
135
|
+
return 'audio'
|
|
136
|
+
}
|
|
137
|
+
if (normalizedMimeType.startsWith('video/')) {
|
|
138
|
+
return 'video'
|
|
139
|
+
}
|
|
140
|
+
if (normalizedMimeType === 'application/pdf') {
|
|
141
|
+
return 'pdf'
|
|
142
|
+
}
|
|
143
|
+
return 'binary'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isKnownBinaryPreviewPath(filePath = '') {
|
|
147
|
+
return BINARY_PREVIEW_MIME_TYPES.has(path.extname(String(filePath || '').trim()).toLowerCase())
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function createApiError(message = '', statusCode = 400, messageKey = 'errors.requestFailed') {
|
|
151
|
+
const error = new Error(message)
|
|
152
|
+
error.statusCode = statusCode
|
|
153
|
+
error.messageKey = messageKey
|
|
154
|
+
return error
|
|
155
|
+
}
|
|
156
|
+
|
|
106
157
|
function countTextLines(value = '') {
|
|
107
158
|
const text = String(value || '')
|
|
108
159
|
if (!text) {
|
|
@@ -419,7 +470,7 @@ function readFileState(repoRoot = '', filePath = '') {
|
|
|
419
470
|
}
|
|
420
471
|
|
|
421
472
|
const buffer = fs.readFileSync(absolutePath)
|
|
422
|
-
const isBinary = buffer.includes(0)
|
|
473
|
+
const isBinary = isKnownBinaryPreviewPath(filePath) || buffer.includes(0)
|
|
423
474
|
const tooLarge = !isBinary && buffer.length > MAX_SNAPSHOT_TEXT_BYTES
|
|
424
475
|
|
|
425
476
|
return {
|
|
@@ -489,7 +540,7 @@ function readHeadFileState(repoRoot = '', headOid = '', filePath = '') {
|
|
|
489
540
|
}
|
|
490
541
|
|
|
491
542
|
const buffer = result.stdout
|
|
492
|
-
const isBinary = buffer.includes(0)
|
|
543
|
+
const isBinary = isKnownBinaryPreviewPath(normalizedPath) || buffer.includes(0)
|
|
493
544
|
const tooLarge = !isBinary && buffer.length > MAX_SNAPSHOT_TEXT_BYTES
|
|
494
545
|
|
|
495
546
|
return {
|
|
@@ -502,6 +553,126 @@ function readHeadFileState(repoRoot = '', headOid = '', filePath = '') {
|
|
|
502
553
|
}
|
|
503
554
|
}
|
|
504
555
|
|
|
556
|
+
function normalizeRepoFilePath(filePath = '') {
|
|
557
|
+
const rawPath = String(filePath || '').trim().replace(/\\/g, '/').replace(/^\/+/, '')
|
|
558
|
+
if (!rawPath || rawPath.includes('\0')) {
|
|
559
|
+
return ''
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const normalizedPath = path.posix.normalize(rawPath)
|
|
563
|
+
if (!normalizedPath || normalizedPath === '.' || normalizedPath === '..' || normalizedPath.startsWith('../')) {
|
|
564
|
+
return ''
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return normalizedPath
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function createBlobPayload(filePath = '', side = '', buffer = Buffer.alloc(0)) {
|
|
571
|
+
const mimeType = inferBinaryMimeType(filePath)
|
|
572
|
+
return {
|
|
573
|
+
supported: true,
|
|
574
|
+
filePath,
|
|
575
|
+
side,
|
|
576
|
+
mimeType,
|
|
577
|
+
previewKind: inferBinaryPreviewKind(mimeType),
|
|
578
|
+
size: buffer.length,
|
|
579
|
+
hash: createHash(buffer),
|
|
580
|
+
body: buffer,
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function createUnsupportedBlobPayload(message = '', statusCode = 404, messageKey = 'errors.gitDiffFailed') {
|
|
585
|
+
return {
|
|
586
|
+
supported: false,
|
|
587
|
+
statusCode,
|
|
588
|
+
messageKey,
|
|
589
|
+
message: String(message || '无法读取该文件内容。'),
|
|
590
|
+
filePath: '',
|
|
591
|
+
side: '',
|
|
592
|
+
mimeType: '',
|
|
593
|
+
previewKind: 'binary',
|
|
594
|
+
size: 0,
|
|
595
|
+
hash: '',
|
|
596
|
+
body: Buffer.alloc(0),
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function enforceBlobSize(buffer = Buffer.alloc(0)) {
|
|
601
|
+
if (buffer.length > MAX_BINARY_DIFF_BLOB_BYTES) {
|
|
602
|
+
throw createApiError('文件较大,暂不支持在线预览。', 413, 'diffReview.binaryPreviewTooLarge')
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function readHeadBlob(repoRoot = '', headOid = '', filePath = '', side = '') {
|
|
607
|
+
const normalizedHeadOid = String(headOid || '').trim()
|
|
608
|
+
const normalizedPath = normalizeRepoFilePath(filePath)
|
|
609
|
+
if (!repoRoot || !normalizedHeadOid || !normalizedPath) {
|
|
610
|
+
return createUnsupportedBlobPayload('文件不存在。', 404, 'errors.fileNotFound')
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const result = runGitBuffer(repoRoot, ['show', `${normalizedHeadOid}:${normalizedPath}`], {
|
|
614
|
+
maxBuffer: MAX_BINARY_DIFF_BLOB_BYTES + 1024,
|
|
615
|
+
})
|
|
616
|
+
if (result.status !== 0) {
|
|
617
|
+
return createUnsupportedBlobPayload('文件不存在。', 404, 'errors.fileNotFound')
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
enforceBlobSize(result.stdout)
|
|
621
|
+
return createBlobPayload(normalizedPath, side, result.stdout)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function readWorkingTreeBlob(repoRoot = '', filePath = '', side = '') {
|
|
625
|
+
const normalizedPath = normalizeRepoFilePath(filePath)
|
|
626
|
+
if (!repoRoot || !normalizedPath) {
|
|
627
|
+
return createUnsupportedBlobPayload('文件不存在。', 404, 'errors.fileNotFound')
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const root = path.resolve(repoRoot)
|
|
631
|
+
const absolutePath = path.resolve(root, normalizedPath)
|
|
632
|
+
if (absolutePath !== root && !absolutePath.startsWith(`${root}${path.sep}`)) {
|
|
633
|
+
return createUnsupportedBlobPayload('文件不存在。', 404, 'errors.fileNotFound')
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const stats = fs.statSync(absolutePath)
|
|
638
|
+
if (!stats.isFile()) {
|
|
639
|
+
return createUnsupportedBlobPayload('文件不存在。', 404, 'errors.fileNotFound')
|
|
640
|
+
}
|
|
641
|
+
if (stats.size > MAX_BINARY_DIFF_BLOB_BYTES) {
|
|
642
|
+
throw createApiError('文件较大,暂不支持在线预览。', 413, 'diffReview.binaryPreviewTooLarge')
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return createBlobPayload(normalizedPath, side, fs.readFileSync(absolutePath))
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (error?.statusCode) {
|
|
648
|
+
throw error
|
|
649
|
+
}
|
|
650
|
+
return createUnsupportedBlobPayload('文件不存在。', 404, 'errors.fileNotFound')
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function readSnapshotStateBlob(repoRoot = '', filePath = '', state = null, side = '') {
|
|
655
|
+
if (!state?.exists) {
|
|
656
|
+
return createUnsupportedBlobPayload('文件不存在。', 404, 'errors.fileNotFound')
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (!state.isBinary && !state.tooLarge) {
|
|
660
|
+
const buffer = Buffer.from(String(state.text || ''), 'utf8')
|
|
661
|
+
return createBlobPayload(filePath, side, buffer)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const livePayload = readWorkingTreeBlob(repoRoot, filePath, side)
|
|
665
|
+
if (livePayload.supported && String(livePayload.hash || '') === String(state.hash || '')) {
|
|
666
|
+
return livePayload
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return createUnsupportedBlobPayload(
|
|
670
|
+
'历史快照中没有保存该二进制文件内容。',
|
|
671
|
+
404,
|
|
672
|
+
'diffReview.binarySnapshotUnavailable'
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
|
|
505
676
|
function createBaselineStateResolver(repoRoot = '', baseline = null) {
|
|
506
677
|
const cache = new Map()
|
|
507
678
|
|
|
@@ -1379,6 +1550,40 @@ function sortDiffFiles(items = []) {
|
|
|
1379
1550
|
})
|
|
1380
1551
|
}
|
|
1381
1552
|
|
|
1553
|
+
function buildDiffFileSideMeta(filePath = '', state = null) {
|
|
1554
|
+
const exists = Boolean(state?.exists)
|
|
1555
|
+
const mimeType = exists ? inferBinaryMimeType(filePath) : ''
|
|
1556
|
+
return {
|
|
1557
|
+
exists,
|
|
1558
|
+
size: exists ? Math.max(0, Number(state?.size) || 0) : 0,
|
|
1559
|
+
hash: exists ? String(state?.hash || '') : '',
|
|
1560
|
+
hashShort: exists ? String(state?.hash || '').slice(0, 7) : '',
|
|
1561
|
+
mimeType,
|
|
1562
|
+
previewKind: exists ? inferBinaryPreviewKind(mimeType) : 'none',
|
|
1563
|
+
tooLarge: exists ? Math.max(0, Number(state?.size) || 0) > MAX_BINARY_DIFF_BLOB_BYTES : false,
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function buildBinaryPreviewMeta(filePath = '', previousState = null, nextState = null, patchPayload = {}) {
|
|
1568
|
+
const hasBinarySide = Boolean(previousState?.isBinary || nextState?.isBinary || patchPayload.binary)
|
|
1569
|
+
if (!hasBinarySide) {
|
|
1570
|
+
return null
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
const before = buildDiffFileSideMeta(filePath, previousState)
|
|
1574
|
+
const after = buildDiffFileSideMeta(filePath, nextState)
|
|
1575
|
+
const previewKind = after.exists ? after.previewKind : before.previewKind
|
|
1576
|
+
const mimeType = after.exists ? after.mimeType : before.mimeType
|
|
1577
|
+
|
|
1578
|
+
return {
|
|
1579
|
+
kind: previewKind,
|
|
1580
|
+
mimeType,
|
|
1581
|
+
maxPreviewBytes: MAX_BINARY_DIFF_BLOB_BYTES,
|
|
1582
|
+
before,
|
|
1583
|
+
after,
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1382
1587
|
function createDiffFileEntry(filePath = '', previousState = null, nextState = null, options = {}) {
|
|
1383
1588
|
if (areFileStatesEqual(previousState, nextState)) {
|
|
1384
1589
|
return null
|
|
@@ -1413,6 +1618,7 @@ function createDiffFileEntry(filePath = '', previousState = null, nextState = nu
|
|
|
1413
1618
|
patch: patchPayload.patch,
|
|
1414
1619
|
patchLoaded: patchPayload.patchLoaded,
|
|
1415
1620
|
message: patchPayload.message,
|
|
1621
|
+
binaryPreview: buildBinaryPreviewMeta(filePath, previousState, nextState, patchPayload),
|
|
1416
1622
|
}
|
|
1417
1623
|
}
|
|
1418
1624
|
|
|
@@ -1921,6 +2127,100 @@ export function getTaskGitDiffReview(taskSlug = '', options = {}) {
|
|
|
1921
2127
|
return payload
|
|
1922
2128
|
}
|
|
1923
2129
|
|
|
2130
|
+
export function getTaskGitDiffBlob(taskSlug = '', options = {}) {
|
|
2131
|
+
const normalizedTaskSlug = String(taskSlug || '').trim()
|
|
2132
|
+
const filePath = normalizeRepoFilePath(options.filePath)
|
|
2133
|
+
const side = String(options.side || '').trim() === 'before' ? 'before' : 'after'
|
|
2134
|
+
const rawScope = String(options.scope || 'workspace').trim()
|
|
2135
|
+
const scope = rawScope === 'run'
|
|
2136
|
+
? 'run'
|
|
2137
|
+
: rawScope === 'task'
|
|
2138
|
+
? 'task'
|
|
2139
|
+
: 'workspace'
|
|
2140
|
+
const runId = String(options.runId || '').trim()
|
|
2141
|
+
|
|
2142
|
+
if (!normalizedTaskSlug || !filePath) {
|
|
2143
|
+
return createUnsupportedBlobPayload('文件不存在。', 404, 'errors.fileNotFound')
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
const diffPayload = getTaskGitDiffReview(normalizedTaskSlug, {
|
|
2147
|
+
scope,
|
|
2148
|
+
runId,
|
|
2149
|
+
filePath,
|
|
2150
|
+
includeStats: false,
|
|
2151
|
+
})
|
|
2152
|
+
const fileInDiff = Boolean(
|
|
2153
|
+
diffPayload?.supported
|
|
2154
|
+
&& Array.isArray(diffPayload.files)
|
|
2155
|
+
&& diffPayload.files.some((item) => String(item?.path || '').trim() === filePath)
|
|
2156
|
+
)
|
|
2157
|
+
if (!fileInDiff) {
|
|
2158
|
+
return createUnsupportedBlobPayload('当前文件不在本次 diff 范围内。', 404, 'diffReview.fileNotInDiff')
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
if (scope === 'workspace') {
|
|
2162
|
+
const repoRoot = resolveTaskRepoRoot(normalizedTaskSlug)
|
|
2163
|
+
if (!repoRoot) {
|
|
2164
|
+
return createUnsupportedBlobPayload('当前工作目录不是 Git 仓库,暂不支持代码变更审查。')
|
|
2165
|
+
}
|
|
2166
|
+
if (side === 'before') {
|
|
2167
|
+
return readHeadBlob(repoRoot, resolveGitHeadOid(repoRoot), filePath, side)
|
|
2168
|
+
}
|
|
2169
|
+
return readWorkingTreeBlob(repoRoot, filePath, side)
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
let baseline = null
|
|
2173
|
+
let comparisonSnapshot = null
|
|
2174
|
+
if (scope === 'run') {
|
|
2175
|
+
if (!runId || getRunTaskSlug(runId) !== normalizedTaskSlug) {
|
|
2176
|
+
return createUnsupportedBlobPayload('没有找到对应的执行记录。', 404, 'diffReview.runNotFound')
|
|
2177
|
+
}
|
|
2178
|
+
baseline = loadRunBaseline(runId)
|
|
2179
|
+
comparisonSnapshot = loadRunFinalSnapshot(runId)
|
|
2180
|
+
} else {
|
|
2181
|
+
baseline = loadTaskBaseline(normalizedTaskSlug)
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
if (!baseline) {
|
|
2185
|
+
return createUnsupportedBlobPayload(
|
|
2186
|
+
scope === 'run'
|
|
2187
|
+
? '这轮执行还没有建立代码变更基线,暂时无法查看本轮 diff。'
|
|
2188
|
+
: '当前任务还没有建立代码变更基线,请先让 Codex 执行一轮。',
|
|
2189
|
+
404,
|
|
2190
|
+
scope === 'run' ? 'diffReview.runBaselineMissing' : 'diffReview.taskBaselineMissing'
|
|
2191
|
+
)
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
if (scope === 'run' && !comparisonSnapshot) {
|
|
2195
|
+
return createUnsupportedBlobPayload(
|
|
2196
|
+
'这轮执行缺少结束快照,暂时无法准确还原本轮代码变更。',
|
|
2197
|
+
404,
|
|
2198
|
+
'diffReview.runSnapshotMissing'
|
|
2199
|
+
)
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
const repoRoot = resolveGitRepoRoot(baseline.repoRoot)
|
|
2203
|
+
if (!repoRoot) {
|
|
2204
|
+
return createUnsupportedBlobPayload('原工作目录已不是有效的 Git 仓库,暂时无法读取代码变更。')
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
if (side === 'before') {
|
|
2208
|
+
if (baseline.entries.has(filePath)) {
|
|
2209
|
+
return readSnapshotStateBlob(repoRoot, filePath, baseline.entries.get(filePath), side)
|
|
2210
|
+
}
|
|
2211
|
+
return readHeadBlob(repoRoot, baseline.headOid, filePath, side)
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
if (scope === 'run') {
|
|
2215
|
+
if (comparisonSnapshot.entries.has(filePath)) {
|
|
2216
|
+
return readSnapshotStateBlob(repoRoot, filePath, comparisonSnapshot.entries.get(filePath), side)
|
|
2217
|
+
}
|
|
2218
|
+
return readHeadBlob(repoRoot, comparisonSnapshot.headOid, filePath, side)
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
return readWorkingTreeBlob(repoRoot, filePath, side)
|
|
2222
|
+
}
|
|
2223
|
+
|
|
1924
2224
|
export function __resetGitDiffCachesForTest() {
|
|
1925
2225
|
diffReviewCache.clear()
|
|
1926
2226
|
fileDiffCache.clear()
|
package/apps/server/src/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
updateTask,
|
|
23
23
|
} from './repository.js'
|
|
24
24
|
import {
|
|
25
|
+
getTaskGitDiffBlob,
|
|
25
26
|
getWorkspaceGitDiffStatusSummaryByCwd,
|
|
26
27
|
} from './gitDiff.js'
|
|
27
28
|
import {
|
|
@@ -389,6 +390,7 @@ registerTaskRoutes(app, {
|
|
|
389
390
|
getPromptxCodexSessionById,
|
|
390
391
|
getRunningCodexRunByTaskSlug,
|
|
391
392
|
getTaskBySlug,
|
|
393
|
+
getTaskGitDiffBlob,
|
|
392
394
|
getTaskGitDiffReviewInSubprocess,
|
|
393
395
|
listTaskCodexRunsWithOptions,
|
|
394
396
|
listTaskWorkspaceDiffSummaries: taskWorkspaceDiffSummaryService.listTaskWorkspaceDiffSummaries,
|
|
@@ -204,6 +204,13 @@ function registerTaskRoutes(app, options = {}) {
|
|
|
204
204
|
getSystemConfig = getSystemConfigForRuntime,
|
|
205
205
|
getRunningCodexRunByTaskSlug,
|
|
206
206
|
getTaskBySlug,
|
|
207
|
+
getTaskGitDiffBlob = () => ({
|
|
208
|
+
supported: false,
|
|
209
|
+
statusCode: 404,
|
|
210
|
+
messageKey: 'errors.gitDiffFailed',
|
|
211
|
+
message: '无法读取该文件内容。',
|
|
212
|
+
body: Buffer.alloc(0),
|
|
213
|
+
}),
|
|
207
214
|
getTaskGitDiffReviewInSubprocess,
|
|
208
215
|
listTaskCodexRunsWithOptions,
|
|
209
216
|
listTaskWorkspaceDiffSummaries,
|
|
@@ -444,6 +451,51 @@ function registerTaskRoutes(app, options = {}) {
|
|
|
444
451
|
}
|
|
445
452
|
})
|
|
446
453
|
|
|
454
|
+
app.get('/api/tasks/:slug/git-diff/blob', async (request, reply) => {
|
|
455
|
+
purgeExpiredContent()
|
|
456
|
+
const task = getTaskBySlug(request.params.slug)
|
|
457
|
+
if (!task || task.expired) {
|
|
458
|
+
return reply.code(404).send({ messageKey: 'errors.taskNotFound', message: '任务不存在。' })
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const scope = String(request.query?.scope || 'workspace').trim()
|
|
462
|
+
if (scope !== 'workspace' && scope !== 'task' && scope !== 'run') {
|
|
463
|
+
return reply.code(400).send({ messageKey: 'errors.invalidDiffScope', message: '无效的 diff 范围。' })
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
const payload = getTaskGitDiffBlob(request.params.slug, {
|
|
468
|
+
scope,
|
|
469
|
+
runId: request.query?.runId,
|
|
470
|
+
filePath: request.query?.filePath,
|
|
471
|
+
side: request.query?.side,
|
|
472
|
+
})
|
|
473
|
+
if (!payload?.supported) {
|
|
474
|
+
return reply
|
|
475
|
+
.code(payload?.statusCode || 404)
|
|
476
|
+
.send({
|
|
477
|
+
messageKey: payload?.messageKey || 'errors.gitDiffFailed',
|
|
478
|
+
message: payload?.message || '无法读取该文件内容。',
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return reply
|
|
483
|
+
.header('cache-control', 'no-store')
|
|
484
|
+
.header('x-promptx-file-size', String(payload.size || 0))
|
|
485
|
+
.header('x-promptx-file-hash', String(payload.hash || ''))
|
|
486
|
+
.type(payload.mimeType || 'application/octet-stream')
|
|
487
|
+
.send(payload.body)
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (error?.statusCode) {
|
|
490
|
+
return reply.code(error.statusCode).send(getApiErrorPayload(error, {
|
|
491
|
+
messageKey: error.messageKey || 'errors.gitDiffFailed',
|
|
492
|
+
message: String(error?.message || '无法读取该文件内容。'),
|
|
493
|
+
}))
|
|
494
|
+
}
|
|
495
|
+
throw error
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
|
|
447
499
|
app.post('/api/tasks/:slug/codex-runs', async (request, reply) => {
|
|
448
500
|
purgeExpiredContent()
|
|
449
501
|
try {
|