@lelouchhe/webagent 0.4.0 → 0.6.0
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/README.md +1 -0
- package/config.toml +12 -3
- package/dist/fonts/temml/Temml.woff2 +0 -0
- package/dist/index.html +4 -4
- package/dist/js/app.HJKQURHR.js +4 -0
- package/dist/js/chunk.ON2W5X2T.js +173 -0
- package/dist/js/viewer.VY4PO2O5.js +1 -0
- package/dist/login.html +1 -1
- package/dist/share-viewer.html +4 -3
- package/dist/{styles.012p32dz.css → styles.01ky3cex.css} +416 -3
- package/lib/auth-middleware.js +3 -0
- package/lib/bridge-event-config.js +29 -0
- package/lib/bridge.js +9 -7
- package/lib/client-registry.js +104 -15
- package/lib/config.js +12 -5
- package/lib/event-handler.js +3 -1
- package/lib/image-dimensions.js +64 -0
- package/lib/model-picker.js +17 -0
- package/lib/preflight.js +41 -22
- package/lib/push-service.js +21 -69
- package/lib/routes.js +140 -19
- package/lib/server.js +19 -23
- package/lib/session-manager.js +1 -1
- package/lib/startup-checks.js +1 -0
- package/lib/store.js +15 -3
- package/lib/title-service.js +3 -19
- package/package.json +3 -2
- package/dist/js/app.GSAIYHML.js +0 -4
- package/dist/js/chunk.CGWFHJI2.js +0 -76
- package/dist/js/viewer.6DT53STL.js +0 -1
package/lib/routes.js
CHANGED
|
@@ -17,6 +17,7 @@ const slog = rlog.scope("session");
|
|
|
17
17
|
const mlog = rlog.scope("msg");
|
|
18
18
|
import { signAttachmentUrl, verifyAttachmentSig, reSignAttachmentUrlsInJson, } from "./auth.js";
|
|
19
19
|
import { buildContentDisposition, classifyKind, isInlineMime, mimeToExt, normalizeDisplayName, sniffMime, } from "./attachments.js";
|
|
20
|
+
import { readImageDimensions } from "./image-dimensions.js";
|
|
20
21
|
const IS_WIN = process.platform === "win32";
|
|
21
22
|
const SAFE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
22
23
|
const MIME = {
|
|
@@ -30,6 +31,8 @@ const MIME = {
|
|
|
30
31
|
".jpeg": "image/jpeg",
|
|
31
32
|
".gif": "image/gif",
|
|
32
33
|
".webp": "image/webp",
|
|
34
|
+
".woff2": "font/woff2",
|
|
35
|
+
".wasm": "application/wasm",
|
|
33
36
|
};
|
|
34
37
|
/**
|
|
35
38
|
* HTML entrypoints served by this app. Any new HTML page MUST be registered
|
|
@@ -51,7 +54,10 @@ export const HTML_ENTRYPOINTS = [
|
|
|
51
54
|
*
|
|
52
55
|
* - default-src 'self': everything same-origin only
|
|
53
56
|
* - img-src adds data: + blob: for image-upload preview
|
|
54
|
-
* - script-src 'self' (no inline; theme bootstrap is
|
|
57
|
+
* - script-src 'self' 'wasm-unsafe-eval' (no inline; theme bootstrap is
|
|
58
|
+
* /theme-init.js. 'wasm-unsafe-eval' is the narrow CSP3 token that
|
|
59
|
+
* permits WebAssembly.instantiate() WITHOUT re-enabling eval()/Function;
|
|
60
|
+
* needed by any wasm consumer.)
|
|
55
61
|
* - style-src 'self' (no inline; login styles live in /styles.css)
|
|
56
62
|
* - object-src 'none', frame-ancestors 'none', base-uri 'self', form-action 'self'
|
|
57
63
|
* - connect-src 'self' for fetch + EventSource
|
|
@@ -59,7 +65,7 @@ export const HTML_ENTRYPOINTS = [
|
|
|
59
65
|
export const CSP_POLICY = [
|
|
60
66
|
"default-src 'self'",
|
|
61
67
|
"img-src 'self' data: blob:",
|
|
62
|
-
"script-src 'self'",
|
|
68
|
+
"script-src 'self' 'wasm-unsafe-eval'",
|
|
63
69
|
"style-src 'self'",
|
|
64
70
|
"connect-src 'self'",
|
|
65
71
|
"object-src 'none'",
|
|
@@ -91,6 +97,20 @@ function getClientOpId(req) {
|
|
|
91
97
|
return v;
|
|
92
98
|
return null;
|
|
93
99
|
}
|
|
100
|
+
function logPromptRejectBeforeSave(fields) {
|
|
101
|
+
plog.warn("rejected before save", {
|
|
102
|
+
sessionId: fields.sessionId.slice(0, 8),
|
|
103
|
+
status: fields.status,
|
|
104
|
+
reason: fields.reason,
|
|
105
|
+
...(fields.opId ? { opId: fields.opId } : {}),
|
|
106
|
+
...(fields.textLength != null ? { textLength: fields.textLength } : {}),
|
|
107
|
+
...(fields.attachmentCount != null
|
|
108
|
+
? { attachmentCount: fields.attachmentCount }
|
|
109
|
+
: {}),
|
|
110
|
+
...(fields.busyKind ? { busyKind: fields.busyKind } : {}),
|
|
111
|
+
...(fields.error ? { error: fields.error } : {}),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
94
114
|
function tryReplayClientOp(req, res, store, sessionId) {
|
|
95
115
|
const opId = getClientOpId(req);
|
|
96
116
|
if (!opId)
|
|
@@ -317,6 +337,7 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
|
|
|
317
337
|
finalPath = join(dir, `${uploadId}.${fileExt}`);
|
|
318
338
|
}
|
|
319
339
|
}
|
|
340
|
+
const imageDimensions = kind === "image" ? readImageDimensions(head) : null;
|
|
320
341
|
await rename(tmpPath, finalPath);
|
|
321
342
|
const rp = await realpath(finalPath);
|
|
322
343
|
const row = store.insertAttachment({
|
|
@@ -327,6 +348,8 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
|
|
|
327
348
|
mime: fileMime,
|
|
328
349
|
size: bytesWritten,
|
|
329
350
|
realpath: rp,
|
|
351
|
+
width: imageDimensions?.width ?? null,
|
|
352
|
+
height: imageDimensions?.height ?? null,
|
|
330
353
|
});
|
|
331
354
|
// Invalidate the per-session attachment label cache so the
|
|
332
355
|
// next egress (SSE broadcast or replay) sees this new row.
|
|
@@ -344,6 +367,8 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
|
|
|
344
367
|
displayName: row.name,
|
|
345
368
|
mimeType: row.mime,
|
|
346
369
|
size: row.size,
|
|
370
|
+
width: row.width,
|
|
371
|
+
height: row.height,
|
|
347
372
|
kind: row.kind,
|
|
348
373
|
path: `sessions/${sessionId}/attachments/${fileName}`,
|
|
349
374
|
url: fileUrl,
|
|
@@ -779,17 +804,36 @@ export function createRequestHandler(deps) {
|
|
|
779
804
|
const promptMatch = url.match(/^\/api\/v1\/sessions\/([^/]+)\/prompt\/?(\?.*)?$/);
|
|
780
805
|
if (promptMatch && req.method === "POST") {
|
|
781
806
|
const sessionId = decodeURIComponent(promptMatch[1]);
|
|
807
|
+
const requestOpId = getClientOpId(req);
|
|
782
808
|
const session = store.getSession(sessionId);
|
|
783
809
|
if (!session) {
|
|
810
|
+
logPromptRejectBeforeSave({
|
|
811
|
+
sessionId,
|
|
812
|
+
status: 404,
|
|
813
|
+
reason: "session_not_found",
|
|
814
|
+
opId: requestOpId,
|
|
815
|
+
});
|
|
784
816
|
json(res, 404, { error: "Session not found" });
|
|
785
817
|
return;
|
|
786
818
|
}
|
|
787
819
|
const bridge = getBridge?.();
|
|
788
820
|
if (!bridge) {
|
|
821
|
+
logPromptRejectBeforeSave({
|
|
822
|
+
sessionId,
|
|
823
|
+
status: 503,
|
|
824
|
+
reason: "agent_not_ready",
|
|
825
|
+
opId: requestOpId,
|
|
826
|
+
});
|
|
789
827
|
json(res, 503, { error: "Agent not ready yet" });
|
|
790
828
|
return;
|
|
791
829
|
}
|
|
792
830
|
if (!sessions) {
|
|
831
|
+
logPromptRejectBeforeSave({
|
|
832
|
+
sessionId,
|
|
833
|
+
status: 503,
|
|
834
|
+
reason: "session_manager_unavailable",
|
|
835
|
+
opId: requestOpId,
|
|
836
|
+
});
|
|
793
837
|
json(res, 503, { error: "Session manager not available" });
|
|
794
838
|
return;
|
|
795
839
|
}
|
|
@@ -801,6 +845,13 @@ export function createRequestHandler(deps) {
|
|
|
801
845
|
await sessions.ensureResumed(bridge, sessionId);
|
|
802
846
|
}
|
|
803
847
|
catch (err) {
|
|
848
|
+
logPromptRejectBeforeSave({
|
|
849
|
+
sessionId,
|
|
850
|
+
status: 500,
|
|
851
|
+
reason: "resume_failed",
|
|
852
|
+
opId,
|
|
853
|
+
error: errorMessage(err),
|
|
854
|
+
});
|
|
804
855
|
json(res, 500, {
|
|
805
856
|
error: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`,
|
|
806
857
|
});
|
|
@@ -809,6 +860,13 @@ export function createRequestHandler(deps) {
|
|
|
809
860
|
// Check if session is busy
|
|
810
861
|
const busyKind = sessions.getBusyKind(sessionId);
|
|
811
862
|
if (busyKind) {
|
|
863
|
+
logPromptRejectBeforeSave({
|
|
864
|
+
sessionId,
|
|
865
|
+
status: 409,
|
|
866
|
+
reason: "session_busy",
|
|
867
|
+
opId,
|
|
868
|
+
busyKind,
|
|
869
|
+
});
|
|
812
870
|
json(res, 409, { error: "Session is busy", busyKind });
|
|
813
871
|
return;
|
|
814
872
|
}
|
|
@@ -817,10 +875,26 @@ export function createRequestHandler(deps) {
|
|
|
817
875
|
body = JSON.parse(await readBody(req));
|
|
818
876
|
}
|
|
819
877
|
catch {
|
|
878
|
+
logPromptRejectBeforeSave({
|
|
879
|
+
sessionId,
|
|
880
|
+
status: 400,
|
|
881
|
+
reason: "invalid_json",
|
|
882
|
+
opId,
|
|
883
|
+
});
|
|
820
884
|
json(res, 400, { error: "Invalid JSON" });
|
|
821
885
|
return;
|
|
822
886
|
}
|
|
823
887
|
if (!body.text) {
|
|
888
|
+
logPromptRejectBeforeSave({
|
|
889
|
+
sessionId,
|
|
890
|
+
status: 400,
|
|
891
|
+
reason: "missing_text",
|
|
892
|
+
opId,
|
|
893
|
+
textLength: 0,
|
|
894
|
+
attachmentCount: Array.isArray(body.attachments)
|
|
895
|
+
? body.attachments.length
|
|
896
|
+
: undefined,
|
|
897
|
+
});
|
|
824
898
|
json(res, 400, { error: "Missing required field: text" });
|
|
825
899
|
return;
|
|
826
900
|
}
|
|
@@ -830,6 +904,13 @@ export function createRequestHandler(deps) {
|
|
|
830
904
|
const attachments = body.attachments;
|
|
831
905
|
if (attachments) {
|
|
832
906
|
if (!Array.isArray(attachments)) {
|
|
907
|
+
logPromptRejectBeforeSave({
|
|
908
|
+
sessionId,
|
|
909
|
+
status: 400,
|
|
910
|
+
reason: "attachments_not_array",
|
|
911
|
+
opId,
|
|
912
|
+
textLength: body.text.length,
|
|
913
|
+
});
|
|
833
914
|
json(res, 400, { error: "attachments must be an array" });
|
|
834
915
|
return;
|
|
835
916
|
}
|
|
@@ -841,14 +922,32 @@ export function createRequestHandler(deps) {
|
|
|
841
922
|
typeof att.attachmentId !== "string" ||
|
|
842
923
|
typeof att.displayName !== "string" ||
|
|
843
924
|
typeof att.mimeType !== "string") {
|
|
925
|
+
logPromptRejectBeforeSave({
|
|
926
|
+
sessionId,
|
|
927
|
+
status: 400,
|
|
928
|
+
reason: "invalid_attachment_entry",
|
|
929
|
+
opId,
|
|
930
|
+
textLength: body.text.length,
|
|
931
|
+
attachmentCount: attachments.length,
|
|
932
|
+
});
|
|
844
933
|
json(res, 400, { error: "Invalid attachment entry" });
|
|
845
934
|
return;
|
|
846
935
|
}
|
|
847
936
|
if (typeof att.uri === "string" ||
|
|
848
937
|
typeof att.data === "string" ||
|
|
849
|
-
typeof att.path === "string"
|
|
938
|
+
typeof att.path === "string" ||
|
|
939
|
+
typeof att.width === "number" ||
|
|
940
|
+
typeof att.height === "number") {
|
|
941
|
+
logPromptRejectBeforeSave({
|
|
942
|
+
sessionId,
|
|
943
|
+
status: 400,
|
|
944
|
+
reason: "client_supplied_attachment_data",
|
|
945
|
+
opId,
|
|
946
|
+
textLength: body.text.length,
|
|
947
|
+
attachmentCount: attachments.length,
|
|
948
|
+
});
|
|
850
949
|
json(res, 400, {
|
|
851
|
-
error: "Client must not supply uri/data/path",
|
|
950
|
+
error: "Client must not supply uri/data/path/width/height",
|
|
852
951
|
});
|
|
853
952
|
return;
|
|
854
953
|
}
|
|
@@ -873,6 +972,9 @@ export function createRequestHandler(deps) {
|
|
|
873
972
|
displayName: a.displayName,
|
|
874
973
|
mimeType: a.mimeType,
|
|
875
974
|
path: `/api/v1/sessions/${sessionId}/attachments/${fileName}`,
|
|
975
|
+
...(row.width != null && row.height != null
|
|
976
|
+
? { width: row.width, height: row.height }
|
|
977
|
+
: {}),
|
|
876
978
|
},
|
|
877
979
|
];
|
|
878
980
|
});
|
|
@@ -1043,11 +1145,14 @@ export function createRequestHandler(deps) {
|
|
|
1043
1145
|
return;
|
|
1044
1146
|
}
|
|
1045
1147
|
// --- PUT /api/v1/sessions/:id/{model,mode,reasoning-effort} ---
|
|
1046
|
-
|
|
1148
|
+
// --- PUT /api/v1/sessions/:id/config/:configId ---
|
|
1149
|
+
const legacyConfigPutMatch = url.match(/^\/api\/v1\/sessions\/([^/]+)\/(model|mode|reasoning-effort)\/?$/);
|
|
1150
|
+
const genericConfigPutMatch = url.match(/^\/api\/v1\/sessions\/([^/]+)\/config\/([^/]+)\/?$/);
|
|
1151
|
+
const configPutMatch = legacyConfigPutMatch ?? genericConfigPutMatch;
|
|
1047
1152
|
if (configPutMatch && req.method === "PUT") {
|
|
1048
1153
|
const sessionId = decodeURIComponent(configPutMatch[1]);
|
|
1049
|
-
const configPath = configPutMatch[2];
|
|
1050
|
-
const configId = configPath
|
|
1154
|
+
const configPath = decodeURIComponent(configPutMatch[2]);
|
|
1155
|
+
const configId = configPath.replace(/-/g, "_");
|
|
1051
1156
|
const session = store.getSession(sessionId);
|
|
1052
1157
|
if (!session) {
|
|
1053
1158
|
json(res, 404, { error: "Session not found" });
|
|
@@ -1066,14 +1171,16 @@ export function createRequestHandler(deps) {
|
|
|
1066
1171
|
json(res, 400, { error: "Invalid JSON" });
|
|
1067
1172
|
return;
|
|
1068
1173
|
}
|
|
1069
|
-
if (
|
|
1174
|
+
if (body.value === undefined) {
|
|
1070
1175
|
json(res, 400, { error: "Missing required field: value" });
|
|
1071
1176
|
return;
|
|
1072
1177
|
}
|
|
1073
1178
|
try {
|
|
1074
1179
|
const configOptions = await bridge.setConfigOption(sessionId, configId, body.value);
|
|
1075
1180
|
for (const opt of configOptions) {
|
|
1076
|
-
|
|
1181
|
+
if (typeof opt.currentValue === "string") {
|
|
1182
|
+
store.updateSessionConfig(sessionId, opt.id, opt.currentValue);
|
|
1183
|
+
}
|
|
1077
1184
|
}
|
|
1078
1185
|
sseManager.broadcast({
|
|
1079
1186
|
type: "config_option_update",
|
|
@@ -1170,7 +1277,9 @@ export function createRequestHandler(deps) {
|
|
|
1170
1277
|
reasoning_effort: cur.reasoning_effort,
|
|
1171
1278
|
};
|
|
1172
1279
|
const override = stored[opt.id];
|
|
1173
|
-
return override
|
|
1280
|
+
return override && "options" in opt
|
|
1281
|
+
? { ...opt, currentValue: override }
|
|
1282
|
+
: opt;
|
|
1174
1283
|
});
|
|
1175
1284
|
sseManager.broadcast({
|
|
1176
1285
|
type: "config_option_update",
|
|
@@ -1237,7 +1346,9 @@ export function createRequestHandler(deps) {
|
|
|
1237
1346
|
reasoning_effort: freshSession.reasoning_effort,
|
|
1238
1347
|
};
|
|
1239
1348
|
const override = stored[opt.id];
|
|
1240
|
-
return override
|
|
1349
|
+
return override && "options" in opt
|
|
1350
|
+
? { ...opt, currentValue: override }
|
|
1351
|
+
: opt;
|
|
1241
1352
|
});
|
|
1242
1353
|
return opts;
|
|
1243
1354
|
})()
|
|
@@ -1885,21 +1996,27 @@ export function createRequestHandler(deps) {
|
|
|
1885
1996
|
else {
|
|
1886
1997
|
sessionIdPatch = null;
|
|
1887
1998
|
}
|
|
1888
|
-
if (deps.
|
|
1889
|
-
|
|
1999
|
+
if (deps.clientRegistry) {
|
|
2000
|
+
// setVisibility no-ops on unknown clients; auto-register here so
|
|
2001
|
+
// SSE-only clients (which haven't sent /hello yet) still take
|
|
2002
|
+
// effect. The trust boundary above already gated on identity.
|
|
2003
|
+
if (!deps.clientRegistry.get(clientId)) {
|
|
2004
|
+
deps.clientRegistry.register(clientId, { capabilities: [] });
|
|
2005
|
+
}
|
|
2006
|
+
const { becameVisibleFor } = deps.clientRegistry.setVisibility(clientId, {
|
|
1890
2007
|
visible: body.visible,
|
|
1891
|
-
|
|
2008
|
+
active: sessionIdPatch,
|
|
1892
2009
|
});
|
|
1893
2010
|
// Edge-triggered only: heartbeat refreshes repeat the same
|
|
1894
2011
|
// (visible:true, sessionId:X) POST every 15s — firing sendClose
|
|
1895
2012
|
// on each would hammer banner recall. Only the first such
|
|
1896
2013
|
// transition after a change should recall stale banners.
|
|
1897
|
-
if (
|
|
1898
|
-
void deps.pushService.sendClose(`sess-${
|
|
2014
|
+
if (becameVisibleFor && deps.pushService) {
|
|
2015
|
+
void deps.pushService.sendClose(`sess-${becameVisibleFor}-done`);
|
|
1899
2016
|
if (sessions) {
|
|
1900
2017
|
for (const perm of sessions.pendingPermissions.values()) {
|
|
1901
|
-
if (perm.sessionId ===
|
|
1902
|
-
void deps.pushService.sendClose(`sess-${
|
|
2018
|
+
if (perm.sessionId === becameVisibleFor) {
|
|
2019
|
+
void deps.pushService.sendClose(`sess-${becameVisibleFor}-perm-${perm.requestId}`);
|
|
1903
2020
|
}
|
|
1904
2021
|
}
|
|
1905
2022
|
}
|
|
@@ -2014,7 +2131,11 @@ export function createRequestHandler(deps) {
|
|
|
2014
2131
|
const ext = extname(filePath);
|
|
2015
2132
|
const base = filePath.slice(filePath.lastIndexOf("/") + 1);
|
|
2016
2133
|
const isHashedAsset = /\.[A-Za-z0-9_-]{8,}\.(js|css)$/.test(base);
|
|
2017
|
-
|
|
2134
|
+
// `/lib/**` is vendored third-party content pinned by upstream SHA in
|
|
2135
|
+
// package metadata — treat as immutable just like content-hashed bundles.
|
|
2136
|
+
// Avoids re-downloading multi-hundred-KB wasm on every consumer init.
|
|
2137
|
+
const isVendoredLib = staticPath.startsWith("/lib/");
|
|
2138
|
+
const cacheControl = isHashedAsset || isVendoredLib
|
|
2018
2139
|
? "public, max-age=31536000, immutable"
|
|
2019
2140
|
: "no-cache";
|
|
2020
2141
|
const headers = {
|
package/lib/server.js
CHANGED
|
@@ -22,6 +22,7 @@ import { join as pathJoin } from "node:path";
|
|
|
22
22
|
import { resolveSessionsAnchor } from "./sessions-anchor.js";
|
|
23
23
|
import { runStartupChecks } from "./startup-checks.js";
|
|
24
24
|
import { AttachmentDispatcher } from "./attachment-dispatch.js";
|
|
25
|
+
import { buildBridgeEventHandlerConfig as _buildBridgeEventHandlerConfig } from "./bridge-event-config.js";
|
|
25
26
|
import { createCounters as createAttachmentInterceptorCounters, } from "./attachment-interceptor.js";
|
|
26
27
|
// Prefix all console output with ISO-ish timestamps (YYYY-MM-DD HH:MM:SS)
|
|
27
28
|
for (const method of ["log", "error", "warn"]) {
|
|
@@ -75,13 +76,14 @@ setInterval(() => {
|
|
|
75
76
|
.info("counters", { ...attachmentInterceptorCounters });
|
|
76
77
|
}, ATTACHMENT_INTERCEPTOR_DUMP_MS).unref();
|
|
77
78
|
const sessions = new SessionManager(store, config.default_cwd, config.data_dir);
|
|
78
|
-
const titleService = new TitleService(store, sessions, config.default_cwd, config.title.
|
|
79
|
+
const titleService = new TitleService(store, sessions, config.default_cwd, config.title.models);
|
|
80
|
+
const sseManager = new SseManager();
|
|
81
|
+
const clientRegistry = new ClientRegistry();
|
|
79
82
|
const pushService = new PushService(store, config.data_dir, config.push.vapid_subject, {
|
|
80
83
|
globalVisibilitySuppression: config.push.global_visibility_suppression,
|
|
84
|
+
clientRegistry,
|
|
81
85
|
});
|
|
82
86
|
console.log(`[push] VAPID public key ready`);
|
|
83
|
-
const sseManager = new SseManager();
|
|
84
|
-
const clientRegistry = new ClientRegistry();
|
|
85
87
|
sseManager.onRemove((clientId) => {
|
|
86
88
|
pushService.removeClient(clientId);
|
|
87
89
|
clientRegistry.remove(clientId);
|
|
@@ -128,26 +130,20 @@ const server = createServer((req, res) => {
|
|
|
128
130
|
async function initBridge(agentCmd) {
|
|
129
131
|
const b = new AgentBridge(agentCmd);
|
|
130
132
|
b.setAttachmentDispatcher(attachmentDispatcher);
|
|
133
|
+
const eventHandlerConfig = _buildBridgeEventHandlerConfig({
|
|
134
|
+
cancelTimeout: config.limits.cancel_timeout,
|
|
135
|
+
recentPathsLimit: config.limits.recent_paths,
|
|
136
|
+
attachmentInterceptorCounters,
|
|
137
|
+
shouldLogSchemaDrift: () => {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
if (now - lastSchemaDriftAt < SCHEMA_DRIFT_THROTTLE_MS)
|
|
140
|
+
return false;
|
|
141
|
+
lastSchemaDriftAt = now;
|
|
142
|
+
return true;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
131
145
|
b.on("event", (event) => {
|
|
132
|
-
handleAgentEvent(event, sessions, store, b,
|
|
133
|
-
cancelTimeout: config.limits.cancel_timeout,
|
|
134
|
-
recentPathsLimit: config.limits.recent_paths,
|
|
135
|
-
attachmentInterceptor: {
|
|
136
|
-
counters: attachmentInterceptorCounters,
|
|
137
|
-
logger: log.scope("attachment-interceptor"),
|
|
138
|
-
onSchemaDrift: (ctx) => {
|
|
139
|
-
const now = Date.now();
|
|
140
|
-
if (now - lastSchemaDriftAt < SCHEMA_DRIFT_THROTTLE_MS)
|
|
141
|
-
return;
|
|
142
|
-
lastSchemaDriftAt = now;
|
|
143
|
-
log
|
|
144
|
-
.scope("attachment-interceptor")
|
|
145
|
-
.error("schema drift detected — rawInput has no known path key", {
|
|
146
|
-
ctx,
|
|
147
|
-
});
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
}, sseManager, pushService, clientRegistry);
|
|
146
|
+
handleAgentEvent(event, sessions, store, b, eventHandlerConfig, sseManager, pushService, clientRegistry);
|
|
151
147
|
});
|
|
152
148
|
await b.start();
|
|
153
149
|
bridge = b;
|
|
@@ -181,7 +177,7 @@ process.on("SIGHUP", () => {
|
|
|
181
177
|
});
|
|
182
178
|
});
|
|
183
179
|
// --- Start ---
|
|
184
|
-
server.listen(config.port,
|
|
180
|
+
server.listen(config.port, config.host, () => {
|
|
185
181
|
void (async () => {
|
|
186
182
|
// The auth gate already ran in runStartupChecks (above) — either in
|
|
187
183
|
// this process or in a parent that handed off via WEBAGENT_STARTUP_
|
package/lib/session-manager.js
CHANGED
package/lib/startup-checks.js
CHANGED
|
@@ -44,6 +44,7 @@ export async function runStartupChecks(config) {
|
|
|
44
44
|
data_dir: config.data_dir,
|
|
45
45
|
agent_cmd: config.agent_cmd,
|
|
46
46
|
port: config.port,
|
|
47
|
+
host: config.host,
|
|
47
48
|
});
|
|
48
49
|
// 2. Auth bootstrap. Same `[check] auth: ...` style; first-run mint
|
|
49
50
|
// or refuse-to-serve land here.
|
package/lib/store.js
CHANGED
|
@@ -214,10 +214,22 @@ export class Store {
|
|
|
214
214
|
size INTEGER NOT NULL,
|
|
215
215
|
realpath TEXT NOT NULL,
|
|
216
216
|
upload_seq INTEGER NOT NULL,
|
|
217
|
+
width INTEGER,
|
|
218
|
+
height INTEGER,
|
|
217
219
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now'))
|
|
218
220
|
);
|
|
219
221
|
CREATE INDEX IF NOT EXISTS idx_attachments_session ON attachments(session_id);
|
|
220
222
|
`);
|
|
223
|
+
const attachmentCols = this.db
|
|
224
|
+
.prepare("PRAGMA table_info(attachments)")
|
|
225
|
+
.all();
|
|
226
|
+
const attachmentColNames = new Set(attachmentCols.map((c) => c.name));
|
|
227
|
+
if (!attachmentColNames.has("width")) {
|
|
228
|
+
this.db.exec("ALTER TABLE attachments ADD COLUMN width INTEGER");
|
|
229
|
+
}
|
|
230
|
+
if (!attachmentColNames.has("height")) {
|
|
231
|
+
this.db.exec("ALTER TABLE attachments ADD COLUMN height INTEGER");
|
|
232
|
+
}
|
|
221
233
|
// owner_prefs — key-value store for owner-scoped defaults (display_name,
|
|
222
234
|
// last /by selection, etc). Single-user model = single owner scope.
|
|
223
235
|
// Stored as plain key/value so we don't grow a new table per pref.
|
|
@@ -618,9 +630,9 @@ export class Store {
|
|
|
618
630
|
const uploadSeq = seqRow.s;
|
|
619
631
|
this.db
|
|
620
632
|
.prepare(`INSERT INTO attachments
|
|
621
|
-
(id, session_id, kind, name, mime, size, realpath, upload_seq)
|
|
622
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
623
|
-
.run(input.id, input.sessionId, input.kind, input.name, input.mime, input.size, input.realpath, uploadSeq);
|
|
633
|
+
(id, session_id, kind, name, mime, size, realpath, upload_seq, width, height)
|
|
634
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
635
|
+
.run(input.id, input.sessionId, input.kind, input.name, input.mime, input.size, input.realpath, uploadSeq, input.width ?? null, input.height ?? null);
|
|
624
636
|
return this.db
|
|
625
637
|
.prepare("SELECT * FROM attachments WHERE id = ?")
|
|
626
638
|
.get(input.id);
|
package/lib/title-service.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { log } from "./log.js";
|
|
2
|
+
import { pickModelByPatterns } from "./model-picker.js";
|
|
2
3
|
const tlog = log.scope("title");
|
|
3
4
|
export class TitleService {
|
|
4
5
|
titleSessionId = null;
|
|
@@ -12,10 +13,7 @@ export class TitleService {
|
|
|
12
13
|
this.store = store;
|
|
13
14
|
this.sessions = sessions;
|
|
14
15
|
this.defaultCwd = defaultCwd;
|
|
15
|
-
|
|
16
|
-
this.modelPatterns = modelPatterns
|
|
17
|
-
.map((p) => p.trim().toLowerCase())
|
|
18
|
-
.filter((p) => p.length > 0);
|
|
16
|
+
this.modelPatterns = modelPatterns;
|
|
19
17
|
}
|
|
20
18
|
/** Generate a title for the session (non-blocking, fire-and-forget). */
|
|
21
19
|
generate(bridge, userMessage, sessionId, onTitle) {
|
|
@@ -83,7 +81,7 @@ export class TitleService {
|
|
|
83
81
|
// the agent's reported availableModels (`configOptions[id=model].options`).
|
|
84
82
|
// Empty pattern list, no model option, or no match → skip the call and
|
|
85
83
|
// inherit the agent's default model (`currentModelId`).
|
|
86
|
-
const picked = this.
|
|
84
|
+
const picked = pickModelByPatterns(configOptions, this.modelPatterns);
|
|
87
85
|
if (picked) {
|
|
88
86
|
await bridge.setConfigOption(id, "model", picked).catch(() => []);
|
|
89
87
|
}
|
|
@@ -94,18 +92,4 @@ export class TitleService {
|
|
|
94
92
|
return null;
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
|
-
/** Find the first available model whose id matches any pattern (case-insensitive). */
|
|
98
|
-
pickTitleModel(configOptions) {
|
|
99
|
-
if (this.modelPatterns.length === 0)
|
|
100
|
-
return null;
|
|
101
|
-
const modelOpt = configOptions.find((c) => c.id === "model");
|
|
102
|
-
if (!modelOpt || modelOpt.options.length === 0)
|
|
103
|
-
return null;
|
|
104
|
-
for (const pattern of this.modelPatterns) {
|
|
105
|
-
const hit = modelOpt.options.find((o) => o.value.toLowerCase().includes(pattern));
|
|
106
|
-
if (hit)
|
|
107
|
-
return hit.value;
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
95
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lelouchhe/webagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A terminal-style web UI for ACP-compatible agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
]
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@agentclientprotocol/sdk": "^0.
|
|
62
|
+
"@agentclientprotocol/sdk": "^0.25.0",
|
|
63
63
|
"@types/proper-lockfile": "^4.1.4",
|
|
64
64
|
"@types/web-push": "^3.6.4",
|
|
65
65
|
"better-sqlite3": "^12.6.2",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"marked": "^18.0.2",
|
|
71
71
|
"proper-lockfile": "^4.1.2",
|
|
72
72
|
"smol-toml": "^1.6.0",
|
|
73
|
+
"temml": "^0.13.2",
|
|
73
74
|
"web-push": "^3.6.7",
|
|
74
75
|
"zod": "^4.3.6"
|
|
75
76
|
},
|