@shawnstack/quickforge 1.3.21 → 1.3.22
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/README.md +10 -10
- package/dist/assets/{anthropic--qj3xmqE.js → anthropic-CDKnv1FQ.js} +1 -1
- package/dist/assets/{azure-openai-responses-DDRaS-MZ.js → azure-openai-responses-BnUwVl-8.js} +1 -1
- package/dist/assets/{google-C-8-FIZS.js → google-DOEyCDZy.js} +1 -1
- package/dist/assets/{google-vertex-Dw2y_nqS.js → google-vertex-BPPf3car.js} +1 -1
- package/dist/assets/{icons-BHkxP7oT.js → icons-WD3UkVNM.js} +1 -1
- package/dist/assets/{index-DRGbHzkd.js → index-CjTN0qaQ.js} +586 -554
- package/dist/assets/index-eeLjaV06.css +3 -0
- package/dist/assets/{mistral-u_5S4wj6.js → mistral-Ber29mja.js} +1 -1
- package/dist/assets/{openai-codex-responses-CWZGpchs.js → openai-codex-responses-D8gq8a3l.js} +1 -1
- package/dist/assets/{openai-completions-C_DdwPuH.js → openai-completions-CATWPFBp.js} +1 -1
- package/dist/assets/{openai-responses-CMp0ziUV.js → openai-responses-DxcB6Ksu.js} +1 -1
- package/dist/assets/{openai-responses-shared-CORWeerT.js → openai-responses-shared-a_PAPxTO.js} +1 -1
- package/dist/assets/{react-vendor-CmyL2roG.js → react-vendor-BcQaTQ90.js} +1 -1
- package/dist/index.html +4 -4
- package/node_modules/@aws-sdk/client-bedrock-runtime/dist-cjs/index.js +1 -0
- package/node_modules/@aws-sdk/client-bedrock-runtime/dist-es/models/enums.js +1 -0
- package/node_modules/@aws-sdk/client-bedrock-runtime/package.json +11 -11
- package/node_modules/@aws-sdk/core/package.json +3 -3
- package/node_modules/@aws-sdk/credential-provider-env/package.json +3 -3
- package/node_modules/@aws-sdk/credential-provider-http/package.json +5 -5
- package/node_modules/@aws-sdk/credential-provider-ini/package.json +11 -11
- package/node_modules/@aws-sdk/credential-provider-login/package.json +4 -4
- package/node_modules/@aws-sdk/credential-provider-node/package.json +9 -9
- package/node_modules/@aws-sdk/credential-provider-process/package.json +3 -3
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/package.json +4 -4
- package/node_modules/@aws-sdk/credential-provider-sso/package.json +5 -5
- package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +4 -4
- package/node_modules/@aws-sdk/eventstream-handler-node/package.json +2 -2
- package/node_modules/@aws-sdk/middleware-eventstream/package.json +2 -2
- package/node_modules/@aws-sdk/middleware-websocket/package.json +5 -5
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/package.json +6 -6
- package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +2 -2
- package/node_modules/@aws-sdk/token-providers/package.json +4 -4
- package/node_modules/@nodable/entities/package.json +1 -1
- package/node_modules/@nodable/entities/src/EntityDecoder.js +1 -1
- package/node_modules/@nodable/entities/src/entities.js +0 -18
- package/node_modules/@smithy/credential-provider-imds/dist-cjs/index.js +14 -13
- package/node_modules/@smithy/credential-provider-imds/dist-es/fromContainerMetadata.js +14 -13
- package/node_modules/@smithy/credential-provider-imds/package.json +1 -1
- package/node_modules/hasown/CHANGELOG.md +7 -0
- package/node_modules/hasown/package.json +4 -5
- package/node_modules/protobufjs/dist/light/protobuf.js +7 -5
- package/node_modules/protobufjs/dist/light/protobuf.min.js +3 -3
- package/node_modules/protobufjs/dist/minimal/protobuf.js +3 -3
- package/node_modules/protobufjs/dist/minimal/protobuf.min.js +3 -3
- package/node_modules/protobufjs/dist/protobuf.js +7 -5
- package/node_modules/protobufjs/dist/protobuf.min.js +3 -3
- package/node_modules/protobufjs/package.json +1 -1
- package/node_modules/protobufjs/src/converter.js +4 -2
- package/node_modules/protobufjs/src/roots.js +1 -1
- package/node_modules/typebox/build/type/script/mapping.mjs +15 -8
- package/node_modules/typebox/build/type/script/parser.mjs +2 -1
- package/node_modules/typebox/package.json +29 -29
- package/package.json +1 -1
- package/server/agent-manager.mjs +63 -25
- package/server/agent-profiles.mjs +191 -0
- package/server/auto-compaction.mjs +20 -0
- package/server/index.mjs +32 -8
- package/server/mcp/registry.mjs +13 -2
- package/server/routes/agent-profiles.mjs +172 -0
- package/server/routes/lan-access.mjs +20 -0
- package/server/routes/scheduled-tasks.mjs +161 -47
- package/server/routes/shared-conversation.mjs +14 -0
- package/server/routes/storage.mjs +10 -0
- package/server/routes/terminal.mjs +13 -3
- package/server/session-utils.mjs +2 -2
- package/server/storage.mjs +3 -4
- package/server/system-prompt.mjs +10 -5
- package/server/terminal/terminal-manager.mjs +9 -1
- package/server/tools/definitions.mjs +2 -7
- package/server/utils/response.mjs +4 -0
- package/dist/assets/index-B-WkttzD.css +0 -3
package/server/mcp/registry.mjs
CHANGED
|
@@ -9,6 +9,7 @@ const TOOL_PREFIX = 'mcp__'
|
|
|
9
9
|
const CONNECT_TIMEOUT_MS = 15_000
|
|
10
10
|
const CALL_TIMEOUT_MS = 120_000
|
|
11
11
|
const MAX_TEXT_LENGTH = 60_000
|
|
12
|
+
const RETRY_ERROR_AFTER_MS = 30_000
|
|
12
13
|
|
|
13
14
|
const connections = new Map()
|
|
14
15
|
let refreshPromise = null
|
|
@@ -126,6 +127,7 @@ async function connectServer(config) {
|
|
|
126
127
|
error: null,
|
|
127
128
|
tools: [],
|
|
128
129
|
connectedAt: null,
|
|
130
|
+
lastAttemptAt: Date.now(),
|
|
129
131
|
stderr: '',
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -133,7 +135,7 @@ async function connectServer(config) {
|
|
|
133
135
|
connection.stderr = truncateText(connection.stderr + chunk.toString(), 4000)
|
|
134
136
|
})
|
|
135
137
|
transport.onclose = () => {
|
|
136
|
-
connection.status = 'disconnected'
|
|
138
|
+
if (connection.status !== 'error') connection.status = 'disconnected'
|
|
137
139
|
}
|
|
138
140
|
transport.onerror = (error) => {
|
|
139
141
|
connection.status = 'error'
|
|
@@ -189,7 +191,15 @@ async function refreshConnections() {
|
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
for (const config of enabled.values()) {
|
|
192
|
-
|
|
194
|
+
const existing = connections.get(config.name)
|
|
195
|
+
if (existing) {
|
|
196
|
+
if (existing.status === 'error' && Date.now() - (existing.lastAttemptAt || 0) >= RETRY_ERROR_AFTER_MS) {
|
|
197
|
+
connections.delete(config.name)
|
|
198
|
+
await closeConnection(existing)
|
|
199
|
+
} else {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
}
|
|
193
203
|
try {
|
|
194
204
|
const connection = await connectServer(config)
|
|
195
205
|
connections.set(config.name, connection)
|
|
@@ -204,6 +214,7 @@ async function refreshConnections() {
|
|
|
204
214
|
tools: [],
|
|
205
215
|
connectedAt: null,
|
|
206
216
|
stderr: '',
|
|
217
|
+
lastAttemptAt: Date.now(),
|
|
207
218
|
})
|
|
208
219
|
}
|
|
209
220
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { streamSimple } from '@mariozechner/pi-ai'
|
|
2
|
+
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
3
|
+
import { readStore } from '../storage.mjs'
|
|
4
|
+
import { logger } from '../utils/logger.mjs'
|
|
5
|
+
import {
|
|
6
|
+
createCustomAgentProfile,
|
|
7
|
+
deleteCustomAgentProfile,
|
|
8
|
+
getAgentProfile,
|
|
9
|
+
listAgentProfiles,
|
|
10
|
+
listAvailableAgentTools,
|
|
11
|
+
updateCustomAgentProfile,
|
|
12
|
+
} from '../agent-profiles.mjs'
|
|
13
|
+
|
|
14
|
+
function requestError(message, statusCode = 400) {
|
|
15
|
+
const error = new Error(message)
|
|
16
|
+
error.statusCode = statusCode
|
|
17
|
+
return error
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeAiJson(text) {
|
|
21
|
+
const raw = String(text || '').trim()
|
|
22
|
+
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)
|
|
23
|
+
const candidate = fenced?.[1] ?? raw
|
|
24
|
+
const start = candidate.indexOf('{')
|
|
25
|
+
const end = candidate.lastIndexOf('}')
|
|
26
|
+
if (start < 0 || end < start) return null
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(candidate.slice(start, end + 1))
|
|
29
|
+
} catch {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function getApiKey(provider) {
|
|
35
|
+
try {
|
|
36
|
+
const keys = await readStore('provider-keys')
|
|
37
|
+
return keys?.[provider] || undefined
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeGeneratedName(value) {
|
|
44
|
+
const raw = String(value || '')
|
|
45
|
+
.trim()
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/\s+/g, '_')
|
|
48
|
+
.replace(/[^a-z0-9_-]/g, '')
|
|
49
|
+
.slice(0, 40)
|
|
50
|
+
const normalized = /^[a-z][a-z0-9_-]{1,39}$/.test(raw) && raw !== 'general' && raw !== 'explore' ? raw : ''
|
|
51
|
+
if (!normalized) throw requestError('AI did not generate a valid agent name', 502)
|
|
52
|
+
return normalized
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeGeneratedAgentProfile(value) {
|
|
56
|
+
const name = normalizeGeneratedName(value?.name)
|
|
57
|
+
const label = String(value?.label || '').trim().slice(0, 80)
|
|
58
|
+
const description = String(value?.description || '').trim().slice(0, 500)
|
|
59
|
+
const systemPrompt = String(value?.systemPrompt || '').trim()
|
|
60
|
+
if (!label) throw requestError('AI did not generate a display name', 502)
|
|
61
|
+
if (!systemPrompt) throw requestError('AI did not generate a system prompt', 502)
|
|
62
|
+
return { name, label, description, systemPrompt }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function generateAgentProfileWithAi(instruction, model, thinkingLevel = 'off') {
|
|
66
|
+
const text = String(instruction || '').trim()
|
|
67
|
+
if (!text) throw requestError('Please describe the agent you want to create')
|
|
68
|
+
if (!model) throw requestError('Please configure a default model first')
|
|
69
|
+
|
|
70
|
+
const systemPrompt = `You are a QuickForge Agent Profile generator.
|
|
71
|
+
Generate only the basic definition fields for a custom Agent Profile from the user's request.
|
|
72
|
+
|
|
73
|
+
Return JSON only. Do not use Markdown. Do not explain.
|
|
74
|
+
|
|
75
|
+
Required JSON shape:
|
|
76
|
+
{
|
|
77
|
+
"name": "lowercase identifier, starts with a letter, 2-40 chars, only lowercase letters, numbers, underscores, hyphens",
|
|
78
|
+
"label": "short display name",
|
|
79
|
+
"description": "one concise sentence describing the agent purpose",
|
|
80
|
+
"systemPrompt": "complete system prompt with role, scope, workflow, boundaries, and output expectations"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Rules:
|
|
84
|
+
- Do not include allowedTools, maxRuntimeMs, maxToolCalls, enabledAsSubagent, or any other fields.
|
|
85
|
+
- name must be English-like lowercase ASCII and must not be general or explore.
|
|
86
|
+
- systemPrompt should be specific and actionable.
|
|
87
|
+
- If the user requests Chinese, write label, description, and systemPrompt in Chinese; otherwise match the user's language.`
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const stream = streamSimple(
|
|
91
|
+
model,
|
|
92
|
+
{
|
|
93
|
+
systemPrompt,
|
|
94
|
+
messages: [{ role: 'user', content: text, timestamp: Date.now() }],
|
|
95
|
+
tools: [],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
apiKey: await getApiKey(model.provider),
|
|
99
|
+
maxTokens: 1600,
|
|
100
|
+
temperature: 0,
|
|
101
|
+
reasoning: thinkingLevel === 'off' ? undefined : thinkingLevel,
|
|
102
|
+
maxRetryDelayMs: 60000,
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
const message = await stream.result()
|
|
106
|
+
const content = Array.isArray(message.content)
|
|
107
|
+
? message.content.filter((block) => block.type === 'text').map((block) => block.text ?? '').join('\n')
|
|
108
|
+
: ''
|
|
109
|
+
const parsed = normalizeAiJson(content)
|
|
110
|
+
if (!parsed) throw requestError('AI did not return valid JSON', 502)
|
|
111
|
+
return normalizeGeneratedAgentProfile(parsed)
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error?.statusCode) throw error
|
|
114
|
+
logger.warn('AI agent profile generation failed:', error?.message || error)
|
|
115
|
+
throw requestError(`AI generation failed: ${error?.message || 'check model configuration and API key'}`, 502)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function handleAgentProfilesApi(req, res, url) {
|
|
120
|
+
const parts = url.pathname.split('/').filter(Boolean)
|
|
121
|
+
|
|
122
|
+
if (req.method === 'GET' && url.pathname === '/api/agent-profiles') {
|
|
123
|
+
sendJson(res, 200, { agents: await listAgentProfiles({ includeDisabled: true }) })
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (req.method === 'GET' && url.pathname === '/api/agent-profiles/available-tools') {
|
|
128
|
+
sendJson(res, 200, { tools: listAvailableAgentTools() })
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (req.method === 'POST' && url.pathname === '/api/agent-profiles/ai-fill') {
|
|
133
|
+
const body = await readJsonBody(req)
|
|
134
|
+
sendJson(res, 200, { agent: await generateAgentProfileWithAi(body?.instruction, body?.model, body?.thinkingLevel) })
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (req.method === 'POST' && url.pathname === '/api/agent-profiles') {
|
|
139
|
+
const body = await readJsonBody(req)
|
|
140
|
+
sendJson(res, 200, { agent: await createCustomAgentProfile(body || {}) })
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (parts[0] === 'api' && parts[1] === 'agent-profiles' && parts[2]) {
|
|
145
|
+
const id = decodeSegment(parts[2])
|
|
146
|
+
|
|
147
|
+
if (req.method === 'GET') {
|
|
148
|
+
const agent = await getAgentProfile(id)
|
|
149
|
+
if (!agent) throw requestError('Agent not found', 404)
|
|
150
|
+
sendJson(res, 200, { agent })
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (req.method === 'PATCH' || req.method === 'PUT') {
|
|
155
|
+
const current = await getAgentProfile(id)
|
|
156
|
+
if (current?.builtin) throw requestError('Built-in agents cannot be modified', 403)
|
|
157
|
+
const body = await readJsonBody(req)
|
|
158
|
+
sendJson(res, 200, { agent: await updateCustomAgentProfile(id, body || {}) })
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (req.method === 'DELETE') {
|
|
163
|
+
const current = await getAgentProfile(id)
|
|
164
|
+
if (current?.builtin) throw requestError('Built-in agents cannot be deleted', 403)
|
|
165
|
+
await deleteCustomAgentProfile(id)
|
|
166
|
+
sendJson(res, 200, { ok: true })
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw requestError('Not found', 404)
|
|
172
|
+
}
|
|
@@ -12,7 +12,26 @@ import {
|
|
|
12
12
|
const MAX_FAILED_ATTEMPTS = 5
|
|
13
13
|
const ATTEMPT_WINDOW_MS = 5 * 60 * 1000
|
|
14
14
|
const LOCK_MS = 5 * 60 * 1000
|
|
15
|
+
const ATTEMPT_CLEANUP_MS = 5 * 60 * 1000
|
|
15
16
|
const attempts = new Map()
|
|
17
|
+
let cleanupTimer = null
|
|
18
|
+
|
|
19
|
+
function cleanupAttempts() {
|
|
20
|
+
const now = Date.now()
|
|
21
|
+
for (const [key, state] of attempts) {
|
|
22
|
+
if (state.resetAt <= now && state.lockedUntil <= now) attempts.delete(key)
|
|
23
|
+
}
|
|
24
|
+
if (attempts.size === 0 && cleanupTimer) {
|
|
25
|
+
clearInterval(cleanupTimer)
|
|
26
|
+
cleanupTimer = null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function scheduleAttemptCleanup() {
|
|
31
|
+
if (cleanupTimer) return
|
|
32
|
+
cleanupTimer = setInterval(cleanupAttempts, ATTEMPT_CLEANUP_MS)
|
|
33
|
+
cleanupTimer.unref?.()
|
|
34
|
+
}
|
|
16
35
|
|
|
17
36
|
function remoteKey(req) {
|
|
18
37
|
return String(req.socket.remoteAddress || 'unknown')
|
|
@@ -25,6 +44,7 @@ function attemptState(req) {
|
|
|
25
44
|
if (!state || state.resetAt <= now) {
|
|
26
45
|
const fresh = { count: 0, resetAt: now + ATTEMPT_WINDOW_MS, lockedUntil: 0 }
|
|
27
46
|
attempts.set(key, fresh)
|
|
47
|
+
scheduleAttemptCleanup()
|
|
28
48
|
return fresh
|
|
29
49
|
}
|
|
30
50
|
return state
|
|
@@ -2,6 +2,7 @@ import { streamSimple } from '@mariozechner/pi-ai'
|
|
|
2
2
|
import { readJsonBody, sendJson, decodeSegment } from '../utils/response.mjs'
|
|
3
3
|
import { readStore, atomicUpdate } from '../storage.mjs'
|
|
4
4
|
import { createAgent, getSessionEventBus, agentEvents, persistSessionState } from '../agent-manager.mjs'
|
|
5
|
+
import { agentProfileSnapshot, getAgentProfile } from '../agent-profiles.mjs'
|
|
5
6
|
import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
|
|
6
7
|
import { logger } from '../utils/logger.mjs'
|
|
7
8
|
|
|
@@ -17,7 +18,55 @@ const weekDayNames = ['周日', '周一', '周二', '周三', '周四', '周五'
|
|
|
17
18
|
|
|
18
19
|
let schedulerTimer = null
|
|
19
20
|
let running = false
|
|
20
|
-
const
|
|
21
|
+
const runningTaskRunIds = new Map()
|
|
22
|
+
|
|
23
|
+
function executionModeFor(task) {
|
|
24
|
+
return task?.executionMode === 'parallel' ? 'parallel' : 'serial'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeExecutionMode(value) {
|
|
28
|
+
if (value === undefined || value === null || value === '') return 'serial'
|
|
29
|
+
const mode = String(value)
|
|
30
|
+
if (mode === 'serial' || mode === 'parallel') return mode
|
|
31
|
+
throw requestError('executionMode must be serial or parallel')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function currentRunIdsFor(task) {
|
|
35
|
+
const ids = []
|
|
36
|
+
if (Array.isArray(task?.currentRunIds)) ids.push(...task.currentRunIds.filter(Boolean))
|
|
37
|
+
if (task?.currentRunId) ids.push(task.currentRunId)
|
|
38
|
+
return [...new Set(ids)]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function activeRunIdsFor(task) {
|
|
42
|
+
const runningIds = [...(runningTaskRunIds.get(task?.id) || [])]
|
|
43
|
+
return [...new Set([...runningIds, ...currentRunIdsFor(task)])]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasActiveTaskRuns(task) {
|
|
47
|
+
return activeRunIdsFor(task).length > 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function addActiveRun(taskId, runId) {
|
|
51
|
+
const ids = runningTaskRunIds.get(taskId) || new Set()
|
|
52
|
+
ids.add(runId)
|
|
53
|
+
runningTaskRunIds.set(taskId, ids)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function removeActiveRun(taskId, runId) {
|
|
57
|
+
const ids = runningTaskRunIds.get(taskId)
|
|
58
|
+
if (!ids) return
|
|
59
|
+
ids.delete(runId)
|
|
60
|
+
if (ids.size === 0) runningTaskRunIds.delete(taskId)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function appendCurrentRunId(task, runId) {
|
|
64
|
+
return [...new Set([...currentRunIdsFor(task), runId])]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function removeCurrentRunId(task, runId) {
|
|
68
|
+
return currentRunIdsFor(task).filter((id) => id !== runId)
|
|
69
|
+
}
|
|
21
70
|
|
|
22
71
|
function createId() {
|
|
23
72
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
|
@@ -260,6 +309,8 @@ function normalizeTaskInput(input, existing = {}) {
|
|
|
260
309
|
const title = nonEmptyString(input?.title ?? existing.title, 'title').slice(0, 80)
|
|
261
310
|
const instruction = nonEmptyString(input?.instruction ?? existing.instruction, 'instruction')
|
|
262
311
|
const scheduleType = String(input?.scheduleType ?? existing.scheduleType ?? 'daily')
|
|
312
|
+
const agentId = Object.prototype.hasOwnProperty.call(input || {}, 'agentId') ? (input.agentId || null) : (existing.agentId || null)
|
|
313
|
+
const executionMode = normalizeExecutionMode(input?.executionMode ?? existing.executionMode)
|
|
263
314
|
|
|
264
315
|
if (scheduleType === 'cron') {
|
|
265
316
|
const cronExpression = String(input?.cronExpression ?? existing.cronExpression ?? '').trim()
|
|
@@ -269,6 +320,8 @@ function normalizeTaskInput(input, existing = {}) {
|
|
|
269
320
|
return {
|
|
270
321
|
title,
|
|
271
322
|
instruction,
|
|
323
|
+
agentId,
|
|
324
|
+
executionMode,
|
|
272
325
|
scheduleType: 'cron',
|
|
273
326
|
scheduleRule: String(input?.scheduleRule ?? existing.scheduleRule ?? cronExpression).trim(),
|
|
274
327
|
cronExpression,
|
|
@@ -288,6 +341,8 @@ function normalizeTaskInput(input, existing = {}) {
|
|
|
288
341
|
return {
|
|
289
342
|
title,
|
|
290
343
|
instruction,
|
|
344
|
+
agentId,
|
|
345
|
+
executionMode,
|
|
291
346
|
scheduleType,
|
|
292
347
|
scheduleRule: `单次 ${formatLocalDateTime(executeAt)}`,
|
|
293
348
|
cronExpression: undefined,
|
|
@@ -306,6 +361,8 @@ function normalizeTaskInput(input, existing = {}) {
|
|
|
306
361
|
return {
|
|
307
362
|
title,
|
|
308
363
|
instruction,
|
|
364
|
+
agentId,
|
|
365
|
+
executionMode,
|
|
309
366
|
scheduleType,
|
|
310
367
|
scheduleRule: `每天 ${executeTime}`,
|
|
311
368
|
cronExpression: undefined,
|
|
@@ -323,6 +380,8 @@ function normalizeTaskInput(input, existing = {}) {
|
|
|
323
380
|
return {
|
|
324
381
|
title,
|
|
325
382
|
instruction,
|
|
383
|
+
agentId,
|
|
384
|
+
executionMode,
|
|
326
385
|
scheduleType,
|
|
327
386
|
scheduleRule: `每${weekDayNames[weekDay]} ${executeTime}`,
|
|
328
387
|
cronExpression: undefined,
|
|
@@ -339,6 +398,8 @@ function normalizeTaskInput(input, existing = {}) {
|
|
|
339
398
|
return {
|
|
340
399
|
title,
|
|
341
400
|
instruction,
|
|
401
|
+
agentId,
|
|
402
|
+
executionMode,
|
|
342
403
|
scheduleType,
|
|
343
404
|
scheduleRule: `每月 ${monthDay} 号 ${executeTime}`,
|
|
344
405
|
cronExpression: undefined,
|
|
@@ -514,27 +575,56 @@ async function resolveExecutionProject(task) {
|
|
|
514
575
|
}
|
|
515
576
|
|
|
516
577
|
async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
517
|
-
|
|
518
|
-
|
|
578
|
+
const mode = executionModeFor(task)
|
|
579
|
+
const advanceNextRunAtAtStart = trigger === 'schedule' && mode === 'parallel'
|
|
580
|
+
if (mode === 'serial' && hasActiveTaskRuns(task)) return
|
|
519
581
|
const runId = createId()
|
|
582
|
+
addActiveRun(task.id, runId)
|
|
520
583
|
const startedAt = new Date().toISOString()
|
|
521
584
|
const scheduledAt = task.nextRunAt
|
|
522
585
|
let sessionId = `scheduled-${task.id}-${Date.now().toString(36)}`
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
586
|
+
let executionAgent = null
|
|
587
|
+
let agentWarning = null
|
|
588
|
+
if (task.agentId) {
|
|
589
|
+
executionAgent = await getAgentProfile(task.agentId)
|
|
590
|
+
if (!executionAgent) agentWarning = `Configured agent not found: ${task.agentId}`
|
|
591
|
+
}
|
|
592
|
+
const agentSnapshot = executionAgent ? agentProfileSnapshot(executionAgent) : null
|
|
593
|
+
|
|
594
|
+
let started = false
|
|
595
|
+
await updateTask(task.id, (current) => {
|
|
596
|
+
const otherRunIds = activeRunIdsFor(current).filter((id) => id !== runId)
|
|
597
|
+
if (mode === 'serial' && otherRunIds.length > 0) return current
|
|
598
|
+
started = true
|
|
599
|
+
const nextRunAt = advanceNextRunAtAtStart
|
|
600
|
+
? calculateNextRun(current, new Date(startedAt))
|
|
601
|
+
: current.nextRunAt
|
|
602
|
+
const activeRunIds = appendCurrentRunId(current, runId)
|
|
603
|
+
return {
|
|
604
|
+
...current,
|
|
605
|
+
currentRunId: activeRunIds[activeRunIds.length - 1] || null,
|
|
606
|
+
currentRunIds: activeRunIds,
|
|
607
|
+
lastSessionId: sessionId,
|
|
608
|
+
nextRunAt,
|
|
609
|
+
runs: [{
|
|
610
|
+
id: runId,
|
|
611
|
+
status: 'running',
|
|
612
|
+
trigger,
|
|
613
|
+
inputContent: current.instruction,
|
|
614
|
+
sessionId,
|
|
615
|
+
agentId: executionAgent?.id || task.agentId || null,
|
|
616
|
+
agentLabel: executionAgent?.label || null,
|
|
617
|
+
agentSnapshot,
|
|
618
|
+
warning: agentWarning || undefined,
|
|
619
|
+
scheduledAt,
|
|
620
|
+
startedAt,
|
|
621
|
+
}, ...(current.runs || [])].slice(0, MAX_RUN_HISTORY_PER_TASK),
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
if (!started) {
|
|
625
|
+
removeActiveRun(task.id, runId)
|
|
626
|
+
return
|
|
627
|
+
}
|
|
538
628
|
|
|
539
629
|
let settled = false
|
|
540
630
|
|
|
@@ -550,6 +640,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
550
640
|
model: task.model,
|
|
551
641
|
thinkingLevel: task.thinkingLevel,
|
|
552
642
|
title: `[定时任务] ${task.title}`,
|
|
643
|
+
agentProfile: executionAgent,
|
|
553
644
|
})
|
|
554
645
|
|
|
555
646
|
const userMessage = {
|
|
@@ -577,7 +668,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
577
668
|
await updateTask(task.id, (current) => ({
|
|
578
669
|
...current,
|
|
579
670
|
lastSessionId: sessionId,
|
|
580
|
-
runs: (current.runs || []).map((run) => run.id === runId ? { ...run, sessionId } : run),
|
|
671
|
+
runs: (current.runs || []).map((run) => run.id === runId ? { ...run, sessionId, agentId: executionAgent?.id || task.agentId || null, agentLabel: executionAgent?.label || null, agentSnapshot, warning: agentWarning || run.warning } : run),
|
|
581
672
|
}))
|
|
582
673
|
onStarted?.({ taskId: task.id, runId, sessionId })
|
|
583
674
|
|
|
@@ -602,7 +693,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
602
693
|
const timeout = setTimeout(() => {
|
|
603
694
|
cleanup(handler, timeout)
|
|
604
695
|
resolve({ ok: false, aborted: false, error: '执行超时', messages: session.agent.state.messages })
|
|
605
|
-
}, 30 * 60 * 1000)
|
|
696
|
+
}, Math.max(1000, Math.min(Number(executionAgent?.maxRuntimeMs || 30 * 60 * 1000), 30 * 60 * 1000)))
|
|
606
697
|
eventBus?.on('agent_event', handler)
|
|
607
698
|
})
|
|
608
699
|
|
|
@@ -622,19 +713,25 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
622
713
|
const aiResult = result.ok ? latestAssistantText(result.messages) : ''
|
|
623
714
|
const latestTask = (await readStore(STORE))[task.id] ?? task
|
|
624
715
|
const recurring = isRecurringTask(latestTask)
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
716
|
+
removeActiveRun(task.id, runId)
|
|
717
|
+
const remainingRunIds = removeCurrentRunId(latestTask, runId)
|
|
718
|
+
const stillRunning = remainingRunIds.length > 0
|
|
719
|
+
const nextRunAt = stillRunning ? latestTask.nextRunAt : (advanceNextRunAtAtStart ? latestTask.nextRunAt : calculateNextRun(latestTask, new Date(finishedAt)))
|
|
720
|
+
const nextStatus = stillRunning
|
|
721
|
+
? latestTask.status
|
|
722
|
+
: latestTask.status === 'paused'
|
|
723
|
+
? 'paused'
|
|
724
|
+
: result.aborted
|
|
725
|
+
? (recurring && nextRunAt ? 'paused' : 'failed')
|
|
726
|
+
: result.ok
|
|
727
|
+
? (nextRunAt ? 'enabled' : 'completed')
|
|
728
|
+
: (recurring && nextRunAt ? 'enabled' : 'failed')
|
|
633
729
|
|
|
634
730
|
await updateTask(task.id, (current) => ({
|
|
635
731
|
...current,
|
|
636
732
|
status: nextStatus,
|
|
637
|
-
currentRunId: null,
|
|
733
|
+
currentRunId: stillRunning ? remainingRunIds[remainingRunIds.length - 1] : null,
|
|
734
|
+
currentRunIds: remainingRunIds,
|
|
638
735
|
lastRunAt: finishedAt,
|
|
639
736
|
nextRunAt: nextRunAt ?? current.nextRunAt,
|
|
640
737
|
lastSessionId: sessionId,
|
|
@@ -646,6 +743,10 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
646
743
|
result: result.ok ? (aiResult || `已完成,结果保存在会话 ${sessionId}`) : undefined,
|
|
647
744
|
errorMessage: result.aborted ? '已暂停执行' : result.error,
|
|
648
745
|
sessionId,
|
|
746
|
+
agentId: executionAgent?.id || latestTask.agentId || null,
|
|
747
|
+
agentLabel: executionAgent?.label || null,
|
|
748
|
+
agentSnapshot,
|
|
749
|
+
warning: agentWarning || run.warning,
|
|
649
750
|
finishedAt,
|
|
650
751
|
durationMs,
|
|
651
752
|
} : run),
|
|
@@ -662,22 +763,32 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
662
763
|
} catch (error) {
|
|
663
764
|
const finishedAt = new Date().toISOString()
|
|
664
765
|
const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
766
|
+
removeActiveRun(task.id, runId)
|
|
767
|
+
await updateTask(task.id, (current) => {
|
|
768
|
+
const remainingRunIds = removeCurrentRunId(current, runId)
|
|
769
|
+
const stillRunning = remainingRunIds.length > 0
|
|
770
|
+
return {
|
|
771
|
+
...current,
|
|
772
|
+
status: stillRunning ? current.status : (current.status === 'paused' ? 'paused' : (isRecurringTask(current) ? 'enabled' : 'failed')),
|
|
773
|
+
currentRunId: stillRunning ? remainingRunIds[remainingRunIds.length - 1] : null,
|
|
774
|
+
currentRunIds: remainingRunIds,
|
|
775
|
+
lastRunAt: finishedAt,
|
|
776
|
+
lastSessionId: sessionId,
|
|
777
|
+
nextRunAt: stillRunning || advanceNextRunAtAtStart ? current.nextRunAt : (isRecurringTask(current) ? (calculateNextRun(current, new Date(finishedAt)) ?? current.nextRunAt) : current.nextRunAt),
|
|
778
|
+
runs: (current.runs || []).map((run) => run.id === runId ? {
|
|
779
|
+
...run,
|
|
780
|
+
status: 'failed',
|
|
781
|
+
errorMessage: error?.message || String(error),
|
|
782
|
+
sessionId,
|
|
783
|
+
agentId: executionAgent?.id || current.agentId || null,
|
|
784
|
+
agentLabel: executionAgent?.label || null,
|
|
785
|
+
agentSnapshot,
|
|
786
|
+
warning: agentWarning || run.warning,
|
|
787
|
+
finishedAt,
|
|
788
|
+
durationMs,
|
|
789
|
+
} : run),
|
|
790
|
+
}
|
|
791
|
+
})
|
|
681
792
|
emitScheduledTaskNotification({
|
|
682
793
|
task,
|
|
683
794
|
runId,
|
|
@@ -687,7 +798,6 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
687
798
|
errorMessage: error?.message || String(error),
|
|
688
799
|
})
|
|
689
800
|
} finally {
|
|
690
|
-
runningTaskIds.delete(task.id)
|
|
691
801
|
if (!settled) logger.warn(`Scheduled task ${task.id} finished without normal agent_end`)
|
|
692
802
|
}
|
|
693
803
|
}
|
|
@@ -702,6 +812,7 @@ async function schedulerTick() {
|
|
|
702
812
|
for (const task of tasks) {
|
|
703
813
|
if (task.status !== 'enabled') continue
|
|
704
814
|
if (!task.nextRunAt || new Date(task.nextRunAt).getTime() > now) continue
|
|
815
|
+
if (executionModeFor(task) === 'serial' && hasActiveTaskRuns(task)) continue
|
|
705
816
|
executeTask(task, 'schedule').catch((error) => logger.error(`Scheduled task ${task.id} failed:`, error))
|
|
706
817
|
}
|
|
707
818
|
} finally {
|
|
@@ -721,6 +832,7 @@ export function stopScheduledTaskRunner() {
|
|
|
721
832
|
if (!schedulerTimer) return
|
|
722
833
|
clearInterval(schedulerTimer)
|
|
723
834
|
schedulerTimer = null
|
|
835
|
+
runningTaskRunIds.clear()
|
|
724
836
|
}
|
|
725
837
|
|
|
726
838
|
export async function handleScheduledTasksApi(req, res, url) {
|
|
@@ -753,6 +865,7 @@ export async function handleScheduledTasksApi(req, res, url) {
|
|
|
753
865
|
id: createId(),
|
|
754
866
|
...normalized,
|
|
755
867
|
scheduleRule: normalized.scheduleRule || scheduleRuleFor(normalized),
|
|
868
|
+
executionMode: normalized.executionMode || 'serial',
|
|
756
869
|
model: body?.model,
|
|
757
870
|
thinkingLevel: body?.thinkingLevel || (body?.model?.reasoning ? 'medium' : 'off'),
|
|
758
871
|
projectId: body?.projectId || null,
|
|
@@ -788,6 +901,7 @@ export async function handleScheduledTasksApi(req, res, url) {
|
|
|
788
901
|
...current,
|
|
789
902
|
...normalized,
|
|
790
903
|
scheduleRule: normalized.scheduleRule || scheduleRuleFor(normalized),
|
|
904
|
+
executionMode: normalized.executionMode || 'serial',
|
|
791
905
|
model: body?.model ?? current.model,
|
|
792
906
|
thinkingLevel: body?.thinkingLevel ?? current.thinkingLevel,
|
|
793
907
|
projectId: hasProject ? (body.projectId || null) : current.projectId,
|
|
@@ -834,7 +948,7 @@ export async function handleScheduledTasksApi(req, res, url) {
|
|
|
834
948
|
const data = await readStore(STORE)
|
|
835
949
|
const task = data[taskId]
|
|
836
950
|
if (!task) throw requestError('Task not found', 404)
|
|
837
|
-
if (
|
|
951
|
+
if (executionModeFor(task) === 'serial' && hasActiveTaskRuns(task)) throw requestError('Task is already running', 409)
|
|
838
952
|
await new Promise((resolve) => {
|
|
839
953
|
executeTask(task, 'manual', resolve).catch((error) => {
|
|
840
954
|
logger.error(`Manual scheduled task ${task.id} failed:`, error)
|
|
@@ -57,6 +57,14 @@ function sanitizeMessage(message) {
|
|
|
57
57
|
return message
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function sanitizeContextCompaction(compaction) {
|
|
61
|
+
if (!compaction || typeof compaction !== 'object') return null
|
|
62
|
+
return {
|
|
63
|
+
...compaction,
|
|
64
|
+
summaryMessage: sanitizeMessage(compaction.summaryMessage),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
function sanitizeSession(session, record) {
|
|
61
69
|
const messages = Array.isArray(session?.messages) ? session.messages.map(sanitizeMessage).filter(Boolean) : []
|
|
62
70
|
return {
|
|
@@ -74,6 +82,8 @@ function sanitizeSession(session, record) {
|
|
|
74
82
|
tools: Array.isArray(session?.tools) ? session.tools : [],
|
|
75
83
|
yoloMode: Boolean(session?.yoloMode),
|
|
76
84
|
messages,
|
|
85
|
+
contextCompaction: sanitizeContextCompaction(session?.contextCompaction),
|
|
86
|
+
contextUsage: null,
|
|
77
87
|
isStreaming: Boolean(session?.isStreaming || session?.taskStatus === 'running'),
|
|
78
88
|
taskStatus: session?.taskStatus || session?.status,
|
|
79
89
|
errorMessage: session?.errorMessage,
|
|
@@ -102,6 +112,10 @@ function sanitizeEvent(event) {
|
|
|
102
112
|
const next = { ...event }
|
|
103
113
|
if (next.message) next.message = sanitizeMessage(next.message)
|
|
104
114
|
if (Array.isArray(next.messages)) next.messages = next.messages.map(sanitizeMessage).filter(Boolean)
|
|
115
|
+
if (next.contextCompaction?.summaryMessage) {
|
|
116
|
+
next.contextCompaction = sanitizeContextCompaction(next.contextCompaction)
|
|
117
|
+
}
|
|
118
|
+
delete next.contextUsage
|
|
105
119
|
return next
|
|
106
120
|
}
|
|
107
121
|
|
|
@@ -56,6 +56,16 @@ export async function handleStorageApi(req, res, url) {
|
|
|
56
56
|
values = values.filter((value) => value?.messageCount !== 0)
|
|
57
57
|
}
|
|
58
58
|
values.sort((a, b) => {
|
|
59
|
+
if (store === 'sessions-metadata' && indexName === 'lastModified') {
|
|
60
|
+
const leftPinned = getComparable(a, 'pinnedAt')
|
|
61
|
+
const rightPinned = getComparable(b, 'pinnedAt')
|
|
62
|
+
if (leftPinned !== rightPinned) {
|
|
63
|
+
if (leftPinned === undefined || leftPinned === null) return 1
|
|
64
|
+
if (rightPinned === undefined || rightPinned === null) return -1
|
|
65
|
+
return -String(leftPinned).localeCompare(String(rightPinned))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
const left = getComparable(a, indexName)
|
|
60
70
|
const right = getComparable(b, indexName)
|
|
61
71
|
if (left === right) return 0
|