ework-web 0.10.122 → 0.10.124

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.122",
3
+ "version": "0.10.124",
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",
package/src/index.ts CHANGED
@@ -82,6 +82,7 @@ import {
82
82
  getUpstreamSync,
83
83
  type ProjectRole,
84
84
  type UserRow,
85
+ getIssueAiStatusByNumber,
85
86
  } from "./store";
86
87
  import { startUpstreamSyncPoller } from "./upstream-sync";
87
88
  import {
@@ -394,6 +395,7 @@ const REPO_VISIBILITY_RE = /^\/([^/]+)\/([^/]+)\/settings\/visibility$/;
394
395
  const REPO_DISPATCH_RE = /^\/([^/]+)\/([^/]+)\/settings\/dispatch$/;
395
396
 
396
397
  const REPO_WAKE_LOGINS_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/wake-logins$/;
398
+ const REPO_COMMUNITY_WAKE_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/community-wake$/;
397
399
  const REPO_CONCURRENCY_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/concurrency$/;
398
400
  const REPO_HALT_ALL_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/halt-all$/;
399
401
  const SETTINGS_DISPATCH_RE = /^\/settings\/dispatch$/;
@@ -624,7 +626,25 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
624
626
  const cfgKv = await getConfigAll();
625
627
  const logins = (cfgKv[`wakeLogins:${owner}/${repo}`] ?? "")
626
628
  .split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
627
- 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 });
628
648
  }
629
649
 
630
650
  // Issues JSON API — read side for the ework-issue CLI (pull/open).
@@ -2259,6 +2279,22 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
2259
2279
  }
2260
2280
  return Response.redirect(aiBack, 303);
2261
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
+ }
2262
2298
  if (repoWakeLoginsMatch) {
2263
2299
  const [, owner, repo] = repoWakeLoginsMatch;
2264
2300
  if (!(owner && repo)) return html(errorPage("bad path", ""), 400);
@@ -2515,7 +2551,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
2515
2551
  }
2516
2552
  const since = url.searchParams.get("since") ?? new Date(0).toISOString();
2517
2553
  const views = await fetchIssueSince(owner, repo, number, since);
2518
- return json({ comments: views });
2554
+ return json({ comments: views, aiStatus: await getIssueAiStatusByNumber(owner, repo, number) });
2519
2555
  } catch (e) {
2520
2556
  return json({ error: errMsg(e) }, e instanceof StoreError ? e.status : 500);
2521
2557
  }
@@ -2613,8 +2649,9 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
2613
2649
  const running = await getRunningSessionsForProject(`${owner}/${repo}`);
2614
2650
  const processingCount = new Set(running.map((r) => r.issueNumber)).size;
2615
2651
  const wakeLoginsRaw = dispatchCfg[`wakeLogins:${owner}/${repo}`] ?? "";
2652
+ const communityWake = dispatchCfg[`communityWake:${owner}/${repo}`] === "1";
2616
2653
  const concurrencyLimit = dispatchCfg[`concurrency:${owner}/${repo}`] ?? "";
2617
- return html(buildProjectAiPage(project, dispatchOff, globalDispatchOff, processingCount, wakeLoginsRaw, concurrencyLimit).html);
2654
+ return html(buildProjectAiPage(project, dispatchOff, globalDispatchOff, processingCount, wakeLoginsRaw, concurrencyLimit, communityWake).html);
2618
2655
  }
2619
2656
 
2620
2657
  const upstreamsPage = url.pathname.match(REPO_UPSTREAMS_RE);
package/src/static/app.js CHANGED
@@ -562,10 +562,29 @@
562
562
  }
563
563
 
564
564
  let pollTimer = null;
565
+ const BADGES = {
566
+ "": ["ai-idle", "💤 空闲"],
567
+ processing: ["ai-processing", "🔄 AI 处理中"],
568
+ queued: ["ai-queued", "⏳ 排队中"],
569
+ completed: ["ai-completed", "✅ AI 已完成"],
570
+ failed: ["ai-failed", "⚠️ AI 失败"],
571
+ halted: ["ai-halted", "⏹ 已停止"],
572
+ dispatch_off: ["ai-dispatch-off", "🔕 不接单中"],
573
+ };
574
+ function syncBadge(status) {
575
+ const el = document.getElementById("aiStatusBadge");
576
+ if (!el || status === undefined || status === null) return;
577
+ if (el.dataset.status === status) return;
578
+ const m = BADGES[status] || BADGES[""];
579
+ el.dataset.status = status;
580
+ el.className = "ai-badge " + m[0];
581
+ el.textContent = m[1];
582
+ }
565
583
  async function poll() {
566
584
  try {
567
585
  const data = await api("since", { since: state.sinceISO });
568
586
  if (!data || data.error) return;
587
+ syncBadge(data.aiStatus);
569
588
  const views = data.comments || [];
570
589
  if (!views.length) return;
571
590
  mergeFront(views, true);
package/src/store.ts CHANGED
@@ -528,6 +528,14 @@ export async function updateIssueAiStatus(issueId: number, status: string): Prom
528
528
  await getDB().run("UPDATE {{issues}} SET ai_status = ? WHERE id = ?", [status, issueId]);
529
529
  }
530
530
 
531
+ export async function getIssueAiStatusByNumber(owner: string, repo: string, number: number): Promise<string> {
532
+ const row = await getDB().get<{ ai_status: string }>(
533
+ "SELECT i.ai_status FROM {{issues}} i JOIN {{projects}} p ON i.project_id = p.id WHERE p.owner = ? AND p.name = ? AND i.number = ?",
534
+ [owner, repo, number],
535
+ );
536
+ return row?.ai_status ?? "";
537
+ }
538
+
531
539
  export async function getIssueAiStatus(issueId: number): Promise<string> {
532
540
  const row = await getDB().get<{ ai_status: string }>("SELECT ai_status FROM {{issues}} WHERE id = ?", [issueId]);
533
541
  return row?.ai_status ?? "";
@@ -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