@xdevops/issue-auto-finish 1.0.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/bin/issue-auto-finish.js +2 -0
- package/dist/ai-runner/AIRunner.d.ts +27 -0
- package/dist/ai-runner/AIRunner.d.ts.map +1 -0
- package/dist/ai-runner/BaseAIRunner.d.ts +19 -0
- package/dist/ai-runner/BaseAIRunner.d.ts.map +1 -0
- package/dist/ai-runner/ClaudeInternalRunner.d.ts +13 -0
- package/dist/ai-runner/ClaudeInternalRunner.d.ts.map +1 -0
- package/dist/ai-runner/CodebuddyRunner.d.ts +13 -0
- package/dist/ai-runner/CodebuddyRunner.d.ts.map +1 -0
- package/dist/ai-runner/CursorAgentRunner.d.ts +13 -0
- package/dist/ai-runner/CursorAgentRunner.d.ts.map +1 -0
- package/dist/ai-runner/index.d.ts +15 -0
- package/dist/ai-runner/index.d.ts.map +1 -0
- package/dist/chunk-HCHEFK4Z.js +80 -0
- package/dist/chunk-HCHEFK4Z.js.map +1 -0
- package/dist/chunk-I3T573SU.js +153 -0
- package/dist/chunk-I3T573SU.js.map +1 -0
- package/dist/chunk-IDUKWCC2.js +1995 -0
- package/dist/chunk-IDUKWCC2.js.map +1 -0
- package/dist/chunk-OWVT3Z34.js +770 -0
- package/dist/chunk-OWVT3Z34.js.map +1 -0
- package/dist/chunk-RIUI4ROA.js +180 -0
- package/dist/chunk-RIUI4ROA.js.map +1 -0
- package/dist/chunk-TBIEB3JY.js +3295 -0
- package/dist/chunk-TBIEB3JY.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/setup/ConfigGenerator.d.ts +44 -0
- package/dist/cli/setup/ConfigGenerator.d.ts.map +1 -0
- package/dist/cli/setup/DependencyChecker.d.ts +21 -0
- package/dist/cli/setup/DependencyChecker.d.ts.map +1 -0
- package/dist/cli.js +73 -0
- package/dist/cli.js.map +1 -0
- package/dist/clients/GongfengClient.d.ts +86 -0
- package/dist/clients/GongfengClient.d.ts.map +1 -0
- package/dist/config.d.ts +92 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/deploy/DevServerManager.d.ts +21 -0
- package/dist/deploy/DevServerManager.d.ts.map +1 -0
- package/dist/deploy/PortAllocator.d.ts +20 -0
- package/dist/deploy/PortAllocator.d.ts.map +1 -0
- package/dist/deploy/index.d.ts +3 -0
- package/dist/deploy/index.d.ts.map +1 -0
- package/dist/doctor-B26Q6JWI.js +33 -0
- package/dist/doctor-B26Q6JWI.js.map +1 -0
- package/dist/e2e/E2eSettings.d.ts +6 -0
- package/dist/e2e/E2eSettings.d.ts.map +1 -0
- package/dist/e2e/ScreenshotCollector.d.ts +11 -0
- package/dist/e2e/ScreenshotCollector.d.ts.map +1 -0
- package/dist/e2e/ScreenshotPublisher.d.ts +16 -0
- package/dist/e2e/ScreenshotPublisher.d.ts.map +1 -0
- package/dist/events/EventBus.d.ts +14 -0
- package/dist/events/EventBus.d.ts.map +1 -0
- package/dist/git/GitOperations.d.ts +30 -0
- package/dist/git/GitOperations.d.ts.map +1 -0
- package/dist/git/WorktreeContext.d.ts +9 -0
- package/dist/git/WorktreeContext.d.ts.map +1 -0
- package/dist/i18n/index.d.ts +8 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/locales/en.d.ts +2 -0
- package/dist/i18n/locales/en.d.ts.map +1 -0
- package/dist/i18n/locales/zh-CN.d.ts +2 -0
- package/dist/i18n/locales/zh-CN.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/init-L3VIWCOV.js +65 -0
- package/dist/init-L3VIWCOV.js.map +1 -0
- package/dist/lib.d.ts +26 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +50 -0
- package/dist/lib.js.map +1 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/notesync/NoteSyncSettings.d.ts +12 -0
- package/dist/notesync/NoteSyncSettings.d.ts.map +1 -0
- package/dist/orchestrator/PipelineOrchestrator.d.ts +53 -0
- package/dist/orchestrator/PipelineOrchestrator.d.ts.map +1 -0
- package/dist/persistence/PlanPersistence.d.ts +37 -0
- package/dist/persistence/PlanPersistence.d.ts.map +1 -0
- package/dist/phases/AnalysisPhase.d.ts +13 -0
- package/dist/phases/AnalysisPhase.d.ts.map +1 -0
- package/dist/phases/BasePhase.d.ts +44 -0
- package/dist/phases/BasePhase.d.ts.map +1 -0
- package/dist/phases/BuildPhase.d.ts +9 -0
- package/dist/phases/BuildPhase.d.ts.map +1 -0
- package/dist/phases/DesignPhase.d.ts +13 -0
- package/dist/phases/DesignPhase.d.ts.map +1 -0
- package/dist/phases/ImplementPhase.d.ts +9 -0
- package/dist/phases/ImplementPhase.d.ts.map +1 -0
- package/dist/phases/PhaseFactory.d.ts +13 -0
- package/dist/phases/PhaseFactory.d.ts.map +1 -0
- package/dist/phases/PlanPhase.d.ts +14 -0
- package/dist/phases/PlanPhase.d.ts.map +1 -0
- package/dist/phases/VerifyPhase.d.ts +13 -0
- package/dist/phases/VerifyPhase.d.ts.map +1 -0
- package/dist/pipeline/PipelineDefinition.d.ts +40 -0
- package/dist/pipeline/PipelineDefinition.d.ts.map +1 -0
- package/dist/poller/IssuePoller.d.ts +26 -0
- package/dist/poller/IssuePoller.d.ts.map +1 -0
- package/dist/prompts/brainstorm-templates.d.ts +4 -0
- package/dist/prompts/brainstorm-templates.d.ts.map +1 -0
- package/dist/prompts/templates.d.ts +27 -0
- package/dist/prompts/templates.d.ts.map +1 -0
- package/dist/rules/RuleResolver.d.ts +13 -0
- package/dist/rules/RuleResolver.d.ts.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/run.d.ts +2 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +15 -0
- package/dist/run.js.map +1 -0
- package/dist/services/BrainstormService.d.ts +39 -0
- package/dist/services/BrainstormService.d.ts.map +1 -0
- package/dist/start-TVN4SS6E.js +25 -0
- package/dist/start-TVN4SS6E.js.map +1 -0
- package/dist/supplement/SupplementStore.d.ts +21 -0
- package/dist/supplement/SupplementStore.d.ts.map +1 -0
- package/dist/tracker/IssueState.d.ts +63 -0
- package/dist/tracker/IssueState.d.ts.map +1 -0
- package/dist/tracker/IssueTracker.d.ts +29 -0
- package/dist/tracker/IssueTracker.d.ts.map +1 -0
- package/dist/utils/AsyncMutex.d.ts +14 -0
- package/dist/utils/AsyncMutex.d.ts.map +1 -0
- package/dist/utils/MergeRequestHelper.d.ts +10 -0
- package/dist/utils/MergeRequestHelper.d.ts.map +1 -0
- package/dist/web/AgentLogStore.d.ts +22 -0
- package/dist/web/AgentLogStore.d.ts.map +1 -0
- package/dist/web/WebServer.d.ts +26 -0
- package/dist/web/WebServer.d.ts.map +1 -0
- package/dist/web/routes/api.d.ts +20 -0
- package/dist/web/routes/api.d.ts.map +1 -0
- package/dist/web/routes/brainstorm.d.ts +9 -0
- package/dist/web/routes/brainstorm.d.ts.map +1 -0
- package/dist/web/routes/setup.d.ts +8 -0
- package/dist/web/routes/setup.d.ts.map +1 -0
- package/dist/webhook/CommandExecutor.d.ts +45 -0
- package/dist/webhook/CommandExecutor.d.ts.map +1 -0
- package/dist/webhook/CommandParser.d.ts +24 -0
- package/dist/webhook/CommandParser.d.ts.map +1 -0
- package/dist/webhook/IntentRecognizer.d.ts +35 -0
- package/dist/webhook/IntentRecognizer.d.ts.map +1 -0
- package/dist/webhook/NoteDeduplicator.d.ts +18 -0
- package/dist/webhook/NoteDeduplicator.d.ts.map +1 -0
- package/dist/webhook/WebhookHandler.d.ts +30 -0
- package/dist/webhook/WebhookHandler.d.ts.map +1 -0
- package/dist/webhook/WebhookServer.d.ts +21 -0
- package/dist/webhook/WebhookServer.d.ts.map +1 -0
- package/dist/webhook/index.d.ts +12 -0
- package/dist/webhook/index.d.ts.map +1 -0
- package/package.json +82 -0
- package/src/web/frontend/dist/assets/index-CQdlU9PE.js +65 -0
- package/src/web/frontend/dist/assets/index-CgMEkyZJ.css +1 -0
- package/src/web/frontend/dist/index.html +13 -0
|
@@ -0,0 +1,1995 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BrainstormService,
|
|
3
|
+
CLASSIC_PIPELINE,
|
|
4
|
+
GitOperations,
|
|
5
|
+
GongfengClient,
|
|
6
|
+
IssueTracker,
|
|
7
|
+
PLAN_MODE_PIPELINE,
|
|
8
|
+
PipelineOrchestrator,
|
|
9
|
+
PlanPersistence,
|
|
10
|
+
collectStateLabels,
|
|
11
|
+
createAIRunner,
|
|
12
|
+
eventBus,
|
|
13
|
+
getE2eEnabled,
|
|
14
|
+
getNoteSyncEnabled,
|
|
15
|
+
getPipelineDef,
|
|
16
|
+
isNoteSyncEnabledForIssue,
|
|
17
|
+
loadConfig,
|
|
18
|
+
logger,
|
|
19
|
+
setE2eOverride,
|
|
20
|
+
setNoteSyncOverride,
|
|
21
|
+
validatePhaseRegistry
|
|
22
|
+
} from "./chunk-TBIEB3JY.js";
|
|
23
|
+
import {
|
|
24
|
+
createSetupRouter
|
|
25
|
+
} from "./chunk-I3T573SU.js";
|
|
26
|
+
import {
|
|
27
|
+
setLocale,
|
|
28
|
+
t
|
|
29
|
+
} from "./chunk-OWVT3Z34.js";
|
|
30
|
+
|
|
31
|
+
// src/supplement/SupplementStore.ts
|
|
32
|
+
import fs from "fs";
|
|
33
|
+
import path from "path";
|
|
34
|
+
var logger2 = logger.child("SupplementStore");
|
|
35
|
+
var SupplementStore = class {
|
|
36
|
+
dir;
|
|
37
|
+
constructor(dataDir) {
|
|
38
|
+
this.dir = path.join(dataDir, "supplements");
|
|
39
|
+
}
|
|
40
|
+
filePath(issueIid) {
|
|
41
|
+
return path.join(this.dir, `${issueIid}.json`);
|
|
42
|
+
}
|
|
43
|
+
ensureDir() {
|
|
44
|
+
if (!fs.existsSync(this.dir)) {
|
|
45
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
get(issueIid) {
|
|
49
|
+
const fp = this.filePath(issueIid);
|
|
50
|
+
if (!fs.existsSync(fp)) return null;
|
|
51
|
+
try {
|
|
52
|
+
const raw = fs.readFileSync(fp, "utf-8");
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
logger2.error("Failed to read supplement", { issueIid, error: err.message });
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
save(issueIid, data) {
|
|
60
|
+
this.ensureDir();
|
|
61
|
+
const info = {
|
|
62
|
+
...data,
|
|
63
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
64
|
+
};
|
|
65
|
+
fs.writeFileSync(this.filePath(issueIid), JSON.stringify(info, null, 2), "utf-8");
|
|
66
|
+
logger2.info("Supplement saved", { issueIid });
|
|
67
|
+
return info;
|
|
68
|
+
}
|
|
69
|
+
delete(issueIid) {
|
|
70
|
+
const fp = this.filePath(issueIid);
|
|
71
|
+
if (!fs.existsSync(fp)) return false;
|
|
72
|
+
try {
|
|
73
|
+
fs.unlinkSync(fp);
|
|
74
|
+
logger2.info("Supplement deleted", { issueIid });
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
toPromptText(issueIid) {
|
|
81
|
+
const info = this.get(issueIid);
|
|
82
|
+
if (!info) return "";
|
|
83
|
+
const sections = [];
|
|
84
|
+
if (info.requirements.trim()) {
|
|
85
|
+
sections.push(`### \u8865\u5145\u9700\u6C42\u8BF4\u660E
|
|
86
|
+
${info.requirements.trim()}`);
|
|
87
|
+
}
|
|
88
|
+
if (info.acceptanceCriteria.trim()) {
|
|
89
|
+
sections.push(`### \u9A8C\u6536\u6807\u51C6
|
|
90
|
+
${info.acceptanceCriteria.trim()}`);
|
|
91
|
+
}
|
|
92
|
+
if (info.scope.trim()) {
|
|
93
|
+
sections.push(`### \u53D8\u66F4\u8303\u56F4
|
|
94
|
+
${info.scope.trim()}`);
|
|
95
|
+
}
|
|
96
|
+
if (info.constraints.trim()) {
|
|
97
|
+
sections.push(`### \u7EA6\u675F\u6761\u4EF6
|
|
98
|
+
${info.constraints.trim()}`);
|
|
99
|
+
}
|
|
100
|
+
if (info.references.trim()) {
|
|
101
|
+
sections.push(`### \u53C2\u8003\u94FE\u63A5
|
|
102
|
+
${info.references.trim()}`);
|
|
103
|
+
}
|
|
104
|
+
if (info.freeText.trim()) {
|
|
105
|
+
sections.push(`### \u5176\u4ED6\u8865\u5145
|
|
106
|
+
${info.freeText.trim()}`);
|
|
107
|
+
}
|
|
108
|
+
if (sections.length === 0) return "";
|
|
109
|
+
return `## \u8865\u5145\u4FE1\u606F
|
|
110
|
+
|
|
111
|
+
${sections.join("\n\n")}`;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/poller/IssuePoller.ts
|
|
116
|
+
var logger3 = logger.child("IssuePoller");
|
|
117
|
+
var AUTO_FINISH_LABEL = "auto-finish";
|
|
118
|
+
var AUTO_APPROVE_CHECK_INTERVAL_MS = 3e4;
|
|
119
|
+
var IssuePoller = class {
|
|
120
|
+
config;
|
|
121
|
+
gongfeng;
|
|
122
|
+
tracker;
|
|
123
|
+
orchestrator;
|
|
124
|
+
discoveryTimer = null;
|
|
125
|
+
driveTimer = null;
|
|
126
|
+
activeIssues = /* @__PURE__ */ new Set();
|
|
127
|
+
lastAutoApproveCheckMs = 0;
|
|
128
|
+
constructor(config, gongfeng, tracker, orchestrator) {
|
|
129
|
+
this.config = config;
|
|
130
|
+
this.gongfeng = gongfeng;
|
|
131
|
+
this.tracker = tracker;
|
|
132
|
+
this.orchestrator = orchestrator;
|
|
133
|
+
}
|
|
134
|
+
start() {
|
|
135
|
+
const { discoveryIntervalMs, driveIntervalMs } = this.config.poll;
|
|
136
|
+
logger3.info("Issue poller starting", { discoveryIntervalMs, driveIntervalMs });
|
|
137
|
+
this.discover();
|
|
138
|
+
this.drive();
|
|
139
|
+
this.discoveryTimer = setInterval(() => this.discover(), discoveryIntervalMs);
|
|
140
|
+
this.driveTimer = setInterval(() => this.drive(), driveIntervalMs);
|
|
141
|
+
}
|
|
142
|
+
stop() {
|
|
143
|
+
if (this.discoveryTimer) {
|
|
144
|
+
clearInterval(this.discoveryTimer);
|
|
145
|
+
this.discoveryTimer = null;
|
|
146
|
+
}
|
|
147
|
+
if (this.driveTimer) {
|
|
148
|
+
clearInterval(this.driveTimer);
|
|
149
|
+
this.driveTimer = null;
|
|
150
|
+
}
|
|
151
|
+
logger3.info("Issue poller stopped");
|
|
152
|
+
}
|
|
153
|
+
getActiveIssueIids() {
|
|
154
|
+
return [...this.activeIssues];
|
|
155
|
+
}
|
|
156
|
+
async discover() {
|
|
157
|
+
try {
|
|
158
|
+
logger3.debug("Discovering new issues...");
|
|
159
|
+
const issues = await this.gongfeng.listIssues("opened", AUTO_FINISH_LABEL);
|
|
160
|
+
const newIssues = this.filterNewIssues(issues);
|
|
161
|
+
if (newIssues.length === 0) {
|
|
162
|
+
logger3.debug("No new issues found");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
logger3.info("Discovered new issues", { count: newIssues.length });
|
|
166
|
+
for (const issue of newIssues) {
|
|
167
|
+
this.tracker.create({
|
|
168
|
+
issueId: issue.id,
|
|
169
|
+
issueIid: issue.iid,
|
|
170
|
+
issueTitle: issue.title,
|
|
171
|
+
state: "pending" /* Pending */,
|
|
172
|
+
branchName: `${this.config.project.branchPrefix}-${issue.iid}`
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
logger3.error("Discovery cycle failed", { error: err.message });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
drive() {
|
|
180
|
+
this.maybeAutoApproveWaiting();
|
|
181
|
+
const maxConcurrent = this.config.poll.maxConcurrent;
|
|
182
|
+
const available = maxConcurrent - this.activeIssues.size;
|
|
183
|
+
if (available <= 0) {
|
|
184
|
+
logger3.debug("Skipping drive \u2014 at concurrency limit", {
|
|
185
|
+
active: this.activeIssues.size,
|
|
186
|
+
max: maxConcurrent
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const drivable = this.tracker.getDrivableIssues(this.config.poll.maxRetries).filter((r) => !this.activeIssues.has(r.issueIid));
|
|
191
|
+
if (drivable.length === 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const batch = drivable.slice(0, available);
|
|
195
|
+
logger3.info("Driving issues", {
|
|
196
|
+
batchSize: batch.length,
|
|
197
|
+
active: this.activeIssues.size,
|
|
198
|
+
max: maxConcurrent
|
|
199
|
+
});
|
|
200
|
+
for (const record of batch) {
|
|
201
|
+
this.activeIssues.add(record.issueIid);
|
|
202
|
+
this.processInBackground(record);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
maybeAutoApproveWaiting() {
|
|
206
|
+
const autoLabels = this.config.review.autoApproveLabels;
|
|
207
|
+
if (!autoLabels.length) return;
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
if (now - this.lastAutoApproveCheckMs < AUTO_APPROVE_CHECK_INTERVAL_MS) return;
|
|
210
|
+
this.lastAutoApproveCheckMs = now;
|
|
211
|
+
const waiting = this.tracker.getAll().filter((r) => r.state === "waiting_for_review" /* WaitingForReview */);
|
|
212
|
+
if (!waiting.length) return;
|
|
213
|
+
this.autoApproveByLabels(waiting, autoLabels).catch((err) => {
|
|
214
|
+
logger3.warn("Auto-approve check failed", { error: err.message });
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async autoApproveByLabels(records, autoLabels) {
|
|
218
|
+
for (const record of records) {
|
|
219
|
+
try {
|
|
220
|
+
const issue = await this.gongfeng.getIssueDetail(record.issueId);
|
|
221
|
+
const matched = issue.labels.filter((l) => autoLabels.includes(l));
|
|
222
|
+
if (matched.length === 0) continue;
|
|
223
|
+
logger3.info("Auto-approving waiting issue (label matched)", {
|
|
224
|
+
iid: record.issueIid,
|
|
225
|
+
matchedLabels: matched
|
|
226
|
+
});
|
|
227
|
+
this.tracker.updateState(record.issueIid, "review_approved" /* ReviewApproved */);
|
|
228
|
+
eventBus.emitTyped("review:approved", { issueIid: record.issueIid });
|
|
229
|
+
try {
|
|
230
|
+
await this.gongfeng.createIssueNote(
|
|
231
|
+
record.issueId,
|
|
232
|
+
t("poller.autoApproveComment", { labels: matched.join(", ") })
|
|
233
|
+
);
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
logger3.warn("Failed to check auto-approve labels", {
|
|
238
|
+
iid: record.issueIid,
|
|
239
|
+
error: err.message
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async processInBackground(record) {
|
|
245
|
+
try {
|
|
246
|
+
const issue = await this.resolveIssue(record.issueId);
|
|
247
|
+
if (!issue) {
|
|
248
|
+
logger3.warn("Could not resolve issue from API, skipping", { iid: record.issueIid });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
await this.orchestrator.processIssue(issue);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
logger3.error("Failed to process issue", {
|
|
254
|
+
iid: record.issueIid,
|
|
255
|
+
error: err.message
|
|
256
|
+
});
|
|
257
|
+
} finally {
|
|
258
|
+
this.activeIssues.delete(record.issueIid);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async resolveIssue(issueId) {
|
|
262
|
+
try {
|
|
263
|
+
return await this.gongfeng.getIssueDetail(issueId);
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
filterNewIssues(issues) {
|
|
269
|
+
return issues.filter((issue) => {
|
|
270
|
+
if (!issue.labels.includes(AUTO_FINISH_LABEL)) return false;
|
|
271
|
+
if (issue.labels.some((l) => l === "auto-finish:done")) return false;
|
|
272
|
+
const record = this.tracker.get(issue.iid);
|
|
273
|
+
if (record) return false;
|
|
274
|
+
return true;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// src/web/WebServer.ts
|
|
280
|
+
import express from "express";
|
|
281
|
+
import path3 from "path";
|
|
282
|
+
import { fileURLToPath } from "url";
|
|
283
|
+
|
|
284
|
+
// src/web/routes/api.ts
|
|
285
|
+
import { Router } from "express";
|
|
286
|
+
import fs2 from "fs";
|
|
287
|
+
import path2 from "path";
|
|
288
|
+
import { marked } from "marked";
|
|
289
|
+
var logger4 = logger.child("ApiRoutes");
|
|
290
|
+
var startTime = Date.now();
|
|
291
|
+
function buildPreviewInfo(iid, orch) {
|
|
292
|
+
const ports = orch.getPortAllocator().getPortsForIssue(iid);
|
|
293
|
+
if (!ports) return null;
|
|
294
|
+
const dsm = orch.getDevServerManager();
|
|
295
|
+
const status = dsm.getStatus(iid);
|
|
296
|
+
return {
|
|
297
|
+
...status,
|
|
298
|
+
ports,
|
|
299
|
+
previewUrl: orch.buildPreviewUrl(iid),
|
|
300
|
+
host: orch.getPreviewHost()
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function createApiRouter(trackerOrDeps, config, agentLogStore, orchestrator, mainGit) {
|
|
304
|
+
let tracker;
|
|
305
|
+
let cfg;
|
|
306
|
+
let logStore;
|
|
307
|
+
let orch;
|
|
308
|
+
let git;
|
|
309
|
+
let gongfeng;
|
|
310
|
+
let supplementStore;
|
|
311
|
+
if (config !== void 0) {
|
|
312
|
+
tracker = trackerOrDeps;
|
|
313
|
+
cfg = config;
|
|
314
|
+
logStore = agentLogStore;
|
|
315
|
+
orch = orchestrator;
|
|
316
|
+
git = mainGit;
|
|
317
|
+
} else {
|
|
318
|
+
const deps = trackerOrDeps;
|
|
319
|
+
tracker = deps.tracker;
|
|
320
|
+
cfg = deps.config;
|
|
321
|
+
logStore = deps.agentLogStore;
|
|
322
|
+
orch = deps.orchestrator;
|
|
323
|
+
git = deps.mainGit;
|
|
324
|
+
gongfeng = deps.gongfeng;
|
|
325
|
+
supplementStore = deps.supplementStore;
|
|
326
|
+
}
|
|
327
|
+
const router = Router();
|
|
328
|
+
router.get("/api/issues", (_req, res) => {
|
|
329
|
+
const issues = tracker.getAll();
|
|
330
|
+
res.json(issues);
|
|
331
|
+
});
|
|
332
|
+
router.get("/api/issues/:iid", async (req, res) => {
|
|
333
|
+
const iid = parseInt(req.params.iid, 10);
|
|
334
|
+
const record = tracker.get(iid);
|
|
335
|
+
if (!record) {
|
|
336
|
+
res.status(404).json({ error: "Issue not found" });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const progress = await readProgress(iid, cfg, tracker, git);
|
|
340
|
+
const preview = buildPreviewInfo(iid, orch);
|
|
341
|
+
res.json({ ...record, progress, preview });
|
|
342
|
+
});
|
|
343
|
+
function getIssuePipelineDef(iid) {
|
|
344
|
+
const record = tracker.get(iid);
|
|
345
|
+
const mode = record?.pipelineMode ?? orch.getPipelineDef().mode;
|
|
346
|
+
return getPipelineDef(mode === "plan-mode" ? "plan-mode" : "classic");
|
|
347
|
+
}
|
|
348
|
+
router.get("/api/issues/:iid/plans/:filename", async (req, res) => {
|
|
349
|
+
const iid = parseInt(req.params.iid, 10);
|
|
350
|
+
const filename = req.params.filename;
|
|
351
|
+
const def = getIssuePipelineDef(iid);
|
|
352
|
+
const allowed = [
|
|
353
|
+
...def.planFiles.map((f) => f.filename),
|
|
354
|
+
"progress.json",
|
|
355
|
+
"issue-meta.json"
|
|
356
|
+
];
|
|
357
|
+
if (!allowed.includes(filename)) {
|
|
358
|
+
res.status(400).json({ error: "Invalid filename" });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const content = await readPlanFile(iid, filename, cfg, tracker, git);
|
|
362
|
+
if (content === null) {
|
|
363
|
+
res.status(404).json({ error: "Plan file not found" });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (filename.endsWith(".json")) {
|
|
367
|
+
res.json(JSON.parse(content));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (req.query.format === "html") {
|
|
371
|
+
const html = await marked(content);
|
|
372
|
+
res.type("html").send(html);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
res.type("text/markdown").send(content);
|
|
376
|
+
});
|
|
377
|
+
router.post("/api/issues/:iid/retry", (req, res) => {
|
|
378
|
+
const iid = parseInt(req.params.iid, 10);
|
|
379
|
+
const ok = tracker.resetForRetry(iid);
|
|
380
|
+
if (!ok) {
|
|
381
|
+
res.status(400).json({ error: "Issue is not in failed state or not found" });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
res.json({ success: true, message: `Issue #${iid} reset for retry` });
|
|
385
|
+
});
|
|
386
|
+
router.post("/api/issues/:iid/cancel", (req, res) => {
|
|
387
|
+
const iid = parseInt(req.params.iid, 10);
|
|
388
|
+
const ok = tracker.delete(iid);
|
|
389
|
+
if (!ok) {
|
|
390
|
+
res.status(404).json({ error: "Issue not found in tracker" });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
res.json({ success: true, message: `Issue #${iid} removed from tracker` });
|
|
394
|
+
});
|
|
395
|
+
router.post("/api/issues/:iid/restart", async (req, res) => {
|
|
396
|
+
const iid = parseInt(req.params.iid, 10);
|
|
397
|
+
try {
|
|
398
|
+
await orch.restartIssue(iid);
|
|
399
|
+
res.json({ success: true, message: `Issue #${iid} restarted` });
|
|
400
|
+
} catch (err) {
|
|
401
|
+
const msg = err.message;
|
|
402
|
+
logger4.error("Restart failed", { iid, error: msg });
|
|
403
|
+
res.status(400).json({ error: msg });
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
router.post("/api/issues/:iid/retry-from-phase", (req, res) => {
|
|
407
|
+
const iid = parseInt(req.params.iid, 10);
|
|
408
|
+
const { phase } = req.body;
|
|
409
|
+
const def = getIssuePipelineDef(iid);
|
|
410
|
+
const validPhases = def.phases.filter((p) => p.kind === "ai").map((p) => p.name);
|
|
411
|
+
if (!phase || !validPhases.includes(phase)) {
|
|
412
|
+
res.status(400).json({ error: `Invalid phase. Must be one of: ${validPhases.join(", ")}` });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
orch.retryFromPhase(iid, phase);
|
|
417
|
+
res.json({ success: true, message: `Issue #${iid} reset to phase: ${phase}` });
|
|
418
|
+
} catch (err) {
|
|
419
|
+
const msg = err.message;
|
|
420
|
+
logger4.error("Retry-from-phase failed", { iid, phase, error: msg });
|
|
421
|
+
res.status(400).json({ error: msg });
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
router.put("/api/issues/:iid/plans/:filename", (req, res) => {
|
|
425
|
+
const iid = parseInt(req.params.iid, 10);
|
|
426
|
+
const filename = req.params.filename;
|
|
427
|
+
const def = getIssuePipelineDef(iid);
|
|
428
|
+
const editableFiles = def.planFiles.filter((f) => f.editable).map((f) => f.filename);
|
|
429
|
+
if (!editableFiles.includes(filename)) {
|
|
430
|
+
res.status(400).json({ error: `File not editable. Allowed: ${editableFiles.join(", ")}` });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const { content } = req.body;
|
|
434
|
+
if (typeof content !== "string") {
|
|
435
|
+
res.status(400).json({ error: 'Request body must contain a "content" string field' });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const planDir = getWorktreePlanDir(iid, cfg);
|
|
439
|
+
const filePath = path2.join(planDir, filename);
|
|
440
|
+
if (!fs2.existsSync(planDir)) {
|
|
441
|
+
res.status(404).json({ error: "Plan directory not found (worktree may have been cleaned)" });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
445
|
+
logger4.info("Plan file updated", { iid, filename });
|
|
446
|
+
res.json({ success: true, message: `Plan file ${filename} saved` });
|
|
447
|
+
});
|
|
448
|
+
router.get("/api/issues/:iid/logs", (req, res) => {
|
|
449
|
+
const iid = parseInt(req.params.iid, 10);
|
|
450
|
+
const record = tracker.get(iid);
|
|
451
|
+
if (!record) {
|
|
452
|
+
res.status(404).json({ error: "Issue not found" });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const logs = logStore.getLogs(iid);
|
|
456
|
+
res.json(logs);
|
|
457
|
+
});
|
|
458
|
+
router.get("/api/issues/:iid/supplement", (req, res) => {
|
|
459
|
+
if (!supplementStore) {
|
|
460
|
+
res.status(501).json({ error: "Supplement store not available" });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const iid = parseInt(req.params.iid, 10);
|
|
464
|
+
const info = supplementStore.get(iid);
|
|
465
|
+
res.json(info);
|
|
466
|
+
});
|
|
467
|
+
router.put("/api/issues/:iid/supplement", (req, res) => {
|
|
468
|
+
if (!supplementStore) {
|
|
469
|
+
res.status(501).json({ error: "Supplement store not available" });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const iid = parseInt(req.params.iid, 10);
|
|
473
|
+
const body = req.body;
|
|
474
|
+
const data = {
|
|
475
|
+
requirements: String(body.requirements || ""),
|
|
476
|
+
acceptanceCriteria: String(body.acceptanceCriteria || ""),
|
|
477
|
+
scope: String(body.scope || ""),
|
|
478
|
+
constraints: String(body.constraints || ""),
|
|
479
|
+
references: String(body.references || ""),
|
|
480
|
+
freeText: String(body.freeText || ""),
|
|
481
|
+
tapdId: String(body.tapdId || "")
|
|
482
|
+
};
|
|
483
|
+
const saved = supplementStore.save(iid, data);
|
|
484
|
+
res.json({ success: true, data: saved });
|
|
485
|
+
});
|
|
486
|
+
router.get("/api/gongfeng/issues", async (req, res) => {
|
|
487
|
+
if (!gongfeng) {
|
|
488
|
+
res.status(501).json({ error: "Gongfeng client not available" });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
const search = req.query.search || "";
|
|
493
|
+
const page = parseInt(req.query.page, 10) || 1;
|
|
494
|
+
const perPage = parseInt(req.query.per_page, 10) || 20;
|
|
495
|
+
const result = await gongfeng.listIssuesAdvanced({
|
|
496
|
+
state: "opened",
|
|
497
|
+
search: search || void 0,
|
|
498
|
+
page,
|
|
499
|
+
perPage
|
|
500
|
+
});
|
|
501
|
+
const trackedIids = new Set(tracker.getAll().map((r) => r.issueIid));
|
|
502
|
+
res.json({
|
|
503
|
+
issues: result.issues,
|
|
504
|
+
total: result.total,
|
|
505
|
+
trackedIids: Array.from(trackedIids)
|
|
506
|
+
});
|
|
507
|
+
} catch (err) {
|
|
508
|
+
const msg = err.message;
|
|
509
|
+
logger4.error("Failed to fetch gongfeng issues", { error: msg });
|
|
510
|
+
res.status(500).json({ error: msg });
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
router.post("/api/issues/start", async (req, res) => {
|
|
514
|
+
if (!gongfeng) {
|
|
515
|
+
res.status(501).json({ error: "Gongfeng client not available" });
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const body = req.body;
|
|
519
|
+
if (!body.issueId || !body.issueIid || !body.issueTitle) {
|
|
520
|
+
res.status(400).json({ error: "issueId, issueIid, and issueTitle are required" });
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const existing = tracker.get(body.issueIid);
|
|
524
|
+
if (existing) {
|
|
525
|
+
res.status(409).json({ error: `Issue #${body.issueIid} is already being tracked` });
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
await gongfeng.addLabel(body.issueId, "auto-finish");
|
|
530
|
+
} catch (err) {
|
|
531
|
+
logger4.warn("Failed to add auto-finish label", { error: err.message });
|
|
532
|
+
}
|
|
533
|
+
const branchName = `${cfg.project.branchPrefix}-${body.issueIid}`;
|
|
534
|
+
const record = tracker.create({
|
|
535
|
+
issueId: body.issueId,
|
|
536
|
+
issueIid: body.issueIid,
|
|
537
|
+
issueTitle: body.issueTitle,
|
|
538
|
+
state: "pending" /* Pending */,
|
|
539
|
+
branchName
|
|
540
|
+
});
|
|
541
|
+
if (supplementStore && body.supplement) {
|
|
542
|
+
supplementStore.save(body.issueIid, {
|
|
543
|
+
requirements: String(body.supplement.requirements || ""),
|
|
544
|
+
acceptanceCriteria: String(body.supplement.acceptanceCriteria || ""),
|
|
545
|
+
scope: String(body.supplement.scope || ""),
|
|
546
|
+
constraints: String(body.supplement.constraints || ""),
|
|
547
|
+
references: String(body.supplement.references || ""),
|
|
548
|
+
freeText: String(body.supplement.freeText || ""),
|
|
549
|
+
tapdId: String(body.supplement.tapdId || "")
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
res.json({ success: true, record });
|
|
553
|
+
});
|
|
554
|
+
router.post("/api/issues/:iid/approve-plan", (req, res) => {
|
|
555
|
+
const iid = parseInt(req.params.iid, 10);
|
|
556
|
+
const record = tracker.get(iid);
|
|
557
|
+
if (!record) {
|
|
558
|
+
res.status(404).json({ error: "Issue not found" });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (record.state !== "waiting_for_review" /* WaitingForReview */) {
|
|
562
|
+
res.status(400).json({ error: `Issue is not waiting for review (current state: ${record.state})` });
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
tracker.updateState(iid, "review_approved" /* ReviewApproved */);
|
|
566
|
+
const def = getIssuePipelineDef(iid);
|
|
567
|
+
const reviewSpec = def.phases.find((p) => p.kind === "gate");
|
|
568
|
+
if (reviewSpec) {
|
|
569
|
+
const workDir = getWorktreeWorkDir(iid, cfg);
|
|
570
|
+
const planPersistence = new PlanPersistence(workDir, iid);
|
|
571
|
+
planPersistence.updatePhaseProgress(reviewSpec.name, "completed");
|
|
572
|
+
}
|
|
573
|
+
eventBus.emitTyped("review:approved", { issueIid: iid });
|
|
574
|
+
logger4.info("Plan approved", { iid });
|
|
575
|
+
res.json({ success: true, message: `Issue #${iid} plan approved, will resume on next drive cycle` });
|
|
576
|
+
});
|
|
577
|
+
router.post("/api/issues/:iid/reject-plan", (req, res) => {
|
|
578
|
+
const iid = parseInt(req.params.iid, 10);
|
|
579
|
+
const record = tracker.get(iid);
|
|
580
|
+
if (!record) {
|
|
581
|
+
res.status(404).json({ error: "Issue not found" });
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (record.state !== "waiting_for_review" /* WaitingForReview */) {
|
|
585
|
+
res.status(400).json({ error: `Issue is not waiting for review (current state: ${record.state})` });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const { feedback } = req.body;
|
|
589
|
+
if (!feedback || typeof feedback !== "string") {
|
|
590
|
+
res.status(400).json({ error: "Feedback is required" });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const workDir = getWorktreeWorkDir(iid, cfg);
|
|
594
|
+
if (fs2.existsSync(workDir)) {
|
|
595
|
+
const planPersistence = new PlanPersistence(workDir, iid);
|
|
596
|
+
planPersistence.writeReviewFeedback(feedback);
|
|
597
|
+
}
|
|
598
|
+
tracker.updateState(iid, "branch_created" /* BranchCreated */);
|
|
599
|
+
eventBus.emitTyped("review:rejected", { issueIid: iid, feedback });
|
|
600
|
+
logger4.info("Plan rejected", { iid, feedback: feedback.slice(0, 100) });
|
|
601
|
+
if (gongfeng && isNoteSyncEnabledForIssue(iid, tracker, cfg)) {
|
|
602
|
+
const baseUrl = cfg.issueNoteSync.webBaseUrl.replace(/\/$/, "");
|
|
603
|
+
const planFile = record.pipelineMode === "plan-mode" ? "01-plan.md" : "02-design.md";
|
|
604
|
+
const history = fs2.existsSync(workDir) ? new PlanPersistence(workDir, iid).readReviewHistory() : [];
|
|
605
|
+
const round = history.length;
|
|
606
|
+
const note = [
|
|
607
|
+
t("api.reviewFeedback", { round }),
|
|
608
|
+
"",
|
|
609
|
+
feedback,
|
|
610
|
+
"",
|
|
611
|
+
"---",
|
|
612
|
+
t("api.viewPlan", { url: `${baseUrl}/doc/${iid}/${planFile}` }),
|
|
613
|
+
t("api.viewDetail", { url: `${baseUrl}/?issue=${iid}` })
|
|
614
|
+
].join("\n");
|
|
615
|
+
gongfeng.createIssueNote(record.issueId, note).catch((err) => {
|
|
616
|
+
logger4.warn("Failed to sync review feedback to issue", { error: err.message });
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
res.json({ success: true, message: `Issue #${iid} plan rejected, will re-plan on next drive cycle` });
|
|
620
|
+
});
|
|
621
|
+
router.post("/api/issues/:iid/skip-review", (req, res) => {
|
|
622
|
+
const iid = parseInt(req.params.iid, 10);
|
|
623
|
+
const record = tracker.get(iid);
|
|
624
|
+
if (!record) {
|
|
625
|
+
res.status(404).json({ error: "Issue not found" });
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (record.state !== "waiting_for_review" /* WaitingForReview */) {
|
|
629
|
+
res.status(400).json({ error: `Issue is not waiting for review (current state: ${record.state})` });
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
tracker.updateState(iid, "review_approved" /* ReviewApproved */);
|
|
633
|
+
const def = getIssuePipelineDef(iid);
|
|
634
|
+
const reviewSpec = def.phases.find((p) => p.kind === "gate");
|
|
635
|
+
if (reviewSpec) {
|
|
636
|
+
const workDir = getWorktreeWorkDir(iid, cfg);
|
|
637
|
+
const planPersistence = new PlanPersistence(workDir, iid);
|
|
638
|
+
planPersistence.updatePhaseProgress(reviewSpec.name, "completed");
|
|
639
|
+
}
|
|
640
|
+
eventBus.emitTyped("review:approved", { issueIid: iid });
|
|
641
|
+
logger4.info("Review skipped", { iid });
|
|
642
|
+
res.json({ success: true, message: `Issue #${iid} review skipped` });
|
|
643
|
+
});
|
|
644
|
+
router.get("/api/issues/:iid/review-history", (req, res) => {
|
|
645
|
+
const iid = parseInt(req.params.iid, 10);
|
|
646
|
+
const record = tracker.get(iid);
|
|
647
|
+
if (!record) {
|
|
648
|
+
res.status(404).json({ error: "Issue not found" });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const workDir = getWorktreeWorkDir(iid, cfg);
|
|
652
|
+
if (!fs2.existsSync(workDir)) {
|
|
653
|
+
res.json([]);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const planPersistence = new PlanPersistence(workDir, iid);
|
|
657
|
+
res.json(planPersistence.readReviewHistory());
|
|
658
|
+
});
|
|
659
|
+
router.put("/api/issues/:iid/note-sync", (req, res) => {
|
|
660
|
+
const iid = parseInt(req.params.iid, 10);
|
|
661
|
+
const record = tracker.get(iid);
|
|
662
|
+
if (!record) {
|
|
663
|
+
res.status(404).json({ error: "Issue not found" });
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const { enabled } = req.body;
|
|
667
|
+
const value = enabled === null ? void 0 : enabled;
|
|
668
|
+
tracker.updateState(iid, record.state, { issueNoteSyncEnabled: value });
|
|
669
|
+
logger4.info("Issue note-sync toggled", { iid, enabled: value });
|
|
670
|
+
res.json({ success: true, issueNoteSyncEnabled: value ?? null });
|
|
671
|
+
});
|
|
672
|
+
router.put("/api/system/note-sync", (req, res) => {
|
|
673
|
+
const { enabled } = req.body;
|
|
674
|
+
if (typeof enabled !== "boolean") {
|
|
675
|
+
res.status(400).json({ error: "enabled must be a boolean" });
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
setNoteSyncOverride(enabled);
|
|
679
|
+
logger4.info("System note-sync toggled", { enabled });
|
|
680
|
+
res.json({ success: true, issueNoteSyncEnabled: enabled });
|
|
681
|
+
});
|
|
682
|
+
router.get("/api/issues/:iid/preview", (req, res) => {
|
|
683
|
+
const iid = parseInt(req.params.iid, 10);
|
|
684
|
+
const record = tracker.get(iid);
|
|
685
|
+
if (!record) {
|
|
686
|
+
res.status(404).json({ error: "Issue not found" });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const preview = buildPreviewInfo(iid, orch);
|
|
690
|
+
res.json(preview ?? { running: false });
|
|
691
|
+
});
|
|
692
|
+
router.post("/api/issues/:iid/stop-preview", (req, res) => {
|
|
693
|
+
const iid = parseInt(req.params.iid, 10);
|
|
694
|
+
const record = tracker.get(iid);
|
|
695
|
+
if (!record) {
|
|
696
|
+
res.status(404).json({ error: "Issue not found" });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
orch.stopPreviewServers(iid);
|
|
700
|
+
res.json({ success: true, message: `Preview servers stopped for issue #${iid}` });
|
|
701
|
+
});
|
|
702
|
+
router.put("/api/issues/:iid/e2e", (req, res) => {
|
|
703
|
+
const iid = parseInt(req.params.iid, 10);
|
|
704
|
+
const record = tracker.get(iid);
|
|
705
|
+
if (!record) {
|
|
706
|
+
res.status(404).json({ error: "Issue not found" });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const { enabled } = req.body;
|
|
710
|
+
const value = enabled === null ? void 0 : enabled;
|
|
711
|
+
tracker.updateState(iid, record.state, { e2eEnabled: value });
|
|
712
|
+
logger4.info("Issue e2e toggled", { iid, enabled: value });
|
|
713
|
+
res.json({ success: true, e2eEnabled: value ?? null });
|
|
714
|
+
});
|
|
715
|
+
router.put("/api/system/e2e", (req, res) => {
|
|
716
|
+
const { enabled } = req.body;
|
|
717
|
+
if (typeof enabled !== "boolean") {
|
|
718
|
+
res.status(400).json({ error: "enabled must be a boolean" });
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
setE2eOverride(enabled);
|
|
722
|
+
logger4.info("System e2e toggled", { enabled });
|
|
723
|
+
res.json({ success: true, e2eEnabled: enabled });
|
|
724
|
+
});
|
|
725
|
+
router.get("/api/system/status", (_req, res) => {
|
|
726
|
+
const runningPreviews = orch.getDevServerManager().getRunningIssues();
|
|
727
|
+
res.json({
|
|
728
|
+
uptime: Date.now() - startTime,
|
|
729
|
+
startedAt: new Date(startTime).toISOString(),
|
|
730
|
+
config: {
|
|
731
|
+
discoveryIntervalMs: cfg.poll.discoveryIntervalMs,
|
|
732
|
+
driveIntervalMs: cfg.poll.driveIntervalMs,
|
|
733
|
+
maxRetries: cfg.poll.maxRetries,
|
|
734
|
+
aiMode: cfg.ai.mode,
|
|
735
|
+
pipelineMode: orch.getPipelineDef().mode,
|
|
736
|
+
baseBranch: cfg.project.baseBranch,
|
|
737
|
+
projectPath: cfg.gongfeng.projectPath,
|
|
738
|
+
gongfengBaseUrl: cfg.gongfeng.apiUrl.replace(/\/$/, ""),
|
|
739
|
+
issueNoteSyncEnabled: getNoteSyncEnabled(cfg),
|
|
740
|
+
e2eEnabled: getE2eEnabled(cfg),
|
|
741
|
+
previewEnabled: cfg.preview.enabled,
|
|
742
|
+
locale: cfg.locale
|
|
743
|
+
},
|
|
744
|
+
issues: {
|
|
745
|
+
total: tracker.getAll().length,
|
|
746
|
+
active: tracker.getAllActive().length
|
|
747
|
+
},
|
|
748
|
+
preview: {
|
|
749
|
+
runningCount: runningPreviews.length,
|
|
750
|
+
runningIssues: runningPreviews
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
router.get("/api/events", (req, res) => {
|
|
755
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
756
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
757
|
+
res.setHeader("Connection", "keep-alive");
|
|
758
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
759
|
+
res.flushHeaders();
|
|
760
|
+
const heartbeat = setInterval(() => {
|
|
761
|
+
res.write(`event: heartbeat
|
|
762
|
+
data: ${JSON.stringify({ time: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
763
|
+
|
|
764
|
+
`);
|
|
765
|
+
}, 15e3);
|
|
766
|
+
const handler = (_eventName, payload) => {
|
|
767
|
+
try {
|
|
768
|
+
res.write(`event: ${payload.type}
|
|
769
|
+
data: ${JSON.stringify(payload)}
|
|
770
|
+
|
|
771
|
+
`);
|
|
772
|
+
} catch {
|
|
773
|
+
logger4.warn("Failed to write SSE event");
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
eventBus.on("*", handler);
|
|
777
|
+
res.write(`event: connected
|
|
778
|
+
data: ${JSON.stringify({ time: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
779
|
+
|
|
780
|
+
`);
|
|
781
|
+
req.on("close", () => {
|
|
782
|
+
clearInterval(heartbeat);
|
|
783
|
+
eventBus.off("*", handler);
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
router.get("/doc/:iid/:filename", async (req, res) => {
|
|
787
|
+
const iid = parseInt(req.params.iid, 10);
|
|
788
|
+
const filename = req.params.filename;
|
|
789
|
+
const record = tracker.get(iid);
|
|
790
|
+
const title = record?.issueTitle ?? `Issue #${iid}`;
|
|
791
|
+
const def = getIssuePipelineDef(iid);
|
|
792
|
+
const allowed = def.planFiles.map((f) => f.filename);
|
|
793
|
+
if (!allowed.includes(filename)) {
|
|
794
|
+
res.status(400).type("html").send(renderDocPage(iid, title, t("api.invalidFilename"), filename));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const content = await readPlanFile(iid, filename, cfg, tracker, git);
|
|
798
|
+
if (content === null) {
|
|
799
|
+
res.status(404).type("html").send(renderDocPage(iid, title, t("api.docNotGenerated"), filename));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const htmlBody = await marked(content);
|
|
803
|
+
res.type("html").send(renderDocPage(iid, title, htmlBody, filename));
|
|
804
|
+
});
|
|
805
|
+
return router;
|
|
806
|
+
}
|
|
807
|
+
var DOC_LABELS_FUNC = (filename) => t(`docLabel.${filename}`) || filename;
|
|
808
|
+
function renderDocPage(iid, issueTitle, htmlBody, filename) {
|
|
809
|
+
const docLabel = DOC_LABELS_FUNC(filename);
|
|
810
|
+
return `<!DOCTYPE html>
|
|
811
|
+
<html lang="zh-CN">
|
|
812
|
+
<head>
|
|
813
|
+
<meta charset="utf-8">
|
|
814
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
815
|
+
<title>Issue #${iid} \u2014 ${docLabel}</title>
|
|
816
|
+
<style>
|
|
817
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; margin: 0; padding: 0; color: #24292f; background: #f6f8fa; }
|
|
818
|
+
.header { background: #fff; border-bottom: 1px solid #d0d7de; padding: 12px 24px; display: flex; align-items: center; gap: 12px; }
|
|
819
|
+
.header .crumb { font-size: 14px; color: #57606a; }
|
|
820
|
+
.header .crumb a { color: #0969da; text-decoration: none; }
|
|
821
|
+
.header .crumb a:hover { text-decoration: underline; }
|
|
822
|
+
.header .crumb .sep { margin: 0 4px; color: #8b949e; }
|
|
823
|
+
.container { max-width: 900px; margin: 24px auto; background: #fff; border: 1px solid #d0d7de; border-radius: 6px; padding: 32px 40px; }
|
|
824
|
+
.markdown-body h1 { font-size: 1.6em; border-bottom: 1px solid #d0d7de; padding-bottom: .3em; }
|
|
825
|
+
.markdown-body h2 { font-size: 1.3em; border-bottom: 1px solid #d0d7de; padding-bottom: .3em; }
|
|
826
|
+
.markdown-body h3 { font-size: 1.1em; }
|
|
827
|
+
.markdown-body pre { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; overflow-x: auto; }
|
|
828
|
+
.markdown-body code { background: #f6f8fa; border-radius: 3px; padding: 0.2em 0.4em; font-size: 85%; }
|
|
829
|
+
.markdown-body pre code { background: none; padding: 0; }
|
|
830
|
+
.markdown-body table { border-collapse: collapse; width: 100%; }
|
|
831
|
+
.markdown-body th, .markdown-body td { border: 1px solid #d0d7de; padding: 6px 13px; }
|
|
832
|
+
.markdown-body th { background: #f6f8fa; }
|
|
833
|
+
.markdown-body blockquote { margin: 0; padding: 0 1em; color: #57606a; border-left: 3px solid #d0d7de; }
|
|
834
|
+
.markdown-body ul, .markdown-body ol { padding-left: 2em; }
|
|
835
|
+
.markdown-body li { margin-top: 0.25em; }
|
|
836
|
+
.markdown-body p { margin: 8px 0; }
|
|
837
|
+
.markdown-body a { color: #0969da; }
|
|
838
|
+
</style>
|
|
839
|
+
</head>
|
|
840
|
+
<body>
|
|
841
|
+
<div class="header">
|
|
842
|
+
<div class="crumb">
|
|
843
|
+
<a href="/?issue=${iid}">Issue #${iid}</a>
|
|
844
|
+
<span class="sep">/</span>
|
|
845
|
+
<strong>${docLabel}</strong>
|
|
846
|
+
<span class="sep">\xB7</span>
|
|
847
|
+
<span style="font-size:12px;color:#57606a">${escapeHtml(issueTitle)}</span>
|
|
848
|
+
<span class="sep">\xB7</span>
|
|
849
|
+
<a href="/?issue=${iid}" style="font-size:12px">${t("api.viewInDashboard")}</a>
|
|
850
|
+
</div>
|
|
851
|
+
</div>
|
|
852
|
+
<div class="container">
|
|
853
|
+
<div class="markdown-body">${htmlBody}</div>
|
|
854
|
+
</div>
|
|
855
|
+
</body>
|
|
856
|
+
</html>`;
|
|
857
|
+
}
|
|
858
|
+
function escapeHtml(text) {
|
|
859
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
860
|
+
}
|
|
861
|
+
function getWorktreeWorkDir(issueIid, config) {
|
|
862
|
+
return path2.join(
|
|
863
|
+
config.project.worktreeBaseDir,
|
|
864
|
+
`issue-${issueIid}`,
|
|
865
|
+
config.project.projectSubDir
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
function getWorktreePlanDir(issueIid, config) {
|
|
869
|
+
return path2.join(getWorktreeWorkDir(issueIid, config), ".claude-plan", `issue-${issueIid}`);
|
|
870
|
+
}
|
|
871
|
+
function getPlanGitPath(issueIid, filename, config) {
|
|
872
|
+
return path2.posix.join(config.project.projectSubDir, ".claude-plan", `issue-${issueIid}`, filename);
|
|
873
|
+
}
|
|
874
|
+
async function readPlanFileFromGit(issueIid, filename, config, tracker, mainGit) {
|
|
875
|
+
if (!mainGit) return null;
|
|
876
|
+
const record = tracker.get(issueIid);
|
|
877
|
+
if (!record) return null;
|
|
878
|
+
const gitPath = getPlanGitPath(issueIid, filename, config);
|
|
879
|
+
return mainGit.showFile(record.branchName, gitPath);
|
|
880
|
+
}
|
|
881
|
+
var VERIFY_REPORT_FALLBACKS = {
|
|
882
|
+
"02-verify-report.md": "04-verify-report.md"
|
|
883
|
+
};
|
|
884
|
+
async function readPlanFile(issueIid, filename, config, tracker, mainGit) {
|
|
885
|
+
const planDir = getWorktreePlanDir(issueIid, config);
|
|
886
|
+
const filePath = path2.join(planDir, filename);
|
|
887
|
+
if (fs2.existsSync(filePath)) {
|
|
888
|
+
try {
|
|
889
|
+
return fs2.readFileSync(filePath, "utf-8");
|
|
890
|
+
} catch {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const content = await readPlanFileFromGit(issueIid, filename, config, tracker, mainGit);
|
|
895
|
+
if (content !== null) return content;
|
|
896
|
+
const fallback = VERIFY_REPORT_FALLBACKS[filename];
|
|
897
|
+
if (fallback) {
|
|
898
|
+
return readPlanFile(issueIid, fallback, config, tracker, mainGit);
|
|
899
|
+
}
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
async function readProgress(issueIid, config, tracker, mainGit) {
|
|
903
|
+
const content = await readPlanFile(issueIid, "progress.json", config, tracker, mainGit);
|
|
904
|
+
if (!content) return null;
|
|
905
|
+
try {
|
|
906
|
+
return JSON.parse(content);
|
|
907
|
+
} catch {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/web/routes/brainstorm.ts
|
|
913
|
+
import { Router as Router2 } from "express";
|
|
914
|
+
var logger5 = logger.child("BrainstormRoutes");
|
|
915
|
+
function sseWriter(res) {
|
|
916
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
917
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
918
|
+
res.setHeader("Connection", "keep-alive");
|
|
919
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
920
|
+
res.flushHeaders();
|
|
921
|
+
return (event) => {
|
|
922
|
+
try {
|
|
923
|
+
res.write(`event: ${event.type}
|
|
924
|
+
data: ${JSON.stringify(event)}
|
|
925
|
+
|
|
926
|
+
`);
|
|
927
|
+
} catch {
|
|
928
|
+
logger5.warn("Failed to write SSE brainstorm event");
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function createBrainstormRouter(deps) {
|
|
933
|
+
const router = Router2();
|
|
934
|
+
const { brainstormService, gongfeng } = deps;
|
|
935
|
+
router.post("/api/brainstorm/sessions", (req, res) => {
|
|
936
|
+
try {
|
|
937
|
+
const { transcript } = req.body;
|
|
938
|
+
if (!transcript?.trim()) {
|
|
939
|
+
res.status(400).json({ error: "transcript is required" });
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const session = brainstormService.createSession(transcript.trim());
|
|
943
|
+
res.json({ success: true, session });
|
|
944
|
+
} catch (err) {
|
|
945
|
+
logger5.error("Failed to create brainstorm session", { error: err.message });
|
|
946
|
+
res.status(500).json({ error: err.message });
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
router.get("/api/brainstorm/sessions/:id", (req, res) => {
|
|
950
|
+
const session = brainstormService.getSession(req.params.id);
|
|
951
|
+
if (!session) {
|
|
952
|
+
res.status(404).json({ error: "Session not found" });
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
res.json({ success: true, session });
|
|
956
|
+
});
|
|
957
|
+
router.post("/api/brainstorm/sessions/:id/generate", async (req, res) => {
|
|
958
|
+
const session = brainstormService.getSession(req.params.id);
|
|
959
|
+
if (!session) {
|
|
960
|
+
res.status(404).json({ error: "Session not found" });
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const write = sseWriter(res);
|
|
964
|
+
try {
|
|
965
|
+
await brainstormService.generate(req.params.id, write);
|
|
966
|
+
res.write(`event: done
|
|
967
|
+
data: ${JSON.stringify({ type: "done" })}
|
|
968
|
+
|
|
969
|
+
`);
|
|
970
|
+
} catch (err) {
|
|
971
|
+
write({ type: "error", data: { message: err.message } });
|
|
972
|
+
} finally {
|
|
973
|
+
res.end();
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
router.post("/api/brainstorm/sessions/:id/review", async (req, res) => {
|
|
977
|
+
const session = brainstormService.getSession(req.params.id);
|
|
978
|
+
if (!session) {
|
|
979
|
+
res.status(404).json({ error: "Session not found" });
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
const write = sseWriter(res);
|
|
983
|
+
try {
|
|
984
|
+
await brainstormService.review(req.params.id, write);
|
|
985
|
+
res.write(`event: done
|
|
986
|
+
data: ${JSON.stringify({ type: "done" })}
|
|
987
|
+
|
|
988
|
+
`);
|
|
989
|
+
} catch (err) {
|
|
990
|
+
write({ type: "error", data: { message: err.message } });
|
|
991
|
+
} finally {
|
|
992
|
+
res.end();
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
router.post("/api/brainstorm/sessions/:id/refine", async (req, res) => {
|
|
996
|
+
const session = brainstormService.getSession(req.params.id);
|
|
997
|
+
if (!session) {
|
|
998
|
+
res.status(404).json({ error: "Session not found" });
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const write = sseWriter(res);
|
|
1002
|
+
try {
|
|
1003
|
+
await brainstormService.refine(req.params.id, write);
|
|
1004
|
+
res.write(`event: done
|
|
1005
|
+
data: ${JSON.stringify({ type: "done" })}
|
|
1006
|
+
|
|
1007
|
+
`);
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
write({ type: "error", data: { message: err.message } });
|
|
1010
|
+
} finally {
|
|
1011
|
+
res.end();
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
router.post("/api/brainstorm/sessions/:id/auto-refine", async (req, res) => {
|
|
1015
|
+
const session = brainstormService.getSession(req.params.id);
|
|
1016
|
+
if (!session) {
|
|
1017
|
+
res.status(404).json({ error: "Session not found" });
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const { rounds } = req.body;
|
|
1021
|
+
const write = sseWriter(res);
|
|
1022
|
+
try {
|
|
1023
|
+
await brainstormService.autoRefine(req.params.id, rounds, write);
|
|
1024
|
+
res.write(`event: done
|
|
1025
|
+
data: ${JSON.stringify({ type: "done" })}
|
|
1026
|
+
|
|
1027
|
+
`);
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
write({ type: "error", data: { message: err.message } });
|
|
1030
|
+
} finally {
|
|
1031
|
+
res.end();
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
router.post("/api/brainstorm/sessions/:id/create-issue", async (req, res) => {
|
|
1035
|
+
const session = brainstormService.getSession(req.params.id);
|
|
1036
|
+
if (!session) {
|
|
1037
|
+
res.status(404).json({ error: "Session not found" });
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (!session.currentSdd) {
|
|
1041
|
+
res.status(400).json({ error: "No SDD generated yet" });
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
const { title, addAutoFinishLabel } = req.body;
|
|
1046
|
+
const issueTitle = title?.trim() || extractTitle(session.currentSdd);
|
|
1047
|
+
const labels = addAutoFinishLabel !== false ? ["auto-finish"] : [];
|
|
1048
|
+
const issue = await gongfeng.createIssue(issueTitle, session.currentSdd, labels);
|
|
1049
|
+
logger5.info("Issue created from brainstorm", { issueIid: issue.iid, sessionId: session.id });
|
|
1050
|
+
res.json({ success: true, issue });
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
logger5.error("Failed to create issue from brainstorm", { error: err.message });
|
|
1053
|
+
res.status(500).json({ error: err.message });
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
return router;
|
|
1057
|
+
}
|
|
1058
|
+
function extractTitle(sdd) {
|
|
1059
|
+
const firstLine = sdd.split("\n").find((l) => l.trim());
|
|
1060
|
+
if (firstLine) {
|
|
1061
|
+
return firstLine.replace(/^#+\s*/, "").trim().slice(0, 200);
|
|
1062
|
+
}
|
|
1063
|
+
return "\u8111\u66B4\u751F\u6210\u7684\u9700\u6C42";
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// src/web/WebServer.ts
|
|
1067
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
1068
|
+
var logger6 = logger.child("WebServer");
|
|
1069
|
+
var WebServer = class {
|
|
1070
|
+
app;
|
|
1071
|
+
server = null;
|
|
1072
|
+
port;
|
|
1073
|
+
constructor(trackerOrDeps, config, agentLogStore, orchestrator, mainGit) {
|
|
1074
|
+
let apiDeps;
|
|
1075
|
+
if (trackerOrDeps instanceof IssueTracker) {
|
|
1076
|
+
this.port = config.web.port;
|
|
1077
|
+
apiDeps = {
|
|
1078
|
+
tracker: trackerOrDeps,
|
|
1079
|
+
config,
|
|
1080
|
+
agentLogStore,
|
|
1081
|
+
orchestrator,
|
|
1082
|
+
mainGit,
|
|
1083
|
+
gongfeng: void 0,
|
|
1084
|
+
supplementStore: void 0
|
|
1085
|
+
};
|
|
1086
|
+
} else {
|
|
1087
|
+
const deps = trackerOrDeps;
|
|
1088
|
+
this.port = deps.config.web.port;
|
|
1089
|
+
apiDeps = {
|
|
1090
|
+
tracker: deps.tracker,
|
|
1091
|
+
config: deps.config,
|
|
1092
|
+
agentLogStore: deps.agentLogStore,
|
|
1093
|
+
orchestrator: deps.orchestrator,
|
|
1094
|
+
mainGit: deps.mainGit,
|
|
1095
|
+
gongfeng: deps.gongfeng,
|
|
1096
|
+
supplementStore: deps.supplementStore
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
this.app = express();
|
|
1100
|
+
this.app.use(express.json());
|
|
1101
|
+
const setupRouter = createSetupRouter({ serviceMode: true });
|
|
1102
|
+
this.app.use(setupRouter);
|
|
1103
|
+
const apiRouter = createApiRouter(apiDeps);
|
|
1104
|
+
this.app.use(apiRouter);
|
|
1105
|
+
if (apiDeps.config.brainstorm.enabled) {
|
|
1106
|
+
const brainstormService = new BrainstormService(apiDeps.config);
|
|
1107
|
+
const brainstormRouter = createBrainstormRouter({
|
|
1108
|
+
brainstormService,
|
|
1109
|
+
gongfeng: apiDeps.gongfeng
|
|
1110
|
+
});
|
|
1111
|
+
this.app.use(brainstormRouter);
|
|
1112
|
+
}
|
|
1113
|
+
const publicDir = apiDeps.config.web.frontendDistDir;
|
|
1114
|
+
this.app.use(express.static(publicDir));
|
|
1115
|
+
this.app.get("{*path}", (_req, res) => {
|
|
1116
|
+
res.sendFile(path3.join(publicDir, "index.html"));
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
start() {
|
|
1120
|
+
return new Promise((resolve) => {
|
|
1121
|
+
this.server = this.app.listen(this.port, () => {
|
|
1122
|
+
logger6.info(`Web UI available at http://localhost:${this.port}`);
|
|
1123
|
+
resolve();
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
stop() {
|
|
1128
|
+
if (this.server) {
|
|
1129
|
+
this.server.close();
|
|
1130
|
+
this.server = null;
|
|
1131
|
+
logger6.info("Web server stopped");
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
// src/webhook/WebhookServer.ts
|
|
1137
|
+
import express2 from "express";
|
|
1138
|
+
|
|
1139
|
+
// src/webhook/WebhookHandler.ts
|
|
1140
|
+
import { Router as Router3 } from "express";
|
|
1141
|
+
|
|
1142
|
+
// src/webhook/CommandParser.ts
|
|
1143
|
+
var TRIGGER = "@issue-auto";
|
|
1144
|
+
var PHASE_ALIAS = {
|
|
1145
|
+
"\u5206\u6790": "analysis",
|
|
1146
|
+
"\u9700\u6C42\u5206\u6790": "analysis",
|
|
1147
|
+
"\u8BBE\u8BA1": "design",
|
|
1148
|
+
"\u7CFB\u7EDF\u8BBE\u8BA1": "design",
|
|
1149
|
+
"\u5B9E\u73B0": "implement",
|
|
1150
|
+
"\u5B9E\u65BD": "implement",
|
|
1151
|
+
"\u7F16\u7801": "implement",
|
|
1152
|
+
"\u9A8C\u8BC1": "verify",
|
|
1153
|
+
"\u6D4B\u8BD5": "verify",
|
|
1154
|
+
"\u89C4\u5212": "plan",
|
|
1155
|
+
"\u6784\u5EFA": "build"
|
|
1156
|
+
};
|
|
1157
|
+
function resolvePhase(raw) {
|
|
1158
|
+
const lower = raw.toLowerCase();
|
|
1159
|
+
return PHASE_ALIAS[lower] ?? lower;
|
|
1160
|
+
}
|
|
1161
|
+
var EXACT_PATTERNS = [
|
|
1162
|
+
{
|
|
1163
|
+
regex: /retry-from\s+(\S+)/i,
|
|
1164
|
+
build: (m) => ({ intent: "retry-from", phase: resolvePhase(m[1]) })
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
regex: /(?:从|回到)\s*(\S+?)\s*(?:阶段)?(?:重[试来做新]|开始)/,
|
|
1168
|
+
build: (m) => ({ intent: "retry-from", phase: resolvePhase(m[1]) })
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
regex: /retry\b/i,
|
|
1172
|
+
build: () => ({ intent: "retry" })
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
regex: /重试|再试/,
|
|
1176
|
+
build: () => ({ intent: "retry" })
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
regex: /restart\b/i,
|
|
1180
|
+
build: () => ({ intent: "restart" })
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
regex: /(?:重新开始|从头(?:开始|来|做)|重做)/,
|
|
1184
|
+
build: () => ({ intent: "restart" })
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
regex: /approve\b/i,
|
|
1188
|
+
build: () => ({ intent: "approve" })
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
regex: /(?:批准|通过|同意|LGTM)/i,
|
|
1192
|
+
build: () => ({ intent: "approve" })
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
regex: /reject\s+([\s\S]+)/i,
|
|
1196
|
+
build: (m) => ({ intent: "reject", feedback: m[1].trim() })
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
regex: /(?:驳回|拒绝|打回)[::\s]*([\s\S]+)/,
|
|
1200
|
+
build: (m) => ({ intent: "reject", feedback: m[1].trim() })
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
regex: /supplement\s+([\s\S]+)/i,
|
|
1204
|
+
build: (m) => ({ intent: "supplement", context: m[1].trim() })
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
regex: /(?:补充)[::\s]*([\s\S]+)/,
|
|
1208
|
+
build: (m) => ({ intent: "supplement", context: m[1].trim() })
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
regex: /status\b/i,
|
|
1212
|
+
build: () => ({ intent: "status" })
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
regex: /(?:状态|什么状态|进度|查询)/,
|
|
1216
|
+
build: () => ({ intent: "status" })
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
regex: /stop-preview\b/i,
|
|
1220
|
+
build: () => ({ intent: "stop-preview" })
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
regex: /(?:停止预览|关闭预览|停止体验)/,
|
|
1224
|
+
build: () => ({ intent: "stop-preview" })
|
|
1225
|
+
},
|
|
1226
|
+
{
|
|
1227
|
+
regex: /preview\b/i,
|
|
1228
|
+
build: () => ({ intent: "preview" })
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
regex: /(?:预览|体验|preview环境)/,
|
|
1232
|
+
build: () => ({ intent: "preview" })
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
regex: /clean-notes\b/i,
|
|
1236
|
+
build: () => ({ intent: "clean-notes" })
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
regex: /(?:清理评论|清除评论|删除评论)/,
|
|
1240
|
+
build: () => ({ intent: "clean-notes" })
|
|
1241
|
+
}
|
|
1242
|
+
];
|
|
1243
|
+
function containsTrigger(text) {
|
|
1244
|
+
return text.includes(TRIGGER);
|
|
1245
|
+
}
|
|
1246
|
+
function extractCommandText(text) {
|
|
1247
|
+
const idx = text.indexOf(TRIGGER);
|
|
1248
|
+
if (idx < 0) return null;
|
|
1249
|
+
return text.slice(idx + TRIGGER.length).trim();
|
|
1250
|
+
}
|
|
1251
|
+
function parseExact(commandText) {
|
|
1252
|
+
for (const { regex, build } of EXACT_PATTERNS) {
|
|
1253
|
+
const match = commandText.match(regex);
|
|
1254
|
+
if (match) return build(match);
|
|
1255
|
+
}
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// src/webhook/CommandExecutor.ts
|
|
1260
|
+
import path4 from "path";
|
|
1261
|
+
import fs3 from "fs";
|
|
1262
|
+
var logger7 = logger.child("CommandExecutor");
|
|
1263
|
+
var CommandExecutor = class {
|
|
1264
|
+
tracker;
|
|
1265
|
+
orchestrator;
|
|
1266
|
+
supplementStore;
|
|
1267
|
+
gongfeng;
|
|
1268
|
+
config;
|
|
1269
|
+
constructor(deps) {
|
|
1270
|
+
this.tracker = deps.tracker;
|
|
1271
|
+
this.orchestrator = deps.orchestrator;
|
|
1272
|
+
this.supplementStore = deps.supplementStore;
|
|
1273
|
+
this.gongfeng = deps.gongfeng;
|
|
1274
|
+
this.config = deps.config;
|
|
1275
|
+
}
|
|
1276
|
+
async execute(issueIid, issueId, command) {
|
|
1277
|
+
logger7.info("Executing webhook command", { issueIid, command });
|
|
1278
|
+
let result;
|
|
1279
|
+
try {
|
|
1280
|
+
result = await this.dispatch(issueIid, command);
|
|
1281
|
+
} catch (err) {
|
|
1282
|
+
const msg = err.message;
|
|
1283
|
+
logger7.error("Webhook command failed", { issueIid, error: msg });
|
|
1284
|
+
result = { success: false, message: msg };
|
|
1285
|
+
}
|
|
1286
|
+
await this.replyToIssue(issueId, result);
|
|
1287
|
+
return result;
|
|
1288
|
+
}
|
|
1289
|
+
async dispatch(iid, cmd) {
|
|
1290
|
+
switch (cmd.intent) {
|
|
1291
|
+
case "retry":
|
|
1292
|
+
return this.handleRetry(iid);
|
|
1293
|
+
case "retry-from":
|
|
1294
|
+
return this.handleRetryFrom(iid, cmd.phase, cmd.context);
|
|
1295
|
+
case "supplement":
|
|
1296
|
+
return this.handleSupplement(iid, cmd.context);
|
|
1297
|
+
case "approve":
|
|
1298
|
+
return this.handleApprove(iid);
|
|
1299
|
+
case "reject":
|
|
1300
|
+
return this.handleReject(iid, cmd.feedback);
|
|
1301
|
+
case "status":
|
|
1302
|
+
return this.handleStatus(iid);
|
|
1303
|
+
case "restart":
|
|
1304
|
+
return this.handleRestart(iid);
|
|
1305
|
+
case "preview":
|
|
1306
|
+
return this.handlePreview(iid);
|
|
1307
|
+
case "stop-preview":
|
|
1308
|
+
return this.handleStopPreview(iid);
|
|
1309
|
+
case "clean-notes":
|
|
1310
|
+
return this.handleCleanNotes(iid);
|
|
1311
|
+
default:
|
|
1312
|
+
return { success: false, message: "Unknown command" };
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
handleRetry(iid) {
|
|
1316
|
+
const record = this.tracker.get(iid);
|
|
1317
|
+
if (!record) return this.notTracked(iid);
|
|
1318
|
+
if (record.state !== "failed" /* Failed */) {
|
|
1319
|
+
return this.fail(`Issue not in failed state (current: ${record.state})`);
|
|
1320
|
+
}
|
|
1321
|
+
const ok = this.tracker.resetForRetry(iid);
|
|
1322
|
+
if (!ok) return this.fail("Reset for retry failed");
|
|
1323
|
+
return this.ok(`Issue #${iid} has been reset and will retry on the next drive cycle.`);
|
|
1324
|
+
}
|
|
1325
|
+
handleRetryFrom(iid, phase, ctx) {
|
|
1326
|
+
if (!phase) return this.fail("Phase is required for retry-from (e.g. design, implement)");
|
|
1327
|
+
if (ctx) this.saveSupplement(iid, ctx);
|
|
1328
|
+
try {
|
|
1329
|
+
this.orchestrator.retryFromPhase(iid, phase);
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
return this.fail(err.message);
|
|
1332
|
+
}
|
|
1333
|
+
const extra = ctx ? "\nSupplement info recorded." : "";
|
|
1334
|
+
return this.ok(`Issue #${iid} will restart from **${phase}** phase.${extra}`);
|
|
1335
|
+
}
|
|
1336
|
+
handleSupplement(iid, ctx) {
|
|
1337
|
+
if (!ctx?.trim()) return this.fail("Supplement content cannot be empty");
|
|
1338
|
+
this.saveSupplement(iid, ctx);
|
|
1339
|
+
return this.ok("Supplement info recorded. AI will reference it on next execution.");
|
|
1340
|
+
}
|
|
1341
|
+
handleApprove(iid) {
|
|
1342
|
+
const record = this.tracker.get(iid);
|
|
1343
|
+
if (!record) return this.notTracked(iid);
|
|
1344
|
+
if (record.state !== "waiting_for_review" /* WaitingForReview */) {
|
|
1345
|
+
return this.fail(`Issue not waiting for review (current: ${record.state})`);
|
|
1346
|
+
}
|
|
1347
|
+
this.tracker.updateState(iid, "review_approved" /* ReviewApproved */);
|
|
1348
|
+
const def = this.getIssuePipelineDef(record);
|
|
1349
|
+
const gate = def.phases.find((p) => p.kind === "gate");
|
|
1350
|
+
if (gate) {
|
|
1351
|
+
const workDir = this.getWorktreeWorkDir(iid);
|
|
1352
|
+
if (fs3.existsSync(workDir)) {
|
|
1353
|
+
new PlanPersistence(workDir, iid).updatePhaseProgress(gate.name, "completed");
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
eventBus.emitTyped("review:approved", { issueIid: iid });
|
|
1357
|
+
return this.ok("Plan approved. Continuing to next phases.");
|
|
1358
|
+
}
|
|
1359
|
+
handleReject(iid, feedback) {
|
|
1360
|
+
const record = this.tracker.get(iid);
|
|
1361
|
+
if (!record) return this.notTracked(iid);
|
|
1362
|
+
if (record.state !== "waiting_for_review" /* WaitingForReview */) {
|
|
1363
|
+
return this.fail(`Issue not waiting for review (current: ${record.state})`);
|
|
1364
|
+
}
|
|
1365
|
+
if (!feedback?.trim()) return this.fail("Feedback is required when rejecting");
|
|
1366
|
+
const workDir = this.getWorktreeWorkDir(iid);
|
|
1367
|
+
if (fs3.existsSync(workDir)) {
|
|
1368
|
+
new PlanPersistence(workDir, iid).writeReviewFeedback(feedback);
|
|
1369
|
+
}
|
|
1370
|
+
this.tracker.updateState(iid, "branch_created" /* BranchCreated */);
|
|
1371
|
+
eventBus.emitTyped("review:rejected", { issueIid: iid, feedback });
|
|
1372
|
+
return this.ok("Plan rejected. Will re-plan based on feedback.");
|
|
1373
|
+
}
|
|
1374
|
+
handleStatus(iid) {
|
|
1375
|
+
const record = this.tracker.get(iid);
|
|
1376
|
+
if (!record) return this.notTracked(iid);
|
|
1377
|
+
const def = this.getIssuePipelineDef(record);
|
|
1378
|
+
const labels = collectStateLabels(def);
|
|
1379
|
+
const stateLabel = labels.get(record.state) ?? record.state;
|
|
1380
|
+
const lines = [
|
|
1381
|
+
`**Issue #${iid} Status**`,
|
|
1382
|
+
"",
|
|
1383
|
+
`- State: **${stateLabel}**`,
|
|
1384
|
+
`- Branch: \`${record.branchName}\``,
|
|
1385
|
+
`- Attempts: ${record.attempts}`,
|
|
1386
|
+
`- Pipeline: ${record.pipelineMode ?? "N/A"}`
|
|
1387
|
+
];
|
|
1388
|
+
if (record.lastError) lines.push(`- Last error: ${record.lastError.slice(0, 200)}`);
|
|
1389
|
+
if (record.mrUrl) lines.push(`- MR: ${record.mrUrl}`);
|
|
1390
|
+
const previewUrl = this.orchestrator.buildPreviewUrl(iid);
|
|
1391
|
+
if (previewUrl) {
|
|
1392
|
+
const dsm = this.orchestrator.getDevServerManager();
|
|
1393
|
+
const status = dsm.getStatus(iid);
|
|
1394
|
+
lines.push(`- Preview: ${status.running ? `\u{1F310} ${previewUrl}` : t("cmd.previewStopped")}`);
|
|
1395
|
+
}
|
|
1396
|
+
const baseUrl = this.config.issueNoteSync.webBaseUrl.replace(/\/$/, "");
|
|
1397
|
+
lines.push("", `[View in dashboard](${baseUrl}/?issue=${iid})`);
|
|
1398
|
+
return this.ok(lines.join("\n"));
|
|
1399
|
+
}
|
|
1400
|
+
async handleRestart(iid) {
|
|
1401
|
+
try {
|
|
1402
|
+
await this.orchestrator.restartIssue(iid);
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
return this.fail(err.message);
|
|
1405
|
+
}
|
|
1406
|
+
return this.ok(`Issue #${iid} has been fully reset and will restart from scratch.`);
|
|
1407
|
+
}
|
|
1408
|
+
handlePreview(iid) {
|
|
1409
|
+
const record = this.tracker.get(iid);
|
|
1410
|
+
if (!record) return this.notTracked(iid);
|
|
1411
|
+
const previewUrl = this.orchestrator.buildPreviewUrl(iid);
|
|
1412
|
+
if (!previewUrl) {
|
|
1413
|
+
return this.ok(
|
|
1414
|
+
t("cmd.previewNotStarted", { iid })
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
const dsm = this.orchestrator.getDevServerManager();
|
|
1418
|
+
const status = dsm.getStatus(iid);
|
|
1419
|
+
const ports = this.orchestrator.getPortAllocator().getPortsForIssue(iid);
|
|
1420
|
+
const host = this.orchestrator.getPreviewHost();
|
|
1421
|
+
const lines = [
|
|
1422
|
+
"\u{1F310} **Preview Environment**",
|
|
1423
|
+
"",
|
|
1424
|
+
`| ${t("cmd.previewTable.component")} | ${t("cmd.previewTable.address")} |`,
|
|
1425
|
+
`|------|------|`,
|
|
1426
|
+
`| ${t("cmd.previewTable.frontend")} | ${previewUrl} |`,
|
|
1427
|
+
`| ${t("cmd.previewTable.backendApi")} | http://${host}:${ports.backendPort}/api |`,
|
|
1428
|
+
"",
|
|
1429
|
+
`${t("cmd.previewTable.status")}: ${status.running ? t("cmd.previewRunning") : t("cmd.previewStopped")}`
|
|
1430
|
+
];
|
|
1431
|
+
if (status.startedAt) lines.push(`${t("cmd.previewTable.startedAt")}: ${status.startedAt}`);
|
|
1432
|
+
return this.ok(lines.join("\n"));
|
|
1433
|
+
}
|
|
1434
|
+
handleStopPreview(iid) {
|
|
1435
|
+
const record = this.tracker.get(iid);
|
|
1436
|
+
if (!record) return this.notTracked(iid);
|
|
1437
|
+
const dsm = this.orchestrator.getDevServerManager();
|
|
1438
|
+
const status = dsm.getStatus(iid);
|
|
1439
|
+
if (!status.running) {
|
|
1440
|
+
return this.ok(t("cmd.noPreview", { iid }));
|
|
1441
|
+
}
|
|
1442
|
+
this.orchestrator.stopPreviewServers(iid);
|
|
1443
|
+
return this.ok(t("cmd.previewStoppedMsg", { iid }));
|
|
1444
|
+
}
|
|
1445
|
+
async handleCleanNotes(iid) {
|
|
1446
|
+
const record = this.tracker.get(iid);
|
|
1447
|
+
if (!record) return this.notTracked(iid);
|
|
1448
|
+
try {
|
|
1449
|
+
const deleted = await this.gongfeng.cleanupAgentNotes(record.issueId);
|
|
1450
|
+
return this.ok(t("cmd.cleanNotesSuccess", { count: deleted }));
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
return this.fail(err.message);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
saveSupplement(iid, context) {
|
|
1456
|
+
const existing = this.supplementStore.get(iid);
|
|
1457
|
+
const prev = existing?.freeText?.trim() ?? "";
|
|
1458
|
+
const merged = prev ? `${prev}
|
|
1459
|
+
|
|
1460
|
+
---
|
|
1461
|
+
${context}` : context;
|
|
1462
|
+
this.supplementStore.save(iid, {
|
|
1463
|
+
requirements: existing?.requirements ?? "",
|
|
1464
|
+
acceptanceCriteria: existing?.acceptanceCriteria ?? "",
|
|
1465
|
+
scope: existing?.scope ?? "",
|
|
1466
|
+
constraints: existing?.constraints ?? "",
|
|
1467
|
+
references: existing?.references ?? "",
|
|
1468
|
+
freeText: merged,
|
|
1469
|
+
tapdId: existing?.tapdId ?? ""
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
async replyToIssue(issueId, result) {
|
|
1473
|
+
try {
|
|
1474
|
+
const prefix = result.success ? "" : "> **Failed**\n>\n> ";
|
|
1475
|
+
await this.gongfeng.createIssueNote(issueId, `${prefix}${result.message}`);
|
|
1476
|
+
} catch (err) {
|
|
1477
|
+
logger7.warn("Failed to reply", { issueId, error: err.message });
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
ok(message) {
|
|
1481
|
+
return { success: true, message };
|
|
1482
|
+
}
|
|
1483
|
+
fail(message) {
|
|
1484
|
+
return { success: false, message };
|
|
1485
|
+
}
|
|
1486
|
+
notTracked(iid) {
|
|
1487
|
+
return this.fail(`Issue #${iid} is not tracked. Add the auto-finish label to start.`);
|
|
1488
|
+
}
|
|
1489
|
+
getIssuePipelineDef(record) {
|
|
1490
|
+
const mode = record.pipelineMode === "plan-mode" ? "plan-mode" : "classic";
|
|
1491
|
+
return getPipelineDef(mode);
|
|
1492
|
+
}
|
|
1493
|
+
getWorktreeWorkDir(iid) {
|
|
1494
|
+
return path4.join(
|
|
1495
|
+
this.config.project.worktreeBaseDir,
|
|
1496
|
+
`issue-${iid}`,
|
|
1497
|
+
this.config.project.projectSubDir
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
// src/webhook/NoteDeduplicator.ts
|
|
1503
|
+
var NoteDeduplicator = class {
|
|
1504
|
+
seen;
|
|
1505
|
+
maxSize;
|
|
1506
|
+
constructor(maxSize = 1e3) {
|
|
1507
|
+
this.seen = /* @__PURE__ */ new Map();
|
|
1508
|
+
this.maxSize = maxSize;
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Returns true if this noteId has already been processed.
|
|
1512
|
+
* If not, marks it as processed and returns false.
|
|
1513
|
+
*/
|
|
1514
|
+
isDuplicate(noteId) {
|
|
1515
|
+
if (this.seen.has(noteId)) return true;
|
|
1516
|
+
this.seen.set(noteId, Date.now());
|
|
1517
|
+
this.evictIfNeeded();
|
|
1518
|
+
return false;
|
|
1519
|
+
}
|
|
1520
|
+
get size() {
|
|
1521
|
+
return this.seen.size;
|
|
1522
|
+
}
|
|
1523
|
+
evictIfNeeded() {
|
|
1524
|
+
if (this.seen.size <= this.maxSize) return;
|
|
1525
|
+
const oldest = this.seen.keys().next().value;
|
|
1526
|
+
if (oldest !== void 0) {
|
|
1527
|
+
this.seen.delete(oldest);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
// src/webhook/WebhookHandler.ts
|
|
1533
|
+
var logger8 = logger.child("WebhookHandler");
|
|
1534
|
+
function createWebhookRouter(deps) {
|
|
1535
|
+
const router = Router3();
|
|
1536
|
+
const executor = new CommandExecutor(deps);
|
|
1537
|
+
const dedup = new NoteDeduplicator();
|
|
1538
|
+
const { config, intentRecognizer } = deps;
|
|
1539
|
+
const selfToken = config.gongfeng.privateToken;
|
|
1540
|
+
router.post("/webhook/gongfeng", async (req, res) => {
|
|
1541
|
+
if (!config.webhook.enabled) {
|
|
1542
|
+
res.status(403).json({ error: "Webhook is disabled" });
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
if (config.webhook.secret) {
|
|
1546
|
+
const token = req.headers["x-gitlab-token"];
|
|
1547
|
+
if (token !== config.webhook.secret) {
|
|
1548
|
+
logger8.warn("Webhook token mismatch");
|
|
1549
|
+
res.status(401).json({ error: "Invalid token" });
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const event = req.body;
|
|
1554
|
+
if (!isNoteOnIssue(event)) {
|
|
1555
|
+
res.json({ ignored: true, reason: "not a note on issue" });
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
const noteBody = event.object_attributes.note;
|
|
1559
|
+
if (!containsTrigger(noteBody)) {
|
|
1560
|
+
res.json({ ignored: true, reason: "no @issue-auto trigger" });
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (isSelfNote(event, selfToken)) {
|
|
1564
|
+
res.json({ ignored: true, reason: "self-posted note" });
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const noteId = event.object_attributes.id;
|
|
1568
|
+
if (dedup.isDuplicate(noteId)) {
|
|
1569
|
+
logger8.debug("Duplicate note, skipping", { noteId });
|
|
1570
|
+
res.json({ ignored: true, reason: "duplicate" });
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const issueIid = event.issue?.iid;
|
|
1574
|
+
const issueId = event.issue?.id;
|
|
1575
|
+
if (!issueIid) {
|
|
1576
|
+
res.status(400).json({ error: "Missing issue iid in webhook payload" });
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (!issueId) {
|
|
1580
|
+
logger8.info("Webhook received with null issue id (likely a test ping)", { issueIid });
|
|
1581
|
+
res.json({ accepted: true, noteId, issueIid, test: true });
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
res.json({ accepted: true, noteId, issueIid });
|
|
1585
|
+
processCommandAsync(
|
|
1586
|
+
noteBody,
|
|
1587
|
+
issueIid,
|
|
1588
|
+
issueId,
|
|
1589
|
+
executor,
|
|
1590
|
+
intentRecognizer,
|
|
1591
|
+
config,
|
|
1592
|
+
deps.tracker,
|
|
1593
|
+
deps.gongfeng
|
|
1594
|
+
);
|
|
1595
|
+
});
|
|
1596
|
+
return router;
|
|
1597
|
+
}
|
|
1598
|
+
var HELP_TEXT_FUNC = () => t("webhook.helpText");
|
|
1599
|
+
async function processCommandAsync(noteBody, issueIid, issueId, executor, intentRecognizer, config, tracker, gongfeng) {
|
|
1600
|
+
try {
|
|
1601
|
+
const cmdText = extractCommandText(noteBody);
|
|
1602
|
+
if (!cmdText) return;
|
|
1603
|
+
let command = parseExact(cmdText);
|
|
1604
|
+
if (!command && intentRecognizer && config.webhook.llmFallback) {
|
|
1605
|
+
const record = tracker.get(issueIid);
|
|
1606
|
+
const state = record?.state;
|
|
1607
|
+
logger8.info("Falling back to LLM intent recognition", { issueIid });
|
|
1608
|
+
command = await intentRecognizer.recognize(cmdText, state);
|
|
1609
|
+
}
|
|
1610
|
+
if (!command) {
|
|
1611
|
+
logger8.info("Could not parse command from note", { issueIid, text: cmdText.slice(0, 100) });
|
|
1612
|
+
await gongfeng.createIssueNote(issueId, HELP_TEXT_FUNC()).catch(
|
|
1613
|
+
(e) => logger8.warn("Failed to reply help text", { error: e.message })
|
|
1614
|
+
);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
await executor.execute(issueIid, issueId, command);
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
logger8.error("Failed to process webhook command", {
|
|
1620
|
+
issueIid,
|
|
1621
|
+
error: err.message
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
function isNoteOnIssue(event) {
|
|
1626
|
+
return event.object_kind === "note" && event.object_attributes?.noteable_type?.toLowerCase() === "issue" && !!event.issue;
|
|
1627
|
+
}
|
|
1628
|
+
function isSelfNote(_event, _selfToken) {
|
|
1629
|
+
return false;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// src/webhook/IntentRecognizer.ts
|
|
1633
|
+
import { spawn } from "child_process";
|
|
1634
|
+
var logger9 = logger.child("IntentRecognizer");
|
|
1635
|
+
var VALID_INTENTS = [
|
|
1636
|
+
"retry",
|
|
1637
|
+
"retry-from",
|
|
1638
|
+
"supplement",
|
|
1639
|
+
"approve",
|
|
1640
|
+
"reject",
|
|
1641
|
+
"status",
|
|
1642
|
+
"restart"
|
|
1643
|
+
];
|
|
1644
|
+
var VALID_PHASES = [
|
|
1645
|
+
"analysis",
|
|
1646
|
+
"design",
|
|
1647
|
+
"implement",
|
|
1648
|
+
"verify",
|
|
1649
|
+
"plan",
|
|
1650
|
+
"build"
|
|
1651
|
+
];
|
|
1652
|
+
var SYSTEM_PROMPT_FUNC = () => t("intent.systemPrompt");
|
|
1653
|
+
var IntentRecognizer = class {
|
|
1654
|
+
binary;
|
|
1655
|
+
nvmNodeVersion;
|
|
1656
|
+
model;
|
|
1657
|
+
timeoutMs;
|
|
1658
|
+
constructor(config) {
|
|
1659
|
+
this.binary = config.binary;
|
|
1660
|
+
this.nvmNodeVersion = config.nvmNodeVersion;
|
|
1661
|
+
this.model = config.model;
|
|
1662
|
+
this.timeoutMs = config.timeoutMs ?? 3e4;
|
|
1663
|
+
}
|
|
1664
|
+
async recognize(userComment, issueState) {
|
|
1665
|
+
const stateHint = issueState ? `
|
|
1666
|
+
\u5F53\u524D Issue \u72B6\u6001: ${issueState}` : "";
|
|
1667
|
+
const userPrompt = `${SYSTEM_PROMPT_FUNC()}
|
|
1668
|
+
|
|
1669
|
+
\u7528\u6237\u8BC4\u8BBA:${stateHint}
|
|
1670
|
+
${userComment}`;
|
|
1671
|
+
try {
|
|
1672
|
+
const rawOutput = await this.runLLM(userPrompt);
|
|
1673
|
+
return this.parseResponse(rawOutput);
|
|
1674
|
+
} catch (err) {
|
|
1675
|
+
logger9.error("Intent recognition failed", { error: err.message });
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
/** Visible for testing */
|
|
1680
|
+
parseResponse(raw) {
|
|
1681
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
1682
|
+
if (!jsonMatch) {
|
|
1683
|
+
logger9.warn("No JSON found in LLM response", { raw: raw.slice(0, 200) });
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
let parsed;
|
|
1687
|
+
try {
|
|
1688
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
1689
|
+
} catch {
|
|
1690
|
+
logger9.warn("Failed to parse JSON from LLM", { raw: jsonMatch[0].slice(0, 200) });
|
|
1691
|
+
return null;
|
|
1692
|
+
}
|
|
1693
|
+
if (parsed.intent === null || parsed.intent === "null") return null;
|
|
1694
|
+
const intent = String(parsed.intent);
|
|
1695
|
+
if (!VALID_INTENTS.includes(intent)) {
|
|
1696
|
+
logger9.warn("Invalid intent from LLM", { intent });
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
const result = { intent };
|
|
1700
|
+
if (parsed.phase && parsed.phase !== "null") {
|
|
1701
|
+
const phase = String(parsed.phase).toLowerCase();
|
|
1702
|
+
if (VALID_PHASES.includes(phase)) {
|
|
1703
|
+
result.phase = phase;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
if (parsed.context && parsed.context !== "null") {
|
|
1707
|
+
result.context = String(parsed.context).trim();
|
|
1708
|
+
}
|
|
1709
|
+
if (parsed.feedback && parsed.feedback !== "null") {
|
|
1710
|
+
result.feedback = String(parsed.feedback).trim();
|
|
1711
|
+
}
|
|
1712
|
+
return result;
|
|
1713
|
+
}
|
|
1714
|
+
runLLM(prompt) {
|
|
1715
|
+
return new Promise((resolve, reject) => {
|
|
1716
|
+
const args = ["-p", "-", "--output-format", "text", "--verbose"];
|
|
1717
|
+
if (this.model) args.push("--model", this.model);
|
|
1718
|
+
const { CLAUDECODE, ...env } = process.env;
|
|
1719
|
+
const child = spawn(this.binary, args, {
|
|
1720
|
+
env: { ...env, NODE_VERSION: this.nvmNodeVersion },
|
|
1721
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1722
|
+
});
|
|
1723
|
+
let stdout = "";
|
|
1724
|
+
let stderr = "";
|
|
1725
|
+
child.stdout.on("data", (d) => {
|
|
1726
|
+
stdout += d.toString();
|
|
1727
|
+
});
|
|
1728
|
+
child.stderr.on("data", (d) => {
|
|
1729
|
+
stderr += d.toString();
|
|
1730
|
+
});
|
|
1731
|
+
const timer = setTimeout(() => {
|
|
1732
|
+
child.kill("SIGTERM");
|
|
1733
|
+
reject(new Error(`Intent recognition timed out after ${this.timeoutMs}ms`));
|
|
1734
|
+
}, this.timeoutMs);
|
|
1735
|
+
child.on("close", (code) => {
|
|
1736
|
+
clearTimeout(timer);
|
|
1737
|
+
if (code !== 0) {
|
|
1738
|
+
logger9.warn("LLM process exited with non-zero code", { code, stderr: stderr.slice(0, 300) });
|
|
1739
|
+
}
|
|
1740
|
+
resolve(stdout);
|
|
1741
|
+
});
|
|
1742
|
+
child.on("error", (err) => {
|
|
1743
|
+
clearTimeout(timer);
|
|
1744
|
+
reject(err);
|
|
1745
|
+
});
|
|
1746
|
+
child.stdin.write(prompt);
|
|
1747
|
+
child.stdin.end();
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
// src/webhook/WebhookServer.ts
|
|
1753
|
+
var logger10 = logger.child("WebhookServer");
|
|
1754
|
+
var WebhookServer = class {
|
|
1755
|
+
app;
|
|
1756
|
+
server = null;
|
|
1757
|
+
port;
|
|
1758
|
+
constructor(deps) {
|
|
1759
|
+
this.port = deps.config.webhook.port;
|
|
1760
|
+
const intentRecognizer = deps.config.webhook.llmFallback ? new IntentRecognizer({
|
|
1761
|
+
binary: deps.config.webhook.llmBinary,
|
|
1762
|
+
nvmNodeVersion: deps.config.ai.nvmNodeVersion,
|
|
1763
|
+
model: deps.config.ai.model
|
|
1764
|
+
}) : void 0;
|
|
1765
|
+
const handlerDeps = {
|
|
1766
|
+
tracker: deps.tracker,
|
|
1767
|
+
orchestrator: deps.orchestrator,
|
|
1768
|
+
supplementStore: deps.supplementStore,
|
|
1769
|
+
gongfeng: deps.gongfeng,
|
|
1770
|
+
config: deps.config,
|
|
1771
|
+
intentRecognizer
|
|
1772
|
+
};
|
|
1773
|
+
this.app = express2();
|
|
1774
|
+
this.app.use(express2.json());
|
|
1775
|
+
this.app.use(createWebhookRouter(handlerDeps));
|
|
1776
|
+
this.app.get("/health", (_req, res) => {
|
|
1777
|
+
res.json({ status: "ok", service: "webhook" });
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
start() {
|
|
1781
|
+
return new Promise((resolve) => {
|
|
1782
|
+
this.server = this.app.listen(this.port, () => {
|
|
1783
|
+
logger10.info(`Webhook server listening on port ${this.port}`);
|
|
1784
|
+
resolve();
|
|
1785
|
+
});
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
stop() {
|
|
1789
|
+
if (this.server) {
|
|
1790
|
+
this.server.close();
|
|
1791
|
+
this.server = null;
|
|
1792
|
+
logger10.info("Webhook server stopped");
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
// src/web/AgentLogStore.ts
|
|
1798
|
+
import fs4 from "fs";
|
|
1799
|
+
import path5 from "path";
|
|
1800
|
+
var logger11 = logger.child("AgentLogStore");
|
|
1801
|
+
var MAX_LOGS_PER_ISSUE = 2e4;
|
|
1802
|
+
var DEBUG_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1803
|
+
"thinking",
|
|
1804
|
+
"content_block_start",
|
|
1805
|
+
"content_block_delta",
|
|
1806
|
+
"content_block_stop",
|
|
1807
|
+
"message_start",
|
|
1808
|
+
"message_delta",
|
|
1809
|
+
"message_stop",
|
|
1810
|
+
"ping"
|
|
1811
|
+
]);
|
|
1812
|
+
var AgentLogStore = class {
|
|
1813
|
+
logDir;
|
|
1814
|
+
constructor(dataDir) {
|
|
1815
|
+
this.logDir = path5.join(dataDir, "agent-logs");
|
|
1816
|
+
if (!fs4.existsSync(this.logDir)) {
|
|
1817
|
+
fs4.mkdirSync(this.logDir, { recursive: true });
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
startListening() {
|
|
1821
|
+
eventBus.on("agent:output", (payload) => {
|
|
1822
|
+
this.handleAgentOutput(payload);
|
|
1823
|
+
});
|
|
1824
|
+
eventBus.on("pipeline:progress", (payload) => {
|
|
1825
|
+
this.handlePipelineProgress(payload);
|
|
1826
|
+
});
|
|
1827
|
+
logger11.info("AgentLogStore listening for events");
|
|
1828
|
+
}
|
|
1829
|
+
getLogs(issueIid) {
|
|
1830
|
+
const filePath = this.logFilePath(issueIid);
|
|
1831
|
+
if (!fs4.existsSync(filePath)) return [];
|
|
1832
|
+
try {
|
|
1833
|
+
const raw = fs4.readFileSync(filePath, "utf-8").trim();
|
|
1834
|
+
if (!raw) return [];
|
|
1835
|
+
return raw.split("\n").map((line) => JSON.parse(line));
|
|
1836
|
+
} catch (err) {
|
|
1837
|
+
logger11.warn("Failed to read agent logs", { issueIid, error: err.message });
|
|
1838
|
+
return [];
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
clearLogs(issueIid) {
|
|
1842
|
+
const filePath = this.logFilePath(issueIid);
|
|
1843
|
+
if (fs4.existsSync(filePath)) {
|
|
1844
|
+
fs4.unlinkSync(filePath);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
logFilePath(issueIid) {
|
|
1848
|
+
return path5.join(this.logDir, `${issueIid}.jsonl`);
|
|
1849
|
+
}
|
|
1850
|
+
appendLog(issueIid, entry) {
|
|
1851
|
+
const filePath = this.logFilePath(issueIid);
|
|
1852
|
+
try {
|
|
1853
|
+
fs4.appendFileSync(filePath, `${JSON.stringify(entry)}
|
|
1854
|
+
`, "utf-8");
|
|
1855
|
+
this.trimIfNeeded(issueIid, filePath);
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
logger11.warn("Failed to write agent log", { issueIid, error: err.message });
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
trimIfNeeded(issueIid, filePath) {
|
|
1861
|
+
try {
|
|
1862
|
+
const raw = fs4.readFileSync(filePath, "utf-8").trim();
|
|
1863
|
+
if (!raw) return;
|
|
1864
|
+
const lines = raw.split("\n");
|
|
1865
|
+
if (lines.length > MAX_LOGS_PER_ISSUE) {
|
|
1866
|
+
const trimmed = lines.slice(-MAX_LOGS_PER_ISSUE);
|
|
1867
|
+
fs4.writeFileSync(filePath, `${trimmed.join("\n")}
|
|
1868
|
+
`, "utf-8");
|
|
1869
|
+
logger11.info("Agent logs trimmed", { issueIid, from: lines.length, to: trimmed.length });
|
|
1870
|
+
}
|
|
1871
|
+
} catch {
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
handleAgentOutput(payload) {
|
|
1875
|
+
const d = payload.data;
|
|
1876
|
+
if (!d?.issueIid || !d.event) return;
|
|
1877
|
+
const eventType = d.event.type || "raw";
|
|
1878
|
+
if (DEBUG_EVENT_TYPES.has(eventType)) return;
|
|
1879
|
+
const entry = {
|
|
1880
|
+
type: eventType,
|
|
1881
|
+
phase: d.phase,
|
|
1882
|
+
timestamp: d.event.timestamp || payload.timestamp,
|
|
1883
|
+
summary: this.summarizeContent(d.event)
|
|
1884
|
+
};
|
|
1885
|
+
this.appendLog(d.issueIid, entry);
|
|
1886
|
+
}
|
|
1887
|
+
handlePipelineProgress(payload) {
|
|
1888
|
+
const d = payload.data;
|
|
1889
|
+
if (!d?.issueIid) return;
|
|
1890
|
+
const entry = {
|
|
1891
|
+
type: "system",
|
|
1892
|
+
phase: d.step,
|
|
1893
|
+
timestamp: payload.timestamp,
|
|
1894
|
+
summary: d.message || ""
|
|
1895
|
+
};
|
|
1896
|
+
this.appendLog(d.issueIid, entry);
|
|
1897
|
+
}
|
|
1898
|
+
summarizeContent(event) {
|
|
1899
|
+
const { content } = event;
|
|
1900
|
+
if (!content || typeof content === "string") return String(content || "");
|
|
1901
|
+
if (event.type === "assistant") {
|
|
1902
|
+
return this.extractAssistantText(content);
|
|
1903
|
+
}
|
|
1904
|
+
if (event.type === "tool_use") {
|
|
1905
|
+
return this.extractToolUseText(content);
|
|
1906
|
+
}
|
|
1907
|
+
if (event.type === "tool_result") {
|
|
1908
|
+
const text = typeof content.content === "string" ? content.content : JSON.stringify(content.content || "");
|
|
1909
|
+
return text.slice(0, 150);
|
|
1910
|
+
}
|
|
1911
|
+
if (event.type === "result") {
|
|
1912
|
+
const r = content;
|
|
1913
|
+
return (r.result || JSON.stringify(content)).slice(0, 200);
|
|
1914
|
+
}
|
|
1915
|
+
return JSON.stringify(content).slice(0, 150);
|
|
1916
|
+
}
|
|
1917
|
+
extractAssistantText(content) {
|
|
1918
|
+
const msg = content.message || content;
|
|
1919
|
+
if (typeof msg === "string") return msg.slice(0, 200);
|
|
1920
|
+
const m = msg;
|
|
1921
|
+
if (m.text) return m.text.slice(0, 200);
|
|
1922
|
+
if (m.content) {
|
|
1923
|
+
const parts = Array.isArray(m.content) ? m.content : [m.content];
|
|
1924
|
+
const texts = parts.filter((c) => c.type === "text").map((c) => c.text).join(" ");
|
|
1925
|
+
return texts.slice(0, 200) || JSON.stringify(content).slice(0, 150);
|
|
1926
|
+
}
|
|
1927
|
+
return JSON.stringify(content).slice(0, 150);
|
|
1928
|
+
}
|
|
1929
|
+
extractToolUseText(content) {
|
|
1930
|
+
const c = content;
|
|
1931
|
+
const tool = c.tool;
|
|
1932
|
+
const name = tool?.name || c.name || "?";
|
|
1933
|
+
const input = tool?.input || c.input || {};
|
|
1934
|
+
const detail = input.path || input.command || input.file_path || "";
|
|
1935
|
+
return name + (detail ? `: ${detail}` : "");
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
// src/index.ts
|
|
1940
|
+
import path6 from "path";
|
|
1941
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1942
|
+
var __dirname2 = path6.dirname(fileURLToPath2(import.meta.url));
|
|
1943
|
+
async function main() {
|
|
1944
|
+
logger.info("Issue Auto-Finish service starting...");
|
|
1945
|
+
const config = loadConfig();
|
|
1946
|
+
setLocale(config.locale);
|
|
1947
|
+
const allAiPhaseNames = [
|
|
1948
|
+
...CLASSIC_PIPELINE.phases.filter((p) => p.kind === "ai").map((p) => p.name),
|
|
1949
|
+
...PLAN_MODE_PIPELINE.phases.filter((p) => p.kind === "ai").map((p) => p.name)
|
|
1950
|
+
];
|
|
1951
|
+
validatePhaseRegistry([...new Set(allAiPhaseNames)]);
|
|
1952
|
+
const gongfeng = new GongfengClient(config.gongfeng);
|
|
1953
|
+
const git = new GitOperations(config.project.gitRootDir);
|
|
1954
|
+
const aiRunner = createAIRunner(config.ai);
|
|
1955
|
+
const dataDir = path6.resolve(__dirname2, "../data");
|
|
1956
|
+
const tracker = new IssueTracker(dataDir);
|
|
1957
|
+
const supplementStore = new SupplementStore(dataDir);
|
|
1958
|
+
const agentLogStore = new AgentLogStore(dataDir);
|
|
1959
|
+
agentLogStore.startListening();
|
|
1960
|
+
const orchestrator = new PipelineOrchestrator(config, gongfeng, git, aiRunner, tracker, supplementStore);
|
|
1961
|
+
const poller = new IssuePoller(config, gongfeng, tracker, orchestrator);
|
|
1962
|
+
let webServer = null;
|
|
1963
|
+
if (config.web.enabled) {
|
|
1964
|
+
webServer = new WebServer({ tracker, config, agentLogStore, orchestrator, gongfeng, supplementStore, mainGit: git });
|
|
1965
|
+
await webServer.start();
|
|
1966
|
+
}
|
|
1967
|
+
let webhookServer = null;
|
|
1968
|
+
if (config.webhook.enabled) {
|
|
1969
|
+
webhookServer = new WebhookServer({ tracker, config, orchestrator, gongfeng, supplementStore });
|
|
1970
|
+
await webhookServer.start();
|
|
1971
|
+
}
|
|
1972
|
+
const shutdown = () => {
|
|
1973
|
+
logger.info("Shutting down...");
|
|
1974
|
+
poller.stop();
|
|
1975
|
+
webServer?.stop();
|
|
1976
|
+
webhookServer?.stop();
|
|
1977
|
+
process.exit(0);
|
|
1978
|
+
};
|
|
1979
|
+
process.on("SIGINT", shutdown);
|
|
1980
|
+
process.on("SIGTERM", shutdown);
|
|
1981
|
+
poller.start();
|
|
1982
|
+
logger.info("Issue Auto-Finish service started", {
|
|
1983
|
+
projectPath: config.gongfeng.projectPath,
|
|
1984
|
+
workDir: config.project.workDir,
|
|
1985
|
+
discoveryIntervalMs: config.poll.discoveryIntervalMs,
|
|
1986
|
+
driveIntervalMs: config.poll.driveIntervalMs,
|
|
1987
|
+
webUI: config.web.enabled ? `http://localhost:${config.web.port}` : "disabled",
|
|
1988
|
+
webhook: config.webhook.enabled ? `http://localhost:${config.webhook.port}/webhook/gongfeng` : "disabled"
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
export {
|
|
1993
|
+
main
|
|
1994
|
+
};
|
|
1995
|
+
//# sourceMappingURL=chunk-IDUKWCC2.js.map
|