ework-web 0.10.65 → 0.10.66
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/package.json +1 -1
- package/src/index.ts +3 -1
- package/src/render/layout.ts +6 -3
- package/src/views/issueThread.ts +3 -1
- package/src/webhooks.ts +9 -55
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2189,7 +2189,9 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
2189
2189
|
const project = await getProject(owner, repo);
|
|
2190
2190
|
if (!project) return html(errorPage("404", "项目不存在"), 404);
|
|
2191
2191
|
if (!(await canReadProject(project.id, ctx.user))) return html(errorPage("404", "项目不存在"), 404);
|
|
2192
|
-
const
|
|
2192
|
+
const dispatchCfg = await getConfigAll();
|
|
2193
|
+
const projectDispatchOff = dispatchCfg[`dispatchOff:${owner}/${repo}`] === "1";
|
|
2194
|
+
const { html: body } = await buildIssueThread(cfg, owner, repo, number, ctx.user?.login, projectDispatchOff);
|
|
2193
2195
|
return html(body);
|
|
2194
2196
|
} catch (e) {
|
|
2195
2197
|
const status = e instanceof StoreError ? e.status : 500;
|
package/src/render/layout.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface LayoutProps {
|
|
|
18
18
|
labels?: { id: number; name: string; color: string }[];
|
|
19
19
|
canEditLabels?: boolean;
|
|
20
20
|
aiStatus?: string;
|
|
21
|
+
projectDispatchOff?: boolean;
|
|
21
22
|
viewerLogin?: string;
|
|
22
23
|
viewerIsAdmin?: boolean;
|
|
23
24
|
customActions?: IssueAction[];
|
|
@@ -192,9 +193,11 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
|
|
|
192
193
|
: `<button type="button" class="action-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/halt" data-action-confirm="确认停止 AI 处理?" title="停止 AI 处理">⏹ 停止</button>`
|
|
193
194
|
: "";
|
|
194
195
|
const dispatchBtnHtml = showActions
|
|
195
|
-
? props.
|
|
196
|
-
? `<
|
|
197
|
-
:
|
|
196
|
+
? props.projectDispatchOff
|
|
197
|
+
? `<span class="action-btn dispatch-btn" style="opacity:.5;cursor:not-allowed" title="项目已关闭自动接单">🔕 项目不接单</span>`
|
|
198
|
+
: props.aiStatus === "dispatch_off"
|
|
199
|
+
? `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-on" title="允许自动接单">🔔 接单</button>`
|
|
200
|
+
: `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-off" data-action-confirm="设为不自动接单?" title="设为不自动接单">🔕 不接单</button>`
|
|
198
201
|
: "";
|
|
199
202
|
return `<!doctype html>
|
|
200
203
|
<html lang="zh">
|
package/src/views/issueThread.ts
CHANGED
|
@@ -91,7 +91,8 @@ export async function buildIssueThread(
|
|
|
91
91
|
owner: string,
|
|
92
92
|
repo: string,
|
|
93
93
|
number: number,
|
|
94
|
-
viewerLogin?: string
|
|
94
|
+
viewerLogin?: string,
|
|
95
|
+
projectDispatchOff?: boolean
|
|
95
96
|
): Promise<{ html: string }> {
|
|
96
97
|
const project = await getProject(owner, repo);
|
|
97
98
|
if (!project) throw new StoreError(404, `项目 ${owner}/${repo} 不存在`);
|
|
@@ -150,6 +151,7 @@ export async function buildIssueThread(
|
|
|
150
151
|
labels: labels.map((l) => ({ id: l.id, name: l.name, color: l.color })),
|
|
151
152
|
canEditLabels: cfg.writesEnabled !== false,
|
|
152
153
|
aiStatus: issue.ai_status ?? "",
|
|
154
|
+
projectDispatchOff: projectDispatchOff ?? false,
|
|
153
155
|
viewerLogin,
|
|
154
156
|
viewerIsAdmin,
|
|
155
157
|
customActions,
|
package/src/webhooks.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// the full retry trail rather than an opaque final status.
|
|
17
17
|
|
|
18
18
|
import { createHmac, randomUUID } from "node:crypto";
|
|
19
|
-
import { getDB
|
|
19
|
+
import { getDB } from "./db";
|
|
20
20
|
import { log } from "./logger";
|
|
21
21
|
import { loadConfig } from "./config";
|
|
22
22
|
import {
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
resolveModel,
|
|
29
29
|
listLabelsForIssue,
|
|
30
30
|
getUserByLogin,
|
|
31
|
-
canWriteProject,
|
|
32
31
|
type IssueRow,
|
|
33
32
|
type ProjectRow,
|
|
34
33
|
type CommentRow,
|
|
@@ -301,6 +300,7 @@ interface PayloadComment {
|
|
|
301
300
|
created_at: string;
|
|
302
301
|
updated_at: string;
|
|
303
302
|
user: PayloadUser;
|
|
303
|
+
author_kind?: string;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
interface IssueEventPayload {
|
|
@@ -636,25 +636,11 @@ export async function emitIssueEvent(
|
|
|
636
636
|
const project = await getProjectById(projectId);
|
|
637
637
|
if (!project) return;
|
|
638
638
|
const issue = await getIssueById(issueId);
|
|
639
|
-
if (!issue) return;
|
|
640
|
-
|
|
641
|
-
if (action === "opened") {
|
|
642
|
-
const cfg = await getConfigAll();
|
|
643
|
-
const scopeKey = `${project.owner}/${project.name}`;
|
|
644
|
-
if (cfg["dispatchEnabled"] === "false") {
|
|
645
|
-
log.info(`webhook: global dispatch disabled — skipping opened for ${scopeKey}#${issue.number}`);
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
if (cfg[`dispatchOff:${scopeKey}`] === "1") {
|
|
649
|
-
log.info(`webhook: project dispatch disabled — skipping opened for ${scopeKey}#${issue.number}`);
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
if ((issue as IssueRow).ai_status === "dispatch_off") {
|
|
653
|
-
log.info(`webhook: issue dispatch disabled — skipping opened for ${scopeKey}#${issue.number}`);
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
639
|
+
if (!issue) { log.warn(`emitIssueEvent: issue ${issueId} not found — silent skip prevented`); return; }
|
|
657
640
|
|
|
641
|
+
// Web is a dumb message bus — always fan out. Wake policy is enforced
|
|
642
|
+
// downstream (daemon handleEvent). Web only suppresses for subscription-
|
|
643
|
+
// level concerns (none currently).
|
|
658
644
|
const commentCount = await countCommentsSafe(issueId);
|
|
659
645
|
// Resolve model: project override > global default. Empty string = no
|
|
660
646
|
// override (daemon omits --model, lets opencode pick). globalDefault
|
|
@@ -689,48 +675,16 @@ export async function emitCommentEvent(
|
|
|
689
675
|
if (!issue) return;
|
|
690
676
|
const comment = await getCommentByIdSafe(commentId);
|
|
691
677
|
if (!comment) return;
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
// Issue-level dispatch_off is a per-issue explicit close — block comments entirely (same as halted).
|
|
695
|
-
if ((issue as IssueRow).ai_status === "dispatch_off") {
|
|
696
|
-
log.info(`webhook: issue dispatch disabled — skipping comment_created (author=${comment.author}) for ${scopeKey}#${issue.number}`);
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
678
|
+
// Web is a dumb message bus — always fan out. Wake policy is enforced
|
|
679
|
+
// downstream (daemon handleEvent checks author_kind, ai_status, env config).
|
|
699
680
|
const authorUser = await getUserByLogin(comment.author);
|
|
700
|
-
const dispatchOff = cfg["dispatchEnabled"] === "false" || cfg[`dispatchOff:${scopeKey}`] === "1";
|
|
701
|
-
if (dispatchOff && authorUser && authorUser.kind !== "human") {
|
|
702
|
-
log.info(`webhook: dispatch disabled — non-human comment blocked (author=${comment.author} kind=${authorUser.kind}) for ${scopeKey}#${issue.number}`);
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
if (dispatchOff) {
|
|
706
|
-
log.info(`webhook: dispatch disabled but human comment waking AI (author=${comment.author}) for ${scopeKey}#${issue.number}`);
|
|
707
|
-
}
|
|
708
|
-
const appCfg = await loadConfig();
|
|
709
|
-
const cfgList = (k: string, d: string) =>
|
|
710
|
-
(cfg[k] ?? d).split(",").map((s) => s.trim()).filter(Boolean);
|
|
711
|
-
const noWake = cfgList("noWakeLogins", appCfg.daemonBotLogin);
|
|
712
|
-
const okLogins = cfgList("wakeLogins", "");
|
|
713
|
-
const okKinds = cfgList("wakeKinds", "human");
|
|
714
|
-
const author = comment.author;
|
|
715
|
-
const denied =
|
|
716
|
-
noWake.includes(author) ? "deny-list" :
|
|
717
|
-
okLogins.includes(author) ? null :
|
|
718
|
-
(authorUser && !okKinds.includes(authorUser.kind)) ? `kind=${authorUser.kind}` :
|
|
719
|
-
null;
|
|
720
|
-
if (denied) {
|
|
721
|
-
log.info(`webhook: comment wake denied (author=${author} reason=${denied}) for ${scopeKey}#${issue.number}`);
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
if (authorUser && !(await canWriteProject(projectId, { login: comment.author, is_admin: authorUser.is_admin }))) {
|
|
725
|
-
log.info(`webhook: comment wake denied (author=${comment.author} not project member) for ${scopeKey}#${issue.number}`);
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
681
|
const commentCount = await countCommentsSafe(issueId);
|
|
729
682
|
const globalDefault = (await loadConfig()).defaultModel;
|
|
730
683
|
const model = resolveModel(project.model, globalDefault);
|
|
731
684
|
const labels = await toPayloadLabels(issueId);
|
|
732
685
|
const payload = buildCommentPayload(issue, comment, project, commentCount, origin, model, labels);
|
|
733
686
|
(payload as CommentEventPayload).event_id = randomUUID();
|
|
687
|
+
(payload as CommentEventPayload).comment.author_kind = authorUser?.kind ?? "human";
|
|
734
688
|
const rawBody = JSON.stringify(payload);
|
|
735
689
|
await fanOut(projectId, "issue_comment", rawBody);
|
|
736
690
|
} catch (e) {
|