ework-web 0.10.123 → 0.10.125
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 +3 -3
- package/src/db.ts +4 -0
- package/src/index.ts +38 -2
- package/src/schema-mysql.sql +1 -0
- package/src/views/projectAi.ts +13 -0
- package/src/webhooks.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ework-web",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.125",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ework-web — standalone multi-project issue tracker. Local SQLite-backed, no external API dependency. Bun + TypeScript + SSR HTML.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"start": "bun src/index.ts",
|
|
43
43
|
"check": "tsc --noEmit",
|
|
44
44
|
"typecheck": "tsc --noEmit",
|
|
45
|
-
"test": "bun test",
|
|
45
|
+
"test": "bun test --timeout=30000",
|
|
46
46
|
"test:mysql": "bash scripts/test-mysql.sh"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"zod": "^3.23.8"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@types/bun": "
|
|
59
|
+
"@types/bun": "1.4.2",
|
|
60
60
|
"@types/jsdom": "^28.0.3",
|
|
61
61
|
"typescript": "^5.6.0"
|
|
62
62
|
}
|
package/src/db.ts
CHANGED
|
@@ -151,6 +151,9 @@ function migrateCommentsTable(db: Database): void {
|
|
|
151
151
|
if (!have.has("model")) {
|
|
152
152
|
db.exec(applyPrefix("ALTER TABLE {{comments}} ADD COLUMN model TEXT NOT NULL DEFAULT ''"));
|
|
153
153
|
}
|
|
154
|
+
if (!have.has("runtime")) {
|
|
155
|
+
db.exec(applyPrefix("ALTER TABLE {{comments}} ADD COLUMN runtime TEXT NOT NULL DEFAULT ''"));
|
|
156
|
+
}
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
function migrateLabelsTable(db: Database): void {
|
|
@@ -397,6 +400,7 @@ async function migrateMysqlIssuesAiStatus(pool: Pool): Promise<void> {
|
|
|
397
400
|
await migrateMysqlColumn(pool, "issues", "runtime", "runtime VARCHAR(32) NOT NULL DEFAULT ''");
|
|
398
401
|
await migrateMysqlColumn(pool, "issues", "upstream_issue_number", "upstream_issue_number INT DEFAULT NULL");
|
|
399
402
|
await migrateMysqlColumn(pool, "comments", "model", "model VARCHAR(128) NOT NULL DEFAULT ''");
|
|
403
|
+
await migrateMysqlColumn(pool, "comments", "runtime", "runtime VARCHAR(32) NOT NULL DEFAULT ''");
|
|
400
404
|
await migrateMysqlColumn(pool, "comments", "upstream_comment_id", "upstream_comment_id BIGINT DEFAULT NULL");
|
|
401
405
|
const indexes: Array<[string, string]> = [
|
|
402
406
|
["uq_issues_project_upstream", applyPrefix("CREATE UNIQUE INDEX uq_issues_project_upstream ON {{issues}} (project_id, upstream_issue_number)")],
|
package/src/index.ts
CHANGED
|
@@ -395,6 +395,7 @@ const REPO_VISIBILITY_RE = /^\/([^/]+)\/([^/]+)\/settings\/visibility$/;
|
|
|
395
395
|
const REPO_DISPATCH_RE = /^\/([^/]+)\/([^/]+)\/settings\/dispatch$/;
|
|
396
396
|
|
|
397
397
|
const REPO_WAKE_LOGINS_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/wake-logins$/;
|
|
398
|
+
const REPO_COMMUNITY_WAKE_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/community-wake$/;
|
|
398
399
|
const REPO_CONCURRENCY_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/concurrency$/;
|
|
399
400
|
const REPO_HALT_ALL_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/halt-all$/;
|
|
400
401
|
const SETTINGS_DISPATCH_RE = /^\/settings\/dispatch$/;
|
|
@@ -625,7 +626,25 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
625
626
|
const cfgKv = await getConfigAll();
|
|
626
627
|
const logins = (cfgKv[`wakeLogins:${owner}/${repo}`] ?? "")
|
|
627
628
|
.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
|
|
628
|
-
|
|
629
|
+
const communityWake = cfgKv[`communityWake:${owner}/${repo}`] === "1";
|
|
630
|
+
return json({ logins, communityWake });
|
|
631
|
+
}
|
|
632
|
+
// Machine route: daemons admit new wake logins (thread-trust contagion —
|
|
633
|
+
// a whitelisted participant engaging an external author endorses them).
|
|
634
|
+
if (url.pathname === "/api/v1/wake-logins" && req.method === "POST") {
|
|
635
|
+
const body = await req.json().catch(() => ({})) as { owner?: string; repo?: string; add?: string };
|
|
636
|
+
const owner = (body.owner ?? "").trim();
|
|
637
|
+
const repo = (body.repo ?? "").trim();
|
|
638
|
+
const add = (body.add ?? "").trim();
|
|
639
|
+
if (!owner || !repo || !add) return json({ error: "owner, repo, add required" }, 400);
|
|
640
|
+
if (!/^[\w.-]+$/.test(add)) return json({ error: "invalid login" }, 400);
|
|
641
|
+
const key = `wakeLogins:${owner}/${repo}`;
|
|
642
|
+
const current = (await getConfigAll())[key] ?? "";
|
|
643
|
+
const logins = current.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
|
|
644
|
+
if (logins.some((l) => l.toLowerCase() === add.toLowerCase())) return json({ ok: true, logins });
|
|
645
|
+
logins.push(add);
|
|
646
|
+
await setConfig(key, logins.join(","));
|
|
647
|
+
return json({ ok: true, logins });
|
|
629
648
|
}
|
|
630
649
|
|
|
631
650
|
// Issues JSON API — read side for the ework-issue CLI (pull/open).
|
|
@@ -2260,6 +2279,22 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
2260
2279
|
}
|
|
2261
2280
|
return Response.redirect(aiBack, 303);
|
|
2262
2281
|
}
|
|
2282
|
+
const repoCommunityWakeMatch = url.pathname.match(REPO_COMMUNITY_WAKE_RE);
|
|
2283
|
+
if (repoCommunityWakeMatch) {
|
|
2284
|
+
const [, owner, repo] = repoCommunityWakeMatch;
|
|
2285
|
+
if (!(owner && repo)) return html(errorPage("bad path", ""), 400);
|
|
2286
|
+
const project = await getProject(owner, repo);
|
|
2287
|
+
if (!project) return html(errorPage("项目不存在", ""), 404);
|
|
2288
|
+
const aiBack = `${url.origin}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/settings/ai`;
|
|
2289
|
+
if (!(await canAdminProject(project.id, ctx.user))) {
|
|
2290
|
+
return Response.redirect(`${aiBack}?err=${encodeURIComponent("无权限")}`, 303);
|
|
2291
|
+
}
|
|
2292
|
+
const fd = await req.formData();
|
|
2293
|
+
const on = String(fd.get("enabled") ?? "") === "1";
|
|
2294
|
+
const key = `communityWake:${owner}/${repo}`;
|
|
2295
|
+
if (on) await setConfig(key, "1"); else await deleteConfig(key);
|
|
2296
|
+
return Response.redirect(aiBack, 303);
|
|
2297
|
+
}
|
|
2263
2298
|
if (repoWakeLoginsMatch) {
|
|
2264
2299
|
const [, owner, repo] = repoWakeLoginsMatch;
|
|
2265
2300
|
if (!(owner && repo)) return html(errorPage("bad path", ""), 400);
|
|
@@ -2614,8 +2649,9 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
2614
2649
|
const running = await getRunningSessionsForProject(`${owner}/${repo}`);
|
|
2615
2650
|
const processingCount = new Set(running.map((r) => r.issueNumber)).size;
|
|
2616
2651
|
const wakeLoginsRaw = dispatchCfg[`wakeLogins:${owner}/${repo}`] ?? "";
|
|
2652
|
+
const communityWake = dispatchCfg[`communityWake:${owner}/${repo}`] === "1";
|
|
2617
2653
|
const concurrencyLimit = dispatchCfg[`concurrency:${owner}/${repo}`] ?? "";
|
|
2618
|
-
return html(buildProjectAiPage(project, dispatchOff, globalDispatchOff, processingCount, wakeLoginsRaw, concurrencyLimit).html);
|
|
2654
|
+
return html(buildProjectAiPage(project, dispatchOff, globalDispatchOff, processingCount, wakeLoginsRaw, concurrencyLimit, communityWake).html);
|
|
2619
2655
|
}
|
|
2620
2656
|
|
|
2621
2657
|
const upstreamsPage = url.pathname.match(REPO_UPSTREAMS_RE);
|
package/src/schema-mysql.sql
CHANGED
|
@@ -80,6 +80,7 @@ CREATE TABLE IF NOT EXISTS {{comments}} (
|
|
|
80
80
|
CONSTRAINT {{fk_comments_issue}} FOREIGN KEY (issue_id) REFERENCES {{issues}}(id) ON DELETE CASCADE,
|
|
81
81
|
CONSTRAINT {{fk_comments_author}} FOREIGN KEY (author) REFERENCES {{users}}(login),
|
|
82
82
|
model VARCHAR(128) NOT NULL DEFAULT ''
|
|
83
|
+
,runtime VARCHAR(32) NOT NULL DEFAULT ''
|
|
83
84
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
84
85
|
CREATE INDEX comments_issue_created ON {{comments}} (issue_id, created_at);
|
|
85
86
|
CREATE INDEX comments_author ON {{comments}} (author);
|
package/src/views/projectAi.ts
CHANGED
|
@@ -9,6 +9,7 @@ export function buildProjectAiPage(
|
|
|
9
9
|
processingCount: number,
|
|
10
10
|
wakeLoginsRaw = "",
|
|
11
11
|
concurrencyLimit = "",
|
|
12
|
+
communityWake = false,
|
|
12
13
|
): { html: string } {
|
|
13
14
|
const aiBase = `/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/ai`;
|
|
14
15
|
const dispatchAction = `/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/dispatch`;
|
|
@@ -30,6 +31,17 @@ ${hint}
|
|
|
30
31
|
</form>`;
|
|
31
32
|
})();
|
|
32
33
|
|
|
34
|
+
const communityCard = `<form class="card" method="POST" action="${escapeAttr(`${aiBase}/community-wake`)}">
|
|
35
|
+
<h2>🌍 社区模式</h2>
|
|
36
|
+
<div class="hint">开启后,<b>issue 的作者</b>可自动唤醒自己的 issue(提交后续评论即派单),受每日配额限制;其他人的评论仍走白名单。白名单成员在别人的 issue 下回复时,该 issue 作者会被自动加入白名单。</div>
|
|
37
|
+
<div class="status-line">
|
|
38
|
+
<span class="status-dot ${communityWake ? "on" : "off"}"></span>
|
|
39
|
+
<span class="status-text">${communityWake ? "🌍 社区模式开启" : "⭕ 关闭(仅白名单)"}</span>
|
|
40
|
+
</div>
|
|
41
|
+
<input type="hidden" name="enabled" value="${communityWake ? "0" : "1"}">
|
|
42
|
+
<button type="submit" class="${communityWake ? "secondary" : "primary"}">${communityWake ? "⭕ 关闭社区模式" : "🌍 开启社区模式"}</button>
|
|
43
|
+
</form>`;
|
|
44
|
+
|
|
33
45
|
const wakeList = wakeLoginsRaw.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
|
|
34
46
|
const wakeCard = `<form class="card" method="POST" action="${escapeAttr(`${aiBase}/wake-logins`)}">
|
|
35
47
|
<h2>👥 唤醒白名单</h2>
|
|
@@ -95,6 +107,7 @@ ${projectSettingsTabsHTML(project.owner, project.name, "ai")}
|
|
|
95
107
|
<p class="hint">两个独立控制:<b>🔔 自动接单</b>控制是否自动派新单(不影响运行中);<b>⏹️ 停止</b>杀死当前所有运行中AI会话(不影响接单状态)。模型选择请去 <a href="${escapeAttr(`/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/model`)}">⚙️ 模型</a> 标签页。</p>
|
|
96
108
|
${dispatchCard}
|
|
97
109
|
${wakeCard}
|
|
110
|
+
${communityCard}
|
|
98
111
|
${concurrencyCard}
|
|
99
112
|
${haltCard}
|
|
100
113
|
|
package/src/webhooks.ts
CHANGED
|
@@ -105,6 +105,15 @@ function releaseDeliverySlot(): void {
|
|
|
105
105
|
if (next) next();
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** Await webhook delivery quiescence (tests/ops); throws after timeoutMs. */
|
|
109
|
+
export async function waitForWebhookQueueIdle(timeoutMs = 5_000): Promise<void> {
|
|
110
|
+
const deadline = Date.now() + timeoutMs;
|
|
111
|
+
while (inFlightDeliveries > 0 || deliveryQueue.length > 0) {
|
|
112
|
+
if (Date.now() >= deadline) throw new Error("waitForWebhookQueueIdle: timeout");
|
|
113
|
+
await Bun.sleep(10);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
function now(): string {
|
|
109
118
|
return new Date().toISOString();
|
|
110
119
|
}
|