ework-web 0.10.59 → 0.10.60

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.59",
3
+ "version": "0.10.60",
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
@@ -38,6 +38,7 @@ import {
38
38
  postComment,
39
39
  setIssueState,
40
40
  updateIssueAiStatus,
41
+ listIssues,
41
42
  createAttachment,
42
43
  getAttachment,
43
44
  verifyUserPassword,
@@ -375,6 +376,7 @@ const REPO_MEMBER_ACTION_RE = /^\/([^/]+)\/([^/]+)\/settings\/members\/([^/]+)\/
375
376
  const REPO_MEMBER_ADD_RE = /^\/([^/]+)\/([^/]+)\/settings\/members\/add$/;
376
377
  const REPO_VISIBILITY_RE = /^\/([^/]+)\/([^/]+)\/settings\/visibility$/;
377
378
  const REPO_DISPATCH_RE = /^\/([^/]+)\/([^/]+)\/settings\/dispatch$/;
379
+ const REPO_HALT_ALL_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/halt-all$/;
378
380
  const SETTINGS_DISPATCH_RE = /^\/settings\/dispatch$/;
379
381
  const REPO_UPSTREAMS_RE = /^\/([^/]+)\/([^/]+)\/settings\/upstreams$/;
380
382
  const REPO_MODEL_RE = /^\/([^/]+)\/([^/]+)\/settings\/model$/;
@@ -1283,7 +1285,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
1283
1285
  return json({ ok: true, enabled });
1284
1286
  }
1285
1287
 
1286
- if (req.method === "GET" && url.pathname === "/api/wake-policy") {
1288
+ if (url.pathname === "/api/wake-policy" && req.method === "GET") {
1287
1289
  if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
1288
1290
  const cfg = await getConfigAll();
1289
1291
  const appCfg = await loadConfig();
@@ -1954,7 +1956,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
1954
1956
  if (!(owner && repo)) return html(errorPage("bad path", ""), 400);
1955
1957
  const project = await getProject(owner, repo);
1956
1958
  if (!project) return html(errorPage("项目不存在", ""), 404);
1957
- const back = `${url.origin}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/settings/members`;
1959
+ const back = `${url.origin}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/settings/ai`;
1958
1960
  if (!(await canAdminProject(project.id, ctx.user))) {
1959
1961
  return Response.redirect(`${back}?err=${encodeURIComponent("无权限")}`, 303);
1960
1962
  }
@@ -1965,6 +1967,25 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
1965
1967
  return Response.redirect(`${back}?ok=1&ok_msg=${encodeURIComponent(isOff ? "已开启自动接单" : "已关闭自动接单")}`, 303);
1966
1968
  }
1967
1969
 
1970
+ const repoHaltAllMatch = url.pathname.match(REPO_HALT_ALL_RE);
1971
+ if (repoHaltAllMatch) {
1972
+ const [, owner, repo] = repoHaltAllMatch;
1973
+ if (!(owner && repo)) return html(errorPage("bad path", ""), 400);
1974
+ const project = await getProject(owner, repo);
1975
+ if (!project) return html(errorPage("项目不存在", ""), 404);
1976
+ const back = `${url.origin}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/settings/ai`;
1977
+ if (!(await canAdminProject(project.id, ctx.user))) {
1978
+ return Response.redirect(`${back}?err=${encodeURIComponent("无权限")}`, 303);
1979
+ }
1980
+ const issues = await listIssues(project.id, { state: "all" });
1981
+ const processing = issues.filter((it) => it.ai_status === "processing");
1982
+ for (const it of processing) {
1983
+ await updateIssueAiStatus(it.id, "halted");
1984
+ void emitStatusChanged(project.id, it.number, "processing", "halted", ctx.user!.login, url.origin);
1985
+ }
1986
+ return Response.redirect(`${back}?ok=1&ok_msg=${encodeURIComponent(`已停止 ${processing.length} 个运行中AI会话`)}`, 303);
1987
+ }
1988
+
1968
1989
  const settingsDispatchMatch = url.pathname.match(SETTINGS_DISPATCH_RE);
1969
1990
  if (settingsDispatchMatch) {
1970
1991
  if (!ctx.user || ctx.user.is_admin !== 1) return html(errorPage("需要管理员权限", ""), 403);
@@ -2218,7 +2239,9 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
2218
2239
  const dispatchCfg = await getConfigAll();
2219
2240
  const dispatchOff = dispatchCfg[`dispatchOff:${owner}/${repo}`] === "1";
2220
2241
  const globalDispatchOff = dispatchCfg["dispatchEnabled"] === "false";
2221
- return html(buildProjectAiPage(project, dispatchOff, globalDispatchOff).html);
2242
+ const processingIssues = await listIssues(project.id, { state: "all" });
2243
+ const processingCount = processingIssues.filter((it) => it.ai_status === "processing").length;
2244
+ return html(buildProjectAiPage(project, dispatchOff, globalDispatchOff, processingCount).html);
2222
2245
  }
2223
2246
 
2224
2247
  const upstreamsPage = url.pathname.match(REPO_UPSTREAMS_RE);
@@ -275,7 +275,7 @@ export function aiStatusBadge(status: string | undefined): string {
275
275
  if (!s) return "";
276
276
  const map: Record<string, string> = {
277
277
  processing: '<span class="ai-status-list" style="color:var(--accent)">⚙️ 处理中</span>',
278
- halted: '<span class="ai-status-list" style="color:#bf8700">⏸️ 已暂停</span>',
278
+ halted: '<span class="ai-status-list" style="color:#bf8700">⏹️ 已停止</span>',
279
279
  dispatch_off: '<span class="ai-status-list" style="color:#6f7781">🔕 不接单</span>',
280
280
  completed: '<span class="ai-status-list" style="color:var(--green)">✓ 已完成</span>',
281
281
  failed: '<span class="ai-status-list" style="color:#cf222e">✗ 失败</span>',
@@ -6,25 +6,43 @@ export function buildProjectAiPage(
6
6
  project: ProjectRow,
7
7
  dispatchOff: boolean,
8
8
  globalDispatchOff: boolean,
9
+ processingCount: number,
9
10
  ): { html: string } {
11
+ const aiBase = `/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/ai`;
10
12
  const dispatchAction = `/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/dispatch`;
13
+ const haltAllAction = `${aiBase}/halt-all`;
11
14
 
12
15
  const dispatchCard = (() => {
13
16
  const disabled = globalDispatchOff;
14
17
  const hint = disabled
15
18
  ? `<div class="hint">⚠️ 全局自动接单已<a href="/settings">关闭</a>。项目级开关在此状态下无效——需先开启全局。</div>`
16
- : `<div class="hint">关闭后,本项目新建 issue 不会自动派给 AI(评论仍可显式召唤)。</div>`;
19
+ : `<div class="hint">关闭后,本项目新建 issue 不会自动派给 AI(评论仍可显式召唤)。<b>不影响已在运行的会话。</b></div>`;
17
20
  return `<form class="card" method="POST" action="${escapeAttr(dispatchAction)}">
18
- <h2>自动接单</h2>
21
+ <h2>🔔 自动接单</h2>
19
22
  ${hint}
20
23
  <div class="status-line">
21
24
  <span class="status-dot ${dispatchOff ? "off" : "on"}"></span>
22
- <span class="status-text">${dispatchOff ? "已暂停" : "运行中"}</span>
25
+ <span class="status-text">${dispatchOff ? "🔕 不接单" : "🔔 接单中"}</span>
23
26
  </div>
24
27
  <button type="submit" class="${dispatchOff ? "primary" : "secondary"}" ${disabled ? "disabled" : ""}>${dispatchOff ? "🔔 开启自动接单" : "🔕 关闭自动接单"}</button>
25
28
  </form>`;
26
29
  })();
27
30
 
31
+ const haltCard = (() => {
32
+ const hint = processingCount > 0
33
+ ? `当前有 <b>${processingCount}</b> 个 AI 会话正在运行。点击下方按钮将全部停止(向 daemon 发送 halt 信号)。`
34
+ : `当前没有正在运行的 AI 会话。`;
35
+ return `<form class="card" method="POST" action="${escapeAttr(haltAllAction)}">
36
+ <h2>⏹️ 停止所有运行中AI</h2>
37
+ <div class="hint">${hint} <b>不影响自动接单状态。</b></div>
38
+ <div class="status-line">
39
+ <span class="status-dot ${processingCount > 0 ? "on" : "off"}"></span>
40
+ <span class="status-text">${processingCount > 0 ? `${processingCount} 个会话运行中` : "空闲"}</span>
41
+ </div>
42
+ <button type="submit" class="danger" ${processingCount === 0 ? "disabled" : ""}>⏹️ 停止全部</button>
43
+ </form>`;
44
+ })();
45
+
28
46
  const html = `<!doctype html>
29
47
  <html lang="zh"><head><meta charset="utf-8">
30
48
  <meta name="viewport" content="width=device-width,initial-scale=1">
@@ -49,6 +67,7 @@ h1{font-size:18px;margin:0 0 .4rem}
49
67
  button{padding:.5rem 1.2rem;border:0;border-radius:6px;font-size:13px;cursor:pointer}
50
68
  button.primary{background:var(--accent);color:#fff}
51
69
  button.secondary{background:var(--bg-muted);color:var(--text);border:1px solid var(--border)}
70
+ button.danger{background:#da3633;color:#fff}
52
71
  button:disabled{opacity:.5;cursor:not-allowed}
53
72
  .future{opacity:.5;pointer-events:none}
54
73
  </style></head><body>
@@ -56,8 +75,9 @@ button:disabled{opacity:.5;cursor:not-allowed}
56
75
  <main class="wrap">
57
76
  ${projectSettingsTabsHTML(project.owner, project.name, "ai")}
58
77
  <h1>🤖 AI 设置</h1>
59
- <p class="hint">AI / 智能体相关的项目级配置。模型选择请去 <a href="${escapeAttr(`/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/model`)}">⚙️ 模型</a> 标签页。</p>
78
+ <p class="hint">两个独立控制:<b>🔔 自动接单</b>控制是否自动派新单(不影响运行中);<b>⏹️ 停止</b>杀死当前所有运行中AI会话(不影响接单状态)。模型选择请去 <a href="${escapeAttr(`/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/model`)}">⚙️ 模型</a> 标签页。</p>
60
79
  ${dispatchCard}
80
+ ${haltCard}
61
81
 
62
82
  <div class="card future">
63
83
  <h2>更多配置(规划中)</h2>