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 +1 -1
- package/src/index.ts +40 -3
- package/src/static/app.js +19 -0
- package/src/store.ts +8 -0
- package/src/views/projectAi.ts +13 -0
package/package.json
CHANGED
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
|
-
|
|
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 ?? "";
|
package/src/views/projectAi.ts
CHANGED
|
@@ -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
|
|