mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
Slack integration (#149)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
parent
3ad3c3a68e
commit
d2b8b9551f
16 changed files with 412 additions and 128 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -21,3 +21,6 @@ Thumbs.db
|
|||
# docker
|
||||
**/.docker/data
|
||||
**/.docker/logs
|
||||
|
||||
# logs
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"dev": "dotenvx run -f ../../.env.development -- next dev --turbopack --port 3001",
|
||||
"build": "dotenvx run -f ../../.env.production -- next build",
|
||||
"start": "dotenvx run -f ../../.env.production -- next start --port 3001",
|
||||
"controller": "dotenvx run -f ../../.env.development -- tsx src/lib/controller.ts",
|
||||
"controller": "dotenvx run -f ../../.env.development -f ../../.env.keys -f ../../.env.local -- tsx src/lib/controller.ts",
|
||||
"controller:production": "dotenvx run -f ../../.env.production -- tsx src/lib/controller.ts",
|
||||
"worker": "dotenvx run -f ../../.env.development -- tsx src/lib/worker.ts",
|
||||
"worker:production": "dotenvx run -f ../../.env.production -- tsx src/lib/worker.ts",
|
||||
|
|
|
|||
236
apps/roomote/src/app/api/webhooks/slack/route.ts
Normal file
236
apps/roomote/src/app/api/webhooks/slack/route.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { db, cloudJobs } from '@roo-code-cloud/db/server';
|
||||
|
||||
import { SlackNotifier } from '@/lib/slack';
|
||||
|
||||
import { createAndEnqueueJob } from '../github/handlers/utils';
|
||||
|
||||
const mentionedThreads = new Set<string>();
|
||||
const pendingWorkspaceSelections = new Map<string, SlackEvent>();
|
||||
const slack = new SlackNotifier();
|
||||
|
||||
interface SlackEvent {
|
||||
type: string;
|
||||
channel: string;
|
||||
user: string;
|
||||
text: string;
|
||||
ts: string;
|
||||
thread_ts?: string;
|
||||
bot_id?: string;
|
||||
app_id?: string;
|
||||
}
|
||||
|
||||
interface SlackInteractivePayload {
|
||||
type: string;
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
channel: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
message: {
|
||||
ts: string;
|
||||
thread_ts?: string;
|
||||
};
|
||||
actions: Array<{
|
||||
action_id: string;
|
||||
value: string;
|
||||
text: {
|
||||
text: string;
|
||||
};
|
||||
}>;
|
||||
response_url: string;
|
||||
trigger_id: string;
|
||||
}
|
||||
|
||||
interface SlackWebhookBody {
|
||||
type: string;
|
||||
challenge?: string;
|
||||
event?: SlackEvent;
|
||||
team_id?: string;
|
||||
payload?: string; // For interactive payloads.
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const contentType = request.headers.get('content-type');
|
||||
|
||||
if (contentType?.includes('application/x-www-form-urlencoded')) {
|
||||
const formData = await request.formData();
|
||||
const payload = formData.get('payload') as string;
|
||||
|
||||
if (payload) {
|
||||
const interactivePayload: SlackInteractivePayload = JSON.parse(payload);
|
||||
await handleInteractivePayload(interactivePayload);
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
}
|
||||
|
||||
const body: SlackWebhookBody = JSON.parse(await request.text());
|
||||
|
||||
if (body.type === 'url_verification') {
|
||||
console.log('🔐 Slack URL verification challenge received');
|
||||
return NextResponse.json({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
if (body.type === 'event_callback' && body.event) {
|
||||
const event = body.event;
|
||||
|
||||
if (event.bot_id || event.app_id) {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
console.log('🛎️ Slack Event ->', {
|
||||
type: event.type,
|
||||
channel: event.channel,
|
||||
user: event.user,
|
||||
text: event.text?.substring(0, 100),
|
||||
thread_ts: event.thread_ts,
|
||||
ts: event.ts,
|
||||
});
|
||||
|
||||
switch (event.type) {
|
||||
case 'app_mention':
|
||||
await handleAppMention(event);
|
||||
break;
|
||||
|
||||
case 'message':
|
||||
await handleMessage(event);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Unhandled event type: ${event.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
async function handleInteractivePayload(payload: SlackInteractivePayload) {
|
||||
console.log('🎯 Interactive payload received:', {
|
||||
type: payload.type,
|
||||
user: payload.user.id,
|
||||
channel: payload.channel.id,
|
||||
action: payload.actions[0]?.action_id,
|
||||
value: payload.actions[0]?.value,
|
||||
});
|
||||
|
||||
if (
|
||||
payload.actions[0]?.action_id === 'select_roo_code_cloud' ||
|
||||
payload.actions[0]?.action_id === 'select_roo_code'
|
||||
) {
|
||||
const workspace = payload.actions[0].value;
|
||||
const threadId = payload.message.thread_ts || payload.message.ts;
|
||||
|
||||
try {
|
||||
const originalEvent = pendingWorkspaceSelections.get(threadId);
|
||||
|
||||
if (!originalEvent) {
|
||||
throw new Error('Original mention event not found');
|
||||
}
|
||||
|
||||
pendingWorkspaceSelections.delete(threadId);
|
||||
|
||||
const { jobId, enqueuedJobId } = await createAndEnqueueJob(
|
||||
'slack.app.mention',
|
||||
{
|
||||
channel: originalEvent.channel,
|
||||
user: originalEvent.user,
|
||||
text: originalEvent.text,
|
||||
ts: originalEvent.ts,
|
||||
thread_ts: threadId,
|
||||
workspace,
|
||||
},
|
||||
);
|
||||
|
||||
console.log(
|
||||
`🔗 Enqueued slack.app.mention job for workspace ${workspace} (id: ${jobId}, enqueued: ${enqueuedJobId})`,
|
||||
);
|
||||
|
||||
await slack.postMessage({
|
||||
text: `✅ Enqueued job ${enqueuedJobId} for workspace ${workspace}.`,
|
||||
channel: payload.channel.id,
|
||||
thread_ts: threadId,
|
||||
});
|
||||
|
||||
await db
|
||||
.update(cloudJobs)
|
||||
.set({ slackThreadTs: threadId })
|
||||
.where(eq(cloudJobs.id, jobId));
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to process workspace selection:', error);
|
||||
|
||||
await slack.postMessage({
|
||||
text: `❌ Sorry, something went wrong processing your request. Please try again.`,
|
||||
channel: payload.channel.id,
|
||||
thread_ts: threadId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAppMention(event: SlackEvent) {
|
||||
console.log('🤖 Bot mentioned in channel:', event.channel);
|
||||
const threadId = event.thread_ts || event.ts;
|
||||
mentionedThreads.add(threadId);
|
||||
console.log(`📌 Tracking thread: ${threadId}`);
|
||||
|
||||
try {
|
||||
pendingWorkspaceSelections.set(threadId, event);
|
||||
|
||||
const result = await slack.postMessage({
|
||||
text: '👋 Which workspace would you like me to work in?',
|
||||
channel: event.channel,
|
||||
thread_ts: threadId,
|
||||
blocks: [
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: '👋 Which workspace would you like me to work in?',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
elements: [
|
||||
{
|
||||
type: 'button',
|
||||
text: { type: 'plain_text', text: 'Roo-Code-Cloud', emoji: true },
|
||||
action_id: 'select_roo_code_cloud',
|
||||
value: 'roo-code-cloud',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: { type: 'plain_text', text: 'Roo-Code', emoji: true },
|
||||
action_id: 'select_roo_code',
|
||||
value: 'roo-code',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log(`✅ Sent workspace selection to thread: ${threadId}`, result);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to process app mention:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMessage(event: SlackEvent) {
|
||||
if (!event.thread_ts || !mentionedThreads.has(event.thread_ts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('💬 New message in tracked thread:', {
|
||||
thread: event.thread_ts,
|
||||
channel: event.channel,
|
||||
user: event.user,
|
||||
text: event.text?.substring(0, 100),
|
||||
});
|
||||
|
||||
// TODO: Process the thread message.
|
||||
// This is where you'd handle follow-up messages in the thread.
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
// npx vitest src/lib/__tests__/controller.test.ts
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
const mockQueue = {
|
||||
getWaiting: vi.fn(() => Promise.resolve([])),
|
||||
getActive: vi.fn(() => Promise.resolve([])),
|
||||
|
|
@ -9,6 +7,10 @@ const mockQueue = {
|
|||
on: vi.fn(),
|
||||
};
|
||||
|
||||
const mockWorker = {
|
||||
startStalledCheckTimer: vi.fn(),
|
||||
};
|
||||
|
||||
const mockSpawn = vi.fn(() => ({
|
||||
stdout: { pipe: vi.fn() },
|
||||
stderr: { pipe: vi.fn() },
|
||||
|
|
@ -37,8 +39,11 @@ vi.mock('fs', () => ({
|
|||
|
||||
const mockQueueConstructor = vi.fn(() => mockQueue);
|
||||
|
||||
const mockWorkerConstructor = vi.fn(() => mockWorker);
|
||||
|
||||
vi.mock('bullmq', () => ({
|
||||
Queue: mockQueueConstructor,
|
||||
Worker: mockWorkerConstructor,
|
||||
}));
|
||||
|
||||
describe('WorkerController', () => {
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
// npx dotenvx run -f ../../.env.production -- tsx src/lib/checkStalledJobs.ts
|
||||
|
||||
import { Worker } from 'bullmq';
|
||||
|
||||
import { redis } from './redis';
|
||||
|
||||
async function checkStalledJobs() {
|
||||
const worker = new Worker('roomote', undefined, {
|
||||
autorun: false,
|
||||
connection: redis,
|
||||
});
|
||||
|
||||
while (true) {
|
||||
console.log('startStalledCheckTimer()');
|
||||
await worker.startStalledCheckTimer();
|
||||
await new Promise((resolve) => setTimeout(resolve, 30_000));
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('SIGTERM');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('SIGINT');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
checkStalledJobs()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
import * as path from 'path';
|
||||
import * as os from 'node:os';
|
||||
|
||||
import {
|
||||
command,
|
||||
run,
|
||||
|
|
@ -19,7 +16,6 @@ import {
|
|||
processPullRequestComment,
|
||||
} from '@/lib/jobs';
|
||||
import { runTask } from '@/lib/runTask';
|
||||
import { Logger } from '@/lib/logger';
|
||||
|
||||
const fixIssueCommand = command({
|
||||
name: 'fix-issue',
|
||||
|
|
@ -277,11 +273,6 @@ const promptCommand = command({
|
|||
jobType: 'test.prompt',
|
||||
jobPayload: { text },
|
||||
prompt: text,
|
||||
logger: new Logger({
|
||||
logDir: path.resolve(os.tmpdir(), 'logs'),
|
||||
filename: 'cli.log',
|
||||
tag: 'worker',
|
||||
}),
|
||||
notify: false,
|
||||
workspacePath,
|
||||
settings: { mode },
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Queue } from 'bullmq';
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import { execa } from 'execa';
|
||||
|
||||
import { redis } from './redis';
|
||||
|
|
@ -9,28 +9,34 @@ export class WorkerController {
|
|||
private readonly MAX_WORKERS = 2;
|
||||
|
||||
private queue: Queue;
|
||||
private stalledJobsWorker: Worker;
|
||||
public isRunning = false;
|
||||
private pollingInterval: NodeJS.Timeout | null = null;
|
||||
private activeWorkers = new Set<string>();
|
||||
|
||||
constructor() {
|
||||
this.queue = new Queue('roomote', { connection: redis });
|
||||
|
||||
this.stalledJobsWorker = new Worker('roomote', undefined, {
|
||||
autorun: false,
|
||||
connection: redis,
|
||||
});
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.isRunning) {
|
||||
console.log('Controller is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
console.log('Worker controller started');
|
||||
|
||||
await this.checkAndSpawnWorker();
|
||||
|
||||
this.pollingInterval = setInterval(async () => {
|
||||
await this.checkAndSpawnWorker();
|
||||
}, this.POLL_INTERVAL_MS);
|
||||
|
||||
await this.stalledJobsWorker.startStalledCheckTimer();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
|
|
@ -92,7 +98,9 @@ export class WorkerController {
|
|||
'--network roo-code-cloud_default',
|
||||
`-e APP_ENV=${process.env.APP_ENV || 'development'}`,
|
||||
`-e GH_TOKEN=${process.env.GH_TOKEN}`,
|
||||
`-e DOTENV_PRIVATE_KEY_DEVELOPMENT=${process.env.DOTENV_PRIVATE_KEY_DEVELOPMENT}`,
|
||||
`-e DOTENV_PRIVATE_KEY_PRODUCTION=${process.env.DOTENV_PRIVATE_KEY_PRODUCTION}`,
|
||||
`-e WORKSPACE_ROOT=${process.env.WORKSPACE_ROOT}`,
|
||||
'-v /var/run/docker.sock:/var/run/docker.sock',
|
||||
'-v /tmp/roomote:/var/log/roomote',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { eq } from 'drizzle-orm';
|
||||
import { Job } from 'bullmq';
|
||||
|
||||
import type { ClineMessage } from '@roo-code/types';
|
||||
import {
|
||||
type JobType,
|
||||
type JobStatus,
|
||||
|
|
@ -14,6 +15,10 @@ import {
|
|||
import { fixGitHubIssue } from './jobs/fixGitHubIssue';
|
||||
import { processPullRequestComment } from './jobs/processPullRequestComment';
|
||||
import { processIssueComment } from './jobs/processIssueComment';
|
||||
import { processSlackMention } from './jobs/processSlackMention';
|
||||
import { SlackNotifier } from './slack';
|
||||
|
||||
const slack = new SlackNotifier();
|
||||
|
||||
export async function processJob<T extends JobType>({
|
||||
data: { type, payload, jobId },
|
||||
|
|
@ -31,20 +36,17 @@ export async function processJob<T extends JobType>({
|
|||
result = await fixGitHubIssue(
|
||||
payload as JobPayload<'github.issue.fix'>,
|
||||
{
|
||||
onTaskStarted: async (
|
||||
onTaskStarted: (
|
||||
slackThreadTs: string | null | undefined,
|
||||
_rooTaskId: string,
|
||||
) => {
|
||||
if (slackThreadTs) {
|
||||
await updateJobStatus(
|
||||
jobId,
|
||||
'processing',
|
||||
undefined,
|
||||
undefined,
|
||||
slackThreadTs,
|
||||
);
|
||||
}
|
||||
},
|
||||
) =>
|
||||
updateJobStatus(
|
||||
jobId,
|
||||
'processing',
|
||||
undefined,
|
||||
undefined,
|
||||
slackThreadTs,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -53,20 +55,17 @@ export async function processJob<T extends JobType>({
|
|||
result = await processIssueComment(
|
||||
payload as JobPayload<'github.issue.comment.respond'>,
|
||||
{
|
||||
onTaskStarted: async (
|
||||
onTaskStarted: (
|
||||
slackThreadTs: string | null | undefined,
|
||||
_rooTaskId: string,
|
||||
) => {
|
||||
if (slackThreadTs) {
|
||||
await updateJobStatus(
|
||||
jobId,
|
||||
'processing',
|
||||
undefined,
|
||||
undefined,
|
||||
slackThreadTs,
|
||||
);
|
||||
}
|
||||
},
|
||||
) =>
|
||||
updateJobStatus(
|
||||
jobId,
|
||||
'processing',
|
||||
undefined,
|
||||
undefined,
|
||||
slackThreadTs,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -75,34 +74,60 @@ export async function processJob<T extends JobType>({
|
|||
result = await processPullRequestComment(
|
||||
payload as JobPayload<'github.pr.comment.respond'>,
|
||||
{
|
||||
onTaskStarted: async (
|
||||
onTaskStarted: (
|
||||
slackThreadTs: string | null | undefined,
|
||||
_rooTaskId: string,
|
||||
) => {
|
||||
if (slackThreadTs) {
|
||||
await updateJobStatus(
|
||||
jobId,
|
||||
'processing',
|
||||
undefined,
|
||||
undefined,
|
||||
slackThreadTs,
|
||||
);
|
||||
}
|
||||
},
|
||||
) =>
|
||||
updateJobStatus(
|
||||
jobId,
|
||||
'processing',
|
||||
undefined,
|
||||
undefined,
|
||||
slackThreadTs,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
break;
|
||||
case 'slack.app.mention': {
|
||||
const jobPayload = payload as JobPayload<'slack.app.mention'>;
|
||||
const { channel, thread_ts } = jobPayload;
|
||||
|
||||
result = await processSlackMention(jobPayload, {
|
||||
onTaskStarted: (
|
||||
slackThreadTs: string | null | undefined,
|
||||
_rooTaskId: string,
|
||||
) =>
|
||||
updateJobStatus(
|
||||
jobId,
|
||||
'processing',
|
||||
undefined,
|
||||
undefined,
|
||||
slackThreadTs,
|
||||
),
|
||||
onTaskMessage: async (message: ClineMessage) => {
|
||||
console.log(`onTaskMessage (${channel}, ${thread_ts}) ->`, message);
|
||||
|
||||
if (
|
||||
(message.say === 'text' || message.say === 'completion_result') &&
|
||||
message.text &&
|
||||
thread_ts
|
||||
) {
|
||||
slack.postMessage({ text: message.text, channel, thread_ts });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown job type: ${type}`);
|
||||
}
|
||||
|
||||
await updateJobStatus(jobId, 'completed', result);
|
||||
console.log(
|
||||
`[${job.name} | ${job.id}] Job ${jobId} completed successfully`,
|
||||
);
|
||||
console.log(`[${job.name} | ${job.id}] ✅`);
|
||||
} catch (error) {
|
||||
console.error(`[${job.name} | ${job.id}] Job ${jobId} failed:`, error);
|
||||
console.error(`[${job.name} | ${job.id}] ❌`, error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
await updateJobStatus(jobId, 'failed', undefined, errorMessage);
|
||||
throw error; // Re-throw to mark job as failed in BullMQ.
|
||||
|
|
@ -114,7 +139,7 @@ async function updateJobStatus(
|
|||
status: JobStatus,
|
||||
result?: unknown,
|
||||
error?: string,
|
||||
slackThreadTs?: string,
|
||||
slackThreadTs?: string | null,
|
||||
) {
|
||||
const values: UpdateCloudJob = { status };
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import * as path from 'path';
|
||||
import * as os from 'node:os';
|
||||
|
||||
import type { JobType, JobPayload } from '@roo-code-cloud/db';
|
||||
|
||||
import { runTask, type RunTaskCallbacks } from '../runTask';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
const jobType: JobType = 'github.issue.fix';
|
||||
|
||||
|
|
@ -31,11 +27,6 @@ Issue #${jobPayload.issue}
|
|||
jobType,
|
||||
jobPayload,
|
||||
prompt,
|
||||
logger: new Logger({
|
||||
logDir: path.resolve(os.tmpdir(), 'logs'),
|
||||
filename: 'worker.log',
|
||||
tag: 'worker',
|
||||
}),
|
||||
callbacks,
|
||||
settings: {
|
||||
mode: 'issue-fixer',
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { fixGitHubIssue } from './fixGitHubIssue';
|
||||
export { processIssueComment } from './processIssueComment';
|
||||
export { processPullRequestComment } from './processPullRequestComment';
|
||||
export { processSlackMention } from './processSlackMention';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import * as path from 'path';
|
||||
import * as os from 'node:os';
|
||||
|
||||
import type { JobType, JobPayload } from '@roo-code-cloud/db';
|
||||
|
||||
import { runTask, type RunTaskCallbacks } from '../runTask';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
const jobType: JobType = 'github.issue.comment.respond';
|
||||
|
||||
|
|
@ -58,11 +54,6 @@ gh api repos/${jobPayload.repo}/issues/${jobPayload.issueNumber}/comments --meth
|
|||
jobType,
|
||||
jobPayload,
|
||||
prompt,
|
||||
logger: new Logger({
|
||||
logDir: path.resolve(os.tmpdir(), 'logs'),
|
||||
filename: 'worker.log',
|
||||
tag: 'worker',
|
||||
}),
|
||||
callbacks,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import * as path from 'path';
|
||||
import * as os from 'node:os';
|
||||
|
||||
import type { JobType, JobPayload } from '@roo-code-cloud/db';
|
||||
|
||||
import { runTask, type RunTaskCallbacks } from '../runTask';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
const jobType: JobType = 'github.pr.comment.respond';
|
||||
|
||||
|
|
@ -72,11 +68,6 @@ Do not create a new pull request - work directly on the existing PR branch.
|
|||
jobType,
|
||||
jobPayload,
|
||||
prompt,
|
||||
logger: new Logger({
|
||||
logDir: path.resolve(os.tmpdir(), 'logs'),
|
||||
filename: 'worker.log',
|
||||
tag: 'worker',
|
||||
}),
|
||||
callbacks,
|
||||
});
|
||||
|
||||
|
|
|
|||
33
apps/roomote/src/lib/jobs/processSlackMention.ts
Normal file
33
apps/roomote/src/lib/jobs/processSlackMention.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import type { JobType, JobPayload } from '@roo-code-cloud/db';
|
||||
|
||||
import { runTask, type RunTaskCallbacks } from '../runTask';
|
||||
|
||||
const jobType: JobType = 'slack.app.mention';
|
||||
|
||||
type ProcessSlackMentionJobPayload = JobPayload<'slack.app.mention'>;
|
||||
|
||||
export async function processSlackMention(
|
||||
jobPayload: ProcessSlackMentionJobPayload,
|
||||
callbacks?: RunTaskCallbacks,
|
||||
): Promise<{
|
||||
channel: string;
|
||||
user: string;
|
||||
result: unknown;
|
||||
}> {
|
||||
const { text: prompt, channel, user } = jobPayload;
|
||||
|
||||
// Add your workspace root to .env.local to override the default
|
||||
// workspace root that our containers use.
|
||||
const workspaceRoot = process.env.WORKSPACE_ROOT || '/roo/repos';
|
||||
|
||||
const result = await runTask({
|
||||
jobType,
|
||||
jobPayload,
|
||||
prompt,
|
||||
callbacks,
|
||||
notify: false,
|
||||
workspacePath: `${workspaceRoot}/${jobPayload.workspace}`,
|
||||
});
|
||||
|
||||
return { channel, user, result };
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { execa } from 'execa';
|
|||
|
||||
import {
|
||||
type RooCodeSettings,
|
||||
type ClineMessage,
|
||||
TaskCommandName,
|
||||
RooCodeEventName,
|
||||
IpcMessageType,
|
||||
|
|
@ -35,6 +36,7 @@ export type RunTaskCallbacks = {
|
|||
slackThreadTs: string | null | undefined,
|
||||
rooTaskId: string,
|
||||
) => Promise<void>;
|
||||
onTaskMessage?: (message: ClineMessage) => Promise<void>;
|
||||
onTaskAborted?: (slackThreadTs: string | null | undefined) => Promise<void>;
|
||||
onTaskCompleted?: (
|
||||
slackThreadTs: string | null | undefined,
|
||||
|
|
@ -50,7 +52,7 @@ type RunTaskOptions<T extends JobType> = {
|
|||
jobType: T;
|
||||
jobPayload: JobPayload<T>;
|
||||
prompt: string;
|
||||
logger: Logger;
|
||||
logger?: Logger;
|
||||
callbacks?: RunTaskCallbacks;
|
||||
notify?: boolean;
|
||||
workspacePath?: string;
|
||||
|
|
@ -80,6 +82,14 @@ export const runTask = async <T extends JobType>({
|
|||
? `ROO_CODE_IPC_SOCKET_PATH=${ipcSocketPath} xvfb-run --auto-servernum --server-num=1 code --wait --log trace --disable-workspace-trust --disable-gpu --disable-lcd-text --no-sandbox --user-data-dir /roo/.vscode --password-store="basic" -n ${workspacePath}`
|
||||
: `ROO_CODE_IPC_SOCKET_PATH=${ipcSocketPath} code --disable-workspace-trust -n ${workspacePath}`;
|
||||
|
||||
if (!logger) {
|
||||
logger = new Logger({
|
||||
logDir: path.resolve(os.tmpdir(), 'logs'),
|
||||
filename: 'worker.log',
|
||||
tag: 'worker',
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(codeCommand);
|
||||
|
||||
const subprocess = execa({
|
||||
|
|
@ -152,6 +162,14 @@ export const runTask = async <T extends JobType>({
|
|||
logger.info(`${eventName} ->`, payload);
|
||||
}
|
||||
|
||||
if (
|
||||
eventName === RooCodeEventName.Message &&
|
||||
payload[0].message.partial !== true &&
|
||||
callbacks?.onTaskMessage
|
||||
) {
|
||||
await callbacks.onTaskMessage(payload[0].message);
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskStarted) {
|
||||
taskStartedAt = Date.now();
|
||||
rooTaskId = payload[0];
|
||||
|
|
@ -252,11 +270,14 @@ export const runTask = async <T extends JobType>({
|
|||
|
||||
if (rooTaskId && !isClientDisconnected) {
|
||||
logger.info('cancelling task');
|
||||
|
||||
client.sendCommand({
|
||||
commandName: TaskCommandName.CancelTask,
|
||||
data: rooTaskId,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 5_000)); // Allow some time for the task to cancel.
|
||||
|
||||
// Allow some time for the task to cancel.
|
||||
await new Promise((resolve) => setTimeout(resolve, 5_000));
|
||||
}
|
||||
|
||||
taskFinishedAt = Date.now();
|
||||
|
|
@ -282,11 +303,14 @@ export const runTask = async <T extends JobType>({
|
|||
|
||||
if (rooTaskId && !isClientDisconnected) {
|
||||
logger.info('closing task');
|
||||
|
||||
client.sendCommand({
|
||||
commandName: TaskCommandName.CloseTask,
|
||||
data: rooTaskId,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000)); // Allow some time for the window to close.
|
||||
|
||||
// Allow some time for the window to close.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
||||
}
|
||||
|
||||
if (!isClientDisconnected) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export class SlackNotifier {
|
|||
this.token = token;
|
||||
}
|
||||
|
||||
private async postMessage(message: SlackMessage): Promise<string | null> {
|
||||
public async postMessage(message: SlackMessage): Promise<string | null> {
|
||||
try {
|
||||
const messageWithChannel = {
|
||||
...message,
|
||||
|
|
@ -120,6 +120,21 @@ export class SlackNotifier {
|
|||
],
|
||||
});
|
||||
}
|
||||
case 'slack.app.mention': {
|
||||
const payload = jobPayload as JobPayload<'slack.app.mention'>;
|
||||
return await this.postMessage({
|
||||
text: `🚀 Task Started`,
|
||||
blocks: [
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `🚀 *Task Started*\nProcessing your Slack mention from <#${payload.channel}>\n*Message:* ${payload.text.slice(0, 100)}${payload.text.length > 100 ? '...' : ''}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown job type: ${jobType}`);
|
||||
}
|
||||
|
|
@ -159,8 +174,4 @@ export class SlackNotifier {
|
|||
thread_ts: threadTs,
|
||||
});
|
||||
}
|
||||
|
||||
public async sendMessage(message: SlackMessage): Promise<string | null> {
|
||||
return await this.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,17 @@ export const createJobSchema = z.discriminatedUnion('type', [
|
|||
commentUrl: z.string(),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('slack.app.mention'),
|
||||
payload: z.object({
|
||||
channel: z.string(),
|
||||
user: z.string(),
|
||||
text: z.string(),
|
||||
ts: z.string(),
|
||||
thread_ts: z.string().optional(),
|
||||
workspace: z.string(),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('test.prompt'),
|
||||
payload: z.object({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue