ework-web 0.10.75 → 0.10.76

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.75",
3
+ "version": "0.10.76",
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
@@ -397,6 +397,7 @@ const REPO_LABEL_ADD_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/add$/;
397
397
  const REPO_LABEL_ACTION_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/(\d+)\/(update|archive|unarchive|delete)$/;
398
398
  const REPO_ISSUE_HALT_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/(halt|resume|dispatch-off|dispatch-on)$/;
399
399
  const REPO_ISSUE_MODEL_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/model$/;
400
+ const REPO_ISSUE_STATUS_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/ai-status$/;
400
401
  const API_ISSUE_LABELS_RE = /^\/api\/([^/]+)\/([^/]+)\/issues\/(\d+)\/labels$/;
401
402
  const WH_ACTION_RE = /^\/__wh\/(\d+)\/(delete|toggle|test)$/;
402
403
  const SESSIONS_RE = /^\/sessions$/;
@@ -1696,6 +1697,17 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
1696
1697
  }
1697
1698
  }
1698
1699
 
1700
+ const aiStatusMatch = url.pathname.match(REPO_ISSUE_STATUS_RE);
1701
+ if (aiStatusMatch && req.method === "GET") {
1702
+ const [, owner, repo, numStr] = aiStatusMatch;
1703
+ if (!(owner && repo && numStr)) return json({ error: "bad path" }, 400);
1704
+ const project = await getProject(owner, repo);
1705
+ if (!project) return json({ error: "project not found" }, 404);
1706
+ const issue = await getIssueWithMeta(project.id, Number(numStr));
1707
+ if (!issue) return json({ error: "issue not found" }, 404);
1708
+ return json({ aiStatus: issue.ai_status ?? "", commentCount: issue.comment_count ?? 0 });
1709
+ }
1710
+
1699
1711
  const issueLabelsApi = url.pathname.match(API_ISSUE_LABELS_RE);
1700
1712
  if (issueLabelsApi) {
1701
1713
  const [, owner, repo, numStr] = issueLabelsApi;
@@ -149,6 +149,7 @@ header.topbar .num{opacity:.7}
149
149
  .ai-dispatch-off{background:#6f7781;color:#fff}
150
150
  .ai-completed{background:#1a7f37;color:#fff}
151
151
  .ai-failed{background:#cf222e;color:#fff}
152
+ .ai-idle{background:#eaeef2;color:#57606a}
152
153
  @keyframes ai-pulse{0%,100%{opacity:1}50%{opacity:.6}}
153
154
  .action-btn{font-size:12px;padding:.15rem .6rem;border-radius:6px;border:1px solid var(--border);background:var(--bg-elev);color:#cf222e;cursor:pointer;font-weight:600}
154
155
  .action-btn:hover{background:#cf222e;color:#fff}
@@ -178,31 +179,32 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
178
179
  : "";
179
180
  const aiBadgeHtml = (() => {
180
181
  const s = props.aiStatus ?? "";
181
- // halted/dispatch_off are represented by their action buttons — skip badge
182
- if (!s || s === "halted" || s === "dispatch_off") return "";
183
182
  const map: Record<string, { cls: string; label: string }> = {
184
- processing: { cls: "ai-processing", label: "⚙️ 处理中" },
185
- completed: { cls: "ai-completed", label: " 已完成" },
186
- failed: { cls: "ai-failed", label: " 失败" },
183
+ processing: { cls: "ai-processing", label: "🔄 AI 处理中" },
184
+ completed: { cls: "ai-completed", label: " AI 已完成" },
185
+ failed: { cls: "ai-failed", label: "⚠️ AI 失败" },
186
+ halted: { cls: "ai-halted", label: "⏹ 已停止" },
187
+ dispatch_off: { cls: "ai-dispatch-off", label: "🔕 不接单中" },
188
+ idle: { cls: "ai-idle", label: "💤 空闲" },
187
189
  ...props.extraStatusBadges,
188
190
  };
189
- const m = map[s];
190
- if (!m) return "";
191
- return `<span class="ai-badge ${m.cls}">${m.label}</span>`;
191
+ const m = map[s] ?? { cls: "ai-idle", label: "💤 空闲" };
192
+ return `<span class="ai-badge ${m.cls}" id="aiStatusBadge" data-status="${escapeAttr(s)}">${m.label}</span>`;
192
193
  })();
193
- const isTerminal = props.aiStatus === "completed";
194
- const showActions = props.writesEnabled !== false && !isTerminal;
194
+ const showActions = props.writesEnabled !== false;
195
195
  const haltBtnHtml = showActions
196
196
  ? (props.aiStatus === "halted" || props.aiStatus === "failed")
197
197
  ? `<button type="button" class="action-btn resume-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/resume" data-action-confirm="确认恢复 AI 处理?" title="恢复 AI 处理">▶ 恢复</button>`
198
- : `<button type="button" class="action-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/halt" data-action-confirm="确认停止 AI 处理?" title="停止 AI 处理">⏹ 停止</button>`
198
+ : props.aiStatus === "processing"
199
+ ? `<button type="button" class="action-btn" id="aiStopBtn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/halt" data-action-confirm="确认停止 AI 处理?" title="停止正在运行的 AI">⏹ 停止运行</button>`
200
+ : ""
199
201
  : "";
200
202
  const dispatchBtnHtml = showActions
201
203
  ? props.projectDispatchOff
202
204
  ? `<span class="action-btn dispatch-btn" style="opacity:.5;cursor:not-allowed" title="项目已关闭自动接单">🔕 项目不接单</span>`
203
205
  : props.aiStatus === "dispatch_off"
204
- ? `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-on" title="允许自动接单">🔔 接单</button>`
205
- : `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-off" data-action-confirm="设为不自动接单?" title="设为不自动接单">🔕 不接单</button>`
206
+ ? `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-on" title="允许自动接单">🔔 恢复接单</button>`
207
+ : `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-off" data-action-confirm="设为不自动接单?" title="关闭此 issue 的自动接单">🔕 暂停接单</button>`
206
208
  : "";
207
209
  const modelSelectHtml = props.modelSelect && props.modelSelect.options.length > 0 && showActions
208
210
  ? `<span class="model-select-wrap"><select class="model-select" id="issueModelSelect" title="此 issue 的模型(覆盖项目/全局默认)">
@@ -6,6 +6,55 @@
6
6
  // data-action-confirm="确认停止?" — if set, confirm() first
7
7
  // data-action-reload="true" — default true, reload on success
8
8
  (function () {
9
+ var BADGES = {
10
+ "processing": ["ai-processing", "🔄 AI 处理中"],
11
+ "completed": ["ai-completed", "✅ AI 已完成"],
12
+ "failed": ["ai-failed", "⚠️ AI 失败"],
13
+ "halted": ["ai-halted", "⏹ 已停止"],
14
+ "dispatch_off": ["ai-dispatch-off", "🔕 不接单中"]
15
+ };
16
+
17
+ function issuePath() {
18
+ var m = location.pathname.match(/^(\/[^/]+\/[^/]+\/issues\/\d+)/);
19
+ return m ? m[1] : null;
20
+ }
21
+
22
+ function refreshBadge() {
23
+ var p = issuePath();
24
+ var badge = document.getElementById("aiStatusBadge");
25
+ if (!p || !badge) return;
26
+ fetch(p + "/ai-status", { headers: { Accept: "application/json" } })
27
+ .then(function (r) { return r.ok ? r.json() : null; })
28
+ .then(function (d) {
29
+ if (!d) return;
30
+ var b = BADGES[d.aiStatus] || ["ai-idle", "💤 空闲"];
31
+ badge.textContent = b[1];
32
+ badge.className = "ai-badge " + b[0];
33
+ var stopBtn = document.getElementById("aiStopBtn");
34
+ if (stopBtn) stopBtn.style.display = d.aiStatus === "processing" ? "" : "none";
35
+ var initial = Number(badge.getAttribute("data-comment-count") || 0);
36
+ if (d.commentCount > initial) {
37
+ var bar = document.getElementById("newReplyBar");
38
+ if (!bar) {
39
+ bar = document.createElement("div");
40
+ bar.id = "newReplyBar";
41
+ bar.style.cssText = "margin:.6rem 0;padding:.5rem .8rem;border-radius:8px;background:#ddf4ff;border:1px solid #54aeff;color:#0969da;cursor:pointer";
42
+ bar.textContent = "💬 有新回复 · 点击刷新查看";
43
+ bar.onclick = function () { location.reload(); };
44
+ badge.parentNode.insertBefore(bar, badge.nextSibling);
45
+ }
46
+ }
47
+ })
48
+ .catch(function () {});
49
+ }
50
+
51
+ if (document.getElementById("aiStatusBadge")) {
52
+ var bc = document.getElementById("aiStatusBadge");
53
+ var items = document.querySelectorAll('[id^="comment-"]');
54
+ bc.setAttribute("data-comment-count", String(items.length));
55
+ setInterval(refreshBadge, 8000);
56
+ }
57
+
9
58
  document.addEventListener("click", function (e) {
10
59
  var btn = e.target.closest("[data-action-href]");
11
60
  if (!btn) return;