@pixelbyte-software/pixcode 1.36.4 → 1.38.0
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/dist/assets/{index-D-YjltED.js → index-C-gVa0Gf.js} +163 -157
- package/dist/assets/index-CfHK8y_H.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/database/db.js +4 -2
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/index.js +8 -0
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +10 -1
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -1
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +7 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +127 -24
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/routes/diagnostics.js +12 -0
- package/dist-server/server/routes/diagnostics.js.map +1 -0
- package/dist-server/server/routes/taskmaster.js +194 -0
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/services/diagnostics.js +91 -0
- package/dist-server/server/services/diagnostics.js.map +1 -0
- package/dist-server/server/services/install-jobs.js +1 -0
- package/dist-server/server/services/install-jobs.js.map +1 -1
- package/dist-server/server/services/notification-orchestrator.js +66 -9
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/telegram/control-center.js +144 -2
- package/dist-server/server/services/telegram/control-center.js.map +1 -1
- package/dist-server/server/services/telegram/translations.js +14 -2
- package/dist-server/server/services/telegram/translations.js.map +1 -1
- package/package.json +5 -1
- package/scripts/github/create-v1.38-issues.mjs +351 -0
- package/scripts/smoke/chat-realtime-hydration.mjs +44 -0
- package/scripts/smoke/discord-release-workflow.mjs +24 -0
- package/scripts/smoke/multi-worker-slots.mjs +42 -0
- package/scripts/smoke/notification-center.mjs +63 -0
- package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -0
- package/scripts/smoke/strict-handoff-compact.mjs +60 -0
- package/scripts/smoke/taskmaster-execution-telegram.mjs +52 -0
- package/scripts/smoke/taskmaster-onboarding.mjs +52 -0
- package/scripts/smoke/update-issue-progress.mjs +69 -0
- package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -0
- package/scripts/smoke/v138-diagnostics.mjs +63 -0
- package/scripts/smoke/v138-issue-planner.mjs +33 -0
- package/server/database/db.js +4 -2
- package/server/index.js +9 -0
- package/server/modules/orchestration/tasks/orchestration-task.routes.ts +10 -1
- package/server/modules/orchestration/tasks/orchestration-task.service.ts +7 -0
- package/server/modules/orchestration/tasks/orchestration-task.types.ts +3 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +132 -24
- package/server/modules/orchestration/workflows/workflow.types.ts +2 -0
- package/server/routes/diagnostics.js +15 -0
- package/server/routes/taskmaster.js +201 -0
- package/server/services/diagnostics.js +105 -0
- package/server/services/install-jobs.js +1 -0
- package/server/services/notification-orchestrator.js +76 -8
- package/server/services/telegram/control-center.js +153 -2
- package/server/services/telegram/translations.js +14 -2
- package/dist/assets/index-CgF0-_6Z.css +0 -32
|
@@ -17,6 +17,12 @@ import express from 'express';
|
|
|
17
17
|
import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
|
|
18
18
|
|
|
19
19
|
import { extractProjectDirectory } from '../projects.js';
|
|
20
|
+
import {
|
|
21
|
+
cancelInstallJob,
|
|
22
|
+
createInstallJob,
|
|
23
|
+
getInstallJob,
|
|
24
|
+
snapshotDonePayload
|
|
25
|
+
} from '../services/install-jobs.js';
|
|
20
26
|
import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../utils/taskmaster-websocket.js';
|
|
21
27
|
import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js';
|
|
22
28
|
|
|
@@ -144,6 +150,17 @@ async function readTaskMasterTasks(projectName) {
|
|
|
144
150
|
return { projectPath, transformedTasks, currentTag };
|
|
145
151
|
}
|
|
146
152
|
|
|
153
|
+
function taskMasterExecutionDescription(task) {
|
|
154
|
+
return [
|
|
155
|
+
task.description ? `Description:\n${task.description}` : '',
|
|
156
|
+
task.details ? `Details:\n${task.details}` : '',
|
|
157
|
+
task.testStrategy ? `Test strategy:\n${task.testStrategy}` : '',
|
|
158
|
+
Array.isArray(task.dependencies) && task.dependencies.length
|
|
159
|
+
? `Dependencies: ${task.dependencies.join(', ')}`
|
|
160
|
+
: '',
|
|
161
|
+
].filter(Boolean).join('\n\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
147
164
|
// API Routes
|
|
148
165
|
|
|
149
166
|
/**
|
|
@@ -181,6 +198,121 @@ router.get('/installation-status', async (req, res) => {
|
|
|
181
198
|
}
|
|
182
199
|
});
|
|
183
200
|
|
|
201
|
+
/**
|
|
202
|
+
* POST /api/taskmaster/install
|
|
203
|
+
* Install TaskMaster CLI into Pixcode's sandboxed CLI bin.
|
|
204
|
+
*/
|
|
205
|
+
router.post('/install', async (req, res) => {
|
|
206
|
+
try {
|
|
207
|
+
const job = createInstallJob({
|
|
208
|
+
provider: 'taskmaster',
|
|
209
|
+
installCmd: 'npm install -g task-master',
|
|
210
|
+
packageName: 'task-master'
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
res.json({
|
|
214
|
+
success: true,
|
|
215
|
+
jobId: job.id,
|
|
216
|
+
provider: 'taskmaster',
|
|
217
|
+
packageName: 'task-master',
|
|
218
|
+
startedAt: job.startedAt
|
|
219
|
+
});
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error('TaskMaster install start error:', error);
|
|
222
|
+
res.status(500).json({
|
|
223
|
+
success: false,
|
|
224
|
+
error: 'Failed to start TaskMaster install',
|
|
225
|
+
message: error.message
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* GET /api/taskmaster/install/:jobId/stream
|
|
232
|
+
* Replay and stream TaskMaster install output.
|
|
233
|
+
*/
|
|
234
|
+
router.get('/install/:jobId/stream', async (req, res) => {
|
|
235
|
+
const job = getInstallJob(req.params.jobId);
|
|
236
|
+
if (!job || job.provider !== 'taskmaster') {
|
|
237
|
+
return res.status(404).json({
|
|
238
|
+
success: false,
|
|
239
|
+
error: 'Install job not found or already expired'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
244
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
245
|
+
res.setHeader('Connection', 'keep-alive');
|
|
246
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
247
|
+
if (typeof res.flushHeaders === 'function') res.flushHeaders();
|
|
248
|
+
|
|
249
|
+
let closed = false;
|
|
250
|
+
const write = (event, payload) => {
|
|
251
|
+
if (closed) return;
|
|
252
|
+
try {
|
|
253
|
+
res.write(`event: ${event}\n`);
|
|
254
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
255
|
+
} catch {
|
|
256
|
+
// Socket is gone.
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
try { res.write(': start\n\n'); } catch { /* noop */ }
|
|
261
|
+
const heartbeat = setInterval(() => {
|
|
262
|
+
if (!closed) {
|
|
263
|
+
try { res.write(': ping\n\n'); } catch { /* noop */ }
|
|
264
|
+
}
|
|
265
|
+
}, 5000);
|
|
266
|
+
|
|
267
|
+
for (const entry of job.logs) {
|
|
268
|
+
write('log', { stream: entry.stream, chunk: entry.chunk });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const cleanup = () => {
|
|
272
|
+
if (closed) return;
|
|
273
|
+
closed = true;
|
|
274
|
+
clearInterval(heartbeat);
|
|
275
|
+
job.emitter.off('log', onLog);
|
|
276
|
+
job.emitter.off('done', onDone);
|
|
277
|
+
};
|
|
278
|
+
const onLog = (entry) => {
|
|
279
|
+
write('log', { stream: entry.stream, chunk: entry.chunk });
|
|
280
|
+
};
|
|
281
|
+
const onDone = (payload) => {
|
|
282
|
+
write('done', payload);
|
|
283
|
+
cleanup();
|
|
284
|
+
try { res.end(); } catch { /* noop */ }
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
if (job.status !== 'running') {
|
|
288
|
+
write('done', snapshotDonePayload(job));
|
|
289
|
+
cleanup();
|
|
290
|
+
try { res.end(); } catch { /* noop */ }
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
job.emitter.on('log', onLog);
|
|
295
|
+
job.emitter.once('done', onDone);
|
|
296
|
+
|
|
297
|
+
req.on('close', cleanup);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* DELETE /api/taskmaster/install/:jobId
|
|
302
|
+
* Cancel a running TaskMaster install job.
|
|
303
|
+
*/
|
|
304
|
+
router.delete('/install/:jobId', async (req, res) => {
|
|
305
|
+
const job = getInstallJob(req.params.jobId);
|
|
306
|
+
if (!job || job.provider !== 'taskmaster') {
|
|
307
|
+
return res.status(404).json({
|
|
308
|
+
success: false,
|
|
309
|
+
error: 'Install job not found'
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
res.json({ success: true, cancelled: cancelInstallJob(req.params.jobId) });
|
|
314
|
+
});
|
|
315
|
+
|
|
184
316
|
/**
|
|
185
317
|
* GET /api/taskmaster/tasks/:projectName
|
|
186
318
|
* Load actual tasks from .taskmaster/tasks/tasks.json
|
|
@@ -230,6 +362,75 @@ router.get('/tasks/:projectName', async (req, res) => {
|
|
|
230
362
|
}
|
|
231
363
|
});
|
|
232
364
|
|
|
365
|
+
/**
|
|
366
|
+
* POST /api/taskmaster/execute/:projectName/:taskId
|
|
367
|
+
* Import a TaskMaster task into orchestration and dispatch it to a CLI agent.
|
|
368
|
+
*/
|
|
369
|
+
router.post('/execute/:projectName/:taskId', async (req, res) => {
|
|
370
|
+
try {
|
|
371
|
+
const { projectName, taskId } = req.params;
|
|
372
|
+
const adapterId = typeof req.body?.adapterId === 'string'
|
|
373
|
+
? req.body.adapterId
|
|
374
|
+
: typeof req.body?.provider === 'string'
|
|
375
|
+
? req.body.provider
|
|
376
|
+
: '';
|
|
377
|
+
const model = typeof req.body?.model === 'string' ? req.body.model : undefined;
|
|
378
|
+
const permissionMode = typeof req.body?.permissionMode === 'string' ? req.body.permissionMode : undefined;
|
|
379
|
+
const isolation = ['host', 'worktree', 'docker'].includes(req.body?.isolation)
|
|
380
|
+
? req.body.isolation
|
|
381
|
+
: 'worktree';
|
|
382
|
+
const projectId = typeof req.body?.projectId === 'string' ? req.body.projectId : projectName;
|
|
383
|
+
|
|
384
|
+
if (!adapterId) {
|
|
385
|
+
return res.status(400).json({
|
|
386
|
+
success: false,
|
|
387
|
+
error: 'Missing adapterId',
|
|
388
|
+
message: 'adapterId or provider is required'
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const { projectPath, transformedTasks } = await readTaskMasterTasks(projectName);
|
|
393
|
+
const task = transformedTasks.find((candidate) => String(candidate.id) === String(taskId));
|
|
394
|
+
if (!task) {
|
|
395
|
+
return res.status(404).json({
|
|
396
|
+
success: false,
|
|
397
|
+
error: 'TaskMaster task not found',
|
|
398
|
+
message: `Task "${taskId}" was not found in project "${projectName}"`
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const orchestrationTask = orchestrationTaskService.upsertFromTaskMaster({
|
|
403
|
+
projectId,
|
|
404
|
+
taskmasterId: String(task.id),
|
|
405
|
+
title: `TaskMaster #${task.id}: ${task.title}`,
|
|
406
|
+
description: taskMasterExecutionDescription(task)
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const dispatchedTask = await orchestrationTaskService.dispatch(orchestrationTask.id, {
|
|
410
|
+
adapterId,
|
|
411
|
+
isolation,
|
|
412
|
+
projectPath,
|
|
413
|
+
model,
|
|
414
|
+
permissionMode
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
res.json({
|
|
418
|
+
success: true,
|
|
419
|
+
projectName,
|
|
420
|
+
projectPath,
|
|
421
|
+
taskmasterTask: task,
|
|
422
|
+
task: dispatchedTask
|
|
423
|
+
});
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error('TaskMaster execute error:', error);
|
|
426
|
+
res.status(500).json({
|
|
427
|
+
success: false,
|
|
428
|
+
error: 'Failed to execute TaskMaster task',
|
|
429
|
+
message: error.message
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
233
434
|
/**
|
|
234
435
|
* POST /api/taskmaster/sync-orchestration/:projectName
|
|
235
436
|
* One-way sync: TaskMaster -> Orchestration tasks
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const SAFE_ENV_KEYS = [
|
|
2
|
+
'NODE_ENV',
|
|
3
|
+
'SERVER_PORT',
|
|
4
|
+
'VITE_PORT',
|
|
5
|
+
'HOST',
|
|
6
|
+
'PORT',
|
|
7
|
+
'DATABASE_PATH',
|
|
8
|
+
'PIXCODE_NO_DAEMON',
|
|
9
|
+
'PIXCODE_DISABLE_UPDATE_CHECK',
|
|
10
|
+
'GITHUB_TOKEN',
|
|
11
|
+
'NPM_TOKEN',
|
|
12
|
+
'TELEGRAM_BOT_TOKEN',
|
|
13
|
+
'ANTHROPIC_API_KEY',
|
|
14
|
+
'OPENAI_API_KEY',
|
|
15
|
+
'GEMINI_API_KEY',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const SENSITIVE_KEY_PATTERN = /(authorization|cookie|credential|password|secret|token|api[_-]?key)/i;
|
|
19
|
+
|
|
20
|
+
function isSensitiveKey(key) {
|
|
21
|
+
return SENSITIVE_KEY_PATTERN.test(key);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function redactEnv(env = process.env) {
|
|
25
|
+
return SAFE_ENV_KEYS.reduce((acc, key) => {
|
|
26
|
+
if (!(key in env)) {
|
|
27
|
+
return acc;
|
|
28
|
+
}
|
|
29
|
+
acc[key] = isSensitiveKey(key) ? '[redacted]' : String(env[key]);
|
|
30
|
+
return acc;
|
|
31
|
+
}, {});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function providerCredentialState(env = process.env) {
|
|
35
|
+
return {
|
|
36
|
+
claude: Boolean(env.ANTHROPIC_API_KEY || env.CLAUDE_API_KEY),
|
|
37
|
+
codex: Boolean(env.OPENAI_API_KEY),
|
|
38
|
+
gemini: Boolean(env.GEMINI_API_KEY || env.GOOGLE_API_KEY),
|
|
39
|
+
telegram: Boolean(env.TELEGRAM_BOT_TOKEN),
|
|
40
|
+
github: Boolean(env.GITHUB_TOKEN),
|
|
41
|
+
npm: Boolean(env.NPM_TOKEN),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeMemory(memory) {
|
|
46
|
+
return Object.fromEntries(
|
|
47
|
+
Object.entries(memory).map(([key, value]) => [key, Number.isFinite(value) ? value : 0])
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveWebSocketClientCount(options) {
|
|
52
|
+
if (Number.isInteger(options.wsClientCount)) {
|
|
53
|
+
return options.wsClientCount;
|
|
54
|
+
}
|
|
55
|
+
return options.wss?.clients?.size || 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function collectDiagnostics(options = {}) {
|
|
59
|
+
const now = options.now || new Date();
|
|
60
|
+
const env = options.env || process.env;
|
|
61
|
+
const versions = options.versions || process.versions;
|
|
62
|
+
const memoryUsage = options.memoryUsage || process.memoryUsage;
|
|
63
|
+
const uptime = options.uptime ?? process.uptime();
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
status: 'ok',
|
|
67
|
+
timestamp: now.toISOString(),
|
|
68
|
+
version: options.serverVersion || '0.0.0',
|
|
69
|
+
installMode: options.installMode || 'unknown',
|
|
70
|
+
runtime: {
|
|
71
|
+
node: versions.node,
|
|
72
|
+
v8: versions.v8,
|
|
73
|
+
platform: options.platform || process.platform,
|
|
74
|
+
arch: options.arch || process.arch,
|
|
75
|
+
uptimeSeconds: Math.round(uptime),
|
|
76
|
+
},
|
|
77
|
+
memory: normalizeMemory(memoryUsage()),
|
|
78
|
+
websocket: {
|
|
79
|
+
clients: resolveWebSocketClientCount(options),
|
|
80
|
+
},
|
|
81
|
+
environment: redactEnv(env),
|
|
82
|
+
credentials: providerCredentialState(env),
|
|
83
|
+
notifications: {
|
|
84
|
+
telegramConfigured: Boolean(env.TELEGRAM_BOT_TOKEN),
|
|
85
|
+
webPushConfigured: Boolean(env.VAPID_PUBLIC_KEY && env.VAPID_PRIVATE_KEY),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function redactDiagnostics(input) {
|
|
91
|
+
if (Array.isArray(input)) {
|
|
92
|
+
return input.map(item => redactDiagnostics(item));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!input || typeof input !== 'object') {
|
|
96
|
+
return input;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Object.fromEntries(
|
|
100
|
+
Object.entries(input).map(([key, value]) => [
|
|
101
|
+
key,
|
|
102
|
+
isSensitiveKey(key) ? '[redacted]' : redactDiagnostics(value),
|
|
103
|
+
])
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -23,6 +23,11 @@ const PROVIDER_LABELS = {
|
|
|
23
23
|
|
|
24
24
|
const recentEventKeys = new Map();
|
|
25
25
|
const DEDUPE_WINDOW_MS = 20000;
|
|
26
|
+
let notificationWebSocketServer = null;
|
|
27
|
+
|
|
28
|
+
function setNotificationWebSocketServer(wss) {
|
|
29
|
+
notificationWebSocketServer = wss;
|
|
30
|
+
}
|
|
26
31
|
|
|
27
32
|
const cleanupOldEventKeys = () => {
|
|
28
33
|
const now = Date.now();
|
|
@@ -41,6 +46,22 @@ function shouldSendPush(preferences, event) {
|
|
|
41
46
|
return webPushEnabled && eventEnabled;
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
function shouldSendInApp(preferences, event) {
|
|
50
|
+
const inAppEnabled = preferences?.channels?.inApp !== false;
|
|
51
|
+
const prefEventKey = KIND_TO_PREF_KEY[event.kind];
|
|
52
|
+
const eventEnabled = prefEventKey ? preferences?.events?.[prefEventKey] !== false : true;
|
|
53
|
+
|
|
54
|
+
return inAppEnabled && eventEnabled;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function shouldSendTelegram(preferences, event) {
|
|
58
|
+
const telegramEnabled = preferences?.channels?.telegram !== false;
|
|
59
|
+
const prefEventKey = KIND_TO_PREF_KEY[event.kind];
|
|
60
|
+
const eventEnabled = prefEventKey ? preferences?.events?.[prefEventKey] !== false : true;
|
|
61
|
+
|
|
62
|
+
return telegramEnabled && eventEnabled;
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
function isDuplicate(event) {
|
|
45
66
|
cleanupOldEventKeys();
|
|
46
67
|
const key = event.dedupeKey || `${event.provider}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}`;
|
|
@@ -149,6 +170,41 @@ function buildPushBody(event) {
|
|
|
149
170
|
};
|
|
150
171
|
}
|
|
151
172
|
|
|
173
|
+
function buildNotificationPayload(event) {
|
|
174
|
+
const pushBody = buildPushBody(event);
|
|
175
|
+
return {
|
|
176
|
+
id: event.dedupeKey || `${event.provider || 'system'}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}:${event.createdAt}`,
|
|
177
|
+
title: pushBody.title,
|
|
178
|
+
body: pushBody.body,
|
|
179
|
+
kind: event.kind || 'info',
|
|
180
|
+
code: event.code || 'generic.info',
|
|
181
|
+
severity: event.severity || 'info',
|
|
182
|
+
provider: event.provider || null,
|
|
183
|
+
sessionId: event.sessionId || null,
|
|
184
|
+
createdAt: event.createdAt,
|
|
185
|
+
requiresUserAction: Boolean(event.requiresUserAction),
|
|
186
|
+
data: pushBody.data
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function broadcastInAppNotification(userId, event) {
|
|
191
|
+
if (!notificationWebSocketServer || !userId || !event) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const message = JSON.stringify({
|
|
196
|
+
type: 'notification:event',
|
|
197
|
+
notification: buildNotificationPayload(event)
|
|
198
|
+
});
|
|
199
|
+
const normalizedUserId = String(userId);
|
|
200
|
+
|
|
201
|
+
notificationWebSocketServer.clients.forEach((client) => {
|
|
202
|
+
if (client.readyState === 1 && String(client.userId || '') === normalizedUserId) {
|
|
203
|
+
client.send(message);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
152
208
|
async function sendWebPush(userId, event) {
|
|
153
209
|
const subscriptions = pushSubscriptionsDb.getSubscriptions(userId);
|
|
154
210
|
if (!subscriptions.length) return;
|
|
@@ -191,6 +247,14 @@ function notifyUserIfEnabled({ userId, event }) {
|
|
|
191
247
|
return;
|
|
192
248
|
}
|
|
193
249
|
|
|
250
|
+
if (shouldSendInApp(preferences, event)) {
|
|
251
|
+
try {
|
|
252
|
+
broadcastInAppNotification(userId, event);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error('In-app notification send error:', err);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
194
258
|
if (shouldSendPush(preferences, event)) {
|
|
195
259
|
sendWebPush(userId, event).catch((err) => {
|
|
196
260
|
console.error('Web push send error:', err);
|
|
@@ -203,14 +267,16 @@ function notifyUserIfEnabled({ userId, event }) {
|
|
|
203
267
|
const providerLabel = PROVIDER_LABELS[event.provider] || event.provider || 'Session';
|
|
204
268
|
const sessionTitle = event.meta?.sessionName || providerLabel;
|
|
205
269
|
const errorText = event.meta?.error || '';
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
270
|
+
if (shouldSendTelegram(preferences, event)) {
|
|
271
|
+
notifyTelegramUser({
|
|
272
|
+
userId,
|
|
273
|
+
kind: event.kind,
|
|
274
|
+
title: sessionTitle,
|
|
275
|
+
error: errorText,
|
|
276
|
+
}).catch((err) => {
|
|
277
|
+
console.warn('[telegram] notify failed:', err?.message || err);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
214
280
|
}
|
|
215
281
|
|
|
216
282
|
function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null }) {
|
|
@@ -247,6 +313,8 @@ function notifyRunFailed({ userId, provider, sessionId = null, error, sessionNam
|
|
|
247
313
|
|
|
248
314
|
export {
|
|
249
315
|
createNotificationEvent,
|
|
316
|
+
setNotificationWebSocketServer,
|
|
317
|
+
broadcastInAppNotification,
|
|
250
318
|
notifyUserIfEnabled,
|
|
251
319
|
notifyRunStopped,
|
|
252
320
|
notifyRunFailed
|
|
@@ -49,6 +49,8 @@ const CONTROL_COMMANDS = new Set([
|
|
|
49
49
|
'/workflows',
|
|
50
50
|
'/orchestration',
|
|
51
51
|
'/runs',
|
|
52
|
+
'/tasks',
|
|
53
|
+
'/task',
|
|
52
54
|
'/settings',
|
|
53
55
|
'/install',
|
|
54
56
|
'/auth',
|
|
@@ -210,8 +212,9 @@ function mainMenuKeyboard(lang) {
|
|
|
210
212
|
return [
|
|
211
213
|
[button(t(lang, 'control.button.projects'), 'projects'), button(t(lang, 'control.button.provider'), 'providers')],
|
|
212
214
|
[button(t(lang, 'control.button.models'), 'models'), button(t(lang, 'control.button.workflows'), 'workflows')],
|
|
213
|
-
[button(t(lang, 'control.button.
|
|
214
|
-
[button(t(lang, 'control.button.
|
|
215
|
+
[button(t(lang, 'control.button.tasks'), 'tasks'), button(t(lang, 'control.button.runs'), 'runs')],
|
|
216
|
+
[button(t(lang, 'control.button.install'), 'install_menu'), button(t(lang, 'control.button.auth'), 'auth_menu')],
|
|
217
|
+
[button(t(lang, 'control.button.settings'), 'settings')],
|
|
215
218
|
];
|
|
216
219
|
}
|
|
217
220
|
|
|
@@ -344,6 +347,37 @@ async function showRuns({ bot, chatId, link, editMessageId }) {
|
|
|
344
347
|
});
|
|
345
348
|
}
|
|
346
349
|
|
|
350
|
+
async function showTaskMasterTasks({ bot, chatId, link, editMessageId }) {
|
|
351
|
+
const lang = languageFor(link);
|
|
352
|
+
const state = getState(link.user_id);
|
|
353
|
+
if (!state.selectedProjectName) {
|
|
354
|
+
await send(bot, chatId, t(lang, 'control.selectProjectFirst'), { editMessageId });
|
|
355
|
+
await showProjectMenu({ bot, chatId, link });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const data = await localApi(link.user_id, `/api/taskmaster/tasks/${encodeURIComponent(state.selectedProjectName)}`);
|
|
360
|
+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
361
|
+
const activeTasks = tasks.filter((task) => !['done', 'completed', 'cancelled', 'canceled'].includes(String(task.status || '').toLowerCase()));
|
|
362
|
+
if (activeTasks.length === 0) {
|
|
363
|
+
await send(bot, chatId, t(lang, 'control.noTaskMasterTasks'), {
|
|
364
|
+
editMessageId,
|
|
365
|
+
reply_markup: { inline_keyboard: [[button(t(lang, 'control.button.mainMenu'), 'menu')]] },
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const buttons = activeTasks.slice(0, 12).map((task) => button(
|
|
371
|
+
`#${task.id} ${compact(task.title, 38)}`,
|
|
372
|
+
'task_run',
|
|
373
|
+
{ taskId: String(task.id) },
|
|
374
|
+
));
|
|
375
|
+
await send(bot, chatId, t(lang, 'control.pickTaskMasterTask'), {
|
|
376
|
+
editMessageId,
|
|
377
|
+
reply_markup: { inline_keyboard: rows(buttons, 1) },
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
347
381
|
function extractAssistantText(response) {
|
|
348
382
|
const messages = Array.isArray(response?.messages) ? response.messages : [];
|
|
349
383
|
const chunks = [];
|
|
@@ -437,6 +471,10 @@ async function fetchRun(userId, runId) {
|
|
|
437
471
|
return localApi(userId, `/api/orchestration/workflows/runs/${runId}`);
|
|
438
472
|
}
|
|
439
473
|
|
|
474
|
+
async function fetchA2ATask(userId, taskId) {
|
|
475
|
+
return localApi(userId, `/a2a/tasks/${encodeURIComponent(taskId)}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
440
478
|
function summarizeRun(run, mode) {
|
|
441
479
|
const lines = [
|
|
442
480
|
`Run ${run.id}`,
|
|
@@ -485,6 +523,100 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
|
|
|
485
523
|
}
|
|
486
524
|
}
|
|
487
525
|
|
|
526
|
+
function extractA2ATaskText(task) {
|
|
527
|
+
const chunks = [];
|
|
528
|
+
const artifacts = Array.isArray(task?.artifacts) ? task.artifacts : [];
|
|
529
|
+
for (const artifact of artifacts) {
|
|
530
|
+
for (const part of Array.isArray(artifact?.parts) ? artifact.parts : []) {
|
|
531
|
+
if (part?.kind === 'text' && typeof part.text === 'string') chunks.push(part.text);
|
|
532
|
+
if (part?.type === 'text' && typeof part.text === 'string') chunks.push(part.text);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const history = Array.isArray(task?.history) ? task.history : [];
|
|
536
|
+
for (const message of history) {
|
|
537
|
+
if (message?.role !== 'assistant') continue;
|
|
538
|
+
for (const part of Array.isArray(message?.parts) ? message.parts : []) {
|
|
539
|
+
if (part?.kind === 'text' && typeof part.text === 'string') chunks.push(part.text);
|
|
540
|
+
if (part?.type === 'text' && typeof part.text === 'string') chunks.push(part.text);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return chunks.join('\n\n').trim();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function summarizeA2ATask(task) {
|
|
547
|
+
const text = extractA2ATaskText(task);
|
|
548
|
+
const error = task?.error?.message || task?.metadata?.error?.message || '';
|
|
549
|
+
return truncate([
|
|
550
|
+
`Task ${task?.id || ''}`,
|
|
551
|
+
`Status: ${task?.state || 'unknown'}`,
|
|
552
|
+
error ? `Error: ${error}` : '',
|
|
553
|
+
text,
|
|
554
|
+
].filter(Boolean).join('\n\n'));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function monitorA2ATask({ bot, chatId, link, taskId }) {
|
|
558
|
+
const key = `a2a:${taskId}`;
|
|
559
|
+
if (runMonitors.has(key)) return;
|
|
560
|
+
runMonitors.set(key, true);
|
|
561
|
+
const lang = languageFor(link);
|
|
562
|
+
try {
|
|
563
|
+
for (;;) {
|
|
564
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
565
|
+
const task = await fetchA2ATask(link.user_id, taskId);
|
|
566
|
+
if (TERMINAL_RUN_STATES.has(task?.state)) {
|
|
567
|
+
await send(bot, chatId, t(lang, 'control.taskFinished', {
|
|
568
|
+
taskId,
|
|
569
|
+
status: task.state,
|
|
570
|
+
summary: summarizeA2ATask(task),
|
|
571
|
+
}));
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
} finally {
|
|
576
|
+
runMonitors.delete(key);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function runTaskMasterTask({ bot, chatId, link, taskId }) {
|
|
581
|
+
const lang = languageFor(link);
|
|
582
|
+
const state = getState(link.user_id);
|
|
583
|
+
if (!state.remoteControlEnabled) {
|
|
584
|
+
await send(bot, chatId, t(lang, 'control.disabled'));
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (!state.selectedProjectName) {
|
|
588
|
+
await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
|
|
589
|
+
await showProjectMenu({ bot, chatId, link });
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const result = await localApi(
|
|
594
|
+
link.user_id,
|
|
595
|
+
`/api/taskmaster/execute/${encodeURIComponent(state.selectedProjectName)}/${encodeURIComponent(taskId)}`,
|
|
596
|
+
{
|
|
597
|
+
method: 'POST',
|
|
598
|
+
body: {
|
|
599
|
+
projectId: state.selectedProjectName,
|
|
600
|
+
adapterId: state.selectedProvider,
|
|
601
|
+
model: state.selectedModel || undefined,
|
|
602
|
+
isolation: 'worktree',
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
);
|
|
606
|
+
const a2aTaskId = result?.task?.a2aTaskId;
|
|
607
|
+
await send(bot, chatId, t(lang, 'control.taskStarted', {
|
|
608
|
+
taskId,
|
|
609
|
+
provider: state.selectedProvider,
|
|
610
|
+
a2aTaskId: a2aTaskId || result?.task?.id || 'unknown',
|
|
611
|
+
}));
|
|
612
|
+
|
|
613
|
+
if (a2aTaskId) {
|
|
614
|
+
monitorA2ATask({ bot, chatId, link, taskId: a2aTaskId }).catch((error) => {
|
|
615
|
+
console.warn('[telegram-control] task monitor failed:', error?.message || error);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
488
620
|
export async function startCliInstall({ bot, chatId, link, provider }) {
|
|
489
621
|
const lang = languageFor(link);
|
|
490
622
|
const data = await localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' });
|
|
@@ -568,6 +700,10 @@ async function handleAwaitingInput({ bot, chatId, link, text }) {
|
|
|
568
700
|
await runWorkflow({ bot, chatId, link, input: text });
|
|
569
701
|
return true;
|
|
570
702
|
}
|
|
703
|
+
if (awaiting.type === 'task_id') {
|
|
704
|
+
await runTaskMasterTask({ bot, chatId, link, taskId: text });
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
571
707
|
return false;
|
|
572
708
|
}
|
|
573
709
|
|
|
@@ -605,6 +741,19 @@ async function handleCommand({ bot, chatId, link, text }) {
|
|
|
605
741
|
await showRuns({ bot, chatId, link });
|
|
606
742
|
return true;
|
|
607
743
|
}
|
|
744
|
+
if (command === '/tasks') {
|
|
745
|
+
await showTaskMasterTasks({ bot, chatId, link });
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
if (command === '/task') {
|
|
749
|
+
if (!argText) {
|
|
750
|
+
updateTelegramControlState(link.user_id, { awaiting: { type: 'task_id' } });
|
|
751
|
+
await send(bot, chatId, t(lang, 'control.sendTaskId'));
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
await runTaskMasterTask({ bot, chatId, link, taskId: argText });
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
608
757
|
if (command === '/settings') {
|
|
609
758
|
await showSettings({ bot, chatId, link });
|
|
610
759
|
return true;
|
|
@@ -728,6 +877,7 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
|
|
|
728
877
|
if (action === 'models_refresh') return showModelMenu({ bot, chatId, link, refresh: true, editMessageId });
|
|
729
878
|
if (action === 'workflows') return showWorkflowMenu({ bot, chatId, link, editMessageId });
|
|
730
879
|
if (action === 'runs') return showRuns({ bot, chatId, link, editMessageId });
|
|
880
|
+
if (action === 'tasks') return showTaskMasterTasks({ bot, chatId, link, editMessageId });
|
|
731
881
|
if (action === 'install_menu') return showInstallMenu({ bot, chatId, link, editMessageId });
|
|
732
882
|
if (action === 'auth_menu') return showAuthMenu({ bot, chatId, link, editMessageId });
|
|
733
883
|
if (action === 'settings') return showSettings({ bot, chatId, link, editMessageId });
|
|
@@ -787,6 +937,7 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
|
|
|
787
937
|
await send(bot, chatId, t(languageFor(link), 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
|
|
788
938
|
return;
|
|
789
939
|
}
|
|
940
|
+
if (action === 'task_run') return runTaskMasterTask({ bot, chatId, link, taskId: payload.taskId });
|
|
790
941
|
if (action === 'install_provider') return startCliInstall({ bot, chatId, link, provider: payload.provider });
|
|
791
942
|
if (action === 'auth_provider') {
|
|
792
943
|
await send(bot, chatId, `${payload.provider} login:\n${AUTH_HELP[payload.provider] || t(languageFor(link), 'control.providerAuthFallback')}`, { editMessageId });
|