ework-web 0.10.58 → 0.10.59

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.58",
3
+ "version": "0.10.59",
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
@@ -107,6 +107,7 @@ import { buildProjectMembersPage } from "./views/projectMembers";
107
107
  import { buildProjectUpstreamsPage, trySetUpstreamUrls } from "./views/projectUpstreams";
108
108
  import { buildProjectLabelsPage } from "./views/projectLabels";
109
109
  import { buildProjectModelPage } from "./views/projectModel";
110
+ import { buildProjectAiPage } from "./views/projectAi";
110
111
  import { handleGiteaApi } from "./giteaApi";
111
112
  import { deployRemoteDaemon, deployBatch, type DeployTarget } from "./daemon-deploy";
112
113
 
@@ -377,6 +378,7 @@ const REPO_DISPATCH_RE = /^\/([^/]+)\/([^/]+)\/settings\/dispatch$/;
377
378
  const SETTINGS_DISPATCH_RE = /^\/settings\/dispatch$/;
378
379
  const REPO_UPSTREAMS_RE = /^\/([^/]+)\/([^/]+)\/settings\/upstreams$/;
379
380
  const REPO_MODEL_RE = /^\/([^/]+)\/([^/]+)\/settings\/model$/;
381
+ const REPO_AI_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai$/;
380
382
  const REPO_LABELS_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels$/;
381
383
  const REPO_LABEL_ADD_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/add$/;
382
384
  const REPO_LABEL_ACTION_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/(\d+)\/(update|archive|unarchive|delete)$/;
@@ -2201,9 +2203,22 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
2201
2203
  const flashKind = url.searchParams.get("ok") === "1" ? "ok" : url.searchParams.get("err") ? "err" : null;
2202
2204
  const flashMsg = flashKind === "ok" ? (url.searchParams.get("ok_msg") ?? "") : (url.searchParams.get("err") ?? "");
2203
2205
  const flash = flashKind ? { kind: flashKind as "ok" | "err", msg: flashMsg } : null;
2206
+ return html(await buildProjectMembersPage(ctx.user!, project, flash));
2207
+ }
2208
+
2209
+ const aiPage = url.pathname.match(REPO_AI_RE);
2210
+ if (aiPage) {
2211
+ const [, owner, repo] = aiPage;
2212
+ if (!(owner && repo)) return html(errorPage("404", "bad path"), 404);
2213
+ const project = await getProject(owner, repo);
2214
+ if (!project) return html(errorPage("项目不存在", "项目未创建"), 404);
2215
+ if (!(await canAdminProject(project.id, ctx.user))) {
2216
+ return html(errorPage("无权限", "需要该项目 admin 角色才能管理 AI 设置"), 403);
2217
+ }
2204
2218
  const dispatchCfg = await getConfigAll();
2205
2219
  const dispatchOff = dispatchCfg[`dispatchOff:${owner}/${repo}`] === "1";
2206
- return html(await buildProjectMembersPage(ctx.user!, project, flash, dispatchOff));
2220
+ const globalDispatchOff = dispatchCfg["dispatchEnabled"] === "false";
2221
+ return html(buildProjectAiPage(project, dispatchOff, globalDispatchOff).html);
2207
2222
  }
2208
2223
 
2209
2224
  const upstreamsPage = url.pathname.match(REPO_UPSTREAMS_RE);
@@ -0,0 +1,68 @@
1
+ import { THEME_CSS, escapeHtml, escapeAttr } from "../render/layout";
2
+ import { projectSettingsTabsHTML } from "./projectUpstreams";
3
+ import type { ProjectRow } from "../store";
4
+
5
+ export function buildProjectAiPage(
6
+ project: ProjectRow,
7
+ dispatchOff: boolean,
8
+ globalDispatchOff: boolean,
9
+ ): { html: string } {
10
+ const dispatchAction = `/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/dispatch`;
11
+
12
+ const dispatchCard = (() => {
13
+ const disabled = globalDispatchOff;
14
+ const hint = disabled
15
+ ? `<div class="hint">⚠️ 全局自动接单已<a href="/settings">关闭</a>。项目级开关在此状态下无效——需先开启全局。</div>`
16
+ : `<div class="hint">关闭后,本项目新建 issue 不会自动派给 AI(评论仍可显式召唤)。</div>`;
17
+ return `<form class="card" method="POST" action="${escapeAttr(dispatchAction)}">
18
+ <h2>自动接单</h2>
19
+ ${hint}
20
+ <div class="status-line">
21
+ <span class="status-dot ${dispatchOff ? "off" : "on"}"></span>
22
+ <span class="status-text">${dispatchOff ? "已暂停" : "运行中"}</span>
23
+ </div>
24
+ <button type="submit" class="${dispatchOff ? "primary" : "secondary"}" ${disabled ? "disabled" : ""}>${dispatchOff ? "🔔 开启自动接单" : "🔕 关闭自动接单"}</button>
25
+ </form>`;
26
+ })();
27
+
28
+ const html = `<!doctype html>
29
+ <html lang="zh"><head><meta charset="utf-8">
30
+ <meta name="viewport" content="width=device-width,initial-scale=1">
31
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
32
+ <title>${escapeHtml(project.owner + "/" + project.name)} · AI 设置</title>
33
+ <style>${THEME_CSS}
34
+ .nav{display:flex;align-items:center;gap:.5rem;padding:.55rem 1rem;background:var(--header-bg);color:var(--header-text);font-size:13px}
35
+ .nav a{color:var(--header-text);opacity:.95}
36
+ .wrap{max-width:720px;margin:0 auto;padding:1rem}
37
+ .subtabs{display:flex;gap:.4rem;margin-bottom:1rem;flex-wrap:wrap}
38
+ .subtab{padding:.35rem .7rem;border:1px solid var(--border);border-radius:6px;background:var(--bg-elev);color:var(--text-muted);font-size:13px;text-decoration:none}
39
+ .subtab.active{background:var(--accent);color:#fff;border-color:var(--accent)}
40
+ h1{font-size:18px;margin:0 0 .4rem}
41
+ .hint{color:var(--text-muted);font-size:13px;margin:.3rem 0 .6rem;line-height:1.5}
42
+ .card{border:1px solid var(--border);border-radius:10px;padding:.9rem 1rem;background:var(--bg-elev);margin-bottom:.9rem}
43
+ .card h2{font-size:14px;margin:0 0 .5rem}
44
+ .status-line{display:flex;align-items:center;gap:.5rem;margin-bottom:.7rem}
45
+ .status-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
46
+ .status-dot.on{background:var(--green,#3fb950)}
47
+ .status-dot.off{background:var(--text-muted)}
48
+ .status-text{font-size:13px;font-weight:600}
49
+ button{padding:.5rem 1.2rem;border:0;border-radius:6px;font-size:13px;cursor:pointer}
50
+ button.primary{background:var(--accent);color:#fff}
51
+ button.secondary{background:var(--bg-muted);color:var(--text);border:1px solid var(--border)}
52
+ button:disabled{opacity:.5;cursor:not-allowed}
53
+ .future{opacity:.5;pointer-events:none}
54
+ </style></head><body>
55
+ <header class="nav"><a href="/" style="color:var(--header-text)">🏠 ework-web</a><span style="opacity:.8"> · ${escapeHtml(project.owner + "/" + project.name)}</span></header>
56
+ <main class="wrap">
57
+ ${projectSettingsTabsHTML(project.owner, project.name, "ai")}
58
+ <h1>🤖 AI 设置</h1>
59
+ <p class="hint">AI / 智能体相关的项目级配置。模型选择请去 <a href="${escapeAttr(`/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/model`)}">⚙️ 模型</a> 标签页。</p>
60
+ ${dispatchCard}
61
+
62
+ <div class="card future">
63
+ <h2>更多配置(规划中)</h2>
64
+ <div class="hint">后续版本将在此标签页增加:项目级唤醒策略、Nudge 间隔、Observer 开关等。</div>
65
+ </div>
66
+ </main></body></html>`;
67
+ return { html };
68
+ }
@@ -26,7 +26,6 @@ export async function buildProjectMembersPage(
26
26
  viewer: UserRow,
27
27
  project: ProjectRow,
28
28
  flash: Flash | null,
29
- dispatchOff: boolean,
30
29
  ): Promise<string> {
31
30
  const members = await listProjectMembersWithUsers(project.id);
32
31
  const users = await listUsers();
@@ -123,12 +122,6 @@ ${flashHtml}
123
122
  <button class="primary" type="submit">保存</button>
124
123
  </form>
125
124
 
126
- <form class="card" method="POST" action="${escapeAttr(`/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/dispatch`)}">
127
- <h2>AI 自动接单</h2>
128
- <div class="hint">关闭后,本项目新建 issue 不会自动派给 AI;评论仍可显式召唤。</div>
129
- <button type="submit" class="${dispatchOff ? "primary" : "secondary"}">${dispatchOff ? "🔔 开启自动接单" : "🔕 关闭自动接单"}</button>
130
- </form>
131
-
132
125
  <div class="card">
133
126
  <h2>当前成员(${members.length})</h2>
134
127
  ${rowsHtml}
@@ -16,7 +16,7 @@ interface Flash {
16
16
  export function projectSettingsTabsHTML(
17
17
  owner: string,
18
18
  name: string,
19
- active: "webhooks" | "members" | "upstreams" | "model" | "labels",
19
+ active: "webhooks" | "members" | "upstreams" | "model" | "labels" | "ai",
20
20
  ): string {
21
21
  const base = `/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/settings`;
22
22
  const cls = (which: typeof active) => (active === which ? " active" : "");
@@ -25,7 +25,8 @@ export function projectSettingsTabsHTML(
25
25
  <a class="subtab${cls("members")}" href="${escapeAttr(base)}/members">成员</a>
26
26
  <a class="subtab${cls("upstreams")}" href="${escapeAttr(base)}/upstreams">上游</a>
27
27
  <a class="subtab${cls("labels")}" href="${escapeAttr(base)}/labels">🏷️ 标签</a>
28
- <a class="subtab${cls("model")}" href="${escapeAttr(base)}/model">🤖 模型</a>
28
+ <a class="subtab${cls("ai")}" href="${escapeAttr(base)}/ai">🤖 AI</a>
29
+ <a class="subtab${cls("model")}" href="${escapeAttr(base)}/model">⚙️ 模型</a>
29
30
  </nav>`;
30
31
  }
31
32