@muyichengshayu/promptx 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/apps/runner/src/engines/index.js +13 -3
- package/apps/runner/src/engines/shellRunner.js +216 -0
- package/apps/runner/src/runManager.js +68 -9
- package/apps/server/src/agents/index.js +18 -3
- package/apps/server/src/codexRuns.js +24 -7
- package/apps/server/src/db.js +2 -0
- package/apps/server/src/gitDiff.js +279 -17
- package/apps/server/src/internalRoutes.js +8 -1
- package/apps/server/src/relayClient.js +5 -1
- package/apps/server/src/relayConfig.js +10 -0
- package/apps/server/src/runDispatchService.js +44 -13
- package/apps/server/src/runEventIngest.js +6 -4
- package/apps/server/src/taskRoutes.js +72 -0
- package/apps/web/dist/assets/{CodexSessionManagerDialog-BbaObUl_.js → CodexSessionManagerDialog-CELTkz9T.js} +1 -1
- package/apps/web/dist/assets/{TaskDiffReviewDialog-ClUn4Ni4.js → TaskDiffReviewDialog-VwZmo00b.js} +1 -1
- package/apps/web/dist/assets/WorkbenchSettingsDialog-CThWkZHd.js +1 -0
- package/apps/web/dist/assets/WorkbenchView-D6auwJnA.js +60 -0
- package/apps/web/dist/assets/index-8vfFmsVl.js +2 -0
- package/apps/web/dist/assets/{index-CTHBQ5Ng.css → index-BPfQQtEB.css} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/package.json +1 -1
- package/packages/shared/src/index.js +6 -0
- package/packages/shared/src/shellCommands.js +81 -0
- package/packages/shared/src/shellCommands.test.js +45 -0
- package/apps/web/dist/assets/WorkbenchSettingsDialog-C74C5fs1.js +0 -1
- package/apps/web/dist/assets/WorkbenchView-BgfyrrvY.js +0 -57
- package/apps/web/dist/assets/index-BIa_ZvMq.js +0 -2
|
@@ -787,6 +787,185 @@ function parseNumstat(output = '') {
|
|
|
787
787
|
}
|
|
788
788
|
}
|
|
789
789
|
|
|
790
|
+
function parseGitEntryMode(output = '') {
|
|
791
|
+
const line = String(output || '')
|
|
792
|
+
.split('\0')
|
|
793
|
+
.find(Boolean)
|
|
794
|
+
|| ''
|
|
795
|
+
const match = line.match(/^(\d{6})\s+/)
|
|
796
|
+
return match?.[1] ? match[1] : ''
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function isGitSubmodulePath(repoRoot = '', filePath = '', headOid = '') {
|
|
800
|
+
const normalizedPath = String(filePath || '').trim()
|
|
801
|
+
if (!repoRoot || !normalizedPath) {
|
|
802
|
+
return false
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (headOid) {
|
|
806
|
+
const result = runGit(repoRoot, ['ls-tree', '-z', headOid, '--', normalizedPath])
|
|
807
|
+
return parseGitEntryMode(result.stdout) === '160000'
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const result = runGit(repoRoot, ['ls-files', '-z', '-s', '--', normalizedPath])
|
|
811
|
+
return parseGitEntryMode(result.stdout) === '160000'
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function resolveSubmoduleRootForNestedPath(repoRoot = '', filePath = '', headOid = '') {
|
|
815
|
+
const normalizedPath = String(filePath || '').trim().replace(/^\/+|\/+$/g, '')
|
|
816
|
+
if (!repoRoot || !normalizedPath || !normalizedPath.includes('/')) {
|
|
817
|
+
return ''
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const segments = normalizedPath.split('/')
|
|
821
|
+
for (let index = segments.length - 1; index >= 1; index -= 1) {
|
|
822
|
+
const candidate = segments.slice(0, index).join('/')
|
|
823
|
+
if (isGitSubmodulePath(repoRoot, candidate, headOid) || isGitSubmodulePath(repoRoot, candidate)) {
|
|
824
|
+
return candidate
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return ''
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function buildSubmoduleDiffPayload(repoRoot = '', filePath = '', options = {}) {
|
|
832
|
+
const normalizedPath = String(filePath || '').trim()
|
|
833
|
+
const fromHeadOid = String(options.fromHeadOid || '').trim()
|
|
834
|
+
const toHeadOid = String(options.toHeadOid || '').trim()
|
|
835
|
+
const workspaceMode = Boolean(options.workspaceMode)
|
|
836
|
+
const includePatch = Boolean(options.includePatch)
|
|
837
|
+
const includeStats = includePatch || Boolean(options.includeStats)
|
|
838
|
+
|
|
839
|
+
if (!repoRoot || !normalizedPath) {
|
|
840
|
+
return {
|
|
841
|
+
binary: false,
|
|
842
|
+
tooLarge: false,
|
|
843
|
+
patch: '',
|
|
844
|
+
patchLoaded: false,
|
|
845
|
+
additions: 0,
|
|
846
|
+
deletions: 0,
|
|
847
|
+
statsLoaded: false,
|
|
848
|
+
message: '',
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const diffArgs = ['diff', '--submodule=diff', '--no-color', '--unified=3']
|
|
853
|
+
if (!workspaceMode && fromHeadOid && toHeadOid) {
|
|
854
|
+
diffArgs.push(`${fromHeadOid}..${toHeadOid}`)
|
|
855
|
+
} else {
|
|
856
|
+
diffArgs.push('HEAD')
|
|
857
|
+
}
|
|
858
|
+
diffArgs.push('--', normalizedPath)
|
|
859
|
+
|
|
860
|
+
const result = runGit(repoRoot, diffArgs)
|
|
861
|
+
const patch = String(result.stdout || '').trim()
|
|
862
|
+
const stats = includeStats ? parsePatchStats(patch) : { additions: null, deletions: null }
|
|
863
|
+
|
|
864
|
+
if (!includePatch) {
|
|
865
|
+
return {
|
|
866
|
+
binary: false,
|
|
867
|
+
tooLarge: false,
|
|
868
|
+
patch: '',
|
|
869
|
+
patchLoaded: false,
|
|
870
|
+
additions: stats.additions,
|
|
871
|
+
deletions: stats.deletions,
|
|
872
|
+
statsLoaded: includeStats,
|
|
873
|
+
message: '',
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (patch.length > MAX_PATCH_TEXT_BYTES) {
|
|
878
|
+
return {
|
|
879
|
+
binary: false,
|
|
880
|
+
tooLarge: true,
|
|
881
|
+
patch: '',
|
|
882
|
+
patchLoaded: true,
|
|
883
|
+
additions: stats.additions,
|
|
884
|
+
deletions: stats.deletions,
|
|
885
|
+
statsLoaded: includeStats,
|
|
886
|
+
message: 'diff 内容较长,暂不在页面内完整展示。',
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
binary: false,
|
|
892
|
+
tooLarge: false,
|
|
893
|
+
patch,
|
|
894
|
+
patchLoaded: true,
|
|
895
|
+
additions: stats.additions,
|
|
896
|
+
deletions: stats.deletions,
|
|
897
|
+
statsLoaded: includeStats,
|
|
898
|
+
message: '',
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function parseSubmodulePatchEntries(submodulePath = '', patch = '') {
|
|
903
|
+
const normalizedSubmodulePath = String(submodulePath || '').trim().replace(/\/+$/, '')
|
|
904
|
+
const text = String(patch || '').trim()
|
|
905
|
+
if (!normalizedSubmodulePath || !text) {
|
|
906
|
+
return []
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const lines = text.split('\n')
|
|
910
|
+
const sections = []
|
|
911
|
+
let currentSection = []
|
|
912
|
+
|
|
913
|
+
lines.forEach((line) => {
|
|
914
|
+
if (line.startsWith('diff --git a/')) {
|
|
915
|
+
if (currentSection.length) {
|
|
916
|
+
sections.push(currentSection.join('\n').trim())
|
|
917
|
+
}
|
|
918
|
+
currentSection = [line]
|
|
919
|
+
return
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (currentSection.length) {
|
|
923
|
+
currentSection.push(line)
|
|
924
|
+
}
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
if (currentSection.length) {
|
|
928
|
+
sections.push(currentSection.join('\n').trim())
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return sections
|
|
932
|
+
.map((section) => {
|
|
933
|
+
const header = section.split('\n', 1)[0] || ''
|
|
934
|
+
const matched = header.match(/^diff --git a\/(.+?) b\/(.+)$/)
|
|
935
|
+
const rawPath = matched?.[2] || matched?.[1] || ''
|
|
936
|
+
let nestedPath = String(rawPath || '').trim()
|
|
937
|
+
if (!nestedPath) {
|
|
938
|
+
return null
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const duplicatedPrefix = `${normalizedSubmodulePath}/`
|
|
942
|
+
if (nestedPath === normalizedSubmodulePath) {
|
|
943
|
+
nestedPath = ''
|
|
944
|
+
} else if (nestedPath.startsWith(duplicatedPrefix)) {
|
|
945
|
+
nestedPath = nestedPath.slice(duplicatedPrefix.length)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (!nestedPath) {
|
|
949
|
+
return null
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const stats = parsePatchStats(section)
|
|
953
|
+
return {
|
|
954
|
+
path: `${normalizedSubmodulePath}/${nestedPath}`,
|
|
955
|
+
status: 'M',
|
|
956
|
+
additions: stats.additions,
|
|
957
|
+
deletions: stats.deletions,
|
|
958
|
+
statsLoaded: true,
|
|
959
|
+
binary: false,
|
|
960
|
+
tooLarge: false,
|
|
961
|
+
patch: section,
|
|
962
|
+
patchLoaded: true,
|
|
963
|
+
message: '',
|
|
964
|
+
}
|
|
965
|
+
})
|
|
966
|
+
.filter(Boolean)
|
|
967
|
+
}
|
|
968
|
+
|
|
790
969
|
function buildDiffPayloadForFile(filePath = '', previousState = null, nextState = null, options = {}) {
|
|
791
970
|
const includePatch = Boolean(options.includePatch)
|
|
792
971
|
const includeStats = includePatch || Boolean(options.includeStats)
|
|
@@ -1057,7 +1236,24 @@ function createDiffFileEntry(filePath = '', previousState = null, nextState = nu
|
|
|
1057
1236
|
return null
|
|
1058
1237
|
}
|
|
1059
1238
|
|
|
1060
|
-
const
|
|
1239
|
+
const repoRoot = String(options.repoRoot || '').trim()
|
|
1240
|
+
const fromHeadOid = String(options.fromHeadOid || '').trim()
|
|
1241
|
+
const toHeadOid = String(options.toHeadOid || '').trim()
|
|
1242
|
+
const isSubmodule = repoRoot && (
|
|
1243
|
+
isGitSubmodulePath(repoRoot, filePath, fromHeadOid)
|
|
1244
|
+
|| isGitSubmodulePath(repoRoot, filePath, toHeadOid)
|
|
1245
|
+
|| isGitSubmodulePath(repoRoot, filePath)
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
const patchPayload = isSubmodule
|
|
1249
|
+
? buildSubmoduleDiffPayload(repoRoot, filePath, {
|
|
1250
|
+
includePatch: Boolean(options.includePatch),
|
|
1251
|
+
includeStats: Boolean(options.includeStats),
|
|
1252
|
+
fromHeadOid,
|
|
1253
|
+
toHeadOid,
|
|
1254
|
+
workspaceMode: Boolean(options.workspaceMode),
|
|
1255
|
+
})
|
|
1256
|
+
: buildDiffPayloadForFile(filePath, previousState, nextState, options)
|
|
1061
1257
|
return {
|
|
1062
1258
|
path: filePath,
|
|
1063
1259
|
status: deriveFileStatus(previousState, nextState),
|
|
@@ -1072,6 +1268,61 @@ function createDiffFileEntry(filePath = '', previousState = null, nextState = nu
|
|
|
1072
1268
|
}
|
|
1073
1269
|
}
|
|
1074
1270
|
|
|
1271
|
+
function createDiffEntriesForPath(filePath = '', previousState = null, nextState = null, options = {}) {
|
|
1272
|
+
if (areFileStatesEqual(previousState, nextState)) {
|
|
1273
|
+
return []
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const repoRoot = String(options.repoRoot || '').trim()
|
|
1277
|
+
const fromHeadOid = String(options.fromHeadOid || '').trim()
|
|
1278
|
+
const toHeadOid = String(options.toHeadOid || '').trim()
|
|
1279
|
+
const submoduleRoot = repoRoot && (
|
|
1280
|
+
isGitSubmodulePath(repoRoot, filePath, fromHeadOid)
|
|
1281
|
+
|| isGitSubmodulePath(repoRoot, filePath, toHeadOid)
|
|
1282
|
+
|| isGitSubmodulePath(repoRoot, filePath)
|
|
1283
|
+
? filePath
|
|
1284
|
+
: resolveSubmoduleRootForNestedPath(repoRoot, filePath, fromHeadOid)
|
|
1285
|
+
|| resolveSubmoduleRootForNestedPath(repoRoot, filePath, toHeadOid)
|
|
1286
|
+
|| resolveSubmoduleRootForNestedPath(repoRoot, filePath)
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1289
|
+
if (!submoduleRoot) {
|
|
1290
|
+
const entry = createDiffFileEntry(filePath, previousState, nextState, options)
|
|
1291
|
+
return entry ? [entry] : []
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const payload = buildSubmoduleDiffPayload(repoRoot, submoduleRoot, {
|
|
1295
|
+
includePatch: true,
|
|
1296
|
+
includeStats: Boolean(options.includeStats),
|
|
1297
|
+
fromHeadOid,
|
|
1298
|
+
toHeadOid,
|
|
1299
|
+
workspaceMode: Boolean(options.workspaceMode),
|
|
1300
|
+
})
|
|
1301
|
+
|
|
1302
|
+
const nestedEntries = payload.patchLoaded
|
|
1303
|
+
? parseSubmodulePatchEntries(submoduleRoot, payload.patch)
|
|
1304
|
+
: []
|
|
1305
|
+
|
|
1306
|
+
if (nestedEntries.length) {
|
|
1307
|
+
const normalizedPath = String(filePath || '').trim()
|
|
1308
|
+
const matchedEntry = nestedEntries.find((entry) => entry.path === normalizedPath)
|
|
1309
|
+
return matchedEntry ? [matchedEntry] : nestedEntries
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
return [{
|
|
1313
|
+
path: filePath,
|
|
1314
|
+
status: deriveFileStatus(previousState, nextState),
|
|
1315
|
+
additions: payload.additions,
|
|
1316
|
+
deletions: payload.deletions,
|
|
1317
|
+
statsLoaded: payload.statsLoaded,
|
|
1318
|
+
binary: payload.binary,
|
|
1319
|
+
tooLarge: payload.tooLarge,
|
|
1320
|
+
patch: payload.patch,
|
|
1321
|
+
patchLoaded: payload.patchLoaded,
|
|
1322
|
+
message: payload.message,
|
|
1323
|
+
}]
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1075
1326
|
function createUnsupportedResult(reason = '', repoRoot = '', branch = '') {
|
|
1076
1327
|
return {
|
|
1077
1328
|
supported: false,
|
|
@@ -1141,20 +1392,26 @@ export function getWorkspaceGitDiffReviewByCwd(cwd = '', options = {}) {
|
|
|
1141
1392
|
candidatePaths.forEach((filePath) => {
|
|
1142
1393
|
const previousState = baselineStateForPath(filePath)
|
|
1143
1394
|
const nextState = readFileState(repoRoot, filePath)
|
|
1144
|
-
const
|
|
1395
|
+
const diffEntries = createDiffEntriesForPath(filePath, previousState, nextState, {
|
|
1145
1396
|
includePatch: Boolean(targetFilePath),
|
|
1146
1397
|
includeStats,
|
|
1398
|
+
repoRoot,
|
|
1399
|
+
fromHeadOid: headOid,
|
|
1400
|
+
toHeadOid: currentHeadOid,
|
|
1401
|
+
workspaceMode: true,
|
|
1147
1402
|
})
|
|
1148
|
-
if (!
|
|
1403
|
+
if (!diffEntries.length) {
|
|
1149
1404
|
return
|
|
1150
1405
|
}
|
|
1151
1406
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1407
|
+
diffEntries.forEach((diffEntry) => {
|
|
1408
|
+
fileCount += 1
|
|
1409
|
+
additions += Math.max(0, Number(diffEntry.additions) || 0)
|
|
1410
|
+
deletions += Math.max(0, Number(diffEntry.deletions) || 0)
|
|
1411
|
+
if (includeFiles) {
|
|
1412
|
+
files.push(diffEntry)
|
|
1413
|
+
}
|
|
1414
|
+
})
|
|
1158
1415
|
})
|
|
1159
1416
|
|
|
1160
1417
|
const payload = {
|
|
@@ -1400,20 +1657,25 @@ export function getTaskGitDiffReview(taskSlug = '', options = {}) {
|
|
|
1400
1657
|
candidatePaths.forEach((filePath) => {
|
|
1401
1658
|
const previousState = baselineStateForPath(filePath)
|
|
1402
1659
|
const nextState = nextStateForPath(filePath)
|
|
1403
|
-
const
|
|
1660
|
+
const diffEntries = createDiffEntriesForPath(filePath, previousState, nextState, {
|
|
1404
1661
|
includePatch: Boolean(targetFilePath),
|
|
1405
1662
|
includeStats,
|
|
1663
|
+
repoRoot,
|
|
1664
|
+
fromHeadOid: baseline.headOid,
|
|
1665
|
+
toHeadOid: currentHeadOid,
|
|
1406
1666
|
})
|
|
1407
|
-
if (!
|
|
1667
|
+
if (!diffEntries.length) {
|
|
1408
1668
|
return
|
|
1409
1669
|
}
|
|
1410
1670
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1671
|
+
diffEntries.forEach((diffEntry) => {
|
|
1672
|
+
fileCount += 1
|
|
1673
|
+
additions += Math.max(0, Number(diffEntry.additions) || 0)
|
|
1674
|
+
deletions += Math.max(0, Number(diffEntry.deletions) || 0)
|
|
1675
|
+
if (includeFiles) {
|
|
1676
|
+
files.push(diffEntry)
|
|
1677
|
+
}
|
|
1678
|
+
})
|
|
1417
1679
|
})
|
|
1418
1680
|
|
|
1419
1681
|
const payload = {
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { assertInternalRequest } from './internalAuth.js'
|
|
2
2
|
|
|
3
|
+
const RUNNER_EVENTS_BODY_LIMIT = Math.max(
|
|
4
|
+
1024 * 1024,
|
|
5
|
+
Number(process.env.PROMPTX_INTERNAL_RUNNER_EVENTS_BODY_LIMIT) || 8 * 1024 * 1024
|
|
6
|
+
)
|
|
7
|
+
|
|
3
8
|
function registerInternalRunnerRoutes(app, options = {}) {
|
|
4
9
|
const {
|
|
5
10
|
runEventIngestService,
|
|
6
11
|
taskAutomationService,
|
|
7
12
|
} = options
|
|
8
13
|
|
|
9
|
-
app.post('/internal/runner-events',
|
|
14
|
+
app.post('/internal/runner-events', {
|
|
15
|
+
bodyLimit: RUNNER_EVENTS_BODY_LIMIT,
|
|
16
|
+
}, async (request, reply) => {
|
|
10
17
|
try {
|
|
11
18
|
assertInternalRequest(request.headers)
|
|
12
19
|
return runEventIngestService.ingestEvents(request.body?.items || [])
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
2
|
import WebSocket from 'ws'
|
|
3
3
|
|
|
4
|
+
import { buildInternalAuthHeaders } from './internalAuth.js'
|
|
4
5
|
import {
|
|
5
6
|
buildRelayWebSocketUrl,
|
|
6
7
|
createRelayRequestId,
|
|
@@ -315,7 +316,10 @@ function createRelayClient({
|
|
|
315
316
|
const targetUrl = new URL(record.path, config.localBaseUrl)
|
|
316
317
|
const response = await fetch(targetUrl, {
|
|
317
318
|
method: record.method,
|
|
318
|
-
headers:
|
|
319
|
+
headers: buildInternalAuthHeaders({
|
|
320
|
+
...sanitizeProxyHeaders(record.headers, ['cookie']),
|
|
321
|
+
'x-promptx-relay-request': '1',
|
|
322
|
+
}),
|
|
319
323
|
body: ['GET', 'HEAD'].includes(record.method) || !bodyBuffer.length ? undefined : bodyBuffer,
|
|
320
324
|
signal: controller.signal,
|
|
321
325
|
})
|
|
@@ -14,6 +14,9 @@ function normalizeRelayConfig(input = {}) {
|
|
|
14
14
|
const relayUrl = String(input?.relayUrl || '').trim()
|
|
15
15
|
const deviceId = String(input?.deviceId || '').trim()
|
|
16
16
|
const deviceToken = String(input?.deviceToken || '').trim()
|
|
17
|
+
const allowRemoteShell = typeof input?.allowRemoteShell === 'boolean'
|
|
18
|
+
? input.allowRemoteShell
|
|
19
|
+
: ['1', 'true', 'on', 'yes'].includes(String(input?.allowRemoteShell || '').trim().toLowerCase())
|
|
17
20
|
const enabled = typeof input?.enabled === 'boolean'
|
|
18
21
|
? input.enabled
|
|
19
22
|
: !['0', 'false', 'off', 'no'].includes(String(input?.enabled || '').trim().toLowerCase())
|
|
@@ -22,6 +25,7 @@ function normalizeRelayConfig(input = {}) {
|
|
|
22
25
|
relayUrl,
|
|
23
26
|
deviceId,
|
|
24
27
|
deviceToken,
|
|
28
|
+
allowRemoteShell,
|
|
25
29
|
enabled: Boolean(enabled && relayUrl && deviceId && deviceToken),
|
|
26
30
|
}
|
|
27
31
|
}
|
|
@@ -49,6 +53,7 @@ function getRelayConfigForClient() {
|
|
|
49
53
|
const relayUrl = String(process.env.PROMPTX_RELAY_URL || stored.relayUrl || '').trim()
|
|
50
54
|
const deviceId = String(process.env.PROMPTX_RELAY_DEVICE_ID || stored.deviceId || '').trim()
|
|
51
55
|
const deviceToken = String(process.env.PROMPTX_RELAY_DEVICE_TOKEN || stored.deviceToken || '').trim()
|
|
56
|
+
const envAllowRemoteShell = String(process.env.PROMPTX_RELAY_ALLOW_REMOTE_SHELL || '').trim()
|
|
52
57
|
const envEnabled = String(process.env.PROMPTX_RELAY_ENABLED || '').trim()
|
|
53
58
|
const managedByEnv = isRelayConfigManagedByEnv()
|
|
54
59
|
const enabled = envEnabled
|
|
@@ -56,11 +61,15 @@ function getRelayConfigForClient() {
|
|
|
56
61
|
: managedByEnv
|
|
57
62
|
? Boolean(relayUrl && deviceId && deviceToken)
|
|
58
63
|
: Boolean(stored.enabled)
|
|
64
|
+
const allowRemoteShell = envAllowRemoteShell
|
|
65
|
+
? ['1', 'true', 'on', 'yes'].includes(envAllowRemoteShell.toLowerCase())
|
|
66
|
+
: Boolean(stored.allowRemoteShell)
|
|
59
67
|
|
|
60
68
|
return normalizeRelayConfig({
|
|
61
69
|
relayUrl,
|
|
62
70
|
deviceId,
|
|
63
71
|
deviceToken,
|
|
72
|
+
allowRemoteShell,
|
|
64
73
|
enabled,
|
|
65
74
|
})
|
|
66
75
|
}
|
|
@@ -70,6 +79,7 @@ function isRelayConfigManagedByEnv() {
|
|
|
70
79
|
String(process.env.PROMPTX_RELAY_URL || '').trim()
|
|
71
80
|
|| String(process.env.PROMPTX_RELAY_DEVICE_ID || '').trim()
|
|
72
81
|
|| String(process.env.PROMPTX_RELAY_DEVICE_TOKEN || '').trim()
|
|
82
|
+
|| String(process.env.PROMPTX_RELAY_ALLOW_REMOTE_SHELL || '').trim()
|
|
73
83
|
|| String(process.env.PROMPTX_RELAY_ENABLED || '').trim()
|
|
74
84
|
)
|
|
75
85
|
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
extractRunnerDispatchPatch,
|
|
3
3
|
reconcileRunAfterRunnerDispatchError,
|
|
4
4
|
} from './runnerDispatch.js'
|
|
5
|
+
import { extractShellCommandIntent } from '../../../packages/shared/src/index.js'
|
|
5
6
|
import { createApiError } from './apiErrors.js'
|
|
6
7
|
|
|
7
8
|
function normalizeBaseUrl(value = '') {
|
|
@@ -82,6 +83,11 @@ function buildRunnerPromptPayload(session = {}, input = {}, options = {}) {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
function normalizeCommandMode(value = '') {
|
|
87
|
+
const normalized = String(value || '').trim().toLowerCase()
|
|
88
|
+
return normalized === 'shell' ? 'shell' : ''
|
|
89
|
+
}
|
|
90
|
+
|
|
85
91
|
export function createRunDispatchService(options = {}) {
|
|
86
92
|
const runnerClient = options.runnerClient
|
|
87
93
|
const logger = options.logger || console
|
|
@@ -100,36 +106,56 @@ export function createRunDispatchService(options = {}) {
|
|
|
100
106
|
|
|
101
107
|
async function startTaskRunForTask(payload = {}) {
|
|
102
108
|
const normalizedTaskSlug = String(payload.taskSlug || '').trim()
|
|
103
|
-
const
|
|
104
|
-
const normalizedProjectSessionId = String(payload.projectSessionId ||
|
|
109
|
+
const requestedSessionId = String(payload.sessionId || '').trim()
|
|
110
|
+
const normalizedProjectSessionId = String(payload.projectSessionId || requestedSessionId).trim()
|
|
105
111
|
const normalizedPrompt = String(payload.prompt || '').trim()
|
|
106
112
|
const promptBlocks = Array.isArray(payload.promptBlocks) ? payload.promptBlocks : []
|
|
113
|
+
const displayEngine = String(payload.displayEngine || '').trim()
|
|
114
|
+
const requestedCommandMode = normalizeCommandMode(payload.commandMode)
|
|
115
|
+
const shellIntent = extractShellCommandIntent({
|
|
116
|
+
prompt: normalizedPrompt,
|
|
117
|
+
promptBlocks,
|
|
118
|
+
})
|
|
119
|
+
const commandMode = shellIntent.mode === 'shell' ? 'shell' : ''
|
|
120
|
+
const normalizedCommand = commandMode === 'shell' ? shellIntent.command : ''
|
|
121
|
+
const allowShellCommand = payload.allowShellCommand === true
|
|
107
122
|
|
|
108
123
|
if (!normalizedTaskSlug) {
|
|
109
124
|
throw createApiError('errors.taskNotFound', '任务不存在。', 404)
|
|
110
125
|
}
|
|
111
|
-
if (!
|
|
126
|
+
if (!requestedSessionId) {
|
|
112
127
|
throw createApiError('errors.sessionRequired', '请先选择一个 PromptX 项目。')
|
|
113
128
|
}
|
|
114
129
|
if (!normalizedPrompt) {
|
|
115
130
|
throw createApiError('errors.noPromptToSend', '没有可发送的提示词。')
|
|
116
131
|
}
|
|
132
|
+
if (requestedCommandMode === 'shell' && shellIntent.reason === 'unsupported_blocks') {
|
|
133
|
+
throw createApiError('errors.shellUnsupportedBlocks', '命令模式暂不支持图片或导入文件,请只保留纯文本命令。', 400)
|
|
134
|
+
}
|
|
135
|
+
if (requestedCommandMode === 'shell' && shellIntent.reason === 'empty_command') {
|
|
136
|
+
throw createApiError('errors.shellEmptyCommand', '请输入要执行的命令,例如 !git status', 400)
|
|
137
|
+
}
|
|
138
|
+
if (commandMode === 'shell' && !allowShellCommand) {
|
|
139
|
+
throw createApiError('errors.shellLocalOnly', '命令模式默认仅允许在本机本地界面中使用;如需对远程访问开放,请先到设置里显式开启。', 403)
|
|
140
|
+
}
|
|
117
141
|
|
|
118
142
|
const task = getTaskBySlug(normalizedTaskSlug)
|
|
119
143
|
if (!task || task.expired) {
|
|
120
144
|
throw createApiError('errors.taskNotFound', '任务不存在。', 404)
|
|
121
145
|
}
|
|
122
146
|
|
|
123
|
-
const
|
|
124
|
-
if (!
|
|
147
|
+
const requestedSession = getPromptxCodexSessionById(requestedSessionId)
|
|
148
|
+
if (!requestedSession) {
|
|
125
149
|
throw createApiError('errors.sessionNotFound', '没有找到对应的 PromptX 项目。', 404)
|
|
126
150
|
}
|
|
127
|
-
const projectSession = normalizedProjectSessionId ===
|
|
128
|
-
?
|
|
151
|
+
const projectSession = normalizedProjectSessionId === requestedSessionId
|
|
152
|
+
? requestedSession
|
|
129
153
|
: getPromptxCodexSessionById(normalizedProjectSessionId)
|
|
130
154
|
if (!projectSession) {
|
|
131
155
|
throw createApiError('errors.sessionNotFound', '没有找到对应的 PromptX 项目。', 404)
|
|
132
156
|
}
|
|
157
|
+
const session = commandMode === 'shell' ? projectSession : requestedSession
|
|
158
|
+
const normalizedSessionId = String(session?.id || '').trim()
|
|
133
159
|
|
|
134
160
|
const relatedSessionIds = new Set([
|
|
135
161
|
normalizedProjectSessionId,
|
|
@@ -145,11 +171,16 @@ export function createRunDispatchService(options = {}) {
|
|
|
145
171
|
throw createApiError('errors.currentProjectRunning', '当前项目正在执行中,请等待完成后再发送。', 409)
|
|
146
172
|
}
|
|
147
173
|
|
|
174
|
+
const runEngine = commandMode === 'shell' ? 'shell' : (session.engine || 'codex')
|
|
175
|
+
const runnerPrompt = commandMode === 'shell' ? normalizedCommand : normalizedPrompt
|
|
176
|
+
|
|
148
177
|
const runRecord = createCodexRun({
|
|
149
178
|
taskSlug: normalizedTaskSlug,
|
|
150
179
|
sessionId: normalizedSessionId,
|
|
151
180
|
prompt: normalizedPrompt,
|
|
152
181
|
promptBlocks,
|
|
182
|
+
engine: runEngine,
|
|
183
|
+
displayEngine,
|
|
153
184
|
status: 'queued',
|
|
154
185
|
})
|
|
155
186
|
|
|
@@ -160,7 +191,7 @@ export function createRunDispatchService(options = {}) {
|
|
|
160
191
|
|
|
161
192
|
try {
|
|
162
193
|
const runnerPromptPayload = buildRunnerPromptPayload(session, {
|
|
163
|
-
prompt:
|
|
194
|
+
prompt: runnerPrompt,
|
|
164
195
|
promptBlocks,
|
|
165
196
|
}, {
|
|
166
197
|
localServerBaseUrl,
|
|
@@ -172,15 +203,15 @@ export function createRunDispatchService(options = {}) {
|
|
|
172
203
|
runId: runRecord.id,
|
|
173
204
|
taskSlug: normalizedTaskSlug,
|
|
174
205
|
sessionId: normalizedSessionId,
|
|
175
|
-
engine:
|
|
206
|
+
engine: runEngine,
|
|
176
207
|
prompt: runnerPromptPayload.prompt,
|
|
177
208
|
promptBlocks: runnerPromptPayload.promptBlocks,
|
|
178
209
|
cwd: session.cwd,
|
|
179
210
|
title: session.title,
|
|
180
|
-
codexThreadId: session.codexThreadId,
|
|
181
|
-
engineSessionId: session.engineSessionId,
|
|
182
|
-
engineThreadId: session.engineThreadId,
|
|
183
|
-
engineMeta: session.engineMeta,
|
|
211
|
+
codexThreadId: commandMode === 'shell' ? '' : session.codexThreadId,
|
|
212
|
+
engineSessionId: commandMode === 'shell' ? '' : session.engineSessionId,
|
|
213
|
+
engineThreadId: commandMode === 'shell' ? '' : session.engineThreadId,
|
|
214
|
+
engineMeta: commandMode === 'shell' ? {} : session.engineMeta,
|
|
184
215
|
sessionCreatedAt: session.createdAt,
|
|
185
216
|
sessionUpdatedAt: session.updatedAt,
|
|
186
217
|
})
|
|
@@ -8,26 +8,28 @@ import {
|
|
|
8
8
|
import { getPromptxCodexSessionById, updatePromptxCodexSession } from './codexSessions.js'
|
|
9
9
|
|
|
10
10
|
function toSafeSessionPatch(session = {}) {
|
|
11
|
+
const isShellEngine = String(session?.engine || '').trim().toLowerCase() === 'shell'
|
|
11
12
|
const hasIdentityPatch = [
|
|
12
13
|
'codexThreadId',
|
|
13
14
|
'engineSessionId',
|
|
14
15
|
'engineThreadId',
|
|
15
16
|
].some((key) => Object.prototype.hasOwnProperty.call(session, key))
|
|
17
|
+
&& !isShellEngine
|
|
16
18
|
|
|
17
19
|
return {
|
|
18
20
|
...(Object.prototype.hasOwnProperty.call(session, 'title')
|
|
19
21
|
? { title: session.title }
|
|
20
22
|
: {}),
|
|
21
|
-
...(Object.prototype.hasOwnProperty.call(session, 'codexThreadId')
|
|
23
|
+
...(!isShellEngine && Object.prototype.hasOwnProperty.call(session, 'codexThreadId')
|
|
22
24
|
? { codexThreadId: session.codexThreadId }
|
|
23
25
|
: {}),
|
|
24
|
-
...(Object.prototype.hasOwnProperty.call(session, 'engineSessionId')
|
|
26
|
+
...(!isShellEngine && Object.prototype.hasOwnProperty.call(session, 'engineSessionId')
|
|
25
27
|
? { engineSessionId: session.engineSessionId }
|
|
26
28
|
: {}),
|
|
27
|
-
...(Object.prototype.hasOwnProperty.call(session, 'engineThreadId')
|
|
29
|
+
...(!isShellEngine && Object.prototype.hasOwnProperty.call(session, 'engineThreadId')
|
|
28
30
|
? { engineThreadId: session.engineThreadId }
|
|
29
31
|
: {}),
|
|
30
|
-
...(Object.prototype.hasOwnProperty.call(session, 'engineMeta')
|
|
32
|
+
...(!isShellEngine && Object.prototype.hasOwnProperty.call(session, 'engineMeta')
|
|
31
33
|
? { engineMeta: session.engineMeta }
|
|
32
34
|
: {}),
|
|
33
35
|
...(hasIdentityPatch
|
|
@@ -1,8 +1,75 @@
|
|
|
1
1
|
import { normalizeCodexRunEventsMode } from '../../../packages/shared/src/index.js'
|
|
2
2
|
import { getApiErrorPayload } from './apiErrors.js'
|
|
3
|
+
import { isValidInternalAuthToken, readInternalAuthToken } from './internalAuth.js'
|
|
4
|
+
import { getRelayConfigForClient } from './relayConfig.js'
|
|
3
5
|
|
|
4
6
|
const MAX_TASK_REORDER_COUNT = 200
|
|
5
7
|
|
|
8
|
+
function isLoopbackHost(value = '') {
|
|
9
|
+
const host = String(value || '').trim().toLowerCase().replace(/^\[|\]$/g, '')
|
|
10
|
+
return host === '127.0.0.1'
|
|
11
|
+
|| host === '::1'
|
|
12
|
+
|| host === 'localhost'
|
|
13
|
+
|| host.endsWith('.localhost')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isLoopbackUrl(value = '') {
|
|
17
|
+
const text = String(value || '').trim()
|
|
18
|
+
if (!text) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return isLoopbackHost(new URL(text).hostname)
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isLocalShellCommandRequest(request) {
|
|
30
|
+
if (isRelayProxyRequest(request)) {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const origin = String(request?.headers?.origin || '').trim()
|
|
35
|
+
if (origin) {
|
|
36
|
+
return isLoopbackUrl(origin)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const referer = String(request?.headers?.referer || '').trim()
|
|
40
|
+
if (referer) {
|
|
41
|
+
return isLoopbackUrl(referer)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hostname = String(request?.hostname || '').trim()
|
|
45
|
+
if (hostname) {
|
|
46
|
+
return isLoopbackHost(hostname)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hostHeader = String(request?.headers?.host || '').trim()
|
|
50
|
+
if (hostHeader) {
|
|
51
|
+
return isLoopbackHost(hostHeader.split(':')[0])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isRelayProxyRequest(request) {
|
|
58
|
+
const relayRequestFlag = String(request?.headers?.['x-promptx-relay-request'] || '').trim()
|
|
59
|
+
if (relayRequestFlag !== '1') {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
return isValidInternalAuthToken(readInternalAuthToken(request?.headers || {}))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isShellCommandRequestAllowed(request, relayConfig = {}) {
|
|
66
|
+
if (isLocalShellCommandRequest(request)) {
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return isRelayProxyRequest(request) && Boolean(relayConfig?.allowRemoteShell)
|
|
71
|
+
}
|
|
72
|
+
|
|
6
73
|
function createEmptyWorkspaceDiffSummary() {
|
|
7
74
|
return {
|
|
8
75
|
supported: false,
|
|
@@ -116,6 +183,7 @@ function registerTaskRoutes(app, options = {}) {
|
|
|
116
183
|
deleteTask,
|
|
117
184
|
deleteTaskCodexRuns,
|
|
118
185
|
getPromptxCodexSessionById,
|
|
186
|
+
getRelayConfig = getRelayConfigForClient,
|
|
119
187
|
getRunningCodexRunByTaskSlug,
|
|
120
188
|
getTaskBySlug,
|
|
121
189
|
getTaskGitDiffReviewInSubprocess,
|
|
@@ -366,6 +434,10 @@ function registerTaskRoutes(app, options = {}) {
|
|
|
366
434
|
sessionId: request.body?.sessionId,
|
|
367
435
|
prompt: request.body?.prompt,
|
|
368
436
|
promptBlocks: request.body?.promptBlocks,
|
|
437
|
+
displayEngine: request.body?.displayEngine,
|
|
438
|
+
commandMode: request.body?.commandMode,
|
|
439
|
+
command: request.body?.command,
|
|
440
|
+
allowShellCommand: isShellCommandRequestAllowed(request, getRelayConfig()),
|
|
369
441
|
})
|
|
370
442
|
return reply.code(payload?.runnerDispatchPending ? 202 : 201).send(payload)
|
|
371
443
|
} catch (error) {
|