ework-web 0.10.64 → 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/giteaApi.ts +2 -1
- package/src/index.ts +3 -1
- package/src/render/components.ts +3 -1
- package/src/render/layout.ts +7 -3
- package/src/static/app.js +2 -1
- package/src/store.ts +16 -10
- package/src/views/issueThread.ts +4 -1
- package/src/webhooks.ts +9 -55
package/package.json
CHANGED
package/src/giteaApi.ts
CHANGED
|
@@ -126,12 +126,13 @@ export async function handleGiteaApi(
|
|
|
126
126
|
if (!(await canSudo(caller))) return giteaError(403, "sudo requires permission");
|
|
127
127
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(sudoLogin)) return giteaError(400, "invalid sudo login");
|
|
128
128
|
const sudoKindRaw = req.headers.get("Sudo-Kind")?.trim();
|
|
129
|
+
const sudoDisplayName = req.headers.get("Sudo-Display-Name")?.trim() || null;
|
|
129
130
|
const existing = await getUserByLogin(sudoLogin);
|
|
130
131
|
if (existing) {
|
|
131
132
|
user = existing;
|
|
132
133
|
} else {
|
|
133
134
|
const kind = sudoKindRaw === "bot" || sudoKindRaw === "system" ? sudoKindRaw : "human";
|
|
134
|
-
user = await ensureUser(sudoLogin, kind);
|
|
135
|
+
user = await ensureUser(sudoLogin, kind, sudoDisplayName);
|
|
135
136
|
}
|
|
136
137
|
if (!user.is_active) return giteaError(403, "sudo target inactive");
|
|
137
138
|
}
|
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/components.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface CommentView {
|
|
|
19
19
|
avatar: string;
|
|
20
20
|
created_at: string;
|
|
21
21
|
body_html: string;
|
|
22
|
+
display_name?: string | null;
|
|
22
23
|
reactions?: { e: string; n: number }[];
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -59,7 +60,8 @@ export function renderCommentCard(c: CommentView, cfg?: { translateUrl?: string;
|
|
|
59
60
|
`<div class="item item-${esc(tag)}" id="comment-${c.id}" data-id="${c.id}">` +
|
|
60
61
|
`<div class="card"><div class="card-h">` +
|
|
61
62
|
`<span class="tag tag-${esc(tag)}">${label} ${esc(tag)}</span>` +
|
|
62
|
-
`<span class="who">${esc(c.login)}</span>` +
|
|
63
|
+
`<span class="who">${esc(c.display_name || c.login)}</span>` +
|
|
64
|
+
(c.display_name ? `<span class="who-login">${esc(c.login)}</span>` : "") +
|
|
63
65
|
`<span class="when" data-ts="${esc(c.created_at)}" title="${esc(c.created_at)}">${relTime(c.created_at)}</span>` +
|
|
64
66
|
rx +
|
|
65
67
|
`<span class="card-actions">` + actionBarHTML({ cid: String(c.id), copy: true, link: true, translate: true, tts: true, translateEnabled, ttsEnabled }) + `</span>` +
|
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[];
|
|
@@ -61,6 +62,7 @@ header.topbar .num{opacity:.7}
|
|
|
61
62
|
.card{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;overflow:hidden;min-width:0}
|
|
62
63
|
.card-h{display:flex;align-items:center;gap:.5rem;padding:.4rem .7rem;background:var(--bg-muted);font-size:13px;flex-wrap:wrap}
|
|
63
64
|
.card-h .who{font-weight:600;color:var(--text)}
|
|
65
|
+
.card-h .who-login{font-weight:400;color:var(--text-muted);font-size:.85em;margin-left:.25rem}
|
|
64
66
|
.tag{font-size:11px;font-weight:600;padding:.05rem .4rem;border-radius:4px;line-height:1.5}
|
|
65
67
|
.tag-human{background:color-mix(in srgb,var(--human) 18%,transparent);color:var(--human)}
|
|
66
68
|
.tag-bot{background:color-mix(in srgb,var(--bot) 18%,transparent);color:var(--bot)}
|
|
@@ -191,9 +193,11 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
|
|
|
191
193
|
: `<button type="button" class="action-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/halt" data-action-confirm="确认停止 AI 处理?" title="停止 AI 处理">⏹ 停止</button>`
|
|
192
194
|
: "";
|
|
193
195
|
const dispatchBtnHtml = showActions
|
|
194
|
-
? props.
|
|
195
|
-
? `<
|
|
196
|
-
:
|
|
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>`
|
|
197
201
|
: "";
|
|
198
202
|
return `<!doctype html>
|
|
199
203
|
<html lang="zh">
|
package/src/static/app.js
CHANGED
|
@@ -116,7 +116,8 @@
|
|
|
116
116
|
`<div class="item item-${esc(tag)}${isNew}" id="comment-${c.id}" data-id="${c.id}">` +
|
|
117
117
|
`<div class="card"><div class="card-h">` +
|
|
118
118
|
`<span class="tag tag-${esc(tag)}">${label} ${esc(tag)}</span>` +
|
|
119
|
-
`<span class="who">${esc(c.login)}</span>` +
|
|
119
|
+
`<span class="who">${esc(c.display_name || c.login)}</span>` +
|
|
120
|
+
(c.display_name ? `<span class="who-login">${esc(c.login)}</span>` : "") +
|
|
120
121
|
`<span class="when" data-ts="${esc(c.created_at)}" title="${esc(c.created_at)}">${relTime(c.created_at)}</span>` +
|
|
121
122
|
rx +
|
|
122
123
|
`<span class="card-actions">` +
|
package/src/store.ts
CHANGED
|
@@ -90,6 +90,7 @@ export interface CommentRow {
|
|
|
90
90
|
created_at: string;
|
|
91
91
|
updated_at: string;
|
|
92
92
|
author_kind?: UserKind;
|
|
93
|
+
author_display_name?: string | null;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
export interface LabelRow {
|
|
@@ -134,19 +135,24 @@ function now(): string {
|
|
|
134
135
|
return new Date().toISOString();
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
export async function ensureUser(login: string, kind: UserKind = "human"): Promise<UserRow> {
|
|
138
|
+
export async function ensureUser(login: string, kind: UserKind = "human", displayName?: string | null): Promise<UserRow> {
|
|
138
139
|
const db = getDB();
|
|
139
140
|
const existing = await db.get<UserRow>("SELECT * FROM {{users}} WHERE login = ?", [login]);
|
|
140
|
-
if (existing)
|
|
141
|
+
if (existing) {
|
|
142
|
+
if (displayName && displayName.trim() && existing.display_name !== displayName) {
|
|
143
|
+
await db.run("UPDATE {{users}} SET display_name = ?, updated_at = ? WHERE login = ?", [displayName, now(), login]);
|
|
144
|
+
}
|
|
145
|
+
return existing;
|
|
146
|
+
}
|
|
141
147
|
const ts = now();
|
|
142
148
|
await db.run(
|
|
143
|
-
"INSERT INTO {{users}} (login, kind, created_at, updated_at) VALUES (?, ?, ?, ?)",
|
|
144
|
-
[login, kind, ts, ts]
|
|
149
|
+
"INSERT INTO {{users}} (login, kind, display_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
|
150
|
+
[login, kind, displayName ?? null, ts, ts]
|
|
145
151
|
);
|
|
146
152
|
return {
|
|
147
153
|
login,
|
|
148
154
|
kind,
|
|
149
|
-
display_name: null,
|
|
155
|
+
display_name: displayName ?? null,
|
|
150
156
|
password_hash: null,
|
|
151
157
|
email: null,
|
|
152
158
|
is_admin: 0,
|
|
@@ -570,7 +576,7 @@ export async function listCommentsPage(issueId: number, page: number, pageSize:
|
|
|
570
576
|
const clamped = Math.min(Math.max(1, page), totalPages);
|
|
571
577
|
const offset = (clamped - 1) * pageSize;
|
|
572
578
|
const rows = await getDB().all<CommentRow>(
|
|
573
|
-
`SELECT c.*, u.kind AS author_kind
|
|
579
|
+
`SELECT c.*, u.kind AS author_kind, u.display_name AS author_display_name
|
|
574
580
|
FROM {{comments}} c
|
|
575
581
|
LEFT JOIN {{users}} u ON u.login = c.author
|
|
576
582
|
WHERE c.issue_id = ?
|
|
@@ -583,7 +589,7 @@ export async function listCommentsPage(issueId: number, page: number, pageSize:
|
|
|
583
589
|
|
|
584
590
|
export async function listCommentsSince(issueId: number, sinceISO: string): Promise<CommentRow[]> {
|
|
585
591
|
return await getDB().all<CommentRow>(
|
|
586
|
-
`SELECT c.*, u.kind AS author_kind
|
|
592
|
+
`SELECT c.*, u.kind AS author_kind, u.display_name AS author_display_name
|
|
587
593
|
FROM {{comments}} c
|
|
588
594
|
LEFT JOIN {{users}} u ON u.login = c.author
|
|
589
595
|
WHERE c.issue_id = ? AND c.created_at > ?
|
|
@@ -594,7 +600,7 @@ export async function listCommentsSince(issueId: number, sinceISO: string): Prom
|
|
|
594
600
|
|
|
595
601
|
export async function listCommentsForIssue(issueId: number): Promise<CommentRow[]> {
|
|
596
602
|
return await getDB().all<CommentRow>(
|
|
597
|
-
`SELECT c.*, u.kind AS author_kind
|
|
603
|
+
`SELECT c.*, u.kind AS author_kind, u.display_name AS author_display_name
|
|
598
604
|
FROM {{comments}} c
|
|
599
605
|
LEFT JOIN {{users}} u ON u.login = c.author
|
|
600
606
|
WHERE c.issue_id = ?
|
|
@@ -628,7 +634,7 @@ export async function postComment(
|
|
|
628
634
|
const row = await getDB().get<{ project_id: number }>("SELECT project_id FROM {{issues}} WHERE id = ?", [issueId]);
|
|
629
635
|
await getDB().run("UPDATE {{projects}} SET updated_at = ? WHERE id = ?", [updatedAt, row!.project_id]);
|
|
630
636
|
return (await getDB().get<CommentRow>(
|
|
631
|
-
`SELECT c.*, u.kind AS author_kind
|
|
637
|
+
`SELECT c.*, u.kind AS author_kind, u.display_name AS author_display_name
|
|
632
638
|
FROM {{comments}} c LEFT JOIN {{users}} u ON u.login = c.author
|
|
633
639
|
WHERE c.id = ?`,
|
|
634
640
|
[info.insertId]
|
|
@@ -638,7 +644,7 @@ export async function postComment(
|
|
|
638
644
|
|
|
639
645
|
export async function getComment(commentId: number): Promise<CommentRow | null> {
|
|
640
646
|
const row = await getDB().get<CommentRow>(
|
|
641
|
-
`SELECT c.*, u.kind AS author_kind
|
|
647
|
+
`SELECT c.*, u.kind AS author_kind, u.display_name AS author_display_name
|
|
642
648
|
FROM {{comments}} c LEFT JOIN {{users}} u ON u.login = c.author
|
|
643
649
|
WHERE c.id = ?`,
|
|
644
650
|
[commentId]
|
package/src/views/issueThread.ts
CHANGED
|
@@ -44,6 +44,7 @@ function toView(c: CommentRow): CommentView {
|
|
|
44
44
|
avatar: "",
|
|
45
45
|
created_at: c.created_at,
|
|
46
46
|
body_html: renderMarkdown(c.body),
|
|
47
|
+
display_name: c.author_display_name ?? null,
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -90,7 +91,8 @@ export async function buildIssueThread(
|
|
|
90
91
|
owner: string,
|
|
91
92
|
repo: string,
|
|
92
93
|
number: number,
|
|
93
|
-
viewerLogin?: string
|
|
94
|
+
viewerLogin?: string,
|
|
95
|
+
projectDispatchOff?: boolean
|
|
94
96
|
): Promise<{ html: string }> {
|
|
95
97
|
const project = await getProject(owner, repo);
|
|
96
98
|
if (!project) throw new StoreError(404, `项目 ${owner}/${repo} 不存在`);
|
|
@@ -149,6 +151,7 @@ export async function buildIssueThread(
|
|
|
149
151
|
labels: labels.map((l) => ({ id: l.id, name: l.name, color: l.color })),
|
|
150
152
|
canEditLabels: cfg.writesEnabled !== false,
|
|
151
153
|
aiStatus: issue.ai_status ?? "",
|
|
154
|
+
projectDispatchOff: projectDispatchOff ?? false,
|
|
152
155
|
viewerLogin,
|
|
153
156
|
viewerIsAdmin,
|
|
154
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) {
|