ework-web 0.10.20 → 0.10.21
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 +35 -5
- package/src/store.ts +8 -3
- package/src/views/home.ts +3 -2
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
archiveLabel,
|
|
69
69
|
deleteLabel,
|
|
70
70
|
setIssueLabels,
|
|
71
|
+
listAllProjectIds,
|
|
71
72
|
type ProjectRole,
|
|
72
73
|
type UserRow,
|
|
73
74
|
} from "./store";
|
|
@@ -127,6 +128,8 @@ async function refreshOpencodeClient(): Promise<void> {
|
|
|
127
128
|
await refreshOpencodeClient();
|
|
128
129
|
setInterval(refreshOpencodeClient, 10_000);
|
|
129
130
|
|
|
131
|
+
void autoWireAllProjects(`http://${cfg.host}:${cfg.port}`);
|
|
132
|
+
|
|
130
133
|
async function autoWireDaemon(projectId: number, origin: string): Promise<void> {
|
|
131
134
|
if (!cfg.autowireActive) {
|
|
132
135
|
log.info("autoWireDaemon: skipped (WORK_AUTOWIRE_ACTIVE=false)", { projectId });
|
|
@@ -160,6 +163,24 @@ async function autoWireDaemon(projectId: number, origin: string): Promise<void>
|
|
|
160
163
|
}
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
let backfillStarted = false;
|
|
167
|
+
async function autoWireAllProjects(origin: string): Promise<void> {
|
|
168
|
+
if (backfillStarted) return;
|
|
169
|
+
backfillStarted = true;
|
|
170
|
+
if (!cfg.autowireActive || (!cfg.daemonBotLogin.trim() && !cfg.daemonWebhookUrl.trim())) return;
|
|
171
|
+
try {
|
|
172
|
+
const ids = await listAllProjectIds();
|
|
173
|
+
let wired = 0;
|
|
174
|
+
for (const id of ids) {
|
|
175
|
+
await autoWireDaemon(id, origin);
|
|
176
|
+
wired++;
|
|
177
|
+
}
|
|
178
|
+
if (wired > 0) log.info("autoWireAllProjects: backfilled", { count: wired });
|
|
179
|
+
} catch (e) {
|
|
180
|
+
log.warn("autoWireAllProjects failed", { err: e as Error });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
163
184
|
const SEC_HEADERS: Record<string, string> = {
|
|
164
185
|
"content-security-policy": `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'`,
|
|
165
186
|
"x-content-type-options": "nosniff",
|
|
@@ -561,7 +582,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
561
582
|
const form = await req.formData().catch(() => new FormData());
|
|
562
583
|
const f: Record<string, string | undefined> = {};
|
|
563
584
|
for (const [k, v] of form.entries()) f[k] = typeof v === "string" ? v : undefined;
|
|
564
|
-
const r = await handleCreateProject(f);
|
|
585
|
+
const r = await handleCreateProject(f, cfg.defaultModel);
|
|
565
586
|
if (r.projectId) {
|
|
566
587
|
await ensureProjectBootstrapAdmin(r.projectId, ctx.user!.login);
|
|
567
588
|
await autoWireDaemon(r.projectId, url.origin);
|
|
@@ -973,6 +994,18 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
973
994
|
return json({ ok: true });
|
|
974
995
|
}
|
|
975
996
|
|
|
997
|
+
if (req.method === "POST" && url.pathname === "/api/daemons/wire-all") {
|
|
998
|
+
if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
|
|
999
|
+
if (!rateLimit(`daemons-wire:${ip}`, 5, 5 / 3600)) return json({ error: "rate limited" }, 429);
|
|
1000
|
+
try {
|
|
1001
|
+
const ids = await listAllProjectIds();
|
|
1002
|
+
for (const id of ids) await autoWireDaemon(id, url.origin);
|
|
1003
|
+
return json({ ok: true, count: ids.length });
|
|
1004
|
+
} catch (e) {
|
|
1005
|
+
return json({ ok: false, error: errMsg(e) });
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
976
1009
|
if (req.method === "POST" && url.pathname === "/api/daemons/deploy") {
|
|
977
1010
|
if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
|
|
978
1011
|
if (!rateLimit(`daemons-deploy:${ip}`, 10, 10 / 3600)) return json({ error: "rate limited" }, 429);
|
|
@@ -1931,10 +1964,7 @@ function parseState(s: string | null): "open" | "closed" | "all" {
|
|
|
1931
1964
|
async function createProjectSafe(owner: string, name: string) {
|
|
1932
1965
|
let project = await getProject(owner, name);
|
|
1933
1966
|
if (project) return project;
|
|
1934
|
-
|
|
1935
|
-
// URL regex shape; allow creation here so `/<owner>/<new-repo>/issues` (POST) bootstraps
|
|
1936
|
-
// a project in one step. Use createIssue's tx for atomicity.
|
|
1937
|
-
project = await createProject(owner, name, "");
|
|
1967
|
+
project = await createProject(owner, name, "", cfg.defaultModel);
|
|
1938
1968
|
return project;
|
|
1939
1969
|
}
|
|
1940
1970
|
|
package/src/store.ts
CHANGED
|
@@ -171,7 +171,12 @@ export async function listProjectsWithCounts(): Promise<ProjectWithCounts[]> {
|
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
export async function
|
|
174
|
+
export async function listAllProjectIds(): Promise<number[]> {
|
|
175
|
+
const rows = await getDB().all<{ id: number }>("SELECT id FROM {{projects}} ORDER BY id");
|
|
176
|
+
return rows.map((r) => r.id);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function createProject(owner: string, name: string, description: string, model?: string): Promise<ProjectRow> {
|
|
175
180
|
owner = owner.trim();
|
|
176
181
|
name = name.trim();
|
|
177
182
|
if (!/^[A-Za-z0-9_.-]+$/.test(owner)) throw new StoreError(400, "owner 含非法字符");
|
|
@@ -180,8 +185,8 @@ export async function createProject(owner: string, name: string, description: st
|
|
|
180
185
|
const ts = now();
|
|
181
186
|
const db = getDB();
|
|
182
187
|
const info = await db.run(
|
|
183
|
-
"INSERT INTO {{projects}} (owner, name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
|
184
|
-
[owner, name, description ?? "", ts, ts]
|
|
188
|
+
"INSERT INTO {{projects}} (owner, name, description, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
189
|
+
[owner, name, description ?? "", model ?? "", ts, ts]
|
|
185
190
|
);
|
|
186
191
|
return (await getProjectById(info.insertId))!;
|
|
187
192
|
}
|
package/src/views/home.ts
CHANGED
|
@@ -92,14 +92,15 @@ ${tabNavHTML("projects")}
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export async function handleCreateProject(
|
|
95
|
-
form: Record<string, string | undefined
|
|
95
|
+
form: Record<string, string | undefined>,
|
|
96
|
+
defaultModel: string,
|
|
96
97
|
): Promise<{ location: string; error?: string; projectId?: number }> {
|
|
97
98
|
const owner = (form.owner ?? "").trim();
|
|
98
99
|
const name = (form.name ?? "").trim();
|
|
99
100
|
const description = (form.description ?? "").trim();
|
|
100
101
|
if (!owner || !name) return { location: "/projects", error: "owner 和 name 必填" };
|
|
101
102
|
try {
|
|
102
|
-
const p = await createProject(owner, name, description);
|
|
103
|
+
const p = await createProject(owner, name, description, defaultModel);
|
|
103
104
|
return { location: `/${encodeURIComponent(p.owner)}/${encodeURIComponent(p.name)}/issues`, projectId: p.id };
|
|
104
105
|
} catch (e) {
|
|
105
106
|
return {
|