ework-web 0.10.108 → 0.10.110
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/coordination.ts +1 -1
- package/src/index.ts +33 -3
- package/src/render/layout.ts +4 -1
package/package.json
CHANGED
package/src/coordination.ts
CHANGED
|
@@ -36,7 +36,7 @@ interface RouterDaemonRow {
|
|
|
36
36
|
|
|
37
37
|
let routerCache: { at: number; rows: RouterDaemonRow[] } | null = null;
|
|
38
38
|
|
|
39
|
-
async function routerDaemons(): Promise<RouterDaemonRow[]> {
|
|
39
|
+
export async function routerDaemons(): Promise<RouterDaemonRow[]> {
|
|
40
40
|
if (routerCache && Date.now() - routerCache.at < 15_000) return routerCache.rows;
|
|
41
41
|
try {
|
|
42
42
|
const cfg = await loadConfig();
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { homedir } from "os";
|
|
|
8
8
|
import { loadConfig, DB_OVERRIDABLE, parseOverride, resolveTtsBackend } from "./config";
|
|
9
9
|
import type { Config } from "./config";
|
|
10
10
|
import { setConfig, deleteConfig, getConfigAll, initDB, getDB } from "./db";
|
|
11
|
-
import { getActiveDaemons, listAllDaemons, getSessionDaemonMap, resolveDaemonEndpoint, getRunningSessionsForProject, resolveSessionUid } from "./coordination";
|
|
11
|
+
import { getActiveDaemons, listAllDaemons, getSessionDaemonMap, resolveDaemonEndpoint, getRunningSessionsForProject, resolveSessionUid, routerDaemons } from "./coordination";
|
|
12
12
|
import { testMysqlConnection, migrateSqliteToMysql, writeMysqlEnv, migrateMysqlToSqlite, writeSqliteEnv, migrateDaemonSqliteToMysql, generateMysqlDDL } from "./db-admin";
|
|
13
13
|
import type { MysqlTargetOpts } from "./db-admin";
|
|
14
14
|
import { checkAuth, makeAuthCookieHeader, clearAuthCookieHeader, loginHTML, sanitizeNext, ensureBootstrapAdmin, ensureBootstrapSystem, isReservedSystemLogin } from "./auth";
|
|
@@ -402,7 +402,7 @@ const REPO_AI_RE = /^\/([^/]+)\/([^/]+)\/settings\/ai$/;
|
|
|
402
402
|
const REPO_LABELS_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels$/;
|
|
403
403
|
const REPO_LABEL_ADD_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/add$/;
|
|
404
404
|
const REPO_LABEL_ACTION_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/(\d+)\/(update|archive|unarchive|delete)$/;
|
|
405
|
-
const REPO_ISSUE_HALT_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/(halt|resume|dispatch-off|dispatch-on)$/;
|
|
405
|
+
const REPO_ISSUE_HALT_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/(halt|resume|dispatch-off|dispatch-on|reset-session)$/;
|
|
406
406
|
const REPO_ISSUE_MODEL_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/model$/;
|
|
407
407
|
const REPO_ISSUE_RUNTIME_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/runtime$/;
|
|
408
408
|
const REPO_ISSUE_STATUS_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/ai-status$/;
|
|
@@ -611,7 +611,8 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
611
611
|
const projectOff = cfgKv[`dispatchOff:${owner}/${repo}`] === "1";
|
|
612
612
|
const aiStatus = issue?.ai_status ?? "";
|
|
613
613
|
const issueOff = aiStatus === "dispatch_off" || aiStatus === "halted";
|
|
614
|
-
|
|
614
|
+
const sessionResetMs = Number(cfgKv[`sessionReset:${owner}/${repo}#${number}`]) || null;
|
|
615
|
+
return json({ dispatchOff: globalOff || projectOff || issueOff, aiStatus, sessionResetMs });
|
|
615
616
|
}
|
|
616
617
|
if (url.pathname === "/api/v1/wake-logins" && req.method === "GET") {
|
|
617
618
|
const owner = url.searchParams.get("owner") ?? "";
|
|
@@ -757,6 +758,23 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
757
758
|
const limit = Math.min(5000, Math.max(1, Number(url.searchParams.get("limit")) || 30));
|
|
758
759
|
const daemonEp = url.searchParams.get("daemon");
|
|
759
760
|
const daemonIdParam = url.searchParams.get("daemon_id");
|
|
761
|
+
// daemon_id is only a hint: ids churn across daemon reinstalls and the
|
|
762
|
+
// {{d_*}} mirror tables exist only in split-DB mysql deployments, so a
|
|
763
|
+
// stale hint must fall back to scanning daemons that hold the session.
|
|
764
|
+
const tryAllDaemons = async (): Promise<Response | null> => {
|
|
765
|
+
if (!/^ses_[0-9A-Za-z]{8,}/.test(sid)) return null;
|
|
766
|
+
let registry: Array<{ endpoint?: string }> = [];
|
|
767
|
+
try { registry = await routerDaemons(); } catch { registry = []; }
|
|
768
|
+
for (const d of registry) {
|
|
769
|
+
if (!d.endpoint) continue;
|
|
770
|
+
try {
|
|
771
|
+
const c = new RemoteOpencodeClient(d.endpoint);
|
|
772
|
+
const { html: body } = await buildSessionView(c, sid, desc, cfg.collapseLines, limit, all);
|
|
773
|
+
return html(body);
|
|
774
|
+
} catch { /* daemon does not hold this session — try next */ }
|
|
775
|
+
}
|
|
776
|
+
return null;
|
|
777
|
+
};
|
|
760
778
|
if (!daemonEp && daemonIdParam) {
|
|
761
779
|
const id = Number(daemonIdParam);
|
|
762
780
|
if (Number.isFinite(id) && id > 0) {
|
|
@@ -775,6 +793,8 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
775
793
|
} catch (e) {
|
|
776
794
|
const status = e instanceof OpencodeError ? e.status : 500;
|
|
777
795
|
if (status === 404) {
|
|
796
|
+
const scanned = await tryAllDaemons();
|
|
797
|
+
if (scanned) return scanned;
|
|
778
798
|
return html(errorPage(
|
|
779
799
|
"会话不在该 daemon 上",
|
|
780
800
|
`会话 ${sid} 在 daemon ${effectiveId} (${ep}) 上找不到 (HTTP ${status})。\n\n` +
|
|
@@ -785,6 +805,9 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
785
805
|
}
|
|
786
806
|
throw e;
|
|
787
807
|
}
|
|
808
|
+
} else {
|
|
809
|
+
const scanned = await tryAllDaemons();
|
|
810
|
+
if (scanned) return scanned;
|
|
788
811
|
}
|
|
789
812
|
}
|
|
790
813
|
}
|
|
@@ -1911,6 +1934,13 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
1911
1934
|
const issue = await getIssueWithMeta(project.id, number);
|
|
1912
1935
|
if (!issue) return json({ error: "issue not found" }, 404);
|
|
1913
1936
|
const statusMap: Record<string, string> = { halt: "halted", resume: "", "dispatch-off": "dispatch_off", "dispatch-on": "" };
|
|
1937
|
+
if (action === "reset-session") {
|
|
1938
|
+
if (!(await canAdminProject(project.id, ctx.user))) {
|
|
1939
|
+
return json({ error: "admin role required" }, 403);
|
|
1940
|
+
}
|
|
1941
|
+
await setConfig(`sessionReset:${owner}/${repo}#${number}`, String(Date.now()));
|
|
1942
|
+
return json({ ok: true, reset: true });
|
|
1943
|
+
}
|
|
1914
1944
|
const newStatus = statusMap[action] ?? "";
|
|
1915
1945
|
const oldStatus = issue.ai_status ?? "";
|
|
1916
1946
|
await updateIssueAiStatus(issue.id, newStatus);
|
package/src/render/layout.ts
CHANGED
|
@@ -214,6 +214,9 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
|
|
|
214
214
|
? `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-on" title="允许自动接单">🔔 恢复接单</button>`
|
|
215
215
|
: `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-off" data-action-confirm="设为不自动接单?" title="关闭此 issue 的自动接单">🔕 暂停接单</button>`
|
|
216
216
|
: "";
|
|
217
|
+
const resetBtnHtml = showActions
|
|
218
|
+
? `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/reset-session" data-action-confirm="重置 AI 会话?下次触发将从全新会话开始(历史评论保留)" title="重置 AI 会话(下次触发开新会话)">🔄 重置会话</button>`
|
|
219
|
+
: "";
|
|
217
220
|
const runtimeSelectHtml = props.runtimeSelect && showActions
|
|
218
221
|
? `<span class="model-select-wrap"><select class="model-select" id="issueRuntimeSelect" title="此 issue 的运行时(新会话生效)">
|
|
219
222
|
<option value="" ${props.runtimeSelect.current === "" ? "selected" : ""}>默认运行时</option>
|
|
@@ -251,7 +254,7 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
|
|
|
251
254
|
<div class="meta-status">
|
|
252
255
|
<span class="state-badge ${stateClass}">${stateLabel}</span>
|
|
253
256
|
${aiBadgeHtml}
|
|
254
|
-
${(haltBtnHtml || dispatchBtnHtml || (props.customActions ?? []).length) ? `<span class="action-group">${haltBtnHtml}${dispatchBtnHtml}${runtimeSelectHtml}${modelSelectHtml}${(props.customActions ?? []).map((a) => {
|
|
257
|
+
${(haltBtnHtml || dispatchBtnHtml || (props.customActions ?? []).length) ? `<span class="action-group">${haltBtnHtml}${dispatchBtnHtml}${resetBtnHtml}${runtimeSelectHtml}${modelSelectHtml}${(props.customActions ?? []).map((a) => {
|
|
255
258
|
const attrs = [`data-action-href="${escapeAttr(a.href)}"`];
|
|
256
259
|
if (a.method && a.method !== "POST") attrs.push(`data-action-method="${escapeAttr(a.method)}"`);
|
|
257
260
|
if (a.confirm) attrs.push(`data-action-confirm="${escapeAttr(a.confirm)}"`);
|