ework-web 0.1.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/LICENSE +21 -0
- package/README.md +170 -0
- package/bin/ework-web.js +2 -0
- package/package.json +61 -0
- package/src/attachments.ts +54 -0
- package/src/auth.ts +218 -0
- package/src/build.ts +17 -0
- package/src/config.ts +178 -0
- package/src/db.ts +106 -0
- package/src/fileview.ts +617 -0
- package/src/giteaApi.ts +333 -0
- package/src/index.ts +1310 -0
- package/src/logger.ts +44 -0
- package/src/opencode.ts +340 -0
- package/src/ratelimit.ts +35 -0
- package/src/reactions.ts +31 -0
- package/src/render/components.ts +66 -0
- package/src/render/layout.ts +240 -0
- package/src/render/markdown.ts +106 -0
- package/src/schema.sql +178 -0
- package/src/static/app.js +571 -0
- package/src/static/favicon.svg +6 -0
- package/src/static/file.js +103 -0
- package/src/static/session.js +380 -0
- package/src/static/tts.js +101 -0
- package/src/store.ts +1020 -0
- package/src/translate.ts +166 -0
- package/src/views/adminTokens.ts +122 -0
- package/src/views/home.ts +109 -0
- package/src/views/issueList.ts +89 -0
- package/src/views/issueNew.ts +35 -0
- package/src/views/issueThread.ts +179 -0
- package/src/views/issues.ts +66 -0
- package/src/views/projectMembers.ts +146 -0
- package/src/views/projectUpstreams.ts +145 -0
- package/src/views/sessionLog.ts +709 -0
- package/src/views/settings.ts +67 -0
- package/src/views/tokens.ts +146 -0
- package/src/views/ttsBackends.ts +98 -0
- package/src/views/users.ts +163 -0
- package/src/views/webhooks.ts +166 -0
- package/src/webhooks.ts +634 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Webhook management page for a project: list existing webhooks, add new ones,
|
|
2
|
+
// toggle active, delete, send test ping, view recent delivery history per hook.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type WebhookRow,
|
|
6
|
+
type WebhookDeliveryRow,
|
|
7
|
+
} from "../webhooks";
|
|
8
|
+
import { type ProjectRow } from "../store";
|
|
9
|
+
import { THEME_CSS, escapeHtml, escapeAttr, tabNavHTML } from "../render/layout";
|
|
10
|
+
import { projectSettingsTabsHTML } from "./projectUpstreams";
|
|
11
|
+
|
|
12
|
+
function statusClass(status: number | null): string {
|
|
13
|
+
if (status === null) return "err";
|
|
14
|
+
if (status >= 200 && status < 300) return "ok";
|
|
15
|
+
if (status >= 400 && status < 500) return "warn";
|
|
16
|
+
return "err";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function deliveryRowHtml(d: WebhookDeliveryRow): string {
|
|
20
|
+
const status = d.response_status === null ? "—" : String(d.response_status);
|
|
21
|
+
const cls = statusClass(d.response_status);
|
|
22
|
+
const err = d.error ? `<div class="derr">${escapeHtml(d.error)}</div>` : "";
|
|
23
|
+
const body = d.response_body
|
|
24
|
+
? `<details class="dbody"><summary>响应</summary><pre>${escapeHtml(d.response_body)}</pre></details>`
|
|
25
|
+
: "";
|
|
26
|
+
const payload = `<details class="dpl"><summary>payload</summary><pre>${escapeHtml(d.payload)}</pre></details>`;
|
|
27
|
+
return `<tr>
|
|
28
|
+
<td class="ts">${escapeHtml(d.created_at)}</td>
|
|
29
|
+
<td class="ev">${escapeHtml(d.event)}</td>
|
|
30
|
+
<td class="uuid" title="${escapeAttr(d.delivery_uuid)}">${escapeHtml(d.delivery_uuid.slice(0, 8))}</td>
|
|
31
|
+
<td class="st ${cls}">${status}</td>
|
|
32
|
+
<td class="ms">${d.duration_ms === null ? "—" : d.duration_ms + "ms"}</td>
|
|
33
|
+
<td class="act">${payload}${body}${err}</td>
|
|
34
|
+
</tr>`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function webhookCardHtml(wh: WebhookRow, deliveries: WebhookDeliveryRow[]): string {
|
|
38
|
+
const recent = deliveries.slice(0, 10);
|
|
39
|
+
const rows = recent.map(deliveryRowHtml).join("") || `<tr><td colspan="6" class="empty">暂无投递记录</td></tr>`;
|
|
40
|
+
const events = (() => {
|
|
41
|
+
try {
|
|
42
|
+
return (JSON.parse(wh.events) as string[]).join(", ") || "(none)";
|
|
43
|
+
} catch {
|
|
44
|
+
return "(invalid)";
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
return `<section class="wh">
|
|
48
|
+
<header class="wh-head">
|
|
49
|
+
<h3>#${wh.id} · <code>${escapeHtml(wh.url)}</code></h3>
|
|
50
|
+
<div class="wh-meta">
|
|
51
|
+
<span class="badge ${wh.active ? "on" : "off"}">${wh.active ? "启用" : "停用"}</span>
|
|
52
|
+
<span class="ev-list">订阅: ${escapeHtml(events)}</span>
|
|
53
|
+
<span class="secret">secret: ${wh.secret ? "•••• (" + wh.secret.length + " chars)" : "<em>无</em>"}</span>
|
|
54
|
+
</div>
|
|
55
|
+
</header>
|
|
56
|
+
<form class="wh-actions" method="POST" action="/__wh/${wh.id}/toggle">
|
|
57
|
+
<input type="hidden" name="_method" value="toggle">
|
|
58
|
+
<button type="submit" name="active" value="${wh.active ? "0" : "1"}">${wh.active ? "停用" : "启用"}</button>
|
|
59
|
+
</form>
|
|
60
|
+
<form class="wh-actions" method="POST" action="/__wh/${wh.id}/test">
|
|
61
|
+
<button type="submit">发送测试 ping</button>
|
|
62
|
+
</form>
|
|
63
|
+
<form class="wh-actions" method="POST" action="/__wh/${wh.id}/delete" onsubmit="return confirm('删除 webhook #${wh.id}?')">
|
|
64
|
+
<button type="submit" class="danger">删除</button>
|
|
65
|
+
</form>
|
|
66
|
+
<details class="deliv">
|
|
67
|
+
<summary>近期投递(最多 10 条)</summary>
|
|
68
|
+
<table class="dtable"><thead><tr>
|
|
69
|
+
<th>时间</th><th>事件</th><th>delivery</th><th>状态</th><th>耗时</th><th>详情</th>
|
|
70
|
+
</tr></thead><tbody>${rows}</tbody></table>
|
|
71
|
+
</details>
|
|
72
|
+
</section>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface WebhooksPageInput {
|
|
76
|
+
project: ProjectRow;
|
|
77
|
+
webhooks: WebhookRow[];
|
|
78
|
+
deliveriesByWebhook: Map<number, WebhookDeliveryRow[]>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildWebhooksPage(input: WebhooksPageInput): string {
|
|
82
|
+
const { project, webhooks, deliveriesByWebhook } = input;
|
|
83
|
+
const cards =
|
|
84
|
+
webhooks
|
|
85
|
+
.map((wh) => webhookCardHtml(wh, deliveriesByWebhook.get(wh.id) ?? []))
|
|
86
|
+
.join("") || `<p class="empty-page">这个项目还没有 webhook。<br>用下面的表单添加一个。</p>`;
|
|
87
|
+
|
|
88
|
+
const formAction = `/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/webhooks`;
|
|
89
|
+
const settingsTabs = projectSettingsTabsHTML(project.owner, project.name, "webhooks");
|
|
90
|
+
const html = `<!doctype html>
|
|
91
|
+
<html lang="zh"><head><meta charset="utf-8">
|
|
92
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
93
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
94
|
+
<title>ework-web · ${escapeHtml(project.owner + "/" + project.name)} · Webhooks</title>
|
|
95
|
+
<style>${THEME_CSS}
|
|
96
|
+
.nav{display:flex;align-items:center;gap:.5rem;padding:.55rem 1rem;background:var(--header-bg);color:var(--header-text);font-size:13px}
|
|
97
|
+
.nav a{color:var(--header-text);opacity:.95}
|
|
98
|
+
.wrap{max-width:920px;margin:0 auto;padding:1rem}
|
|
99
|
+
h1{font-size:18px;margin:0 0 .3rem}
|
|
100
|
+
h2{font-size:15px;margin:1.2rem 0 .5rem}
|
|
101
|
+
h3{font-size:14px;margin:0 0 .3rem}
|
|
102
|
+
.hint{color:var(--text-muted);font-size:13px;margin:0 0 1rem}
|
|
103
|
+
.crumb{font-size:13px;color:var(--text-muted);margin-bottom:.6rem}
|
|
104
|
+
.crumb a{color:var(--text-muted)}
|
|
105
|
+
.wh{border:1px solid var(--border);border-radius:10px;padding:.9rem 1rem;margin-bottom:.9rem;background:var(--bg-elev)}
|
|
106
|
+
.wh-head{display:flex;flex-direction:column;gap:.3rem;margin-bottom:.5rem}
|
|
107
|
+
.wh-head h3 code{font-family:ui-monospace,monospace;font-size:13px;color:var(--text);word-break:break-all}
|
|
108
|
+
.wh-meta{display:flex;flex-wrap:wrap;gap:.6rem;font-size:12px;color:var(--text-muted)}
|
|
109
|
+
.badge{padding:1px 7px;border-radius:10px;font-size:11px;font-weight:600}
|
|
110
|
+
.badge.on{background:#1f6feb33;color:#58a6ff}
|
|
111
|
+
.badge.off{background:#88888833;color:var(--text-muted)}
|
|
112
|
+
.wh-actions{display:inline-block;margin:.3rem .6rem 0 0}
|
|
113
|
+
.wh-actions button{padding:.3rem .8rem;font-size:12px;border-radius:5px;background:var(--accent);color:#fff;border:0;cursor:pointer}
|
|
114
|
+
.wh-actions button.danger{background:#da3633}
|
|
115
|
+
.deliv{margin-top:.6rem;font-size:12px}
|
|
116
|
+
.deliv summary{cursor:pointer;color:var(--text-muted)}
|
|
117
|
+
.dtable{width:100%;border-collapse:collapse;margin-top:.4rem;font-size:12px}
|
|
118
|
+
.dtable th,.dtable td{border:1px solid var(--border);padding:.3rem .4rem;text-align:left;vertical-align:top}
|
|
119
|
+
.dtable th{background:var(--bg);color:var(--text-muted);font-weight:600}
|
|
120
|
+
.dtable .ts{white-space:nowrap;font-family:ui-monospace,monospace}
|
|
121
|
+
.dtable .uuid{font-family:ui-monospace,monospace;color:var(--text-muted)}
|
|
122
|
+
.dtable .st.ok{color:#3fb950;font-weight:600}
|
|
123
|
+
.dtable .st.warn{color:#d29922;font-weight:600}
|
|
124
|
+
.dtable .st.err{color:#f85149;font-weight:600}
|
|
125
|
+
.dtable .ms{color:var(--text-muted);white-space:nowrap}
|
|
126
|
+
.dbody pre,.dpl pre{font-size:11px;max-height:240px;overflow:auto;background:var(--bg);padding:.4rem;border-radius:5px;border:1px solid var(--border);white-space:pre-wrap;word-break:break-all}
|
|
127
|
+
.derr{color:#f85149;font-size:11px;margin-top:.2rem}
|
|
128
|
+
.empty{color:var(--text-muted);text-align:center;padding:.6rem}
|
|
129
|
+
.empty-page{color:var(--text-muted);padding:1.2rem;text-align:center;background:var(--bg-elev);border-radius:8px;margin:.6rem 0}
|
|
130
|
+
form.add-form{background:var(--bg-elev);border:1px solid var(--border);border-radius:10px;padding:1rem;margin-top:1rem}
|
|
131
|
+
form.add-form label{display:block;margin:.5rem 0}
|
|
132
|
+
form.add-form input[type=text],form.add-form input[type=password]{width:100%;padding:.4rem .55rem;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:13px;font-family:inherit}
|
|
133
|
+
form.add-form .checks{display:flex;gap:1rem;font-size:13px}
|
|
134
|
+
form.add-form .checks label{display:inline-flex;align-items:center;gap:.3rem;margin:0}
|
|
135
|
+
.bar{display:flex;gap:.6rem;align-items:center;margin-top:.8rem}
|
|
136
|
+
button[type=submit]{padding:.5rem 1.2rem;border:0;border-radius:6px;background:var(--accent);color:#fff;font-size:13px;cursor:pointer}
|
|
137
|
+
.a-back{color:var(--text-muted);font-size:13px}
|
|
138
|
+
.tabs{display:flex;gap:.3rem;padding:.5rem 1rem;border-bottom:1px solid var(--border);font-size:13px}
|
|
139
|
+
.tab{padding:.3rem .7rem;border-radius:6px 6px 0 0;text-decoration:none;color:var(--text-muted)}
|
|
140
|
+
.tab.active{background:var(--accent);color:#fff}
|
|
141
|
+
.subtabs{display:flex;gap:.4rem;padding:.4rem 0 0;border-bottom:1px solid var(--border);margin-bottom:.9rem}
|
|
142
|
+
.subtab{padding:.35rem .8rem;border-radius:6px 6px 0 0;font-size:13px;color:var(--text-muted)}
|
|
143
|
+
.subtab.active{background:var(--bg-muted);color:var(--text);font-weight:600}
|
|
144
|
+
</style></head><body>
|
|
145
|
+
<header class="nav"><a href="/" style="color:var(--header-text)">🏠 ework-web</a></header>
|
|
146
|
+
${tabNavHTML("projects")}
|
|
147
|
+
<main class="wrap">
|
|
148
|
+
<div class="crumb"><a href="/projects">项目</a> · <a href="/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}">${escapeHtml(project.owner + "/" + project.name)}</a> · 设置</div>
|
|
149
|
+
<h1>Webhooks</h1>
|
|
150
|
+
${settingsTabs}
|
|
151
|
+
<p class="hint">Payload 协议兼容 Gitea(<code>X-Gitea-Signature</code> HMAC-SHA256 hex),下游 Action 不用改。<br>事件: <code>issues</code> (opened/closed/reopened), <code>issue_comment</code> (created)。</p>
|
|
152
|
+
${cards}
|
|
153
|
+
<form class="add-form" method="POST" action="${escapeAttr(formAction)}">
|
|
154
|
+
<h2>添加 webhook</h2>
|
|
155
|
+
<label>目标 URL <input type="text" name="url" required placeholder="http://localhost:8099/hook"></label>
|
|
156
|
+
<label>Secret (留空=不签名) <input type="password" name="secret" placeholder="任意随机串"></label>
|
|
157
|
+
<div class="checks">
|
|
158
|
+
<span>订阅:</span>
|
|
159
|
+
<label><input type="checkbox" name="events" value="issues" checked> issues</label>
|
|
160
|
+
<label><input type="checkbox" name="events" value="issue_comment" checked> issue_comment</label>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="bar"><button type="submit">添加</button><a class="a-back" href="/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}">返回项目</a></div>
|
|
163
|
+
</form>
|
|
164
|
+
</main></body></html>`;
|
|
165
|
+
return html;
|
|
166
|
+
}
|