@muyichengshayu/promptx 0.1.13 → 0.1.15
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/README.en.md +3 -3
- package/README.md +3 -3
- package/apps/server/src/apiErrors.js +29 -0
- package/apps/server/src/assetRoutes.js +13 -6
- package/apps/server/src/codexRoutes.js +11 -8
- package/apps/server/src/codexSessions.js +8 -9
- package/apps/server/src/db.js +2 -0
- package/apps/server/src/index.js +9 -6
- package/apps/server/src/relayClient.js +32 -1
- package/apps/server/src/repository.js +15 -7
- package/apps/server/src/runDispatchService.js +7 -6
- package/apps/server/src/runnerClient.js +17 -6
- package/apps/server/src/systemRoutes.js +7 -2
- package/apps/server/src/taskAutomation.js +46 -28
- package/apps/server/src/taskRoutes.js +49 -21
- package/apps/server/src/workspaceFiles.js +9 -10
- package/apps/web/dist/assets/CodexSessionManagerDialog-DRvITBqE.js +1 -0
- package/apps/web/dist/assets/TaskDiffReviewDialog-CkJpxeLZ.js +12 -0
- package/apps/web/dist/assets/WorkbenchSettingsDialog-uWxVDrKG.js +26 -0
- package/apps/web/dist/assets/WorkbenchView-C_zr7Daj.js +236 -0
- package/apps/web/dist/assets/{index-gJE-amF1.css → index-XmpKh7Q0.css} +1 -1
- package/apps/web/dist/assets/index-v6Y430fB.js +25 -0
- package/apps/web/dist/assets/{info-Cj7IWLiL.js → info-Cs3Xt3mr.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/package.json +1 -1
- package/packages/shared/src/index.js +16 -0
- package/apps/web/dist/assets/CodexSessionManagerDialog-iKV71_Q0.js +0 -1
- package/apps/web/dist/assets/TaskDiffReviewDialog-RZU6cecW.js +0 -12
- package/apps/web/dist/assets/WorkbenchSettingsDialog-wBQni_hm.js +0 -26
- package/apps/web/dist/assets/WorkbenchView-CfS_BXaL.js +0 -236
- package/apps/web/dist/assets/index-kAN-5XeP.js +0 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.15
|
|
4
|
+
|
|
5
|
+
- 修复右侧编辑区在输入、自动保存和后台状态刷新同时发生时,偶发被旧数据覆盖导致尾字丢失的问题;当前任务只要仍处于编辑态,就以本地输入为准。
|
|
6
|
+
- 调整编辑区 block 渲染标识,改用稳定 key,减少插入图片、导入文件或删除前置 block 后的焦点跳动、光标丢失与输入状态抖动。
|
|
7
|
+
- 优化编辑区滚动策略:输入过程中不再强制把内容区推到底部,长内容中间编辑时视口更稳定,只在需要时把当前输入框保持在可视区域内。
|
|
8
|
+
- 新增浏览器级 E2E,覆盖编辑中防覆盖、自动保存后防覆盖、空闲后恢复同步、长内容中间输入不跳底,以及删除前置 block 后焦点与内容稳定等真实场景。
|
|
9
|
+
|
|
10
|
+
## 0.1.14
|
|
11
|
+
|
|
12
|
+
- 工作台、设置、项目管理、代码变更、编辑任务等核心界面补齐中英双语支持,并新增前端 `i18n` 基础设施,后续扩展语言成本更低。
|
|
13
|
+
- 服务端错误返回补齐稳定的 `messageKey`,前端优先按 key 翻译;同时把 Relay 诊断、自动化通知、任务设置等动态文本一并接入 locale,减少英文界面穿透中文的问题。
|
|
14
|
+
- 优化英文界面的文案长度与布局细节,收短 `Changes`、`Projects` 等标题,调整项目列表与代码变更面板在英文场景下的换行与排布表现。
|
|
15
|
+
- 补齐多语言相关自动化验证:修复前端错误格式化回归,更新页面级 E2E 基座,并验证最新 turn 展开、历史执行过程加载和滚动跟随等关键流程未受影响。
|
|
16
|
+
|
|
3
17
|
## 0.1.13
|
|
4
18
|
|
|
5
19
|
- 加固 Relay 登录链路:登录与管理登录改为 `POST`,并新增登录限流,降低暴力尝试与口令暴露风险。
|
package/README.en.md
CHANGED
|
@@ -60,15 +60,15 @@ promptx stop
|
|
|
60
60
|
|
|
61
61
|
### Workspace
|
|
62
62
|
|
|
63
|
-

|
|
64
64
|
|
|
65
65
|
### Settings
|
|
66
66
|
|
|
67
|
-

|
|
68
68
|
|
|
69
69
|
### Mobile
|
|
70
70
|
|
|
71
|
-

|
|
72
72
|
|
|
73
73
|
## Why It Helps
|
|
74
74
|
|
package/README.md
CHANGED
|
@@ -60,15 +60,15 @@ promptx stop
|
|
|
60
60
|
|
|
61
61
|
### 工作台
|
|
62
62
|
|
|
63
|
-

|
|
64
64
|
|
|
65
65
|
### 设置
|
|
66
66
|
|
|
67
|
-

|
|
68
68
|
|
|
69
69
|
### 手机端
|
|
70
70
|
|
|
71
|
-

|
|
72
72
|
|
|
73
73
|
## 为什么好用
|
|
74
74
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function createApiError(messageKey, message, statusCode = 400, extras = {}) {
|
|
2
|
+
const error = new Error(String(message || '请求失败。'))
|
|
3
|
+
error.statusCode = Math.max(400, Number(statusCode) || 400)
|
|
4
|
+
if (messageKey) {
|
|
5
|
+
error.messageKey = String(messageKey)
|
|
6
|
+
}
|
|
7
|
+
Object.assign(error, extras)
|
|
8
|
+
return error
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getApiErrorPayload(error, fallback = {}) {
|
|
12
|
+
const payload = error?.payload && typeof error.payload === 'object' ? error.payload : {}
|
|
13
|
+
const messageKey = String(
|
|
14
|
+
error?.messageKey
|
|
15
|
+
|| payload?.messageKey
|
|
16
|
+
|| fallback.messageKey
|
|
17
|
+
|| ''
|
|
18
|
+
).trim()
|
|
19
|
+
const message = String(
|
|
20
|
+
error?.message
|
|
21
|
+
|| payload?.message
|
|
22
|
+
|| fallback.message
|
|
23
|
+
|| '请求失败。'
|
|
24
|
+
).trim() || '请求失败。'
|
|
25
|
+
|
|
26
|
+
return messageKey
|
|
27
|
+
? { ...payload, messageKey, message }
|
|
28
|
+
: { ...payload, message }
|
|
29
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { pipeline } from 'node:stream/promises'
|
|
4
4
|
import { Jimp } from 'jimp'
|
|
5
5
|
import { nanoid } from 'nanoid'
|
|
6
|
+
import { createApiError } from './apiErrors.js'
|
|
6
7
|
|
|
7
8
|
function registerAssetRoutes(app, options = {}) {
|
|
8
9
|
const {
|
|
@@ -17,10 +18,10 @@ function registerAssetRoutes(app, options = {}) {
|
|
|
17
18
|
app.post('/api/uploads', async (request, reply) => {
|
|
18
19
|
const part = await request.file()
|
|
19
20
|
if (!part) {
|
|
20
|
-
return reply.code(400).send({ message: '没有收到上传文件。' })
|
|
21
|
+
return reply.code(400).send({ messageKey: 'errors.uploadFileMissing', message: '没有收到上传文件。' })
|
|
21
22
|
}
|
|
22
23
|
if (!String(part.mimetype || '').startsWith('image/')) {
|
|
23
|
-
return reply.code(400).send({ message: '只支持上传图片文件。' })
|
|
24
|
+
return reply.code(400).send({ messageKey: 'errors.uploadImageOnly', message: '只支持上传图片文件。' })
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const tempPath = createTempFilePath(tmpDir, part.filename)
|
|
@@ -58,13 +59,13 @@ function registerAssetRoutes(app, options = {}) {
|
|
|
58
59
|
app.post('/api/imports/pdf', async (request, reply) => {
|
|
59
60
|
const part = await request.file()
|
|
60
61
|
if (!part) {
|
|
61
|
-
return reply.code(400).send({ message: '没有收到 PDF 文件。' })
|
|
62
|
+
return reply.code(400).send({ messageKey: 'errors.pdfFileMissing', message: '没有收到 PDF 文件。' })
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
const fileName = normalizeUploadFileName(part.filename, 'task.pdf')
|
|
65
66
|
const mimetype = String(part.mimetype || '').toLowerCase()
|
|
66
67
|
if (mimetype !== 'application/pdf' && !fileName.toLowerCase().endsWith('.pdf')) {
|
|
67
|
-
return reply.code(400).send({ message: '只支持导入 PDF 文件。' })
|
|
68
|
+
return reply.code(400).send({ messageKey: 'errors.pdfOnly', message: '只支持导入 PDF 文件。' })
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
const tempPath = createTempFilePath(tmpDir, fileName, '.pdf')
|
|
@@ -80,7 +81,10 @@ function registerAssetRoutes(app, options = {}) {
|
|
|
80
81
|
|
|
81
82
|
if (!imported.blocks.length) {
|
|
82
83
|
removeAssetFiles(createdAssets)
|
|
83
|
-
return reply.code(422).send({
|
|
84
|
+
return reply.code(422).send({
|
|
85
|
+
messageKey: 'errors.pdfNoImportableContent',
|
|
86
|
+
message: '没有从 PDF 中提取到可导入的文本或图片。',
|
|
87
|
+
})
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
return reply.code(201).send({
|
|
@@ -90,7 +94,10 @@ function registerAssetRoutes(app, options = {}) {
|
|
|
90
94
|
})
|
|
91
95
|
} catch (error) {
|
|
92
96
|
removeAssetFiles(error.createdAssets || createdAssets)
|
|
93
|
-
throw error
|
|
97
|
+
throw createApiError(error?.messageKey || '', error?.message || 'PDF 导入失败。', error?.statusCode || 500, {
|
|
98
|
+
createdAssets: error?.createdAssets || createdAssets,
|
|
99
|
+
cause: error,
|
|
100
|
+
})
|
|
94
101
|
} finally {
|
|
95
102
|
fs.rmSync(tempPath, { force: true })
|
|
96
103
|
}
|
|
@@ -102,7 +102,7 @@ function registerCodexRoutes(app, options = {}) {
|
|
|
102
102
|
app.get('/api/codex/sessions/:sessionId/files/tree', async (request, reply) => {
|
|
103
103
|
const session = getPromptxCodexSessionById(request.params.sessionId)
|
|
104
104
|
if (!session) {
|
|
105
|
-
return reply.code(404).send({ message: '没有找到对应的 PromptX 项目。' })
|
|
105
|
+
return reply.code(404).send({ messageKey: 'errors.sessionNotFound', message: '没有找到对应的 PromptX 项目。' })
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
return listWorkspaceTree(session.cwd, {
|
|
@@ -114,7 +114,7 @@ function registerCodexRoutes(app, options = {}) {
|
|
|
114
114
|
app.get('/api/codex/sessions/:sessionId/files/search', async (request, reply) => {
|
|
115
115
|
const session = getPromptxCodexSessionById(request.params.sessionId)
|
|
116
116
|
if (!session) {
|
|
117
|
-
return reply.code(404).send({ message: '没有找到对应的 PromptX 项目。' })
|
|
117
|
+
return reply.code(404).send({ messageKey: 'errors.sessionNotFound', message: '没有找到对应的 PromptX 项目。' })
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
return searchWorkspaceEntries(session.cwd, {
|
|
@@ -134,7 +134,7 @@ function registerCodexRoutes(app, options = {}) {
|
|
|
134
134
|
app.patch('/api/codex/sessions/:sessionId', async (request, reply) => {
|
|
135
135
|
const session = updatePromptxCodexSession(request.params.sessionId, request.body || {})
|
|
136
136
|
if (!session) {
|
|
137
|
-
return reply.code(404).send({ message: '没有找到对应的 PromptX 项目。' })
|
|
137
|
+
return reply.code(404).send({ messageKey: 'errors.sessionNotFound', message: '没有找到对应的 PromptX 项目。' })
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
broadcastServerEvent('sessions.changed', {
|
|
@@ -145,13 +145,16 @@ function registerCodexRoutes(app, options = {}) {
|
|
|
145
145
|
|
|
146
146
|
app.delete('/api/codex/sessions/:sessionId', async (request, reply) => {
|
|
147
147
|
if (getRunningCodexRunBySessionId(request.params.sessionId)) {
|
|
148
|
-
return reply.code(409).send({
|
|
148
|
+
return reply.code(409).send({
|
|
149
|
+
messageKey: 'errors.currentProjectDeleteWhileRunning',
|
|
150
|
+
message: '当前项目正在执行中,请先停止后再删除。',
|
|
151
|
+
})
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
const affectedTaskSlugs = clearTaskCodexSessionReferences(request.params.sessionId)
|
|
152
155
|
const session = deletePromptxCodexSession(request.params.sessionId)
|
|
153
156
|
if (!session) {
|
|
154
|
-
return reply.code(404).send({ message: '没有找到对应的 PromptX 项目。' })
|
|
157
|
+
return reply.code(404).send({ messageKey: 'errors.sessionNotFound', message: '没有找到对应的 PromptX 项目。' })
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
broadcastServerEvent('sessions.changed', {
|
|
@@ -176,7 +179,7 @@ function registerCodexRoutes(app, options = {}) {
|
|
|
176
179
|
app.post('/api/codex/runs/:runId/stop', async (request, reply) => {
|
|
177
180
|
const runRecord = getCodexRunById(request.params.runId)
|
|
178
181
|
if (!runRecord) {
|
|
179
|
-
return reply.code(404).send({ message: '没有找到对应的执行记录。' })
|
|
182
|
+
return reply.code(404).send({ messageKey: 'errors.runNotFound', message: '没有找到对应的执行记录。' })
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
const stopResult = await runDispatchService.requestRunStop(request.params.runId, {
|
|
@@ -199,7 +202,7 @@ function registerCodexRoutes(app, options = {}) {
|
|
|
199
202
|
app.get('/api/codex/runs/:runId/events', async (request, reply) => {
|
|
200
203
|
const runRecord = getCodexRunById(request.params.runId)
|
|
201
204
|
if (!runRecord) {
|
|
202
|
-
return reply.code(404).send({ message: '没有找到对应的执行记录。' })
|
|
205
|
+
return reply.code(404).send({ messageKey: 'errors.runNotFound', message: '没有找到对应的执行记录。' })
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
return {
|
|
@@ -213,7 +216,7 @@ function registerCodexRoutes(app, options = {}) {
|
|
|
213
216
|
app.get('/api/codex/runs/:runId/stream', async (request, reply) => {
|
|
214
217
|
const runRecord = getCodexRunById(request.params.runId)
|
|
215
218
|
if (!runRecord) {
|
|
216
|
-
return reply.code(404).send({ message: '没有找到对应的执行记录。' })
|
|
219
|
+
return reply.code(404).send({ messageKey: 'errors.runNotFound', message: '没有找到对应的执行记录。' })
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
reply.hijack()
|
|
@@ -4,11 +4,10 @@ import { nanoid } from 'nanoid'
|
|
|
4
4
|
import { normalizeAgentEngine } from '../../../packages/shared/src/index.js'
|
|
5
5
|
import { all, get, run, transaction } from './db.js'
|
|
6
6
|
import { assertAgentRunner } from './agents/index.js'
|
|
7
|
+
import { createApiError } from './apiErrors.js'
|
|
7
8
|
|
|
8
9
|
function createHttpError(message, statusCode = 400) {
|
|
9
|
-
|
|
10
|
-
error.statusCode = statusCode
|
|
11
|
-
return error
|
|
10
|
+
return createApiError('', message, statusCode)
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
function toCodexSession(row) {
|
|
@@ -45,7 +44,7 @@ function ensureAgentRunnerAvailable(engine) {
|
|
|
45
44
|
try {
|
|
46
45
|
assertAgentRunner(engine)
|
|
47
46
|
} catch (error) {
|
|
48
|
-
throw
|
|
47
|
+
throw createApiError('errors.agentEngineUnavailable', error.message || '当前执行引擎不可用。')
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
50
|
|
|
@@ -62,17 +61,17 @@ function normalizeTitle(input = '', cwd = '') {
|
|
|
62
61
|
export function normalizeCwd(input = '') {
|
|
63
62
|
const cwd = String(input || '').trim()
|
|
64
63
|
if (!cwd) {
|
|
65
|
-
throw
|
|
64
|
+
throw createApiError('errors.cwdRequired', '请先填写工作目录。')
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
const resolved = path.resolve(cwd)
|
|
69
68
|
if (!fs.existsSync(resolved)) {
|
|
70
|
-
throw
|
|
69
|
+
throw createApiError('errors.cwdNotFound', '工作目录不存在,请重新确认。')
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
const stats = fs.statSync(resolved)
|
|
74
73
|
if (!stats.isDirectory()) {
|
|
75
|
-
throw
|
|
74
|
+
throw createApiError('errors.cwdMustBeDirectory', '工作目录必须是文件夹。')
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
return resolved
|
|
@@ -144,10 +143,10 @@ export function updatePromptxCodexSession(sessionId, patch = {}) {
|
|
|
144
143
|
ensureAgentRunnerAvailable(nextEngine)
|
|
145
144
|
|
|
146
145
|
if (existing.started && wantsCwd && nextCwd !== existing.cwd) {
|
|
147
|
-
throw
|
|
146
|
+
throw createApiError('errors.startedProjectCwdLocked', '已启动的 PromptX 项目不能直接修改工作目录。', 409)
|
|
148
147
|
}
|
|
149
148
|
if (existing.started && wantsEngine && nextEngine !== existing.engine) {
|
|
150
|
-
throw
|
|
149
|
+
throw createApiError('errors.startedProjectEngineLocked', '已启动的 PromptX 项目不能直接切换执行引擎,请新建项目。', 409)
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
const title = Object.prototype.hasOwnProperty.call(patch, 'title')
|
package/apps/server/src/db.js
CHANGED
|
@@ -183,6 +183,7 @@ function migrateToV1() {
|
|
|
183
183
|
notification_webhook_url TEXT NOT NULL DEFAULT '',
|
|
184
184
|
notification_secret TEXT NOT NULL DEFAULT '',
|
|
185
185
|
notification_trigger_on TEXT NOT NULL DEFAULT 'completed',
|
|
186
|
+
notification_locale TEXT NOT NULL DEFAULT 'zh-CN',
|
|
186
187
|
notification_message_mode TEXT NOT NULL DEFAULT 'summary',
|
|
187
188
|
notification_last_status TEXT NOT NULL DEFAULT '',
|
|
188
189
|
notification_last_error TEXT NOT NULL DEFAULT '',
|
|
@@ -338,6 +339,7 @@ function applyAdditiveSchemaPatches() {
|
|
|
338
339
|
`ALTER TABLE tasks ADD COLUMN notification_webhook_url TEXT NOT NULL DEFAULT ''`,
|
|
339
340
|
`ALTER TABLE tasks ADD COLUMN notification_secret TEXT NOT NULL DEFAULT ''`,
|
|
340
341
|
`ALTER TABLE tasks ADD COLUMN notification_trigger_on TEXT NOT NULL DEFAULT 'completed'`,
|
|
342
|
+
`ALTER TABLE tasks ADD COLUMN notification_locale TEXT NOT NULL DEFAULT 'zh-CN'`,
|
|
341
343
|
`ALTER TABLE tasks ADD COLUMN notification_message_mode TEXT NOT NULL DEFAULT 'summary'`,
|
|
342
344
|
`ALTER TABLE tasks ADD COLUMN notification_last_status TEXT NOT NULL DEFAULT ''`,
|
|
343
345
|
`ALTER TABLE tasks ADD COLUMN notification_last_error TEXT NOT NULL DEFAULT ''`,
|
package/apps/server/src/index.js
CHANGED
|
@@ -76,7 +76,8 @@ import {
|
|
|
76
76
|
registerInternalRunnerRoutes,
|
|
77
77
|
registerRealtimeRoutes,
|
|
78
78
|
} from './internalRoutes.js'
|
|
79
|
-
import { registerAssetRoutes } from './assetRoutes.js'
|
|
79
|
+
import { registerAssetRoutes } from './assetRoutes.js'
|
|
80
|
+
import { getApiErrorPayload } from './apiErrors.js'
|
|
80
81
|
import { registerWebAppRoutes } from './webAppRoutes.js'
|
|
81
82
|
import { createTempFilePath, normalizeUploadFileName } from './upload.js'
|
|
82
83
|
import { importPdfBlocks } from './pdf.js'
|
|
@@ -417,11 +418,13 @@ registerWebAppRoutes(app, {
|
|
|
417
418
|
webDistDir,
|
|
418
419
|
})
|
|
419
420
|
|
|
420
|
-
app.setErrorHandler((error, request, reply) => {
|
|
421
|
-
request.log.error(error)
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
})
|
|
421
|
+
app.setErrorHandler((error, request, reply) => {
|
|
422
|
+
request.log.error(error)
|
|
423
|
+
const payload = getApiErrorPayload(error, error.statusCode === 413
|
|
424
|
+
? { messageKey: 'errors.fileTooLarge', message: '文件太大了。' }
|
|
425
|
+
: { messageKey: 'errors.unexpectedServerError', message: error.message || '发生了意外错误。' })
|
|
426
|
+
reply.code(error.statusCode || 500).send(payload)
|
|
427
|
+
})
|
|
425
428
|
|
|
426
429
|
purgeExpiredContent(true)
|
|
427
430
|
|
|
@@ -33,10 +33,14 @@ function createDisabledStatus() {
|
|
|
33
33
|
lastHeartbeatAt: '',
|
|
34
34
|
lastCloseCode: 0,
|
|
35
35
|
lastCloseReason: '',
|
|
36
|
+
lastCloseReasonCode: '',
|
|
36
37
|
lastError: '',
|
|
38
|
+
lastErrorKey: '',
|
|
39
|
+
lastErrorParams: null,
|
|
37
40
|
reconnectCount: 0,
|
|
38
41
|
reconnectPaused: false,
|
|
39
42
|
reconnectPausedReason: '',
|
|
43
|
+
reconnectPausedReasonCode: '',
|
|
40
44
|
nextReconnectDelayMs: 0,
|
|
41
45
|
recentEvents: [],
|
|
42
46
|
}
|
|
@@ -244,10 +248,12 @@ function createRelayClient({
|
|
|
244
248
|
updateStatus({
|
|
245
249
|
reconnectPaused: true,
|
|
246
250
|
reconnectPausedReason: reason || String(rawReason || '').trim(),
|
|
251
|
+
reconnectPausedReasonCode: String(rawReason || '').trim(),
|
|
247
252
|
nextReconnectDelayMs: 0,
|
|
248
253
|
})
|
|
249
254
|
appendRecentEvent('reconnect_paused', {
|
|
250
255
|
reason: reason || String(rawReason || '').trim() || 'unknown',
|
|
256
|
+
reasonCode: String(rawReason || '').trim() || 'unknown',
|
|
251
257
|
})
|
|
252
258
|
logWarn('[relay] 检测到不可重试错误,已暂停自动重连', getLogContext({
|
|
253
259
|
reason: reason || String(rawReason || '').trim() || 'unknown',
|
|
@@ -408,6 +414,8 @@ function createRelayClient({
|
|
|
408
414
|
return connect().catch((error) => {
|
|
409
415
|
updateStatus({
|
|
410
416
|
lastError: error?.message || 'Relay 连接失败。',
|
|
417
|
+
lastErrorKey: 'connect_failed',
|
|
418
|
+
lastErrorParams: null,
|
|
411
419
|
})
|
|
412
420
|
appendRecentEvent('connect_failed', {
|
|
413
421
|
source,
|
|
@@ -463,8 +471,11 @@ function createRelayClient({
|
|
|
463
471
|
updateStatus({
|
|
464
472
|
reconnectPaused: false,
|
|
465
473
|
reconnectPausedReason: '',
|
|
474
|
+
reconnectPausedReasonCode: '',
|
|
466
475
|
nextReconnectDelayMs: 0,
|
|
467
476
|
lastError: '',
|
|
477
|
+
lastErrorKey: '',
|
|
478
|
+
lastErrorParams: null,
|
|
468
479
|
})
|
|
469
480
|
pendingReconnectSource = source
|
|
470
481
|
appendRecentEvent('reconnect_requested', {
|
|
@@ -598,6 +609,9 @@ function createRelayClient({
|
|
|
598
609
|
lastCloseCode: 0,
|
|
599
610
|
lastCloseReason: '',
|
|
600
611
|
lastError: '',
|
|
612
|
+
lastCloseReasonCode: '',
|
|
613
|
+
lastErrorKey: '',
|
|
614
|
+
lastErrorParams: null,
|
|
601
615
|
})
|
|
602
616
|
appendRecentEvent('auth_ok', {
|
|
603
617
|
tenantKey: String(message?.tenantKey || '').trim(),
|
|
@@ -651,16 +665,26 @@ function createRelayClient({
|
|
|
651
665
|
const reconnectSource = pendingReconnectSource
|
|
652
666
|
pendingReconnectSource = ''
|
|
653
667
|
const { rawReason, closeReason } = parseCloseReason(reason)
|
|
668
|
+
const rawReasonCode = String(rawReason || '').trim()
|
|
654
669
|
const nextError = closeReason && closeReason !== '配置已更新,正在重连'
|
|
655
670
|
? `${wasAuthenticated ? 'Relay 已断开' : 'Relay 连接被拒绝'}:${closeReason}`
|
|
656
671
|
: (!wasAuthenticated && code && code !== 1000 ? `Relay 连接已关闭(code=${code})` : '')
|
|
672
|
+
const nextErrorKey = closeReason && closeReason !== '配置已更新,正在重连'
|
|
673
|
+
? (wasAuthenticated ? 'disconnected' : 'rejected')
|
|
674
|
+
: (!wasAuthenticated && code && code !== 1000 ? 'closed_with_code' : '')
|
|
675
|
+
const nextErrorParams = nextErrorKey === 'closed_with_code'
|
|
676
|
+
? { code: Number(code || 0) }
|
|
677
|
+
: (nextErrorKey ? { reasonCode: rawReasonCode || closeReason || '', code: Number(code || 0) } : null)
|
|
657
678
|
|
|
658
679
|
updateStatus({
|
|
659
680
|
connected: false,
|
|
660
681
|
lastDisconnectedAt: nowIso(),
|
|
661
682
|
lastCloseCode: Number(code || 0),
|
|
662
683
|
lastCloseReason: closeReason,
|
|
663
|
-
|
|
684
|
+
lastCloseReasonCode: rawReasonCode,
|
|
685
|
+
...(nextError
|
|
686
|
+
? { lastError: nextError, lastErrorKey: nextErrorKey, lastErrorParams: nextErrorParams }
|
|
687
|
+
: { lastErrorKey: '', lastErrorParams: null }),
|
|
664
688
|
})
|
|
665
689
|
appendRecentEvent('close', {
|
|
666
690
|
code: Number(code || 0),
|
|
@@ -699,6 +723,8 @@ function createRelayClient({
|
|
|
699
723
|
}
|
|
700
724
|
updateStatus({
|
|
701
725
|
lastError: error?.message || 'Relay 连接失败。',
|
|
726
|
+
lastErrorKey: 'connect_failed',
|
|
727
|
+
lastErrorParams: null,
|
|
702
728
|
})
|
|
703
729
|
appendRecentEvent('error', {
|
|
704
730
|
error: error?.message || String(error || ''),
|
|
@@ -746,6 +772,8 @@ function createRelayClient({
|
|
|
746
772
|
updateStatus({
|
|
747
773
|
connected: false,
|
|
748
774
|
nextReconnectDelayMs: 0,
|
|
775
|
+
lastErrorKey: '',
|
|
776
|
+
lastErrorParams: null,
|
|
749
777
|
})
|
|
750
778
|
appendRecentEvent('stopped')
|
|
751
779
|
logInfo('[relay] 已停止', getLogContext())
|
|
@@ -759,8 +787,11 @@ function createRelayClient({
|
|
|
759
787
|
syncStatusFromConfig()
|
|
760
788
|
updateStatus({
|
|
761
789
|
lastError: '',
|
|
790
|
+
lastErrorKey: '',
|
|
791
|
+
lastErrorParams: null,
|
|
762
792
|
reconnectPaused: false,
|
|
763
793
|
reconnectPausedReason: '',
|
|
794
|
+
reconnectPausedReasonCode: '',
|
|
764
795
|
nextReconnectDelayMs: 0,
|
|
765
796
|
})
|
|
766
797
|
appendRecentEvent('config_updated', {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
normalizeTaskAutomationConcurrencyPolicy,
|
|
13
13
|
normalizeTaskAutomationTimezone,
|
|
14
14
|
normalizeTaskNotificationChannel,
|
|
15
|
+
normalizeTaskNotificationLocale,
|
|
15
16
|
normalizeTaskNotificationMessageMode,
|
|
16
17
|
normalizeTaskNotificationTrigger,
|
|
17
18
|
normalizeExpiry,
|
|
@@ -65,6 +66,7 @@ function toTask(row, blocks = [], options = {}) {
|
|
|
65
66
|
webhookUrl: String(row.notification_webhook_url || ''),
|
|
66
67
|
secret: String(row.notification_secret || ''),
|
|
67
68
|
triggerOn: normalizeTaskNotificationTrigger(row.notification_trigger_on),
|
|
69
|
+
locale: normalizeTaskNotificationLocale(row.notification_locale),
|
|
68
70
|
messageMode: normalizeTaskNotificationMessageMode(row.notification_message_mode),
|
|
69
71
|
lastStatus: String(row.notification_last_status || ''),
|
|
70
72
|
lastError: String(row.notification_last_error || ''),
|
|
@@ -90,6 +92,7 @@ function mapTaskNotificationSummary(row) {
|
|
|
90
92
|
enabled: Boolean(Number(row.notification_enabled) || 0),
|
|
91
93
|
channelType: normalizeTaskNotificationChannel(row.notification_channel_type),
|
|
92
94
|
triggerOn: normalizeTaskNotificationTrigger(row.notification_trigger_on),
|
|
95
|
+
locale: normalizeTaskNotificationLocale(row.notification_locale),
|
|
93
96
|
lastStatus: String(row.notification_last_status || ''),
|
|
94
97
|
lastSentAt: String(row.notification_last_sent_at || ''),
|
|
95
98
|
}
|
|
@@ -122,6 +125,7 @@ function normalizeNotificationInput(input = {}, fallback = {}) {
|
|
|
122
125
|
webhookUrl: enabled ? clampText(input?.webhookUrl || '', 2000).trim() : '',
|
|
123
126
|
secret: enabled ? clampText(input?.secret || '', 200).trim() : '',
|
|
124
127
|
triggerOn: normalizeTaskNotificationTrigger(input?.triggerOn || fallback.triggerOn),
|
|
128
|
+
locale: normalizeTaskNotificationLocale(input?.locale || fallback.locale),
|
|
125
129
|
messageMode: normalizeTaskNotificationMessageMode(input?.messageMode || fallback.messageMode),
|
|
126
130
|
lastStatus: clampText(input?.lastStatus || fallback.lastStatus || '', 32).trim(),
|
|
127
131
|
lastError: clampText(input?.lastError || fallback.lastError || '', 500).trim(),
|
|
@@ -359,7 +363,7 @@ export function listTasks(limit = 30) {
|
|
|
359
363
|
const rows = all(
|
|
360
364
|
`SELECT id, slug, title, auto_title, last_prompt_preview, todo_items_json, codex_session_id,
|
|
361
365
|
automation_enabled, automation_cron, automation_timezone, automation_concurrency_policy, automation_last_triggered_at, automation_next_trigger_at,
|
|
362
|
-
notification_enabled, notification_channel_type, notification_webhook_url, notification_secret, notification_trigger_on, notification_message_mode, notification_last_status, notification_last_error, notification_last_sent_at,
|
|
366
|
+
notification_enabled, notification_channel_type, notification_webhook_url, notification_secret, notification_trigger_on, notification_locale, notification_message_mode, notification_last_status, notification_last_error, notification_last_sent_at,
|
|
363
367
|
visibility, expires_at, created_at, updated_at
|
|
364
368
|
FROM tasks
|
|
365
369
|
ORDER BY created_at DESC, id DESC
|
|
@@ -388,7 +392,7 @@ export function getTaskBySlug(slug) {
|
|
|
388
392
|
const row = get(
|
|
389
393
|
`SELECT id, slug, title, auto_title, last_prompt_preview, todo_items_json, codex_session_id,
|
|
390
394
|
automation_enabled, automation_cron, automation_timezone, automation_concurrency_policy, automation_last_triggered_at, automation_next_trigger_at,
|
|
391
|
-
notification_enabled, notification_channel_type, notification_webhook_url, notification_secret, notification_trigger_on, notification_message_mode, notification_last_status, notification_last_error, notification_last_sent_at,
|
|
395
|
+
notification_enabled, notification_channel_type, notification_webhook_url, notification_secret, notification_trigger_on, notification_locale, notification_message_mode, notification_last_status, notification_last_error, notification_last_sent_at,
|
|
392
396
|
visibility, expires_at, created_at, updated_at
|
|
393
397
|
FROM tasks
|
|
394
398
|
WHERE slug = ?`,
|
|
@@ -424,10 +428,10 @@ export function createTask(input = {}) {
|
|
|
424
428
|
`INSERT INTO tasks (
|
|
425
429
|
slug, edit_token, title, auto_title, last_prompt_preview, todo_items_json, codex_session_id,
|
|
426
430
|
automation_enabled, automation_cron, automation_timezone, automation_concurrency_policy, automation_last_triggered_at, automation_next_trigger_at,
|
|
427
|
-
notification_enabled, notification_channel_type, notification_webhook_url, notification_secret, notification_trigger_on, notification_message_mode, notification_last_status, notification_last_error, notification_last_sent_at,
|
|
431
|
+
notification_enabled, notification_channel_type, notification_webhook_url, notification_secret, notification_trigger_on, notification_locale, notification_message_mode, notification_last_status, notification_last_error, notification_last_sent_at,
|
|
428
432
|
visibility, expires_at, created_at, updated_at
|
|
429
433
|
)
|
|
430
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ,
|
|
434
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ,
|
|
431
435
|
[
|
|
432
436
|
slug,
|
|
433
437
|
editToken,
|
|
@@ -447,6 +451,7 @@ export function createTask(input = {}) {
|
|
|
447
451
|
notification.webhookUrl,
|
|
448
452
|
notification.secret,
|
|
449
453
|
notification.triggerOn,
|
|
454
|
+
notification.locale,
|
|
450
455
|
notification.messageMode,
|
|
451
456
|
notification.lastStatus,
|
|
452
457
|
notification.lastError,
|
|
@@ -469,7 +474,7 @@ export function updateTask(slug, input = {}) {
|
|
|
469
474
|
const existing = get(
|
|
470
475
|
`SELECT id, edit_token, title, auto_title, last_prompt_preview, todo_items_json, codex_session_id,
|
|
471
476
|
automation_enabled, automation_cron, automation_timezone, automation_concurrency_policy, automation_last_triggered_at, automation_next_trigger_at,
|
|
472
|
-
notification_enabled, notification_channel_type, notification_webhook_url, notification_secret, notification_trigger_on, notification_message_mode, notification_last_status, notification_last_error, notification_last_sent_at,
|
|
477
|
+
notification_enabled, notification_channel_type, notification_webhook_url, notification_secret, notification_trigger_on, notification_locale, notification_message_mode, notification_last_status, notification_last_error, notification_last_sent_at,
|
|
473
478
|
visibility, expires_at
|
|
474
479
|
FROM tasks
|
|
475
480
|
WHERE slug = ?`,
|
|
@@ -524,6 +529,7 @@ export function updateTask(slug, input = {}) {
|
|
|
524
529
|
webhookUrl: existing.notification_webhook_url,
|
|
525
530
|
secret: existing.notification_secret,
|
|
526
531
|
triggerOn: existing.notification_trigger_on,
|
|
532
|
+
locale: existing.notification_locale,
|
|
527
533
|
messageMode: existing.notification_message_mode,
|
|
528
534
|
lastStatus: existing.notification_last_status,
|
|
529
535
|
lastError: existing.notification_last_error,
|
|
@@ -535,6 +541,7 @@ export function updateTask(slug, input = {}) {
|
|
|
535
541
|
webhookUrl: existing.notification_webhook_url,
|
|
536
542
|
secret: existing.notification_secret,
|
|
537
543
|
triggerOn: existing.notification_trigger_on,
|
|
544
|
+
locale: existing.notification_locale,
|
|
538
545
|
messageMode: existing.notification_message_mode,
|
|
539
546
|
lastStatus: existing.notification_last_status,
|
|
540
547
|
lastError: existing.notification_last_error,
|
|
@@ -551,7 +558,7 @@ export function updateTask(slug, input = {}) {
|
|
|
551
558
|
`UPDATE tasks
|
|
552
559
|
SET title = ?, auto_title = ?, last_prompt_preview = ?, todo_items_json = ?, codex_session_id = ?,
|
|
553
560
|
automation_enabled = ?, automation_cron = ?, automation_timezone = ?, automation_concurrency_policy = ?, automation_last_triggered_at = ?, automation_next_trigger_at = ?,
|
|
554
|
-
notification_enabled = ?, notification_channel_type = ?, notification_webhook_url = ?, notification_secret = ?, notification_trigger_on = ?, notification_message_mode = ?, notification_last_status = ?, notification_last_error = ?, notification_last_sent_at = ?,
|
|
561
|
+
notification_enabled = ?, notification_channel_type = ?, notification_webhook_url = ?, notification_secret = ?, notification_trigger_on = ?, notification_locale = ?, notification_message_mode = ?, notification_last_status = ?, notification_last_error = ?, notification_last_sent_at = ?,
|
|
555
562
|
visibility = ?, expires_at = ?, updated_at = ?
|
|
556
563
|
WHERE slug = ?`,
|
|
557
564
|
[
|
|
@@ -571,6 +578,7 @@ export function updateTask(slug, input = {}) {
|
|
|
571
578
|
notification.webhookUrl,
|
|
572
579
|
notification.secret,
|
|
573
580
|
notification.triggerOn,
|
|
581
|
+
notification.locale,
|
|
574
582
|
notification.messageMode,
|
|
575
583
|
notification.lastStatus,
|
|
576
584
|
notification.lastError,
|
|
@@ -742,7 +750,7 @@ export function listAutomationEnabledTasks(limit = 200) {
|
|
|
742
750
|
const rows = all(
|
|
743
751
|
`SELECT id, slug, title, auto_title, last_prompt_preview, codex_session_id,
|
|
744
752
|
automation_enabled, automation_cron, automation_timezone, automation_concurrency_policy, automation_last_triggered_at, automation_next_trigger_at,
|
|
745
|
-
notification_enabled, notification_channel_type, notification_webhook_url, notification_secret, notification_trigger_on, notification_message_mode, notification_last_status, notification_last_error, notification_last_sent_at,
|
|
753
|
+
notification_enabled, notification_channel_type, notification_webhook_url, notification_secret, notification_trigger_on, notification_locale, notification_message_mode, notification_last_status, notification_last_error, notification_last_sent_at,
|
|
746
754
|
visibility, expires_at, created_at, updated_at
|
|
747
755
|
FROM tasks
|
|
748
756
|
WHERE automation_enabled = 1
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
extractRunnerDispatchPatch,
|
|
3
3
|
reconcileRunAfterRunnerDispatchError,
|
|
4
4
|
} from './runnerDispatch.js'
|
|
5
|
+
import { createApiError } from './apiErrors.js'
|
|
5
6
|
|
|
6
7
|
export function createRunDispatchService(options = {}) {
|
|
7
8
|
const runnerClient = options.runnerClient
|
|
@@ -23,28 +24,28 @@ export function createRunDispatchService(options = {}) {
|
|
|
23
24
|
const promptBlocks = Array.isArray(payload.promptBlocks) ? payload.promptBlocks : []
|
|
24
25
|
|
|
25
26
|
if (!normalizedTaskSlug) {
|
|
26
|
-
throw
|
|
27
|
+
throw createApiError('errors.taskNotFound', '任务不存在。', 404)
|
|
27
28
|
}
|
|
28
29
|
if (!normalizedSessionId) {
|
|
29
|
-
throw
|
|
30
|
+
throw createApiError('errors.sessionRequired', '请先选择一个 PromptX 项目。')
|
|
30
31
|
}
|
|
31
32
|
if (!normalizedPrompt) {
|
|
32
|
-
throw
|
|
33
|
+
throw createApiError('errors.noPromptToSend', '没有可发送的提示词。')
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const task = getTaskBySlug(normalizedTaskSlug)
|
|
36
37
|
if (!task || task.expired) {
|
|
37
|
-
throw
|
|
38
|
+
throw createApiError('errors.taskNotFound', '任务不存在。', 404)
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const session = getPromptxCodexSessionById(normalizedSessionId)
|
|
41
42
|
if (!session) {
|
|
42
|
-
throw
|
|
43
|
+
throw createApiError('errors.sessionNotFound', '没有找到对应的 PromptX 项目。', 404)
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
const runningRunOnSession = getRunningCodexRunBySessionId(normalizedSessionId)
|
|
46
47
|
if (runningRunOnSession) {
|
|
47
|
-
throw
|
|
48
|
+
throw createApiError('errors.currentProjectRunning', '当前项目正在执行中,请等待完成后再发送。', 409)
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const runRecord = createCodexRun({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildInternalAuthHeaders } from './internalAuth.js'
|
|
2
|
+
import { createApiError } from './apiErrors.js'
|
|
2
3
|
|
|
3
4
|
const DEFAULT_RUNNER_HTTP_TIMEOUT_MS = Math.max(
|
|
4
5
|
500,
|
|
@@ -16,10 +17,12 @@ function getDefaultRunnerBaseUrl() {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
function createHttpError(status, payload, fallbackMessage) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
return createApiError(
|
|
21
|
+
payload?.messageKey || '',
|
|
22
|
+
String(payload?.message || fallbackMessage || `HTTP ${status}`),
|
|
23
|
+
status,
|
|
24
|
+
{ payload }
|
|
25
|
+
)
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
function createRequestAbortController(timeoutMs, upstreamSignal) {
|
|
@@ -73,9 +76,17 @@ export function createRunnerClient(options = {}) {
|
|
|
73
76
|
} catch (error) {
|
|
74
77
|
abortController.cleanup()
|
|
75
78
|
if (abortController.wasTimeout()) {
|
|
76
|
-
throw createHttpError(
|
|
79
|
+
throw createHttpError(
|
|
80
|
+
504,
|
|
81
|
+
{ messageKey: 'errors.runnerRequestTimeout', message: `runner 请求超时(>${timeoutMs}ms)。` },
|
|
82
|
+
'runner 请求超时。'
|
|
83
|
+
)
|
|
77
84
|
}
|
|
78
|
-
throw createHttpError(
|
|
85
|
+
throw createHttpError(
|
|
86
|
+
503,
|
|
87
|
+
{ messageKey: 'errors.runnerServiceUnavailable', message: error.message || '无法连接 runner 服务。' },
|
|
88
|
+
'无法连接 runner 服务。'
|
|
89
|
+
)
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
const text = await response.text()
|