comfy-pr 1.4.2 → 1.5.1
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/bot/cli.ts +33 -0
- package/bot/error-collector.ts +53 -3
- package/bot/index.ts +48 -1
- package/bot/slack-bot.ts +549 -296
- package/bot/spawn-as-user.ts +82 -0
- package/bot/task-user.ts +136 -0
- package/bot/taskInputFlow.spec.ts +87 -0
- package/package.json +1 -1
package/bot/cli.ts
CHANGED
|
@@ -374,6 +374,39 @@ async function main() {
|
|
|
374
374
|
await loadEnvLocal();
|
|
375
375
|
|
|
376
376
|
const url = args.url as string;
|
|
377
|
+
|
|
378
|
+
// Handle @username — resolve to DM channel then read recent messages
|
|
379
|
+
if (url.startsWith("@")) {
|
|
380
|
+
const { getSlack } = await import("@/lib/slack");
|
|
381
|
+
const slack = getSlack();
|
|
382
|
+
const name = url.slice(1).toLowerCase();
|
|
383
|
+
type SlackUser = { id?: string; name?: string; real_name?: string };
|
|
384
|
+
let found: SlackUser | undefined;
|
|
385
|
+
let cursor: string | undefined;
|
|
386
|
+
do {
|
|
387
|
+
const res = await slack.users.list({ limit: 200, ...(cursor ? { cursor } : {}) });
|
|
388
|
+
found = (res.members as SlackUser[] | undefined)?.find(
|
|
389
|
+
(u) =>
|
|
390
|
+
(u.name ?? "").toLowerCase() === name ||
|
|
391
|
+
(u.real_name ?? "").toLowerCase() === name,
|
|
392
|
+
);
|
|
393
|
+
cursor = res.response_metadata?.next_cursor || undefined;
|
|
394
|
+
} while (!found && cursor);
|
|
395
|
+
if (!found?.id) {
|
|
396
|
+
console.error(`User not found: ${url}`);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
const dmRes = await slack.conversations.open({ users: found.id });
|
|
400
|
+
const channelId = dmRes.channel?.id;
|
|
401
|
+
if (!channelId) {
|
|
402
|
+
console.error("Could not open DM");
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
const messages = await readRecentMessages(channelId, 20);
|
|
406
|
+
console.log(yaml.stringify(messages));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
377
410
|
const parsed = parseSlackUrlSmart(url);
|
|
378
411
|
|
|
379
412
|
switch (parsed.type) {
|
package/bot/error-collector.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Error Collector - Monitors child workspace for errors and collects them
|
|
3
|
+
*
|
|
4
|
+
* Strategy: fs.watch for fast event-driven response, plus a slow safety-net
|
|
5
|
+
* poll (default 60s) since recursive watch can drop events on some FS layers
|
|
6
|
+
* and inside containers.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
|
-
import { readdir, readFile, appendFile, mkdir } from "fs/promises";
|
|
9
|
+
import { readdir, readFile, appendFile, mkdir, watch } from "fs/promises";
|
|
6
10
|
import { existsSync } from "fs";
|
|
7
11
|
import path from "path";
|
|
8
12
|
|
|
@@ -19,13 +23,16 @@ export class ErrorCollector {
|
|
|
19
23
|
private onError?: (errorPath: string, content: string) => void;
|
|
20
24
|
private checkInterval: number;
|
|
21
25
|
private intervalId?: Timer;
|
|
26
|
+
private watchAbort?: AbortController;
|
|
22
27
|
private processedErrors = new Set<string>();
|
|
28
|
+
private debounceTimers = new Map<string, Timer>();
|
|
23
29
|
|
|
24
30
|
constructor(options: ErrorCollectorOptions) {
|
|
25
31
|
this.workspaceDir = options.workspaceDir;
|
|
26
32
|
this.outputLogPath = options.outputLogPath;
|
|
27
33
|
this.onError = options.onError;
|
|
28
|
-
|
|
34
|
+
// Slow safety-net poll. Real-time detection comes from fs.watch.
|
|
35
|
+
this.checkInterval = options.checkInterval || 60_000;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
async start() {
|
|
@@ -35,7 +42,46 @@ export class ErrorCollector {
|
|
|
35
42
|
// Initial scan
|
|
36
43
|
await this.scanForErrors();
|
|
37
44
|
|
|
38
|
-
//
|
|
45
|
+
// Event-driven watcher (recursive). Bursts of writes are coalesced via
|
|
46
|
+
// a 500ms per-file debounce.
|
|
47
|
+
if (existsSync(this.workspaceDir)) {
|
|
48
|
+
this.watchAbort = new AbortController();
|
|
49
|
+
(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const watcher = watch(this.workspaceDir, {
|
|
52
|
+
recursive: true,
|
|
53
|
+
signal: this.watchAbort!.signal,
|
|
54
|
+
});
|
|
55
|
+
for await (const ev of watcher) {
|
|
56
|
+
const filename = ev.filename;
|
|
57
|
+
if (!filename) continue;
|
|
58
|
+
const lower = filename.toLowerCase();
|
|
59
|
+
const looksLikeError =
|
|
60
|
+
lower.includes("error") ||
|
|
61
|
+
/-errors?\.md$/i.test(filename) ||
|
|
62
|
+
/tools[_-]errors\.md$/i.test(filename);
|
|
63
|
+
if (!looksLikeError) continue;
|
|
64
|
+
|
|
65
|
+
const full = path.join(this.workspaceDir, filename);
|
|
66
|
+
const prev = this.debounceTimers.get(full);
|
|
67
|
+
if (prev) clearTimeout(prev);
|
|
68
|
+
this.debounceTimers.set(
|
|
69
|
+
full,
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
this.debounceTimers.delete(full);
|
|
72
|
+
this.processErrorFile(full).catch(() => {});
|
|
73
|
+
}, 500),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
} catch (err: unknown) {
|
|
77
|
+
if ((err as { name?: string })?.name !== "AbortError") {
|
|
78
|
+
console.error("[ErrorCollector] watch failed, falling back to poll only:", err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Periodic scanning (safety net for FS layers that drop watch events)
|
|
39
85
|
this.intervalId = setInterval(() => {
|
|
40
86
|
this.scanForErrors().catch((err) => {
|
|
41
87
|
console.error("[ErrorCollector] Scan failed:", err);
|
|
@@ -48,6 +94,10 @@ export class ErrorCollector {
|
|
|
48
94
|
clearInterval(this.intervalId);
|
|
49
95
|
this.intervalId = undefined;
|
|
50
96
|
}
|
|
97
|
+
this.watchAbort?.abort();
|
|
98
|
+
this.watchAbort = undefined;
|
|
99
|
+
for (const t of this.debounceTimers.values()) clearTimeout(t);
|
|
100
|
+
this.debounceTimers.clear();
|
|
51
101
|
}
|
|
52
102
|
|
|
53
103
|
private async scanForErrors() {
|
package/bot/index.ts
CHANGED
|
@@ -5,9 +5,56 @@
|
|
|
5
5
|
* Slack Bot
|
|
6
6
|
* @author snomiao <snomiao@gmail.com>
|
|
7
7
|
*/
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load .env.local with override semantics so that values in the file
|
|
12
|
+
* take precedence over stale shell env vars injected by pm2/parent shell.
|
|
13
|
+
* Prevents the SLACK_SIGNING_SECRET mismatch incident (2026-04-24).
|
|
14
|
+
*/
|
|
15
|
+
async function loadEnvLocalWithOverride() {
|
|
16
|
+
const envPath = join(import.meta.dir, "../.env.local");
|
|
17
|
+
try {
|
|
18
|
+
const text = await Bun.file(envPath).text();
|
|
19
|
+
let applied = 0;
|
|
20
|
+
for (const line of text.split("\n")) {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
23
|
+
const eq = trimmed.indexOf("=");
|
|
24
|
+
if (eq <= 0) continue;
|
|
25
|
+
const key = trimmed.slice(0, eq).trim();
|
|
26
|
+
const value = trimmed
|
|
27
|
+
.slice(eq + 1)
|
|
28
|
+
.trim()
|
|
29
|
+
.replace(/^["']|["']$/g, "");
|
|
30
|
+
process.env[key] = value;
|
|
31
|
+
applied++;
|
|
32
|
+
}
|
|
33
|
+
console.log(`[env] Loaded ${applied} entries from ${envPath} (override)`);
|
|
34
|
+
} catch {
|
|
35
|
+
console.log(`[env] ${envPath} not found, using shell env only`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Log short prefixes (≤6 chars) so an operator can confirm the right
|
|
39
|
+
// value loaded without exposing enough to attempt token reuse if the log
|
|
40
|
+
// ends up shared. PRBOT_PORT and NODE_ENV are not secrets so the full
|
|
41
|
+
// value is fine.
|
|
42
|
+
const prefix = (k: string) =>
|
|
43
|
+
`${k}=${process.env[k] ? process.env[k]!.slice(0, 6) + "…" : "(unset)"}`;
|
|
44
|
+
const literal = (k: string) => `${k}=${process.env[k] ?? "(unset)"}`;
|
|
45
|
+
console.log(
|
|
46
|
+
"[env] " +
|
|
47
|
+
[
|
|
48
|
+
prefix("SLACK_SIGNING_SECRET"),
|
|
49
|
+
prefix("SLACK_BOT_TOKEN"),
|
|
50
|
+
literal("PRBOT_PORT"),
|
|
51
|
+
literal("NODE_ENV"),
|
|
52
|
+
].join(" | "),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
8
55
|
|
|
9
|
-
// supports only slack now
|
|
10
56
|
if (import.meta.main) {
|
|
57
|
+
await loadEnvLocalWithOverride();
|
|
11
58
|
console.log("Starting ComfyPR Slack Bot...");
|
|
12
59
|
const client = await (await import("./slack-bot.ts")).startSlackBot();
|
|
13
60
|
console.log("ComfyPR Slack Bot Done.");
|