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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-web",
3
- "version": "0.10.123",
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": "latest",
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
- return json({ logins });
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);
@@ -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);
@@ -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
  }