Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const configSchema = z.object({
daemonBotLogin: z.string().default(""),
daemonWebhookUrl: z.string().default(""),
daemonWebhookSecret: z.string().default(""),
routerAdminToken: z.string().default(""),
// Default "provider/model" string passed to `opencode run --model <X>`.
// Empty = let opencode pick per its own opencode.json + env. ework-daemon
// pushes this (or the per-project override) on every spawn to defend
Expand Down Expand Up @@ -175,8 +176,9 @@ export async function loadConfig(): Promise<Config> {
ttsDefaultBackend: db.ttsDefaultBackend ?? process.env.WORK_TTS_DEFAULT_BACKEND,
ttsSpeed: db.ttsSpeed ?? process.env.WORK_TTS_SPEED,
daemonBotLogin: process.env.WORK_DAEMON_BOT_LOGIN ?? "",
daemonWebhookUrl: process.env.WORK_DAEMON_WEBHOOK_URL ?? "",
daemonWebhookSecret: process.env.WORK_DAEMON_WEBHOOK_SECRET ?? "",
daemonWebhookUrl: process.env.WORK_DAEMON_WEBHOOK_URL ?? "",
daemonWebhookSecret: process.env.WORK_DAEMON_WEBHOOK_SECRET ?? "",
routerAdminToken: process.env.WORK_ROUTER_ADMIN_TOKEN ?? "",
defaultModel: db.defaultModel ?? process.env.WORK_DEFAULT_MODEL,
autowireActive: process.env.WORK_AUTOWIRE_ACTIVE !== "false",
webhookMaxConcurrent: Number(process.env.WORK_WEBHOOK_MAX_CONCURRENT ?? "6"),
Expand Down
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1060,16 +1060,18 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
if (url.pathname === "/api/router/strategy") {
if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
const routerUrl = cfg.daemonWebhookUrl.replace(/\/$/, "");
const routerHeaders: Record<string, string> = { "Content-Type": "application/json" };
if (cfg.routerAdminToken) routerHeaders["Authorization"] = `Bearer ${cfg.routerAdminToken}`;
try {
if (req.method === "GET") {
const res = await fetch(`${routerUrl}/api/strategy`, { signal: AbortSignal.timeout(5000) });
const res = await fetch(`${routerUrl}/api/strategy`, { headers: routerHeaders, signal: AbortSignal.timeout(5000) });
return json(await res.json());
}
if (req.method === "POST") {
const body = await req.text();
const res = await fetch(`${routerUrl}/api/strategy`, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: routerHeaders,
body,
signal: AbortSignal.timeout(5000),
});
Expand Down
63 changes: 62 additions & 1 deletion src/static/daemon-groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,55 @@
const saveBtn = document.getElementById("strategy-save");
const addBindingBtn = document.getElementById("binding-add");

let currentStrategy = { strategy: "least-loaded", groupBindings: {}, daemonGroups: {} };
let currentStrategy = { strategy: "least-loaded", groupBindings: {}, daemonGroups: {}, groupConfigs: {} };

function showResult(msg, ok) {
result.textContent = msg;
result.className = "db-result " + (ok ? "db-ok" : "db-err");
setTimeout(() => { result.textContent = ""; result.className = "db-result"; }, 3000);
}

function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}

function collectGroupNames() {
var names = new Set();
var dg = currentStrategy.daemonGroups || {};
Object.values(dg).forEach(function (arr) { (arr || []).forEach(function (g) { names.add(g); }); });
var gb = currentStrategy.groupBindings || {};
Object.values(gb).forEach(function (g) { names.add(g); });
Object.keys(currentStrategy.groupConfigs || {}).forEach(function (g) { names.add(g); });
return Array.from(names).sort();
}

function renderGroupConfigs() {
var container = document.getElementById("group-configs-list");
if (!container) return;
var names = collectGroupNames();
if (!names.length) {
container.innerHTML = '<p class="hint" style="margin:0">先添加分组(上方给 daemon 打组或绑定 repo→组)后这里会出现配置项。</p>';
return;
}
container.innerHTML = names.map(function (g) {
var cfg = (currentStrategy.groupConfigs || {})[g] || {};
return '<details class="gc-card" style="border:1px solid var(--border);border-radius:8px;padding:.6rem .8rem;margin:.4rem 0">' +
'<summary style="cursor:pointer;font-weight:600;font-size:13px">' + esc(g) + '</summary>' +
'<div style="margin-top:.5rem">' +
'<label style="font-size:12px;color:var(--text-muted)">工作目录模板(变量: {owner} {repo} {issue} {session})</label>' +
'<input type="text" class="gc-workdir" data-group="' + esc(g) + '" value="' + esc(cfg.workdirTemplate || "") + '" placeholder="/data/work/{owner}/{repo}/{issue}" maxlength="512" style="width:100%;padding:.35rem .5rem;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:13px;margin-bottom:.5rem;font-family:ui-monospace,monospace">' +
'<label style="font-size:12px;color:var(--text-muted)">Init 脚本(投递时跑,cwd=workdir,env 有 $EWORK_OWNER/$EWORK_REPO/$EWORK_ISSUE/$EWORK_WORKDIR)</label>' +
'<textarea class="gc-init" data-group="' + esc(g) + '" placeholder="git clone https://gitea.example.com/$EWORK_OWNER/$EWORK_REPO.git ." rows="2" maxlength="4096" style="width:100%;padding:.35rem .5rem;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:12px;margin-bottom:.5rem;font-family:ui-monospace,monospace;resize:vertical">' + esc(cfg.initScript || "") + '</textarea>' +
'<label style="font-size:12px;color:var(--text-muted)">Destroy 脚本(关闭时跑,cwd=workdir)</label>' +
'<textarea class="gc-destroy" data-group="' + esc(g) + '" rows="2" maxlength="4096" style="width:100%;padding:.35rem .5rem;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:12px;margin-bottom:.5rem;font-family:ui-monospace,monospace;resize:vertical">' + esc(cfg.destroyScript || "") + '</textarea>' +
'<label style="font-size:12px;color:var(--text-muted)">Env-Init 脚本(机器级,预留)</label>' +
'<textarea class="gc-envinit" data-group="' + esc(g) + '" rows="1" disabled maxlength="4096" placeholder="预留,暂不执行" style="width:100%;padding:.35rem .5rem;border:1px solid var(--border);border-radius:6px;background:var(--bg-muted);color:var(--text-muted);font-size:12px;resize:vertical">' + esc(cfg.envInitScript || "") + '</textarea>' +
'</div></details>';
}).join("");
}

async function load() {
try {
const [daemonsRes, strategyRes] = await Promise.all([
Expand All @@ -35,6 +76,7 @@
}).join("") || '<tr><td colspan="4" class="daemon-empty">没有已注册的 daemon(节点启动后自动出现)</td></tr>';

renderBindings();
renderGroupConfigs();
} catch (e) {
tbody.innerHTML = '<tr><td colspan="4" class="daemon-empty">无法连接 router — 确认 router 已启动</td></tr>';
}
Expand Down Expand Up @@ -67,6 +109,7 @@
document.getElementById("binding-repo").value = "";
document.getElementById("binding-group").value = "";
renderBindings();
renderGroupConfigs();
};

saveBtn.onclick = async () => {
Expand All @@ -79,6 +122,24 @@
});
currentStrategy.daemonGroups = newGroups;

var newConfigs = {};
document.querySelectorAll("details.gc-card").forEach(function (card) {
var g = card.querySelector("summary").textContent.trim();
var workdir = (card.querySelector(".gc-workdir") || {}).value || "";
var init = (card.querySelector(".gc-init") || {}).value || "";
var destroy = (card.querySelector(".gc-destroy") || {}).value || "";
var envinit = (card.querySelector(".gc-envinit") || {}).value || "";
if (workdir.trim() || init.trim() || destroy.trim() || envinit.trim()) {
newConfigs[g] = {
workdirTemplate: workdir.trim() || undefined,
initScript: init.trim() || undefined,
destroyScript: destroy.trim() || undefined,
envInitScript: envinit.trim() || undefined,
};
}
});
currentStrategy.groupConfigs = newConfigs;

try {
const res = await fetch("/api/router/strategy", {
method: "POST",
Expand Down
5 changes: 5 additions & 0 deletions src/views/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ function buildGroupsSection(): string {
<button type="button" id="binding-add" class="secondary">添加绑定</button>
</div>
</div>
<div id="group-configs-editor" style="margin-top:.7rem">
<h2 style="margin-bottom:.5rem">分组工作目录与脚本</h2>
<p class="hint" style="margin:0 0 .5rem">为每个分组配置工作目录模板和生命周期脚本。模板变量:<code>{owner}</code> <code>{repo}</code> <code>{issue}</code> <code>{session}</code>。脚本在 daemon 上以工作目录为 cwd 执行。</p>
<div id="group-configs-list"></div>
</div>
<div class="db-controls" style="margin-top:.7rem">
<button type="button" id="strategy-save">保存策略</button>
</div>
Expand Down
Loading