chore: remove scout-bot and update workspace root package.json
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s

This commit is contained in:
OG T
2026-06-08 13:30:54 +08:00
parent 2eabfbbd87
commit 6b243ded7e
6 changed files with 4 additions and 452 deletions

View File

@@ -1,15 +0,0 @@
FROM node:20-alpine
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/scout-bot/package.json apps/scout-bot/
RUN pnpm install --frozen-lockfile
COPY apps/scout-bot ./apps/scout-bot
CMD ["pnpm", "--dir", "apps/scout-bot", "exec", "tsx", "src/index.ts"]

View File

@@ -1,25 +0,0 @@
{
"name": "scout-bot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@octokit/rest": "^22.0.1",
"dotenv": "^17.4.2",
"node-cron": "^4.2.1",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/node": "^20.19.42",
"@types/node-cron": "^3.0.11",
"tsx": "^4.22.4",
"typescript": "^5.9.3"
}
}

View File

@@ -1,373 +0,0 @@
import { Octokit } from "@octokit/rest";
import cron from "node-cron";
import dotenv from "dotenv";
dotenv.config();
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const VIBEWORK_API_URL = process.env.VIBEWORK_API_URL || "https://agent.wooo.work/api";
const SCOUT_API_KEY = process.env.SCOUT_API_KEY;
const SCOUT_AGENT_ID = process.env.SCOUT_AGENT_ID || "scout_official_1";
const SCOUT_CRON_EXPRESSION = process.env.SCOUT_CRON_EXPRESSION || "*/3 * * * *";
const SCOUT_ISSUE_LABELS_RAW = process.env.SCOUT_ISSUE_LABELS || process.env.SCOUT_ISSUE_LABEL || "";
const SCOUT_ENABLED = (process.env.SCOUT_ENABLED || "true").toLowerCase() !== "false";
const SCOUT_TARGET_REPOS_RAW =
process.env.SCOUT_TARGET_REPOS ||
"open-webui/open-webui,microsoft/vscode,vercel/next.js,langchain-ai/langgraph,facebook/react,microsoft/TypeScript,openai/openai-cookbook,astral-sh/ruff,sequelize/sequelize,pnpm/pnpm,prisma/prisma";
const SCOUT_PER_PAGE = Math.min(Math.max(parseInt(process.env.SCOUT_PER_PAGE || "50", 10), 1), 100);
const SCOUT_MAX_ISSUES_PER_SCAN = Math.max(parseInt(process.env.SCOUT_MAX_ISSUES_PER_SCAN || "60", 10), 1);
const SCOUT_COMMENT_DELAY_SECONDS = Math.max(parseInt(process.env.SCOUT_COMMENT_DELAY_SECONDS || "2", 10), 0);
const SCOUT_COMMENT_REPOS_RAW = process.env.SCOUT_COMMENT_REPOS || "";
const SCOUT_COMMENT_REPOS = SCOUT_COMMENT_REPOS_RAW === "*"
? ["*"]
: SCOUT_COMMENT_REPOS_RAW.split(",").map(r => r.trim().toLowerCase()).filter(Boolean);
const SCOUT_ISSUE_LABELS = SCOUT_ISSUE_LABELS_RAW
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
const SCOUT_TARGET_REPOS = SCOUT_TARGET_REPOS_RAW
.split(",")
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => {
const [owner, repo] = entry.split("/");
return { owner, repo };
})
.filter((repo) => repo.owner && repo.repo);
const fallbackRepo = { owner: "open-webui", repo: "open-webui" };
const TARGET_REPOS = SCOUT_TARGET_REPOS.length > 0 ? SCOUT_TARGET_REPOS : [fallbackRepo];
if (!SCOUT_API_KEY) {
console.warn("WARNING: SCOUT_API_KEY is not set. Draft API may be rejected by server.");
}
if (!GITHUB_TOKEN) {
console.warn("WARNING: GITHUB_TOKEN is not set. Scout bot cannot post comments.");
}
function normalizeText(value: string, maxLength: number, fallback = "N/A") {
const cleanText = (value || "").replace(/\s+/g, " ").trim();
if (!cleanText) {
return fallback;
}
return cleanText.length <= maxLength ? cleanText : `${cleanText.slice(0, Math.max(maxLength - 1, 0))}`;
}
function buildDraftPayload(issueTitle: string, issueBody: string, issueUrl: string) {
const prefix = "GitHub Issue: ";
const titleMax = 120;
const descriptionMax = 2000;
const sourceSuffix = `\n\nSource: ${issueUrl}`;
const sourceAllowance = Math.max(descriptionMax - sourceSuffix.length, 20);
const safeTitle = normalizeText(issueTitle, Math.max(titleMax - prefix.length, 20), "Open Source Issue");
const cleanBody = normalizeText(issueBody || "", sourceAllowance, "請參考 issue 連結取得完整需求內容。");
return {
title: `${prefix}${safeTitle}`,
description: `${cleanBody}${sourceSuffix}`,
reward_amount: 0, // $0.00 Free AI Bounty
reward_currency: "USD",
required_stack: ["TypeScript", "GitHub"],
test_file_content: "// Needs manual test writing based on issue",
};
}
const octokit = new Octokit({
auth: GITHUB_TOKEN,
});
async function wait(seconds: number) {
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
function getGitHubBackoffSeconds(error: unknown): number {
const status =
typeof error === "object" &&
error !== null &&
"status" in error &&
typeof (error as { status?: unknown }).status === "number"
? (error as { status: number }).status
: undefined;
const message =
typeof error === "object" &&
error !== null &&
"message" in error &&
typeof (error as { message?: unknown }).message === "string"
? (error as { message: string }).message.toLowerCase()
: "";
const responseHeaders =
typeof error === "object" &&
error !== null &&
"response" in error &&
error.response &&
typeof error.response === "object" &&
"headers" in error.response
? (error.response as { headers?: Record<string, string> }).headers
: undefined;
const lowerCaseHeaders = responseHeaders
? Object.fromEntries(
Object.entries(responseHeaders).map(([key, value]) => [key.toLowerCase(), value])
)
: undefined;
if (
typeof status === "number" &&
status === 403 &&
message.includes("secondary rate limit")
) {
const resetAtRaw = lowerCaseHeaders?.["x-ratelimit-reset"];
if (resetAtRaw) {
const resetAt = parseInt(resetAtRaw, 10);
const now = Math.floor(Date.now() / 1000);
const resetWait = resetAt - now + 5;
if (resetWait > 0) {
return resetWait;
}
}
return 60;
}
if (typeof status === "number" && status === 403 && message.includes("rate limit exceeded")) {
const resetAtRaw = lowerCaseHeaders?.["x-ratelimit-reset"];
if (resetAtRaw) {
const resetAt = parseInt(resetAtRaw, 10);
const now = Math.floor(Date.now() / 1000);
const resetWait = resetAt - now + 5;
if (resetWait > 0) {
return resetWait;
}
}
return 60;
}
return 0;
}
async function postDraftWithRetry(payload: {
scout_id: string;
title: string;
description: string;
reward_amount: number;
reward_currency: string;
required_stack: string[];
test_file_content: string;
}) {
let lastError: unknown = null;
for (let attempt = 0; attempt < 3; attempt++) {
try {
const response = await fetch(`${VIBEWORK_API_URL}/scout/draft`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${SCOUT_API_KEY}`
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const bodyText = await response.text();
throw new Error(`Draft API failed (HTTP ${response.status}): ${bodyText}`);
}
return await response.json();
} catch (error) {
lastError = error;
const attemptText = attempt + 1;
const maxAttempts = 3;
console.error(`Failed to draft bounty task (attempt ${attemptText}/${maxAttempts}):`, error);
if (attempt < maxAttempts - 1) {
await wait(Math.min(5 * (attempt + 1), 15));
}
}
}
console.error("Failed to draft bounty task after retries:", lastError);
return null;
}
async function isIssueAlreadyDrafted(issueUrl: string) {
try {
const response = await fetch(
`${VIBEWORK_API_URL}/scout/issue-exists?issue_url=${encodeURIComponent(issueUrl)}`,
{
headers: {
"Authorization": `Bearer ${SCOUT_API_KEY}`,
},
}
);
if (!response.ok) {
const bodyText = await response.text();
console.warn(`Issue existence check failed (HTTP ${response.status}): ${bodyText}`);
return false;
}
const result = await response.json() as { exists?: boolean };
return Boolean(result.exists);
} catch (error) {
console.warn("Issue existence check error:", error);
return false;
}
}
async function draftBountyTask(issueTitle: string, issueBody: string, issueUrl: string) {
const payload = {
scout_id: SCOUT_AGENT_ID,
...buildDraftPayload(issueTitle, issueBody || "", issueUrl),
};
return postDraftWithRetry(payload);
}
async function processIssue(owner: string, repo: string, issue: any) {
console.log(`Processing issue #${issue.number}: ${issue.title}`);
const issueUrl = issue.html_url;
if (!issueUrl) {
console.log("Issue URL missing, skip.");
return;
}
const alreadyDrafted = await isIssueAlreadyDrafted(issueUrl);
if (alreadyDrafted) {
console.log(`Issue already has draft task: ${issueUrl}, skipping.`);
return;
}
// Check if we already commented
if (GITHUB_TOKEN) {
const comments = await octokit.issues.listComments({
owner,
repo,
issue_number: issue.number,
});
const alreadyCommented = comments.data.some((c) => c.body?.includes("agent.wooo.work"));
if (alreadyCommented) {
console.log(`Already commented on #${issue.number}, skipping.`);
return;
}
}
// Generate draft task on VibeWork
const draft = await draftBountyTask(issue.title, issue.body || "", issueUrl);
if (!draft) {
console.log(`Failed to generate draft for #${issue.number}, skipping comment.`);
return;
}
const commentBody = `Hi there! 👋 我們注意到這個 issue 被標記為需要社群協助。
**VibeWork** 是一個開源的 AI Agent 協定,目前已經連結了全球大量的自主 AI Agent。這些 Agent 可以為您**免費 (FREE)** 嘗試解決這個問題。
任何人類開發者或是 AI Agent 都可以認領這個 issue並透過我們的 MCP 伺服器自動提交 Pull Request
\`\`\`bash
npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work
\`\`\`
🔗 [點此追蹤 AI 的解題進度](${draft.checkout_url}) (Task ID: \`${draft.task_id}\`)
*(備註:我們是一個開源的防詐欺 Bounty 協定,旨在幫助開源專案維護者更快地利用 AI 解決問題。如果您不希望我們在此張貼此資訊,請隨時告知!)*
`;
if (GITHUB_TOKEN) {
const isAllowedToComment = SCOUT_COMMENT_REPOS.includes("*") || SCOUT_COMMENT_REPOS.includes(`${owner}/${repo}`.toLowerCase());
if (!isAllowedToComment) {
console.log(`[SILENT MODE] Successfully generated draft task for #${issue.number}, but skipping GitHub comment (repo not in SCOUT_COMMENT_REPOS).`);
return;
}
try {
await octokit.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: commentBody,
});
console.log(`Successfully commented on #${issue.number}`);
await wait(SCOUT_COMMENT_DELAY_SECONDS);
} catch (error) {
const backoffSeconds = getGitHubBackoffSeconds(error);
if (backoffSeconds > 0) {
console.warn(`GitHub API rate limited; sleeping ${backoffSeconds}s before continuing.`);
await wait(backoffSeconds);
}
console.error(`Failed to comment on #${issue.number}:`, error);
}
} else {
console.log(`[DRY RUN] Would have posted scout comment for #${issue.number}: ${draft.status}`);
}
}
async function scanRepositories() {
console.log("Starting GitHub scan...");
for (const target of TARGET_REPOS) {
try {
const issues = await octokit.issues.listForRepo({
owner: target.owner,
repo: target.repo,
state: "open",
...(SCOUT_ISSUE_LABELS.length > 0 ? { labels: SCOUT_ISSUE_LABELS.join(",") } : {}),
per_page: SCOUT_PER_PAGE
});
console.log(`Found ${issues.data.length} open issues in ${target.owner}/${target.repo}`);
const issueCandidates = issues.data.slice(0, SCOUT_MAX_ISSUES_PER_SCAN);
for (const issue of issueCandidates) {
if (!issue.pull_request) { // Ignore PRs
await processIssue(target.owner, target.repo, issue);
}
}
if (issues.data.length > issueCandidates.length) {
console.log(`Limited scan to first ${issueCandidates.length} issues in ${target.owner}/${target.repo}`);
}
} catch (error) {
const backoffSeconds = getGitHubBackoffSeconds(error);
if (backoffSeconds > 0) {
console.warn(`GitHub API rate limited on ${target.owner}/${target.repo}; sleeping ${backoffSeconds}s and skip remaining repos.`);
await wait(backoffSeconds);
return;
}
console.error(`Error scanning ${target.owner}/${target.repo}:`, error);
}
}
}
async function bootstrap() {
if (!SCOUT_ENABLED) {
console.log("SCOUT_ENABLED is false. Scout bot is disabled.");
return;
}
// Delay first scan a bit so main web API is ready after startup.
await wait(10);
await scanRepositories();
}
bootstrap();
if (SCOUT_ENABLED) {
// Default: every 1 minute for phase-1 high-velocity inbound traffic
cron.schedule(SCOUT_CRON_EXPRESSION, () => {
console.log(`Running scheduled GitHub scan (${SCOUT_CRON_EXPRESSION})...`);
scanRepositories();
});
} else {
console.log("Cron schedule is disabled because SCOUT_ENABLED=false.");
}
console.log("VibeWork Scout Bot started and scheduled.");

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,24 +0,0 @@
version: '3.8'
services:
scout-bot:
build:
context: .
dockerfile: apps/scout-bot/Dockerfile
container_name: agent_bounty_scout_bot
restart: unless-stopped
environment:
- NODE_ENV=production
# - GITHUB_TOKEN=your_token_here
- VIBEWORK_API_URL=http://agent_bounty_web:3000/api
- SCOUT_API_KEY=${SCOUT_API_KEY:-dev_scout_key}
- SCOUT_AGENT_ID=scout_official_1
- SCOUT_ENABLED=${SCOUT_ENABLED:-false}
- SCOUT_PER_PAGE=${SCOUT_PER_PAGE:-80}
- SCOUT_MAX_ISSUES_PER_SCAN=${SCOUT_MAX_ISSUES_PER_SCAN:-30}
networks:
- agent-bounty-network
networks:
agent-bounty-network:
external: true

View File

@@ -12,5 +12,9 @@
"packageManager": "pnpm@10.32.1", "packageManager": "pnpm@10.32.1",
"devDependencies": { "devDependencies": {
"vitest": "^4.1.8" "vitest": "^4.1.8"
},
"dependencies": {
"dotenv": "^17.4.2",
"openai": "^6.42.0"
} }
} }