@simonyea/holysheep-cli 1.7.134 → 1.7.136
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 +1 -0
- package/package.json +2 -2
- package/src/tools/droid.js +5 -4
- package/src/webui/index.html +1329 -584
- package/src/webui/server.js +217 -2
- package/src/webui/workspace-runtime.js +275 -0
- package/src/webui/workspace-store.js +325 -0
- package/tests/droid.test.js +1 -1
- package/tests/workspace-store.test.js +57 -0
package/src/webui/server.js
CHANGED
|
@@ -18,6 +18,8 @@ const { removeEnvFromShell, writeEnvToShell, getShellRcFiles } = require('../uti
|
|
|
18
18
|
const { commandExists } = require('../utils/which')
|
|
19
19
|
const TOOLS = require('../tools')
|
|
20
20
|
const pkg = require('../../package.json')
|
|
21
|
+
const workspaceStore = require('./workspace-store')
|
|
22
|
+
const workspaceRuntime = require('./workspace-runtime')
|
|
21
23
|
|
|
22
24
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
23
25
|
|
|
@@ -190,6 +192,11 @@ async function handleLogin(req, res) {
|
|
|
190
192
|
const valid = await validateApiKey(apiKey)
|
|
191
193
|
if (!valid) return json(res, { success: false, message: 'API Key 无效' }, 401)
|
|
192
194
|
saveConfig({ apiKey, savedAt: new Date().toISOString() })
|
|
195
|
+
workspaceStore.saveHolySheepApiConfig({
|
|
196
|
+
apiKey,
|
|
197
|
+
baseUrl: workspaceStore.getHolySheepApiConfig().baseUrl || BASE_URL_OPENAI,
|
|
198
|
+
model: workspaceStore.getHolySheepApiConfig().model || 'gpt-5.4',
|
|
199
|
+
})
|
|
193
200
|
json(res, { success: true, apiKey: maskKey(apiKey) })
|
|
194
201
|
} catch (e) {
|
|
195
202
|
json(res, { success: false, message: `验证失败: ${e.message}` }, 500)
|
|
@@ -201,6 +208,11 @@ async function handleLogout(_req, res) {
|
|
|
201
208
|
delete config.apiKey
|
|
202
209
|
delete config.savedAt
|
|
203
210
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
|
211
|
+
workspaceStore.saveHolySheepApiConfig({
|
|
212
|
+
apiKey: '',
|
|
213
|
+
baseUrl: workspaceStore.getHolySheepApiConfig().baseUrl || BASE_URL_OPENAI,
|
|
214
|
+
model: workspaceStore.getHolySheepApiConfig().model || 'gpt-5.4',
|
|
215
|
+
})
|
|
204
216
|
json(res, { success: true })
|
|
205
217
|
}
|
|
206
218
|
|
|
@@ -1034,6 +1046,192 @@ function handleModels(_req, res) {
|
|
|
1034
1046
|
])
|
|
1035
1047
|
}
|
|
1036
1048
|
|
|
1049
|
+
function getWorkspacePayload() {
|
|
1050
|
+
const config = workspaceRuntime.normalizeRuntimeConfig({})
|
|
1051
|
+
const hasRuntimeConfig = Boolean(config.apiKey && config.baseUrl && config.model)
|
|
1052
|
+
return {
|
|
1053
|
+
conversations: workspaceStore.listConversations(),
|
|
1054
|
+
scheduledTasks: workspaceStore.listTasks(),
|
|
1055
|
+
holySheepApi: {
|
|
1056
|
+
...config,
|
|
1057
|
+
apiKeyMasked: config.apiKey ? maskKey(config.apiKey) : null,
|
|
1058
|
+
ready: hasRuntimeConfig,
|
|
1059
|
+
},
|
|
1060
|
+
tools: TOOLS.map((tool) => ({
|
|
1061
|
+
id: tool.id,
|
|
1062
|
+
name: tool.name,
|
|
1063
|
+
launchCmd: tool.launchCmd || null,
|
|
1064
|
+
hint: tool.hint || null,
|
|
1065
|
+
})),
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async function handleWorkspaceState(_req, res) {
|
|
1070
|
+
json(res, getWorkspacePayload())
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async function handleWorkspaceSearch(req, res, url) {
|
|
1074
|
+
const query = url.searchParams.get('q') || ''
|
|
1075
|
+
json(res, workspaceStore.searchWorkspace(query))
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
async function handleWorkspaceApiConfig(req, res) {
|
|
1079
|
+
if (req.method === 'GET') {
|
|
1080
|
+
const config = workspaceRuntime.normalizeRuntimeConfig({})
|
|
1081
|
+
return json(res, {
|
|
1082
|
+
apiKey: config.apiKey,
|
|
1083
|
+
apiKeyMasked: config.apiKey ? maskKey(config.apiKey) : null,
|
|
1084
|
+
baseUrl: config.baseUrl,
|
|
1085
|
+
model: config.model,
|
|
1086
|
+
ready: Boolean(config.apiKey && config.baseUrl && config.model),
|
|
1087
|
+
})
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const body = await parseBody(req)
|
|
1091
|
+
const apiKey = String(body.apiKey || '').trim()
|
|
1092
|
+
const baseUrl = String(body.baseUrl || '').trim() || BASE_URL_OPENAI
|
|
1093
|
+
const model = String(body.model || '').trim()
|
|
1094
|
+
if (!apiKey || !apiKey.startsWith('cr_')) {
|
|
1095
|
+
return json(res, { success: false, error: 'HolySheep API Key 必须以 cr_ 开头' }, 400)
|
|
1096
|
+
}
|
|
1097
|
+
if (!model) {
|
|
1098
|
+
return json(res, { success: false, error: '模型不能为空' }, 400)
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
workspaceStore.saveHolySheepApiConfig({ apiKey, baseUrl, model })
|
|
1102
|
+
saveConfig({ apiKey, savedAt: new Date().toISOString() })
|
|
1103
|
+
json(res, { success: true, config: getWorkspacePayload().holySheepApi })
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
async function handleWorkspaceConversations(req, res) {
|
|
1107
|
+
if (req.method === 'GET') {
|
|
1108
|
+
return json(res, workspaceStore.listConversations())
|
|
1109
|
+
}
|
|
1110
|
+
const body = await parseBody(req)
|
|
1111
|
+
const conversation = workspaceStore.createConversation({
|
|
1112
|
+
title: body.title,
|
|
1113
|
+
toolId: body.toolId,
|
|
1114
|
+
pinned: body.pinned,
|
|
1115
|
+
})
|
|
1116
|
+
json(res, { success: true, conversation })
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
async function handleWorkspaceConversationById(req, res, conversationId) {
|
|
1120
|
+
const conversation = workspaceStore.getConversation(conversationId)
|
|
1121
|
+
if (!conversation) return json(res, { success: false, error: '会话不存在' }, 404)
|
|
1122
|
+
|
|
1123
|
+
if (req.method === 'GET') {
|
|
1124
|
+
return json(res, conversation)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (req.method === 'PATCH') {
|
|
1128
|
+
const body = await parseBody(req)
|
|
1129
|
+
const updated = workspaceStore.updateConversation(conversationId, {
|
|
1130
|
+
title: body.title,
|
|
1131
|
+
toolId: body.toolId,
|
|
1132
|
+
pinned: body.pinned,
|
|
1133
|
+
})
|
|
1134
|
+
return json(res, { success: true, conversation: updated })
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return json(res, { success: false, error: 'Method not allowed' }, 405)
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async function handleWorkspaceConversationMessages(req, res, conversationId) {
|
|
1141
|
+
if (req.method === 'GET') {
|
|
1142
|
+
const conversation = workspaceStore.getConversation(conversationId)
|
|
1143
|
+
if (!conversation) return json(res, { success: false, error: '会话不存在' }, 404)
|
|
1144
|
+
return json(res, conversation.messages)
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const body = await parseBody(req)
|
|
1148
|
+
const content = String(body.content || '').trim()
|
|
1149
|
+
if (!content) return json(res, { success: false, error: '消息不能为空' }, 400)
|
|
1150
|
+
|
|
1151
|
+
try {
|
|
1152
|
+
const result = await workspaceRuntime.sendConversationMessage(conversationId, content, body.runtimeConfig || {})
|
|
1153
|
+
return json(res, {
|
|
1154
|
+
success: true,
|
|
1155
|
+
messages: [result.userMessage, result.assistantMessage],
|
|
1156
|
+
conversation: workspaceStore.getConversation(conversationId),
|
|
1157
|
+
})
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
return json(res, {
|
|
1160
|
+
success: false,
|
|
1161
|
+
error: error.message,
|
|
1162
|
+
conversation: workspaceStore.getConversation(conversationId),
|
|
1163
|
+
}, 500)
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
async function handleWorkspaceTasks(req, res) {
|
|
1168
|
+
if (req.method === 'GET') {
|
|
1169
|
+
return json(res, workspaceStore.listTasks())
|
|
1170
|
+
}
|
|
1171
|
+
const body = await parseBody(req)
|
|
1172
|
+
const title = String(body.title || '').trim()
|
|
1173
|
+
const prompt = String(body.prompt || '').trim()
|
|
1174
|
+
if (!title || !prompt) return json(res, { success: false, error: '任务标题和提示词不能为空' }, 400)
|
|
1175
|
+
|
|
1176
|
+
try {
|
|
1177
|
+
workspaceRuntime.parseScheduleToMs(body.schedule || '1h')
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
return json(res, { success: false, error: error.message }, 400)
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const task = workspaceStore.saveTask({
|
|
1183
|
+
title,
|
|
1184
|
+
prompt,
|
|
1185
|
+
schedule: body.schedule || '1h',
|
|
1186
|
+
active: body.active !== false,
|
|
1187
|
+
conversationId: body.conversationId || null,
|
|
1188
|
+
modelOverride: body.modelOverride || '',
|
|
1189
|
+
})
|
|
1190
|
+
workspaceRuntime.rescheduleAllTasks()
|
|
1191
|
+
json(res, { success: true, task })
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
async function handleWorkspaceTaskById(req, res, taskId) {
|
|
1195
|
+
if (req.method === 'DELETE') {
|
|
1196
|
+
workspaceStore.deleteTask(taskId)
|
|
1197
|
+
workspaceRuntime.rescheduleAllTasks()
|
|
1198
|
+
return json(res, { success: true })
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (req.method === 'PATCH') {
|
|
1202
|
+
const body = await parseBody(req)
|
|
1203
|
+
if (body.schedule) {
|
|
1204
|
+
try {
|
|
1205
|
+
workspaceRuntime.parseScheduleToMs(body.schedule)
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
return json(res, { success: false, error: error.message }, 400)
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
const task = workspaceStore.saveTask({
|
|
1211
|
+
id: taskId,
|
|
1212
|
+
title: body.title,
|
|
1213
|
+
prompt: body.prompt,
|
|
1214
|
+
schedule: body.schedule,
|
|
1215
|
+
active: body.active,
|
|
1216
|
+
conversationId: body.conversationId,
|
|
1217
|
+
modelOverride: body.modelOverride,
|
|
1218
|
+
})
|
|
1219
|
+
workspaceRuntime.rescheduleAllTasks()
|
|
1220
|
+
return json(res, { success: true, task })
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (req.method === 'POST') {
|
|
1224
|
+
try {
|
|
1225
|
+
const result = await workspaceRuntime.runTask(taskId)
|
|
1226
|
+
return json(res, { success: true, result })
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
return json(res, { success: false, error: error.message }, 500)
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
return json(res, { success: false, error: 'Method not allowed' }, 405)
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1037
1235
|
// ── Router ───────────────────────────────────────────────────────────────────
|
|
1038
1236
|
|
|
1039
1237
|
async function handleRequest(req, res) {
|
|
@@ -1062,6 +1260,17 @@ async function handleRequest(req, res) {
|
|
|
1062
1260
|
if (route === '/api/doctor' && req.method === 'GET') return await handleDoctor(req, res)
|
|
1063
1261
|
if (route === '/api/tools' && req.method === 'GET') return await handleTools(req, res)
|
|
1064
1262
|
if (route === '/api/models' && req.method === 'GET') return await handleModels(req, res)
|
|
1263
|
+
if (route === '/api/workspace/state' && req.method === 'GET') return await handleWorkspaceState(req, res)
|
|
1264
|
+
if (route === '/api/workspace/search' && req.method === 'GET') return await handleWorkspaceSearch(req, res, url)
|
|
1265
|
+
if (route === '/api/workspace/api-config' && (req.method === 'GET' || req.method === 'POST')) return await handleWorkspaceApiConfig(req, res)
|
|
1266
|
+
if (route === '/api/workspace/conversations' && (req.method === 'GET' || req.method === 'POST')) return await handleWorkspaceConversations(req, res)
|
|
1267
|
+
const conversationMatch = route.match(/^\/api\/workspace\/conversations\/([^/]+)$/)
|
|
1268
|
+
if (conversationMatch) return await handleWorkspaceConversationById(req, res, conversationMatch[1])
|
|
1269
|
+
const messagesMatch = route.match(/^\/api\/workspace\/conversations\/([^/]+)\/messages$/)
|
|
1270
|
+
if (messagesMatch) return await handleWorkspaceConversationMessages(req, res, messagesMatch[1])
|
|
1271
|
+
if (route === '/api/workspace/tasks' && (req.method === 'GET' || req.method === 'POST')) return await handleWorkspaceTasks(req, res)
|
|
1272
|
+
const taskMatch = route.match(/^\/api\/workspace\/tasks\/([^/]+)$/)
|
|
1273
|
+
if (taskMatch) return await handleWorkspaceTaskById(req, res, taskMatch[1])
|
|
1065
1274
|
if (route === '/api/upgrade' && req.method === 'POST') return await handleUpgrade(req, res)
|
|
1066
1275
|
if (route === '/api/tool/install' && req.method === 'POST') return await handleToolInstall(req, res)
|
|
1067
1276
|
if (route === '/api/tool/configure' && req.method === 'POST') return await handleToolConfigure(req, res)
|
|
@@ -1113,13 +1322,19 @@ function startServer(port) {
|
|
|
1113
1322
|
setTimeout(() => {
|
|
1114
1323
|
const retry = http.createServer(handleRequest)
|
|
1115
1324
|
retry.on('error', (err2) => reject(err2))
|
|
1116
|
-
retry.listen(port, '127.0.0.1', () =>
|
|
1325
|
+
retry.listen(port, '127.0.0.1', () => {
|
|
1326
|
+
workspaceRuntime.startScheduler()
|
|
1327
|
+
resolve(retry)
|
|
1328
|
+
})
|
|
1117
1329
|
}, 500)
|
|
1118
1330
|
} else {
|
|
1119
1331
|
reject(err)
|
|
1120
1332
|
}
|
|
1121
1333
|
})
|
|
1122
|
-
server.listen(port, '127.0.0.1', () =>
|
|
1334
|
+
server.listen(port, '127.0.0.1', () => {
|
|
1335
|
+
workspaceRuntime.startScheduler()
|
|
1336
|
+
resolve(server)
|
|
1337
|
+
})
|
|
1123
1338
|
})
|
|
1124
1339
|
}
|
|
1125
1340
|
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fetch = require('node-fetch')
|
|
4
|
+
const {
|
|
5
|
+
addMessage,
|
|
6
|
+
createConversation,
|
|
7
|
+
ensureConversation,
|
|
8
|
+
getConversation,
|
|
9
|
+
getHolySheepApiConfig,
|
|
10
|
+
listTasks,
|
|
11
|
+
updateConversation,
|
|
12
|
+
updateTaskRun,
|
|
13
|
+
} = require('./workspace-store')
|
|
14
|
+
const { getApiKey, BASE_URL_OPENAI } = require('../utils/config')
|
|
15
|
+
|
|
16
|
+
const schedulerHandles = new Map()
|
|
17
|
+
|
|
18
|
+
function normalizeRuntimeConfig(config = {}) {
|
|
19
|
+
const saved = getHolySheepApiConfig()
|
|
20
|
+
const apiKey = String(config.apiKey || saved.apiKey || getApiKey() || '').trim()
|
|
21
|
+
const baseUrl = String(config.baseUrl || saved.baseUrl || BASE_URL_OPENAI).replace(/\/+$/, '')
|
|
22
|
+
const model = String(config.model || saved.model || '').trim()
|
|
23
|
+
return { apiKey, baseUrl, model }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function assertRuntimeConfig(config) {
|
|
27
|
+
if (!config.apiKey || !config.apiKey.startsWith('cr_')) {
|
|
28
|
+
throw new Error('HolySheep API Key is required')
|
|
29
|
+
}
|
|
30
|
+
if (!config.baseUrl) {
|
|
31
|
+
throw new Error('HolySheep API Base URL is required')
|
|
32
|
+
}
|
|
33
|
+
if (!config.model) {
|
|
34
|
+
throw new Error('HolySheep API model is required')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildSystemPrompt(toolId) {
|
|
39
|
+
const toolName = String(toolId || 'codex')
|
|
40
|
+
return [
|
|
41
|
+
`You are operating inside HolySheep Workspace.`,
|
|
42
|
+
`The selected coding tool is "${toolName}".`,
|
|
43
|
+
`Respond with concise, implementation-focused guidance suitable for a coding assistant workspace.`,
|
|
44
|
+
`When you mention commands, prefer copy-paste-ready shell commands.`,
|
|
45
|
+
].join(' ')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function requestCompletion(messages, config) {
|
|
49
|
+
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
model: config.model,
|
|
57
|
+
temperature: 0.2,
|
|
58
|
+
messages,
|
|
59
|
+
}),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const payload = await response.json().catch(() => null)
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const reason = payload?.error?.message || payload?.message || `HTTP ${response.status}`
|
|
65
|
+
throw new Error(reason)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const content = payload?.choices?.[0]?.message?.content
|
|
69
|
+
if (typeof content === 'string' && content.trim()) return content.trim()
|
|
70
|
+
if (Array.isArray(content)) {
|
|
71
|
+
const text = content
|
|
72
|
+
.map((part) => {
|
|
73
|
+
if (typeof part === 'string') return part
|
|
74
|
+
if (typeof part?.text === 'string') return part.text
|
|
75
|
+
if (typeof part?.content === 'string') return part.content
|
|
76
|
+
return ''
|
|
77
|
+
})
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.join('\n')
|
|
80
|
+
.trim()
|
|
81
|
+
if (text) return text
|
|
82
|
+
}
|
|
83
|
+
const fallbacks = [
|
|
84
|
+
payload?.choices?.[0]?.text,
|
|
85
|
+
payload?.output_text,
|
|
86
|
+
payload?.response,
|
|
87
|
+
payload?.message,
|
|
88
|
+
]
|
|
89
|
+
for (const item of fallbacks) {
|
|
90
|
+
if (typeof item === 'string' && item.trim()) return item.trim()
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(payload?.output)) {
|
|
93
|
+
const text = payload.output
|
|
94
|
+
.map((part) => {
|
|
95
|
+
if (typeof part?.content === 'string') return part.content
|
|
96
|
+
if (typeof part?.text === 'string') return part.text
|
|
97
|
+
if (Array.isArray(part?.content)) {
|
|
98
|
+
return part.content.map((child) => child?.text || child?.content || '').join('\n')
|
|
99
|
+
}
|
|
100
|
+
return ''
|
|
101
|
+
})
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.join('\n')
|
|
104
|
+
.trim()
|
|
105
|
+
if (text) return text
|
|
106
|
+
}
|
|
107
|
+
return 'No response content returned.'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function sendConversationMessage(conversationId, input, options = {}) {
|
|
111
|
+
const conversation = ensureConversation(conversationId)
|
|
112
|
+
const config = normalizeRuntimeConfig(options)
|
|
113
|
+
assertRuntimeConfig(config)
|
|
114
|
+
|
|
115
|
+
const userMessage = addMessage(conversationId, {
|
|
116
|
+
role: 'user',
|
|
117
|
+
content: String(input || ''),
|
|
118
|
+
meta: {
|
|
119
|
+
source: 'workspace',
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const messages = [
|
|
125
|
+
{ role: 'system', content: buildSystemPrompt(conversation.toolId) },
|
|
126
|
+
...conversation.messages
|
|
127
|
+
.concat(userMessage)
|
|
128
|
+
.map((message) => ({
|
|
129
|
+
role: message.role,
|
|
130
|
+
content: message.content,
|
|
131
|
+
})),
|
|
132
|
+
]
|
|
133
|
+
const reply = await requestCompletion(messages, config)
|
|
134
|
+
const assistantMessage = addMessage(conversationId, {
|
|
135
|
+
role: 'assistant',
|
|
136
|
+
content: reply,
|
|
137
|
+
meta: {
|
|
138
|
+
model: config.model,
|
|
139
|
+
source: 'holysheep-api',
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
updateConversation(conversationId, {
|
|
143
|
+
summary: reply.slice(0, 240),
|
|
144
|
+
})
|
|
145
|
+
return { userMessage, assistantMessage }
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const assistantMessage = addMessage(conversationId, {
|
|
148
|
+
role: 'assistant',
|
|
149
|
+
content: `Error: ${error.message}`,
|
|
150
|
+
status: 'error',
|
|
151
|
+
meta: {
|
|
152
|
+
model: config.model,
|
|
153
|
+
source: 'holysheep-api',
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
updateConversation(conversationId, {
|
|
157
|
+
summary: assistantMessage.content.slice(0, 240),
|
|
158
|
+
})
|
|
159
|
+
throw error
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseScheduleToMs(schedule) {
|
|
164
|
+
const value = String(schedule || '').trim().toLowerCase()
|
|
165
|
+
const match = value.match(/^(\d+)\s*([smhd])$/)
|
|
166
|
+
if (!match) throw new Error('Schedule must use format like 30s, 5m, 1h, 1d')
|
|
167
|
+
const amount = Number(match[1])
|
|
168
|
+
const unit = match[2]
|
|
169
|
+
if (!Number.isFinite(amount) || amount <= 0) throw new Error('Invalid schedule amount')
|
|
170
|
+
const factors = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }
|
|
171
|
+
return amount * factors[unit]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function runTask(taskId, options = {}) {
|
|
175
|
+
const task = listTasks().find((item) => item.id === taskId)
|
|
176
|
+
if (!task) throw new Error('Task not found')
|
|
177
|
+
|
|
178
|
+
const config = normalizeRuntimeConfig({
|
|
179
|
+
...options,
|
|
180
|
+
model: task.modelOverride || options.model,
|
|
181
|
+
})
|
|
182
|
+
assertRuntimeConfig(config)
|
|
183
|
+
updateTaskRun(taskId, {
|
|
184
|
+
lastStatus: 'running',
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
let conversationId = task.conversationId
|
|
188
|
+
if (!conversationId) {
|
|
189
|
+
const conversation = createConversation({
|
|
190
|
+
title: task.title,
|
|
191
|
+
toolId: 'codex',
|
|
192
|
+
})
|
|
193
|
+
conversationId = conversation.id
|
|
194
|
+
updateTaskRun(taskId, { conversationId })
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const { assistantMessage } = await sendConversationMessage(conversationId, task.prompt, config)
|
|
199
|
+
updateTaskRun(taskId, {
|
|
200
|
+
lastRunAt: new Date().toISOString(),
|
|
201
|
+
lastStatus: 'ok',
|
|
202
|
+
lastResult: assistantMessage.content,
|
|
203
|
+
conversationId,
|
|
204
|
+
})
|
|
205
|
+
return assistantMessage
|
|
206
|
+
} catch (error) {
|
|
207
|
+
updateTaskRun(taskId, {
|
|
208
|
+
lastRunAt: new Date().toISOString(),
|
|
209
|
+
lastStatus: 'error',
|
|
210
|
+
lastResult: error.message,
|
|
211
|
+
conversationId,
|
|
212
|
+
})
|
|
213
|
+
throw error
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function clearTaskHandle(taskId) {
|
|
218
|
+
const existing = schedulerHandles.get(taskId)
|
|
219
|
+
if (!existing) return
|
|
220
|
+
clearInterval(existing)
|
|
221
|
+
schedulerHandles.delete(taskId)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function scheduleTask(task) {
|
|
225
|
+
clearTaskHandle(task.id)
|
|
226
|
+
if (!task.active) return
|
|
227
|
+
|
|
228
|
+
let intervalMs
|
|
229
|
+
try {
|
|
230
|
+
intervalMs = parseScheduleToMs(task.schedule)
|
|
231
|
+
} catch {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const handle = setInterval(() => {
|
|
236
|
+
void runTask(task.id).catch(() => {})
|
|
237
|
+
}, intervalMs)
|
|
238
|
+
schedulerHandles.set(task.id, handle)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function rescheduleAllTasks() {
|
|
242
|
+
const tasks = listTasks()
|
|
243
|
+
const seen = new Set(tasks.map((task) => task.id))
|
|
244
|
+
for (const taskId of schedulerHandles.keys()) {
|
|
245
|
+
if (!seen.has(taskId)) {
|
|
246
|
+
clearTaskHandle(taskId)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const task of tasks) {
|
|
250
|
+
scheduleTask(task)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function stopScheduler() {
|
|
255
|
+
for (const taskId of schedulerHandles.keys()) {
|
|
256
|
+
clearTaskHandle(taskId)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function startScheduler() {
|
|
261
|
+
rescheduleAllTasks()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
normalizeRuntimeConfig,
|
|
266
|
+
assertRuntimeConfig,
|
|
267
|
+
sendConversationMessage,
|
|
268
|
+
parseScheduleToMs,
|
|
269
|
+
runTask,
|
|
270
|
+
startScheduler,
|
|
271
|
+
stopScheduler,
|
|
272
|
+
rescheduleAllTasks,
|
|
273
|
+
createConversation,
|
|
274
|
+
getConversation,
|
|
275
|
+
}
|