ework-web 0.10.35 → 0.10.37
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/config.ts +8 -0
- package/src/db.ts +2 -1
- package/src/index.ts +22 -13
- package/src/opencode.ts +16 -4
- package/src/ratelimit.ts +4 -0
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -93,6 +93,13 @@ export const configSchema = z.object({
|
|
|
93
93
|
if (!Number.isFinite(n) || n < 1 || n > 64) return 6;
|
|
94
94
|
return Math.floor(n);
|
|
95
95
|
}, z.number().int().min(1).max(64)),
|
|
96
|
+
// Space-separated list of origins (e.g. "https://ework-web.taobao.net http://ework-web.taobao.net")
|
|
97
|
+
// injected into CSP form-action + connect-src. For reverse-proxy/gateway deployments
|
|
98
|
+
// where the browser sees a different scheme/host than the app's 'self'.
|
|
99
|
+
publicOrigins: z
|
|
100
|
+
.string()
|
|
101
|
+
.default("")
|
|
102
|
+
.transform((s) => s.split(/\s+/).filter(Boolean)),
|
|
96
103
|
});
|
|
97
104
|
|
|
98
105
|
export type Config = z.infer<typeof configSchema>;
|
|
@@ -188,6 +195,7 @@ export async function loadConfig(): Promise<Config> {
|
|
|
188
195
|
defaultModel: db.defaultModel ?? process.env.WORK_DEFAULT_MODEL,
|
|
189
196
|
autowireActive: process.env.WORK_AUTOWIRE_ACTIVE !== "false",
|
|
190
197
|
webhookMaxConcurrent: Number(process.env.WORK_WEBHOOK_MAX_CONCURRENT ?? "6"),
|
|
198
|
+
publicOrigins: process.env.WORK_PUBLIC_ORIGINS ?? "",
|
|
191
199
|
});
|
|
192
200
|
}
|
|
193
201
|
|
package/src/db.ts
CHANGED
|
@@ -301,7 +301,8 @@ function translateForMysql(sql: string): string {
|
|
|
301
301
|
return sql
|
|
302
302
|
.replace(/INSERT OR IGNORE INTO/g, "INSERT IGNORE INTO")
|
|
303
303
|
.replace(/ON CONFLICT\((\w+)\) DO UPDATE SET/g, "ON DUPLICATE KEY UPDATE")
|
|
304
|
-
.replace(/excluded\.(\w+)/g, "VALUES($1)")
|
|
304
|
+
.replace(/excluded\.(\w+)/g, "VALUES($1)")
|
|
305
|
+
.replace(/LIKE \? ESCAPE '\\'/g, "LIKE ?");
|
|
305
306
|
}
|
|
306
307
|
|
|
307
308
|
async function migrateMysqlSurrogateId(pool: Pool): Promise<void> {
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { buildAdminTokensPage } from "./views/adminTokens";
|
|
|
27
27
|
import { buildSessionList, buildSessionView, renderNewMessages, renderBatchHTML } from "./views/sessionLog";
|
|
28
28
|
import { buildFileView, FileViewError, readFileSince, serveRawFile } from "./fileview";
|
|
29
29
|
import { translateText, translateTextStream, TranslateError } from "./translate";
|
|
30
|
-
import { rateLimit } from "./ratelimit";
|
|
30
|
+
import { rateLimit, clearRateLimit } from "./ratelimit";
|
|
31
31
|
import {
|
|
32
32
|
StoreError,
|
|
33
33
|
getProject,
|
|
@@ -185,13 +185,20 @@ async function autoWireAllProjects(origin: string): Promise<void> {
|
|
|
185
185
|
void autoWireAllProjects(`http://${cfg.host}:${cfg.port}`);
|
|
186
186
|
|
|
187
187
|
const SEC_HEADERS: Record<string, string> = {
|
|
188
|
-
"content-security-policy":
|
|
188
|
+
"content-security-policy": buildCsp(cfg),
|
|
189
189
|
"x-content-type-options": "nosniff",
|
|
190
190
|
"x-frame-options": "DENY",
|
|
191
191
|
"referrer-policy": "same-origin",
|
|
192
192
|
"permissions-policy": "()",
|
|
193
193
|
};
|
|
194
194
|
|
|
195
|
+
function buildCsp(cfg: Config): string {
|
|
196
|
+
const origins = cfg.publicOrigins.join(" ");
|
|
197
|
+
const formAction = origins ? `'self' ${origins}` : "'self'";
|
|
198
|
+
const connectSrc = origins ? `'self' ${origins}` : "'self'";
|
|
199
|
+
return `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src ${connectSrc}; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action ${formAction}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
195
202
|
const hlCss = loadHighlightCss();
|
|
196
203
|
|
|
197
204
|
function loadHighlightCss(): string {
|
|
@@ -483,14 +490,18 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
483
490
|
if (req.method === "POST") {
|
|
484
491
|
const form = await req.formData().catch(() => new FormData());
|
|
485
492
|
const next = sanitizeNext(String(form.get("next") ?? "/"));
|
|
486
|
-
if (!rateLimit(`login:${ip}`, 5, 5 / (15 * 60))) {
|
|
487
|
-
return html(loginHTML(next, "尝试过多,15 分钟后再试", cfg), 429);
|
|
488
|
-
}
|
|
489
493
|
const login = String(form.get("login") ?? "").trim();
|
|
490
494
|
const password = String(form.get("password") ?? "");
|
|
491
495
|
const token = String(form.get("token") ?? "").trim();
|
|
492
496
|
|
|
493
|
-
|
|
497
|
+
const rlKey = `login:${ip}:${login || token || "?"}`;
|
|
498
|
+
if (!rateLimit(rlKey, 5, 5 / (15 * 60))) {
|
|
499
|
+
return new Response(loginHTML(next, "尝试过多,15 分钟后再试", cfg), {
|
|
500
|
+
status: 429,
|
|
501
|
+
headers: { "content-type": "text/html; charset=utf-8", "retry-after": "900" },
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
494
505
|
let resolvedLogin: string | null = null;
|
|
495
506
|
if (token && token === cfg.authToken) {
|
|
496
507
|
resolvedLogin = cfg.operatorLogin;
|
|
@@ -508,6 +519,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
508
519
|
}
|
|
509
520
|
|
|
510
521
|
if (resolvedLogin) {
|
|
522
|
+
clearRateLimit(rlKey);
|
|
511
523
|
const setCookie = await makeAuthCookieHeader(cfg, resolvedLogin);
|
|
512
524
|
return new Response(null, {
|
|
513
525
|
status: 302,
|
|
@@ -681,13 +693,10 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
681
693
|
const status = e instanceof OpencodeError ? e.status : 500;
|
|
682
694
|
if (status === 404 && /^ses_[0-9A-Za-z]{8,}/.test(sid)) {
|
|
683
695
|
return html(errorPage(
|
|
684
|
-
"
|
|
685
|
-
`会话 ID ${sid} 在 opencode
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
`持久化到 DB。\n\n` +
|
|
689
|
-
`排查:docker logs ework-aio 看 opencode 子进程的报错;确认 OPENCODE_AI_API_KEY 已配置\n` +
|
|
690
|
-
`或 ~/.config/opencode/auth.json 存在。`
|
|
696
|
+
"找不到会话",
|
|
697
|
+
`会话 ID ${sid} 在 opencode 数据库中不存在。\n\n` +
|
|
698
|
+
`可能原因:opencode 进程异常退出、session 尚未持久化、或 daemon 连接问题。\n\n` +
|
|
699
|
+
`请检查 daemon 日志或确认 session ID 正确。`
|
|
691
700
|
), 404);
|
|
692
701
|
}
|
|
693
702
|
return html(errorPage(status === 404 ? "找不到会话" : "加载失败", errMsg(e)), status);
|
package/src/opencode.ts
CHANGED
|
@@ -416,16 +416,16 @@ export class MultiDaemonOpencodeClient implements OpencodeClientInterface {
|
|
|
416
416
|
async exportSession(id: string): Promise<SessionExport> {
|
|
417
417
|
try {
|
|
418
418
|
return await Promise.any(this.clients.map((c) => c.exportSession(id)));
|
|
419
|
-
} catch {
|
|
420
|
-
throw
|
|
419
|
+
} catch (e) {
|
|
420
|
+
throw pickErrorFromAggregate(e, id);
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
async exportSessionRaw(id: string): Promise<string> {
|
|
425
425
|
try {
|
|
426
426
|
return await Promise.any(this.clients.map((c) => c.exportSessionRaw(id)));
|
|
427
|
-
} catch {
|
|
428
|
-
throw
|
|
427
|
+
} catch (e) {
|
|
428
|
+
throw pickErrorFromAggregate(e, id);
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
|
|
@@ -450,6 +450,18 @@ export function createOpencodeClient(cfg: Config, daemonEndpoint?: string): Open
|
|
|
450
450
|
return new OpencodeClient(cfg);
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
function pickErrorFromAggregate(e: unknown, id: string): OpencodeError {
|
|
454
|
+
if (e instanceof AggregateError) {
|
|
455
|
+
const errors = e.errors as unknown[];
|
|
456
|
+
const non404 = errors.find(
|
|
457
|
+
(err) => err instanceof OpencodeError && err.status !== 404,
|
|
458
|
+
) as OpencodeError | undefined;
|
|
459
|
+
if (non404) return non404;
|
|
460
|
+
}
|
|
461
|
+
if (e instanceof OpencodeError) return e;
|
|
462
|
+
return new OpencodeError(`session ${id} not found on any daemon`, 404);
|
|
463
|
+
}
|
|
464
|
+
|
|
453
465
|
async function readCapped(stream: ReadableStream<Uint8Array> | null, maxBytes: number): Promise<string> {
|
|
454
466
|
if (!stream) return "";
|
|
455
467
|
const reader = stream.getReader();
|
package/src/ratelimit.ts
CHANGED