ework-web 0.10.74 → 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 +1 -1
- package/src/index.ts +12 -0
- package/src/render/layout.ts +17 -14
- package/src/static/issue-actions.js +49 -0
package/package.json
CHANGED
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;
|
package/src/render/layout.ts
CHANGED
|
@@ -110,6 +110,7 @@ header.topbar .num{opacity:.7}
|
|
|
110
110
|
.composer .btn-close.armed{background:#d23f31;color:#fff;border-color:#d23f31}
|
|
111
111
|
.composer-ro{max-width:900px;margin:0 auto .9rem;padding:.7rem 1rem;color:var(--text-muted);font-size:13px;text-align:center;border:1px dashed var(--border);border-radius:8px}
|
|
112
112
|
.tabs{display:flex;gap:.4rem;padding:.6rem 1rem;border-bottom:1px solid var(--border);max-width:900px;margin:0 auto;align-items:center}
|
|
113
|
+
.tabs .brand-tab{font-weight:600;margin-right:.4rem}
|
|
113
114
|
.tab{padding:.4rem 1rem;border-radius:6px;font-size:14px;color:var(--text-muted)}
|
|
114
115
|
.tab.active{background:var(--bg-muted);color:var(--text);font-weight:600}
|
|
115
116
|
.tab-spacer{flex:1}
|
|
@@ -148,6 +149,7 @@ header.topbar .num{opacity:.7}
|
|
|
148
149
|
.ai-dispatch-off{background:#6f7781;color:#fff}
|
|
149
150
|
.ai-completed{background:#1a7f37;color:#fff}
|
|
150
151
|
.ai-failed{background:#cf222e;color:#fff}
|
|
152
|
+
.ai-idle{background:#eaeef2;color:#57606a}
|
|
151
153
|
@keyframes ai-pulse{0%,100%{opacity:1}50%{opacity:.6}}
|
|
152
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}
|
|
153
155
|
.action-btn:hover{background:#cf222e;color:#fff}
|
|
@@ -177,31 +179,32 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
|
|
|
177
179
|
: "";
|
|
178
180
|
const aiBadgeHtml = (() => {
|
|
179
181
|
const s = props.aiStatus ?? "";
|
|
180
|
-
// halted/dispatch_off are represented by their action buttons — skip badge
|
|
181
|
-
if (!s || s === "halted" || s === "dispatch_off") return "";
|
|
182
182
|
const map: Record<string, { cls: string; label: string }> = {
|
|
183
|
-
processing: { cls: "ai-processing", label: "
|
|
184
|
-
completed: { cls: "ai-completed", label: "
|
|
185
|
-
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: "💤 空闲" },
|
|
186
189
|
...props.extraStatusBadges,
|
|
187
190
|
};
|
|
188
|
-
const m = map[s];
|
|
189
|
-
|
|
190
|
-
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>`;
|
|
191
193
|
})();
|
|
192
|
-
const
|
|
193
|
-
const showActions = props.writesEnabled !== false && !isTerminal;
|
|
194
|
+
const showActions = props.writesEnabled !== false;
|
|
194
195
|
const haltBtnHtml = showActions
|
|
195
196
|
? (props.aiStatus === "halted" || props.aiStatus === "failed")
|
|
196
197
|
? `<button type="button" class="action-btn resume-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/resume" data-action-confirm="确认恢复 AI 处理?" title="恢复 AI 处理">▶ 恢复</button>`
|
|
197
|
-
:
|
|
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
|
+
: ""
|
|
198
201
|
: "";
|
|
199
202
|
const dispatchBtnHtml = showActions
|
|
200
203
|
? props.projectDispatchOff
|
|
201
204
|
? `<span class="action-btn dispatch-btn" style="opacity:.5;cursor:not-allowed" title="项目已关闭自动接单">🔕 项目不接单</span>`
|
|
202
205
|
: props.aiStatus === "dispatch_off"
|
|
203
|
-
? `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-on" title="允许自动接单">🔔
|
|
204
|
-
: `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-off" data-action-confirm="设为不自动接单?" title="
|
|
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>`
|
|
205
208
|
: "";
|
|
206
209
|
const modelSelectHtml = props.modelSelect && props.modelSelect.options.length > 0 && showActions
|
|
207
210
|
? `<span class="model-select-wrap"><select class="model-select" id="issueModelSelect" title="此 issue 的模型(覆盖项目/全局默认)">
|
|
@@ -335,7 +338,7 @@ export function tabNavHTML(active: "projects" | "issues" | "sessions" | "me", us
|
|
|
335
338
|
const userArea = user
|
|
336
339
|
? `<span class="user-area"><a class="tab${m}" href="/me" title="${escapeAttr(user.login)}">👤 ${escapeHtml(user.login)}</a><form method="post" action="/logout" class="logout-form"><button type="submit" class="logout-btn" title="退出登录">退出</button></form></span>`
|
|
337
340
|
: "";
|
|
338
|
-
return `<nav class="tabs"><a class="tab${p}" href="/projects">项目</a><a class="tab${i}" href="/issues">Issues</a><a class="tab${s}" href="/sessions">会话</a><span style="margin-left:auto" class="tab-spacer"></span><a class="tab" href="/settings">⚙️ 设置</a>${userArea}</nav>`;
|
|
341
|
+
return `<nav class="tabs"><a class="tab brand-tab" href="/" title="ework 主页">🏠 ework</a><a class="tab${p}" href="/projects">项目</a><a class="tab${i}" href="/issues">Issues</a><a class="tab${s}" href="/sessions">会话</a><span style="margin-left:auto" class="tab-spacer"></span><a class="tab" href="/settings">⚙️ 设置</a>${userArea}</nav>`;
|
|
339
342
|
}
|
|
340
343
|
|
|
341
344
|
export interface ActionBarOpts {
|
|
@@ -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;
|