ework-web 0.10.112 → 0.10.114
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 +28 -2
- package/src/views/projectAi.ts +9 -0
- package/src/webhooks.ts +4 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -393,6 +393,7 @@ const REPO_VISIBILITY_RE = /^\/([^/]+)\/([^/]+)\/settings\/visibility$/;
|
|
|
393
393
|
const REPO_DISPATCH_RE = /^\/([^/]+)\/([^/]+)\/settings\/dispatch$/;
|
|
394
394
|
|
|
395
395
|
const REPO_WAKE_LOGINS_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/wake-logins$/;
|
|
396
|
+
const REPO_CONCURRENCY_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/concurrency$/;
|
|
396
397
|
const REPO_HALT_ALL_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai\/halt-all$/;
|
|
397
398
|
const SETTINGS_DISPATCH_RE = /^\/settings\/dispatch$/;
|
|
398
399
|
const REPO_UPSTREAMS_RE = /^\/([^/]+)\/([^/]+)\/settings\/upstreams$/;
|
|
@@ -612,7 +613,8 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
612
613
|
const aiStatus = issue?.ai_status ?? "";
|
|
613
614
|
const issueOff = aiStatus === "dispatch_off" || aiStatus === "halted";
|
|
614
615
|
const sessionResetMs = Number(cfgKv[`sessionReset:${owner}/${repo}#${number}`]) || null;
|
|
615
|
-
|
|
616
|
+
const concurrency = Number(cfgKv[`concurrency:${owner}/${repo}`]) || null;
|
|
617
|
+
return json({ dispatchOff: globalOff || projectOff || issueOff, aiStatus, sessionResetMs, concurrency });
|
|
616
618
|
}
|
|
617
619
|
if (url.pathname === "/api/v1/wake-logins" && req.method === "GET") {
|
|
618
620
|
const owner = url.searchParams.get("owner") ?? "";
|
|
@@ -2173,7 +2175,30 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
2173
2175
|
return Response.redirect(`${back}?ok=1&ok_msg=${encodeURIComponent(isOff ? "已开启自动接单" : "已关闭自动接单")}`, 303);
|
|
2174
2176
|
}
|
|
2175
2177
|
|
|
2178
|
+
const repoConcurrencyMatch = url.pathname.match(REPO_CONCURRENCY_RE);
|
|
2176
2179
|
const repoWakeLoginsMatch = url.pathname.match(REPO_WAKE_LOGINS_RE);
|
|
2180
|
+
if (repoConcurrencyMatch) {
|
|
2181
|
+
const [, owner, repo] = repoConcurrencyMatch;
|
|
2182
|
+
if (!(owner && repo)) return html(errorPage("bad path", ""), 400);
|
|
2183
|
+
const project = await getProject(owner, repo);
|
|
2184
|
+
if (!project) return html(errorPage("项目不存在", ""), 404);
|
|
2185
|
+
const aiBack = `${url.origin}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/settings/ai`;
|
|
2186
|
+
if (!(await canAdminProject(project.id, ctx.user))) {
|
|
2187
|
+
return Response.redirect(`${aiBack}?err=${encodeURIComponent("无权限")}`, 303);
|
|
2188
|
+
}
|
|
2189
|
+
const fd = await req.formData();
|
|
2190
|
+
const raw = String(fd.get("limit") ?? "").trim();
|
|
2191
|
+
const parsed = Number(raw);
|
|
2192
|
+
const key = `concurrency:${owner}/${repo}`;
|
|
2193
|
+
if (raw === "" || parsed === 0) {
|
|
2194
|
+
await deleteConfig(key);
|
|
2195
|
+
} else if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
|
|
2196
|
+
return Response.redirect(`${aiBack}?err=${encodeURIComponent("并发上限需为 1-100 的整数(0 或留空表示不限制)")}`, 303);
|
|
2197
|
+
} else {
|
|
2198
|
+
await setConfig(key, String(parsed));
|
|
2199
|
+
}
|
|
2200
|
+
return Response.redirect(aiBack, 303);
|
|
2201
|
+
}
|
|
2177
2202
|
if (repoWakeLoginsMatch) {
|
|
2178
2203
|
const [, owner, repo] = repoWakeLoginsMatch;
|
|
2179
2204
|
if (!(owner && repo)) return html(errorPage("bad path", ""), 400);
|
|
@@ -2528,7 +2553,8 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
2528
2553
|
const running = await getRunningSessionsForProject(`${owner}/${repo}`);
|
|
2529
2554
|
const processingCount = new Set(running.map((r) => r.issueNumber)).size;
|
|
2530
2555
|
const wakeLoginsRaw = dispatchCfg[`wakeLogins:${owner}/${repo}`] ?? "";
|
|
2531
|
-
|
|
2556
|
+
const concurrencyLimit = dispatchCfg[`concurrency:${owner}/${repo}`] ?? "";
|
|
2557
|
+
return html(buildProjectAiPage(project, dispatchOff, globalDispatchOff, processingCount, wakeLoginsRaw, concurrencyLimit).html);
|
|
2532
2558
|
}
|
|
2533
2559
|
|
|
2534
2560
|
const upstreamsPage = url.pathname.match(REPO_UPSTREAMS_RE);
|
package/src/views/projectAi.ts
CHANGED
|
@@ -8,6 +8,7 @@ export function buildProjectAiPage(
|
|
|
8
8
|
globalDispatchOff: boolean,
|
|
9
9
|
processingCount: number,
|
|
10
10
|
wakeLoginsRaw = "",
|
|
11
|
+
concurrencyLimit = "",
|
|
11
12
|
): { html: string } {
|
|
12
13
|
const aiBase = `/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/ai`;
|
|
13
14
|
const dispatchAction = `/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/dispatch`;
|
|
@@ -35,6 +36,13 @@ ${hint}
|
|
|
35
36
|
<div class="hint">默认只有平台成员能唤醒 AI。把外部 GitHub 用户加进来(逗号或换行分隔),他们在<b>本项目</b>的新 issue / 评论就会自动触发 AI 处理。bot 账号(如 github-actions[bot])永远无效;全局与项目接单开关仍是总闸。issue 评论区对外部用户也有「+白名单」一键按钮。清空内容保存即删除白名单。</div>
|
|
36
37
|
<textarea name="logins" rows="4" style="width:100%;box-sizing:border-box;background:var(--bg-muted);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:.5rem;font-family:inherit;font-size:13px" placeholder="stirp Rika-xie">${escapeHtml(wakeList.join("\n"))}</textarea>
|
|
37
38
|
<button type="submit" class="primary" style="margin-top:.5rem">保存白名单${wakeList.length > 0 ? `(当前 ${wakeList.length} 人)` : ""}</button>
|
|
39
|
+
</form>`;
|
|
40
|
+
|
|
41
|
+
const concurrencyCard = `<form class="card" method="POST" action="${escapeAttr(`${aiBase}/concurrency`)}">
|
|
42
|
+
<h2>⚡ 项目并发上限</h2>
|
|
43
|
+
<div class="hint">限制<b>本项目</b>同时运行的 AI 会话数量(例如 10 = 最多 10 个并行)。超出部分自动排队,有空位再跑。留空或 0 = 不限制(只受 daemon 全局并发约束)。对所有 daemon 生效。</div>
|
|
44
|
+
<input name="limit" type="number" min="0" max="100" inputmode="numeric" value="${escapeAttr(concurrencyLimit)}" placeholder="0" style="width:8rem;background:var(--bg-muted);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:.5rem;font-size:14px">
|
|
45
|
+
<button type="submit" class="primary" style="margin-left:.6rem">保存${concurrencyLimit ? `(当前上限 ${escapeHtml(concurrencyLimit)})` : "(当前不限制)"}</button>
|
|
38
46
|
</form>`;
|
|
39
47
|
|
|
40
48
|
const haltCard = (() => {
|
|
@@ -87,6 +95,7 @@ ${projectSettingsTabsHTML(project.owner, project.name, "ai")}
|
|
|
87
95
|
<p class="hint">两个独立控制:<b>🔔 自动接单</b>控制是否自动派新单(不影响运行中);<b>⏹️ 停止</b>杀死当前所有运行中AI会话(不影响接单状态)。模型选择请去 <a href="${escapeAttr(`/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}/settings/model`)}">⚙️ 模型</a> 标签页。</p>
|
|
88
96
|
${dispatchCard}
|
|
89
97
|
${wakeCard}
|
|
98
|
+
${concurrencyCard}
|
|
90
99
|
${haltCard}
|
|
91
100
|
|
|
92
101
|
<div class="card future">
|
package/src/webhooks.ts
CHANGED
|
@@ -307,6 +307,9 @@ interface PayloadComment {
|
|
|
307
307
|
updated_at: string;
|
|
308
308
|
user: PayloadUser;
|
|
309
309
|
author_kind?: string;
|
|
310
|
+
/** Model that produced this comment (daemon writes it per-reply); mirrors
|
|
311
|
+
* surface it in the agent badge so upstream readers see which model ran. */
|
|
312
|
+
model?: string | null;
|
|
310
313
|
/** Set when this comment was imported from an upstream tracker — receivers
|
|
311
314
|
* must skip echoing it back (defense beyond the body marker). */
|
|
312
315
|
upstream_comment_id?: number | null;
|
|
@@ -471,6 +474,7 @@ function buildComment(
|
|
|
471
474
|
created_at: comment.created_at,
|
|
472
475
|
updated_at: comment.updated_at,
|
|
473
476
|
upstream_comment_id: comment.upstream_comment_id ?? null,
|
|
477
|
+
model: comment.model ?? null,
|
|
474
478
|
user: buildUser(comment.author, origin),
|
|
475
479
|
};
|
|
476
480
|
}
|