@steipete/summarize 0.8.2 → 0.10.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/CHANGELOG.md +114 -1
- package/LICENSE +1 -1
- package/README.md +309 -182
- package/dist/cli.js +1 -1
- package/dist/esm/cache.js +72 -4
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/config.js +197 -1
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/content/asset.js +75 -2
- package/dist/esm/content/asset.js.map +1 -1
- package/dist/esm/daemon/agent.js +547 -0
- package/dist/esm/daemon/agent.js.map +1 -0
- package/dist/esm/daemon/chat.js +97 -0
- package/dist/esm/daemon/chat.js.map +1 -0
- package/dist/esm/daemon/cli.js +105 -10
- package/dist/esm/daemon/cli.js.map +1 -1
- package/dist/esm/daemon/env-snapshot.js +3 -0
- package/dist/esm/daemon/env-snapshot.js.map +1 -1
- package/dist/esm/daemon/flow-context.js +53 -28
- package/dist/esm/daemon/flow-context.js.map +1 -1
- package/dist/esm/daemon/launchd.js +27 -0
- package/dist/esm/daemon/launchd.js.map +1 -1
- package/dist/esm/daemon/process-registry.js +206 -0
- package/dist/esm/daemon/process-registry.js.map +1 -0
- package/dist/esm/daemon/schtasks.js +64 -0
- package/dist/esm/daemon/schtasks.js.map +1 -1
- package/dist/esm/daemon/server.js +1034 -52
- package/dist/esm/daemon/server.js.map +1 -1
- package/dist/esm/daemon/summarize.js +66 -18
- package/dist/esm/daemon/summarize.js.map +1 -1
- package/dist/esm/daemon/systemd.js +61 -0
- package/dist/esm/daemon/systemd.js.map +1 -1
- package/dist/esm/flags.js +24 -0
- package/dist/esm/flags.js.map +1 -1
- package/dist/esm/llm/attachments.js +2 -0
- package/dist/esm/llm/attachments.js.map +1 -0
- package/dist/esm/llm/errors.js +6 -0
- package/dist/esm/llm/errors.js.map +1 -0
- package/dist/esm/llm/generate-text.js +206 -356
- package/dist/esm/llm/generate-text.js.map +1 -1
- package/dist/esm/llm/html-to-markdown.js +1 -2
- package/dist/esm/llm/html-to-markdown.js.map +1 -1
- package/dist/esm/llm/prompt.js.map +1 -1
- package/dist/esm/llm/providers/anthropic.js +126 -0
- package/dist/esm/llm/providers/anthropic.js.map +1 -0
- package/dist/esm/llm/providers/google.js +78 -0
- package/dist/esm/llm/providers/google.js.map +1 -0
- package/dist/esm/llm/providers/models.js +111 -0
- package/dist/esm/llm/providers/models.js.map +1 -0
- package/dist/esm/llm/providers/openai.js +150 -0
- package/dist/esm/llm/providers/openai.js.map +1 -0
- package/dist/esm/llm/providers/shared.js +48 -0
- package/dist/esm/llm/providers/shared.js.map +1 -0
- package/dist/esm/llm/providers/types.js +2 -0
- package/dist/esm/llm/providers/types.js.map +1 -0
- package/dist/esm/llm/transcript-to-markdown.js +1 -2
- package/dist/esm/llm/transcript-to-markdown.js.map +1 -1
- package/dist/esm/llm/types.js +2 -0
- package/dist/esm/llm/types.js.map +1 -0
- package/dist/esm/llm/usage.js +69 -0
- package/dist/esm/llm/usage.js.map +1 -0
- package/dist/esm/logging/daemon.js +124 -0
- package/dist/esm/logging/daemon.js.map +1 -0
- package/dist/esm/logging/ring-file.js +66 -0
- package/dist/esm/logging/ring-file.js.map +1 -0
- package/dist/esm/media-cache.js +251 -0
- package/dist/esm/media-cache.js.map +1 -0
- package/dist/esm/model-auto.js +103 -5
- package/dist/esm/model-auto.js.map +1 -1
- package/dist/esm/processes.js +2 -0
- package/dist/esm/processes.js.map +1 -0
- package/dist/esm/refresh-free.js +3 -3
- package/dist/esm/refresh-free.js.map +1 -1
- package/dist/esm/run/attachments.js +8 -4
- package/dist/esm/run/attachments.js.map +1 -1
- package/dist/esm/run/bird.js +118 -5
- package/dist/esm/run/bird.js.map +1 -1
- package/dist/esm/run/cache-state.js +3 -2
- package/dist/esm/run/cache-state.js.map +1 -1
- package/dist/esm/run/cli-preflight.js +19 -1
- package/dist/esm/run/cli-preflight.js.map +1 -1
- package/dist/esm/run/constants.js +0 -7
- package/dist/esm/run/constants.js.map +1 -1
- package/dist/esm/run/finish-line.js +58 -11
- package/dist/esm/run/finish-line.js.map +1 -1
- package/dist/esm/run/flows/asset/extract.js +70 -0
- package/dist/esm/run/flows/asset/extract.js.map +1 -0
- package/dist/esm/run/flows/asset/input.js +209 -25
- package/dist/esm/run/flows/asset/input.js.map +1 -1
- package/dist/esm/run/flows/asset/media-policy.js +3 -0
- package/dist/esm/run/flows/asset/media-policy.js.map +1 -0
- package/dist/esm/run/flows/asset/media.js +224 -0
- package/dist/esm/run/flows/asset/media.js.map +1 -0
- package/dist/esm/run/flows/asset/output.js +98 -0
- package/dist/esm/run/flows/asset/output.js.map +1 -0
- package/dist/esm/run/flows/asset/preprocess.js +92 -16
- package/dist/esm/run/flows/asset/preprocess.js.map +1 -1
- package/dist/esm/run/flows/asset/summary.js +165 -11
- package/dist/esm/run/flows/asset/summary.js.map +1 -1
- package/dist/esm/run/flows/url/extract.js +6 -6
- package/dist/esm/run/flows/url/extract.js.map +1 -1
- package/dist/esm/run/flows/url/flow.js +338 -36
- package/dist/esm/run/flows/url/flow.js.map +1 -1
- package/dist/esm/run/flows/url/markdown.js +6 -1
- package/dist/esm/run/flows/url/markdown.js.map +1 -1
- package/dist/esm/run/flows/url/slides-output.js +485 -0
- package/dist/esm/run/flows/url/slides-output.js.map +1 -0
- package/dist/esm/run/flows/url/slides-text.js +628 -0
- package/dist/esm/run/flows/url/slides-text.js.map +1 -0
- package/dist/esm/run/flows/url/summary.js +358 -83
- package/dist/esm/run/flows/url/summary.js.map +1 -1
- package/dist/esm/run/help.js +94 -5
- package/dist/esm/run/help.js.map +1 -1
- package/dist/esm/run/logging.js +12 -4
- package/dist/esm/run/logging.js.map +1 -1
- package/dist/esm/run/media-cache-state.js +33 -0
- package/dist/esm/run/media-cache-state.js.map +1 -0
- package/dist/esm/run/progress.js +19 -1
- package/dist/esm/run/progress.js.map +1 -1
- package/dist/esm/run/run-context.js +19 -0
- package/dist/esm/run/run-context.js.map +1 -0
- package/dist/esm/run/run-output.js +1 -1
- package/dist/esm/run/run-output.js.map +1 -1
- package/dist/esm/run/run-settings.js +182 -0
- package/dist/esm/run/run-settings.js.map +1 -0
- package/dist/esm/run/runner.js +225 -32
- package/dist/esm/run/runner.js.map +1 -1
- package/dist/esm/run/slides-cli.js +225 -0
- package/dist/esm/run/slides-cli.js.map +1 -0
- package/dist/esm/run/slides-render.js +163 -0
- package/dist/esm/run/slides-render.js.map +1 -0
- package/dist/esm/run/stream-output.js +63 -0
- package/dist/esm/run/stream-output.js.map +1 -0
- package/dist/esm/run/streaming.js +16 -43
- package/dist/esm/run/streaming.js.map +1 -1
- package/dist/esm/run/summary-engine.js +59 -41
- package/dist/esm/run/summary-engine.js.map +1 -1
- package/dist/esm/run/transcriber-cli.js +148 -0
- package/dist/esm/run/transcriber-cli.js.map +1 -0
- package/dist/esm/shared/sse-events.js +26 -0
- package/dist/esm/shared/sse-events.js.map +1 -0
- package/dist/esm/shared/streaming-merge.js +44 -0
- package/dist/esm/shared/streaming-merge.js.map +1 -0
- package/dist/esm/slides/extract.js +1942 -0
- package/dist/esm/slides/extract.js.map +1 -0
- package/dist/esm/slides/index.js +4 -0
- package/dist/esm/slides/index.js.map +1 -0
- package/dist/esm/slides/settings.js +73 -0
- package/dist/esm/slides/settings.js.map +1 -0
- package/dist/esm/slides/store.js +111 -0
- package/dist/esm/slides/store.js.map +1 -0
- package/dist/esm/slides/types.js +2 -0
- package/dist/esm/slides/types.js.map +1 -0
- package/dist/esm/tty/osc-progress.js +21 -1
- package/dist/esm/tty/osc-progress.js.map +1 -1
- package/dist/esm/tty/progress/fetch-html.js +8 -4
- package/dist/esm/tty/progress/fetch-html.js.map +1 -1
- package/dist/esm/tty/progress/transcript.js +82 -31
- package/dist/esm/tty/progress/transcript.js.map +1 -1
- package/dist/esm/tty/spinner.js +2 -2
- package/dist/esm/tty/spinner.js.map +1 -1
- package/dist/esm/tty/theme.js +189 -0
- package/dist/esm/tty/theme.js.map +1 -0
- package/dist/esm/tty/website-progress.js +17 -13
- package/dist/esm/tty/website-progress.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/types/cache.d.ts +14 -2
- package/dist/types/config.d.ts +34 -0
- package/dist/types/daemon/agent.d.ts +25 -0
- package/dist/types/daemon/chat.d.ts +27 -0
- package/dist/types/daemon/env-snapshot.d.ts +1 -1
- package/dist/types/daemon/flow-context.d.ts +24 -3
- package/dist/types/daemon/launchd.d.ts +4 -0
- package/dist/types/daemon/process-registry.d.ts +73 -0
- package/dist/types/daemon/schtasks.d.ts +4 -0
- package/dist/types/daemon/server.d.ts +7 -1
- package/dist/types/daemon/summarize.d.ts +47 -5
- package/dist/types/daemon/systemd.d.ts +4 -0
- package/dist/types/flags.d.ts +1 -0
- package/dist/types/llm/attachments.d.ts +6 -0
- package/dist/types/llm/errors.d.ts +1 -0
- package/dist/types/llm/generate-text.d.ts +29 -13
- package/dist/types/llm/prompt.d.ts +7 -2
- package/dist/types/llm/providers/anthropic.d.ts +30 -0
- package/dist/types/llm/providers/google.d.ts +29 -0
- package/dist/types/llm/providers/models.d.ts +27 -0
- package/dist/types/llm/providers/openai.d.ts +38 -0
- package/dist/types/llm/providers/shared.d.ts +14 -0
- package/dist/types/llm/providers/types.d.ts +6 -0
- package/dist/types/llm/types.d.ts +5 -0
- package/dist/types/llm/usage.d.ts +5 -0
- package/dist/types/logging/daemon.d.ts +26 -0
- package/dist/types/logging/ring-file.d.ts +10 -0
- package/dist/types/media-cache.d.ts +22 -0
- package/dist/types/model-auto.d.ts +1 -0
- package/dist/types/processes.d.ts +1 -0
- package/dist/types/run/attachments.d.ts +9 -6
- package/dist/types/run/bird.d.ts +7 -0
- package/dist/types/run/constants.d.ts +0 -2
- package/dist/types/run/finish-line.d.ts +59 -1
- package/dist/types/run/flows/asset/extract.d.ts +18 -0
- package/dist/types/run/flows/asset/input.d.ts +12 -2
- package/dist/types/run/flows/asset/media-policy.d.ts +2 -0
- package/dist/types/run/flows/asset/media.d.ts +21 -0
- package/dist/types/run/flows/asset/output.d.ts +42 -0
- package/dist/types/run/flows/asset/preprocess.d.ts +22 -2
- package/dist/types/run/flows/asset/summary.d.ts +6 -0
- package/dist/types/run/flows/url/extract.d.ts +2 -1
- package/dist/types/run/flows/url/slides-output.d.ts +66 -0
- package/dist/types/run/flows/url/slides-text.d.ts +87 -0
- package/dist/types/run/flows/url/summary.d.ts +11 -3
- package/dist/types/run/flows/url/types.d.ts +29 -2
- package/dist/types/run/help.d.ts +3 -0
- package/dist/types/run/logging.d.ts +3 -2
- package/dist/types/run/media-cache-state.d.ts +7 -0
- package/dist/types/run/progress.d.ts +2 -1
- package/dist/types/run/run-context.d.ts +44 -0
- package/dist/types/run/run-settings.d.ts +62 -0
- package/dist/types/run/slides-cli.d.ts +9 -0
- package/dist/types/run/slides-render.d.ts +30 -0
- package/dist/types/run/stream-output.d.ts +12 -0
- package/dist/types/run/streaming.d.ts +10 -4
- package/dist/types/run/summary-engine.d.ts +15 -3
- package/dist/types/run/summary-llm.d.ts +2 -2
- package/dist/types/run/transcriber-cli.d.ts +8 -0
- package/dist/types/shared/sse-events.d.ts +64 -0
- package/dist/types/shared/streaming-merge.d.ts +4 -0
- package/dist/types/slides/extract.d.ts +42 -0
- package/dist/types/slides/index.d.ts +5 -0
- package/dist/types/slides/settings.d.ts +20 -0
- package/dist/types/slides/store.d.ts +15 -0
- package/dist/types/slides/types.d.ts +40 -0
- package/dist/types/tty/osc-progress.d.ts +2 -2
- package/dist/types/tty/progress/fetch-html.d.ts +3 -1
- package/dist/types/tty/progress/transcript.d.ts +3 -1
- package/dist/types/tty/spinner.d.ts +3 -1
- package/dist/types/tty/theme.d.ts +44 -0
- package/dist/types/tty/website-progress.d.ts +3 -1
- package/dist/types/version.d.ts +1 -1
- package/docs/README.md +13 -8
- package/docs/_config.yml +26 -0
- package/docs/_layouts/default.html +60 -0
- package/docs/agent.md +333 -0
- package/docs/assets/site.css +748 -0
- package/docs/assets/site.js +72 -0
- package/docs/assets/summarize-cli.png +0 -0
- package/docs/assets/summarize-extension.png +0 -0
- package/docs/assets/youtube-slides.png +0 -0
- package/docs/cache.md +29 -3
- package/docs/chrome-extension.md +85 -7
- package/docs/config.md +74 -2
- package/docs/extract-only.md +10 -2
- package/docs/index.html +205 -0
- package/docs/index.md +25 -0
- package/docs/language.md +1 -1
- package/docs/llm.md +17 -1
- package/docs/manual-tests.md +2 -0
- package/docs/media.md +37 -0
- package/docs/model-auto.md +2 -1
- package/docs/nvidia-onnx-transcription.md +55 -0
- package/docs/openai.md +5 -0
- package/docs/releasing.md +26 -0
- package/docs/site/assets/site.css +399 -228
- package/docs/site/assets/summarize-cli.png +0 -0
- package/docs/site/assets/summarize-extension.png +0 -0
- package/docs/site/docs/chrome-extension.html +89 -0
- package/docs/site/docs/config.html +1 -0
- package/docs/site/docs/extract-only.html +1 -0
- package/docs/site/docs/firecrawl.html +1 -0
- package/docs/site/docs/index.html +5 -0
- package/docs/site/docs/llm.html +1 -0
- package/docs/site/docs/openai.html +1 -0
- package/docs/site/docs/website.html +1 -0
- package/docs/site/docs/youtube.html +1 -0
- package/docs/site/index.html +148 -84
- package/docs/slides.md +74 -0
- package/docs/timestamps.md +103 -0
- package/docs/website.md +13 -0
- package/docs/youtube.md +16 -0
- package/package.json +22 -18
- package/dist/esm/daemon/request-settings.js +0 -91
- package/dist/esm/daemon/request-settings.js.map +0 -1
- package/dist/types/daemon/request-settings.d.ts +0 -27
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream, promises as fs } from 'node:fs';
|
|
2
3
|
import http from 'node:http';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { Writable } from 'node:stream';
|
|
3
6
|
import { loadSummarizeConfig } from '../config.js';
|
|
7
|
+
import { createDaemonLogger } from '../logging/daemon.js';
|
|
8
|
+
import { runWithProcessContext, setProcessObserver } from '../processes.js';
|
|
9
|
+
import { refreshFree } from '../refresh-free.js';
|
|
4
10
|
import { createCacheStateFromConfig, refreshCacheStoreIfMissing } from '../run/cache-state.js';
|
|
11
|
+
import { resolveExecutableInPath } from '../run/env.js';
|
|
5
12
|
import { formatModelLabelForDisplay } from '../run/finish-line.js';
|
|
13
|
+
import { createMediaCacheFromConfig } from '../run/media-cache-state.js';
|
|
14
|
+
import { resolveRunOverrides } from '../run/run-settings.js';
|
|
15
|
+
import { encodeSseEvent } from '../shared/sse-events.js';
|
|
16
|
+
import { resolveSlideImagePath, resolveSlideSettings } from '../slides/index.js';
|
|
6
17
|
import { resolvePackageVersion } from '../version.js';
|
|
18
|
+
import { completeAgentResponse, streamAgentResponse } from './agent.js';
|
|
7
19
|
import { resolveAutoDaemonMode } from './auto-mode.js';
|
|
8
20
|
import { DAEMON_HOST, DAEMON_PORT_DEFAULT } from './constants.js';
|
|
21
|
+
import { resolveDaemonLogPaths } from './launchd.js';
|
|
9
22
|
import { buildModelPickerOptions } from './models.js';
|
|
10
|
-
import {
|
|
11
|
-
import { streamSummaryForUrl, streamSummaryForVisiblePage } from './summarize.js';
|
|
23
|
+
import { buildProcessListResult, buildProcessLogsResult, ProcessRegistry, } from './process-registry.js';
|
|
24
|
+
import { extractContentForUrl, streamSummaryForUrl, streamSummaryForVisiblePage, } from './summarize.js';
|
|
12
25
|
function json(res, status, payload, headers) {
|
|
13
26
|
const body = `${JSON.stringify(payload)}\n`;
|
|
14
27
|
res.writeHead(status, {
|
|
@@ -18,6 +31,39 @@ function json(res, status, payload, headers) {
|
|
|
18
31
|
});
|
|
19
32
|
res.end(body);
|
|
20
33
|
}
|
|
34
|
+
function clampNumber(value, min, max) {
|
|
35
|
+
if (!Number.isFinite(value))
|
|
36
|
+
return min;
|
|
37
|
+
return Math.max(min, Math.min(max, value));
|
|
38
|
+
}
|
|
39
|
+
async function readLogTail({ filePath, maxBytes, maxLines, }) {
|
|
40
|
+
const stat = await fs.stat(filePath);
|
|
41
|
+
const size = stat.size;
|
|
42
|
+
const readBytes = Math.max(0, Math.min(size, maxBytes));
|
|
43
|
+
const handle = await fs.open(filePath, 'r');
|
|
44
|
+
try {
|
|
45
|
+
const buffer = Buffer.alloc(readBytes);
|
|
46
|
+
const start = Math.max(0, size - readBytes);
|
|
47
|
+
await handle.read(buffer, 0, readBytes, start);
|
|
48
|
+
let text = buffer.toString('utf8');
|
|
49
|
+
let truncated = size > readBytes;
|
|
50
|
+
if (truncated) {
|
|
51
|
+
const firstNewline = text.indexOf('\n');
|
|
52
|
+
if (firstNewline !== -1) {
|
|
53
|
+
text = text.slice(firstNewline + 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
let lines = text.split(/\r?\n/).filter((line) => line.length > 0);
|
|
57
|
+
if (lines.length > maxLines) {
|
|
58
|
+
lines = lines.slice(lines.length - maxLines);
|
|
59
|
+
truncated = true;
|
|
60
|
+
}
|
|
61
|
+
return { lines, truncated, bytesRead: readBytes };
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await handle.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
21
67
|
function text(res, status, body, headers) {
|
|
22
68
|
const out = body.endsWith('\n') ? body : `${body}\n`;
|
|
23
69
|
res.writeHead(status, {
|
|
@@ -50,9 +96,6 @@ function corsHeaders(origin) {
|
|
|
50
96
|
vary: 'Origin',
|
|
51
97
|
};
|
|
52
98
|
}
|
|
53
|
-
function sseEncode(event) {
|
|
54
|
-
return `event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`;
|
|
55
|
-
}
|
|
56
99
|
function readBearerToken(req) {
|
|
57
100
|
const header = req.headers.authorization;
|
|
58
101
|
if (typeof header !== 'string')
|
|
@@ -73,30 +116,116 @@ async function readJsonBody(req, maxBytes) {
|
|
|
73
116
|
const text = Buffer.concat(chunks).toString('utf8');
|
|
74
117
|
return JSON.parse(text);
|
|
75
118
|
}
|
|
119
|
+
function wantsJsonResponse(req, url) {
|
|
120
|
+
const format = url.searchParams.get('format');
|
|
121
|
+
if (format && format.toLowerCase() === 'json')
|
|
122
|
+
return true;
|
|
123
|
+
const accept = req.headers.accept;
|
|
124
|
+
if (typeof accept !== 'string')
|
|
125
|
+
return false;
|
|
126
|
+
const lower = accept.toLowerCase();
|
|
127
|
+
if (lower.includes('text/event-stream'))
|
|
128
|
+
return false;
|
|
129
|
+
return lower.includes('application/json');
|
|
130
|
+
}
|
|
131
|
+
function parseDiagnostics(raw) {
|
|
132
|
+
if (!raw || typeof raw !== 'object') {
|
|
133
|
+
return { includeContent: false };
|
|
134
|
+
}
|
|
135
|
+
const obj = raw;
|
|
136
|
+
return { includeContent: Boolean(obj.includeContent) };
|
|
137
|
+
}
|
|
138
|
+
function createLineWriter(onLine) {
|
|
139
|
+
let buffer = '';
|
|
140
|
+
return new Writable({
|
|
141
|
+
write(chunk, _encoding, callback) {
|
|
142
|
+
buffer += chunk.toString();
|
|
143
|
+
let index = buffer.indexOf('\n');
|
|
144
|
+
while (index >= 0) {
|
|
145
|
+
const line = buffer.slice(0, index).trimEnd();
|
|
146
|
+
buffer = buffer.slice(index + 1);
|
|
147
|
+
if (line.trim().length > 0)
|
|
148
|
+
onLine(line);
|
|
149
|
+
index = buffer.indexOf('\n');
|
|
150
|
+
}
|
|
151
|
+
callback();
|
|
152
|
+
},
|
|
153
|
+
final(callback) {
|
|
154
|
+
const line = buffer.trim();
|
|
155
|
+
if (line)
|
|
156
|
+
onLine(line);
|
|
157
|
+
buffer = '';
|
|
158
|
+
callback();
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
76
162
|
function createSession() {
|
|
77
163
|
return {
|
|
78
164
|
id: randomUUID(),
|
|
79
165
|
createdAtMs: Date.now(),
|
|
80
166
|
buffer: [],
|
|
167
|
+
bufferBytes: 0,
|
|
81
168
|
done: false,
|
|
82
169
|
clients: new Set(),
|
|
170
|
+
slidesBuffer: [],
|
|
171
|
+
slidesBufferBytes: 0,
|
|
172
|
+
slidesClients: new Set(),
|
|
173
|
+
slidesDone: false,
|
|
174
|
+
slidesRequested: false,
|
|
175
|
+
slidesLastStatus: null,
|
|
83
176
|
lastMeta: { model: null, modelLabel: null, inputSummary: null, summaryFromCache: null },
|
|
177
|
+
slides: null,
|
|
84
178
|
};
|
|
85
179
|
}
|
|
86
|
-
|
|
87
|
-
|
|
180
|
+
const MAX_SESSION_BUFFER_EVENTS = 2000;
|
|
181
|
+
const MAX_SESSION_BUFFER_BYTES = 512 * 1024;
|
|
182
|
+
const MAX_SLIDES_BUFFER_EVENTS = 600;
|
|
183
|
+
const MAX_SLIDES_BUFFER_BYTES = 256 * 1024;
|
|
184
|
+
const MAX_SESSION_LIFETIME_MS = 30 * 60_000;
|
|
185
|
+
function pushToSession(session, evt, onSessionEvent) {
|
|
186
|
+
const encoded = encodeSseEvent(evt);
|
|
88
187
|
for (const res of session.clients) {
|
|
89
188
|
res.write(encoded);
|
|
90
189
|
}
|
|
91
|
-
session.
|
|
92
|
-
|
|
93
|
-
|
|
190
|
+
onSessionEvent?.(evt, session.id);
|
|
191
|
+
const bytes = Buffer.byteLength(encoded);
|
|
192
|
+
session.buffer.push({ event: evt, bytes });
|
|
193
|
+
session.bufferBytes += bytes;
|
|
194
|
+
while (session.buffer.length > MAX_SESSION_BUFFER_EVENTS ||
|
|
195
|
+
session.bufferBytes > MAX_SESSION_BUFFER_BYTES) {
|
|
196
|
+
const removed = session.buffer.shift();
|
|
197
|
+
if (!removed)
|
|
198
|
+
break;
|
|
199
|
+
session.bufferBytes -= removed.bytes;
|
|
94
200
|
}
|
|
95
201
|
if (evt.event === 'done' || evt.event === 'error') {
|
|
96
202
|
session.done = true;
|
|
97
203
|
}
|
|
98
204
|
}
|
|
99
|
-
function
|
|
205
|
+
function pushSlidesToSession(session, evt, onSessionEvent) {
|
|
206
|
+
const encoded = encodeSseEvent(evt);
|
|
207
|
+
for (const res of session.slidesClients) {
|
|
208
|
+
res.write(encoded);
|
|
209
|
+
}
|
|
210
|
+
onSessionEvent?.(evt, session.id);
|
|
211
|
+
const bytes = Buffer.byteLength(encoded);
|
|
212
|
+
session.slidesBuffer.push({ event: evt, bytes });
|
|
213
|
+
session.slidesBufferBytes += bytes;
|
|
214
|
+
while (session.slidesBuffer.length > MAX_SLIDES_BUFFER_EVENTS ||
|
|
215
|
+
session.slidesBufferBytes > MAX_SLIDES_BUFFER_BYTES) {
|
|
216
|
+
const removed = session.slidesBuffer.shift();
|
|
217
|
+
if (!removed)
|
|
218
|
+
break;
|
|
219
|
+
session.slidesBufferBytes -= removed.bytes;
|
|
220
|
+
}
|
|
221
|
+
if (evt.event === 'done' || evt.event === 'error') {
|
|
222
|
+
session.slidesDone = true;
|
|
223
|
+
}
|
|
224
|
+
if (evt.event === 'status') {
|
|
225
|
+
session.slidesLastStatus = evt.data.text;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function emitMeta(session, patch, onSessionEvent) {
|
|
100
229
|
const next = { ...session.lastMeta, ...patch };
|
|
101
230
|
if (next.model === session.lastMeta.model &&
|
|
102
231
|
next.modelLabel === session.lastMeta.modelLabel &&
|
|
@@ -105,26 +234,117 @@ function emitMeta(session, patch) {
|
|
|
105
234
|
return;
|
|
106
235
|
}
|
|
107
236
|
session.lastMeta = next;
|
|
108
|
-
pushToSession(session, { event: 'meta', data: next });
|
|
237
|
+
pushToSession(session, { event: 'meta', data: next }, onSessionEvent);
|
|
238
|
+
}
|
|
239
|
+
function emitSlides(session, data, onSessionEvent) {
|
|
240
|
+
pushToSession(session, { event: 'slides', data }, onSessionEvent);
|
|
241
|
+
pushSlidesToSession(session, { event: 'slides', data }, onSessionEvent);
|
|
242
|
+
}
|
|
243
|
+
function emitSlidesStatus(session, text, onSessionEvent) {
|
|
244
|
+
const trimmed = text.trim();
|
|
245
|
+
if (!trimmed)
|
|
246
|
+
return;
|
|
247
|
+
pushSlidesToSession(session, { event: 'status', data: { text: trimmed } }, onSessionEvent);
|
|
248
|
+
}
|
|
249
|
+
function emitSlidesDone(session, result, onSessionEvent) {
|
|
250
|
+
if (!result.ok) {
|
|
251
|
+
const message = result.error?.trim() || 'Slides failed.';
|
|
252
|
+
pushSlidesToSession(session, { event: 'error', data: { message } }, onSessionEvent);
|
|
253
|
+
}
|
|
254
|
+
pushSlidesToSession(session, { event: 'done', data: {} }, onSessionEvent);
|
|
255
|
+
}
|
|
256
|
+
function resolveHomeDir(env) {
|
|
257
|
+
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
|
|
258
|
+
if (!home)
|
|
259
|
+
return process.cwd();
|
|
260
|
+
return home;
|
|
261
|
+
}
|
|
262
|
+
function resolveSlidesSettings({ env, request, }) {
|
|
263
|
+
const slidesValue = request.slides;
|
|
264
|
+
const tesseractAvailable = resolveToolPath('tesseract', env, 'TESSERACT_PATH') !== null;
|
|
265
|
+
const slidesOcrValue = tesseractAvailable ? request.slidesOcr : false;
|
|
266
|
+
return resolveSlideSettings({
|
|
267
|
+
slides: slidesValue,
|
|
268
|
+
slidesOcr: slidesOcrValue,
|
|
269
|
+
slidesDir: request.slidesDir ?? '.summarize/slides',
|
|
270
|
+
slidesSceneThreshold: request.slidesSceneThreshold,
|
|
271
|
+
slidesSceneThresholdExplicit: typeof request.slidesSceneThreshold !== 'undefined',
|
|
272
|
+
slidesMax: request.slidesMax,
|
|
273
|
+
slidesMinDuration: request.slidesMinDuration,
|
|
274
|
+
cwd: resolveHomeDir(env),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function buildSlidesPayload({ slides, port, }) {
|
|
278
|
+
// Use a stable URL that survives session GC, so images don't break while scrolling.
|
|
279
|
+
const baseUrl = `http://127.0.0.1:${port}/v1/slides/${slides.sourceId}`;
|
|
280
|
+
return {
|
|
281
|
+
sourceUrl: slides.sourceUrl,
|
|
282
|
+
sourceId: slides.sourceId,
|
|
283
|
+
sourceKind: slides.sourceKind,
|
|
284
|
+
ocrAvailable: slides.ocrAvailable,
|
|
285
|
+
slides: slides.slides.map((slide) => ({
|
|
286
|
+
index: slide.index,
|
|
287
|
+
timestamp: slide.timestamp,
|
|
288
|
+
imageUrl: `${baseUrl}/${slide.index}${typeof slide.imageVersion === 'number' && slide.imageVersion > 0
|
|
289
|
+
? `?v=${slide.imageVersion}`
|
|
290
|
+
: ''}`,
|
|
291
|
+
ocrText: slide.ocrText ?? null,
|
|
292
|
+
ocrConfidence: slide.ocrConfidence ?? null,
|
|
293
|
+
})),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function resolveToolPath(binary, env, explicitEnvKey) {
|
|
297
|
+
const explicit = explicitEnvKey && typeof env[explicitEnvKey] === 'string' ? env[explicitEnvKey]?.trim() : '';
|
|
298
|
+
if (explicit)
|
|
299
|
+
return resolveExecutableInPath(explicit, env);
|
|
300
|
+
return resolveExecutableInPath(binary, env);
|
|
109
301
|
}
|
|
110
302
|
function endSession(session) {
|
|
111
303
|
for (const res of session.clients) {
|
|
112
304
|
res.end();
|
|
113
305
|
}
|
|
114
306
|
session.clients.clear();
|
|
307
|
+
for (const res of session.slidesClients) {
|
|
308
|
+
res.end();
|
|
309
|
+
}
|
|
310
|
+
session.slidesClients.clear();
|
|
311
|
+
}
|
|
312
|
+
function scheduleSessionCleanup({ session, sessions, delayMs = 60_000, }) {
|
|
313
|
+
setTimeout(() => {
|
|
314
|
+
const ageMs = Date.now() - session.createdAtMs;
|
|
315
|
+
const slidesPending = session.slidesRequested && !session.slidesDone;
|
|
316
|
+
if (!slidesPending || ageMs > MAX_SESSION_LIFETIME_MS) {
|
|
317
|
+
sessions.delete(session.id);
|
|
318
|
+
endSession(session);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
scheduleSessionCleanup({ session, sessions, delayMs });
|
|
322
|
+
}, delayMs).unref();
|
|
115
323
|
}
|
|
116
324
|
export function buildHealthPayload(importMetaUrl) {
|
|
117
325
|
return { ok: true, pid: process.pid, version: resolvePackageVersion(importMetaUrl) };
|
|
118
326
|
}
|
|
119
|
-
export async function runDaemonServer({ env, fetchImpl, config, port = config.port ?? DAEMON_PORT_DEFAULT, }) {
|
|
327
|
+
export async function runDaemonServer({ env, fetchImpl, config, port = config.port ?? DAEMON_PORT_DEFAULT, signal, onListening, onSessionEvent, }) {
|
|
120
328
|
const { config: summarizeConfig } = loadSummarizeConfig({ env });
|
|
329
|
+
const daemonLogger = createDaemonLogger({ env, config: summarizeConfig });
|
|
330
|
+
const daemonLogPaths = resolveDaemonLogPaths(env);
|
|
331
|
+
const daemonLogFile = daemonLogger.config?.file ?? path.join(daemonLogPaths.logDir, 'daemon.jsonl');
|
|
121
332
|
const cacheState = await createCacheStateFromConfig({
|
|
122
333
|
envForRun: env,
|
|
123
334
|
config: summarizeConfig,
|
|
124
335
|
noCacheFlag: false,
|
|
125
336
|
transcriptNamespace: 'yt:auto',
|
|
126
337
|
});
|
|
338
|
+
const mediaCache = await createMediaCacheFromConfig({
|
|
339
|
+
envForRun: env,
|
|
340
|
+
config: summarizeConfig,
|
|
341
|
+
noMediaCacheFlag: false,
|
|
342
|
+
});
|
|
343
|
+
const processRegistry = new ProcessRegistry();
|
|
344
|
+
setProcessObserver(processRegistry.createObserver());
|
|
127
345
|
const sessions = new Map();
|
|
346
|
+
const refreshSessions = new Map();
|
|
347
|
+
let activeRefreshSessionId = null;
|
|
128
348
|
const server = http.createServer((req, res) => {
|
|
129
349
|
void (async () => {
|
|
130
350
|
const origin = resolveOriginHeader(req);
|
|
@@ -150,6 +370,76 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
150
370
|
json(res, 200, { ok: true }, cors);
|
|
151
371
|
return;
|
|
152
372
|
}
|
|
373
|
+
if (req.method === 'GET' && pathname === '/v1/logs') {
|
|
374
|
+
const source = url.searchParams.get('source')?.trim() || 'daemon';
|
|
375
|
+
const tailParam = url.searchParams.get('tail')?.trim() || '';
|
|
376
|
+
const tail = clampNumber(Number(tailParam || '800'), 50, 5000);
|
|
377
|
+
const maxBytes = clampNumber(Number(url.searchParams.get('maxBytes') ?? '262144'), 16_384, 2_000_000);
|
|
378
|
+
const sources = {
|
|
379
|
+
daemon: {
|
|
380
|
+
filePath: daemonLogFile,
|
|
381
|
+
format: daemonLogger.config?.format ?? 'json',
|
|
382
|
+
enabled: daemonLogger.enabled,
|
|
383
|
+
},
|
|
384
|
+
stdout: { filePath: daemonLogPaths.stdoutPath, format: 'text' },
|
|
385
|
+
stderr: { filePath: daemonLogPaths.stderrPath, format: 'text' },
|
|
386
|
+
};
|
|
387
|
+
const selected = sources[source];
|
|
388
|
+
if (!selected) {
|
|
389
|
+
json(res, 400, { ok: false, error: `Unknown log source "${source}".` }, cors);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const stat = await fs.stat(selected.filePath).catch(() => null);
|
|
393
|
+
if (!stat?.isFile()) {
|
|
394
|
+
const disabledNote = source === 'daemon' && selected.enabled === false
|
|
395
|
+
? 'Daemon logging is disabled (no log file).'
|
|
396
|
+
: 'Log file not found.';
|
|
397
|
+
json(res, 404, { ok: false, error: disabledNote }, cors);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const { lines, truncated, bytesRead } = await readLogTail({
|
|
401
|
+
filePath: selected.filePath,
|
|
402
|
+
maxBytes,
|
|
403
|
+
maxLines: tail,
|
|
404
|
+
});
|
|
405
|
+
const warning = source === 'daemon' && selected.enabled === false
|
|
406
|
+
? 'Daemon logging disabled; showing existing file only.'
|
|
407
|
+
: null;
|
|
408
|
+
json(res, 200, {
|
|
409
|
+
ok: true,
|
|
410
|
+
source,
|
|
411
|
+
format: selected.format,
|
|
412
|
+
lines,
|
|
413
|
+
truncated,
|
|
414
|
+
bytesRead,
|
|
415
|
+
sizeBytes: stat.size,
|
|
416
|
+
mtimeMs: stat.mtimeMs,
|
|
417
|
+
...(warning ? { warning } : {}),
|
|
418
|
+
}, cors);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const processLogsMatch = pathname.match(/^\/v1\/processes\/([^/]+)\/logs$/);
|
|
422
|
+
if (req.method === 'GET' && processLogsMatch) {
|
|
423
|
+
const id = processLogsMatch[1];
|
|
424
|
+
const tail = clampNumber(Number(url.searchParams.get('tail') ?? '200'), 20, 1000);
|
|
425
|
+
const streamRaw = (url.searchParams.get('stream') ?? 'merged').toLowerCase();
|
|
426
|
+
const stream = streamRaw === 'stdout' || streamRaw === 'stderr' ? streamRaw : 'merged';
|
|
427
|
+
const result = buildProcessLogsResult(processRegistry, id, { tail, stream });
|
|
428
|
+
if (!result) {
|
|
429
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
json(res, 200, result, cors);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (req.method === 'GET' && pathname === '/v1/processes') {
|
|
436
|
+
const includeCompleted = (url.searchParams.get('includeCompleted') ?? '').toLowerCase() === 'true' ||
|
|
437
|
+
url.searchParams.get('includeCompleted') === '1';
|
|
438
|
+
const limit = clampNumber(Number(url.searchParams.get('limit') ?? '80'), 10, 200);
|
|
439
|
+
const result = buildProcessListResult(processRegistry, { includeCompleted, limit });
|
|
440
|
+
json(res, 200, result, cors);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
153
443
|
if (req.method === 'GET' && pathname === '/v1/models') {
|
|
154
444
|
const result = await buildModelPickerOptions({
|
|
155
445
|
env,
|
|
@@ -160,6 +450,57 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
160
450
|
json(res, 200, result, cors);
|
|
161
451
|
return;
|
|
162
452
|
}
|
|
453
|
+
if (req.method === 'GET' && pathname === '/v1/tools') {
|
|
454
|
+
const ytDlpPath = resolveToolPath('yt-dlp', env, 'YT_DLP_PATH');
|
|
455
|
+
const ffmpegPath = resolveToolPath('ffmpeg', env, 'FFMPEG_PATH');
|
|
456
|
+
const tesseractPath = resolveToolPath('tesseract', env, 'TESSERACT_PATH');
|
|
457
|
+
json(res, 200, {
|
|
458
|
+
ok: true,
|
|
459
|
+
tools: {
|
|
460
|
+
ytDlp: { available: Boolean(ytDlpPath), path: ytDlpPath },
|
|
461
|
+
ffmpeg: { available: Boolean(ffmpegPath), path: ffmpegPath },
|
|
462
|
+
tesseract: { available: Boolean(tesseractPath), path: tesseractPath },
|
|
463
|
+
},
|
|
464
|
+
}, cors);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (req.method === 'POST' && pathname === '/v1/refresh-free') {
|
|
468
|
+
if (activeRefreshSessionId) {
|
|
469
|
+
json(res, 200, { ok: true, id: activeRefreshSessionId, running: true }, cors);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const session = createSession();
|
|
473
|
+
refreshSessions.set(session.id, session);
|
|
474
|
+
activeRefreshSessionId = session.id;
|
|
475
|
+
json(res, 200, { ok: true, id: session.id }, cors);
|
|
476
|
+
void (async () => {
|
|
477
|
+
const pushStatus = (text) => {
|
|
478
|
+
pushToSession(session, { event: 'status', data: { text } }, onSessionEvent);
|
|
479
|
+
};
|
|
480
|
+
try {
|
|
481
|
+
pushStatus('Refresh free: starting…');
|
|
482
|
+
const stdout = createLineWriter(pushStatus);
|
|
483
|
+
const stderr = createLineWriter(pushStatus);
|
|
484
|
+
await refreshFree({ env, fetchImpl, stdout, stderr });
|
|
485
|
+
pushToSession(session, { event: 'done', data: {} }, onSessionEvent);
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
489
|
+
pushToSession(session, { event: 'error', data: { message } }, onSessionEvent);
|
|
490
|
+
console.error('[summarize-daemon] refresh-free failed', error);
|
|
491
|
+
}
|
|
492
|
+
finally {
|
|
493
|
+
if (activeRefreshSessionId === session.id) {
|
|
494
|
+
activeRefreshSessionId = null;
|
|
495
|
+
}
|
|
496
|
+
setTimeout(() => {
|
|
497
|
+
refreshSessions.delete(session.id);
|
|
498
|
+
endSession(session);
|
|
499
|
+
}, 60_000).unref();
|
|
500
|
+
}
|
|
501
|
+
})();
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
163
504
|
if (req.method === 'POST' && pathname === '/v1/summarize') {
|
|
164
505
|
await refreshCacheStoreIfMissing({ cacheState, transcriptNamespace: 'yt:auto' });
|
|
165
506
|
let body;
|
|
@@ -186,37 +527,169 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
186
527
|
const promptRaw = typeof obj.prompt === 'string' ? obj.prompt : '';
|
|
187
528
|
const promptOverride = promptRaw.trim() || null;
|
|
188
529
|
const noCache = Boolean(obj.noCache);
|
|
530
|
+
const extractOnly = Boolean(obj.extractOnly);
|
|
189
531
|
const modeRaw = typeof obj.mode === 'string' ? obj.mode.trim().toLowerCase() : '';
|
|
190
532
|
const mode = modeRaw === 'url' ? 'url' : modeRaw === 'page' ? 'page' : 'auto';
|
|
191
|
-
const
|
|
192
|
-
? obj.
|
|
193
|
-
:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
533
|
+
const maxCharactersCandidate = typeof obj.maxExtractCharacters === 'number' && Number.isFinite(obj.maxExtractCharacters)
|
|
534
|
+
? obj.maxExtractCharacters
|
|
535
|
+
: typeof obj.maxCharacters === 'number' && Number.isFinite(obj.maxCharacters)
|
|
536
|
+
? obj.maxCharacters
|
|
537
|
+
: null;
|
|
538
|
+
const maxCharacters = maxCharactersCandidate && maxCharactersCandidate > 0 ? maxCharactersCandidate : null;
|
|
539
|
+
const formatRaw = typeof obj.format === 'string' ? obj.format.trim().toLowerCase() : '';
|
|
540
|
+
const format = formatRaw === 'markdown' || formatRaw === 'md' ? 'markdown' : 'text';
|
|
541
|
+
const overrides = resolveRunOverrides({
|
|
542
|
+
firecrawl: obj.firecrawl,
|
|
543
|
+
markdownMode: obj.markdownMode,
|
|
544
|
+
preprocess: obj.preprocess,
|
|
545
|
+
youtube: obj.youtube,
|
|
546
|
+
videoMode: obj.videoMode,
|
|
547
|
+
timestamps: obj.timestamps,
|
|
548
|
+
forceSummary: obj.forceSummary,
|
|
549
|
+
timeout: obj.timeout,
|
|
550
|
+
retries: obj.retries,
|
|
551
|
+
maxOutputTokens: obj.maxOutputTokens,
|
|
552
|
+
});
|
|
553
|
+
const slidesSettings = resolveSlidesSettings({ env, request: obj });
|
|
554
|
+
const diagnostics = parseDiagnostics(obj.diagnostics);
|
|
555
|
+
const includeContentLog = daemonLogger.enabled && diagnostics.includeContent;
|
|
201
556
|
const hasText = Boolean(textContent.trim());
|
|
202
557
|
if (!pageUrl || !/^https?:\/\//i.test(pageUrl)) {
|
|
203
558
|
json(res, 400, { ok: false, error: 'missing url' }, cors);
|
|
204
559
|
return;
|
|
205
560
|
}
|
|
561
|
+
if (extractOnly) {
|
|
562
|
+
if (mode === 'page') {
|
|
563
|
+
json(res, 400, { ok: false, error: 'extractOnly requires mode=url' }, cors);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
const requestCache = noCache
|
|
568
|
+
? { ...cacheState, mode: 'bypass', store: null }
|
|
569
|
+
: cacheState;
|
|
570
|
+
const runId = randomUUID();
|
|
571
|
+
const { extracted, slides } = await runWithProcessContext({ runId, source: 'extract' }, async () => extractContentForUrl({
|
|
572
|
+
env,
|
|
573
|
+
fetchImpl,
|
|
574
|
+
input: { url: pageUrl, title, maxCharacters },
|
|
575
|
+
cache: requestCache,
|
|
576
|
+
mediaCache,
|
|
577
|
+
overrides,
|
|
578
|
+
format,
|
|
579
|
+
slides: slidesSettings,
|
|
580
|
+
}));
|
|
581
|
+
const slidesPayload = slides && slides.slides.length > 0
|
|
582
|
+
? {
|
|
583
|
+
sourceUrl: slides.sourceUrl,
|
|
584
|
+
sourceId: slides.sourceId,
|
|
585
|
+
sourceKind: slides.sourceKind,
|
|
586
|
+
ocrAvailable: slides.ocrAvailable,
|
|
587
|
+
slides: slides.slides.map((slide) => ({
|
|
588
|
+
index: slide.index,
|
|
589
|
+
timestamp: slide.timestamp,
|
|
590
|
+
ocrText: slide.ocrText ?? null,
|
|
591
|
+
ocrConfidence: slide.ocrConfidence ?? null,
|
|
592
|
+
})),
|
|
593
|
+
}
|
|
594
|
+
: null;
|
|
595
|
+
json(res, 200, {
|
|
596
|
+
ok: true,
|
|
597
|
+
extracted: {
|
|
598
|
+
content: extracted.content,
|
|
599
|
+
title: extracted.title,
|
|
600
|
+
url: extracted.url,
|
|
601
|
+
wordCount: extracted.wordCount,
|
|
602
|
+
totalCharacters: extracted.totalCharacters,
|
|
603
|
+
truncated: extracted.truncated,
|
|
604
|
+
transcriptSource: extracted.transcriptSource ?? null,
|
|
605
|
+
transcriptCharacters: extracted.transcriptCharacters ?? null,
|
|
606
|
+
transcriptWordCount: extracted.transcriptWordCount ?? null,
|
|
607
|
+
transcriptLines: extracted.transcriptLines ?? null,
|
|
608
|
+
transcriptSegments: extracted.transcriptSegments ?? null,
|
|
609
|
+
transcriptTimedText: extracted.transcriptTimedText ?? null,
|
|
610
|
+
transcriptionProvider: extracted.transcriptionProvider ?? null,
|
|
611
|
+
mediaDurationSeconds: extracted.mediaDurationSeconds ?? null,
|
|
612
|
+
diagnostics: extracted.diagnostics,
|
|
613
|
+
},
|
|
614
|
+
...(slidesPayload ? { slides: slidesPayload } : {}),
|
|
615
|
+
}, cors);
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
619
|
+
json(res, 500, { ok: false, error: message }, cors);
|
|
620
|
+
}
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
206
623
|
if (mode === 'page' && !hasText) {
|
|
207
624
|
json(res, 400, { ok: false, error: 'missing text' }, cors);
|
|
208
625
|
return;
|
|
209
626
|
}
|
|
210
627
|
const session = createSession();
|
|
628
|
+
session.slidesRequested = Boolean(slidesSettings);
|
|
211
629
|
sessions.set(session.id, session);
|
|
630
|
+
const requestLogger = daemonLogger.getSubLogger('daemon.summarize', {
|
|
631
|
+
requestId: session.id,
|
|
632
|
+
});
|
|
633
|
+
const logStartedAt = Date.now();
|
|
634
|
+
let logSummaryFromCache = false;
|
|
635
|
+
let logInputSummary = null;
|
|
636
|
+
let logSummaryText = '';
|
|
637
|
+
let logExtracted = null;
|
|
638
|
+
const logInput = includeContentLog
|
|
639
|
+
? {
|
|
640
|
+
url: pageUrl,
|
|
641
|
+
title,
|
|
642
|
+
text: hasText ? textContent : null,
|
|
643
|
+
truncated: hasText ? truncated : null,
|
|
644
|
+
}
|
|
645
|
+
: null;
|
|
646
|
+
const logSlidesSettings = includeContentLog && slidesSettings
|
|
647
|
+
? {
|
|
648
|
+
enabled: slidesSettings.enabled,
|
|
649
|
+
ocr: slidesSettings.ocr,
|
|
650
|
+
outputDir: slidesSettings.outputDir,
|
|
651
|
+
sceneThreshold: slidesSettings.sceneThreshold,
|
|
652
|
+
autoTuneThreshold: slidesSettings.autoTuneThreshold,
|
|
653
|
+
maxSlides: slidesSettings.maxSlides,
|
|
654
|
+
minDurationSeconds: slidesSettings.minDurationSeconds,
|
|
655
|
+
}
|
|
656
|
+
: null;
|
|
657
|
+
requestLogger?.info({
|
|
658
|
+
event: 'summarize.request',
|
|
659
|
+
url: pageUrl,
|
|
660
|
+
mode,
|
|
661
|
+
hasText,
|
|
662
|
+
noCache,
|
|
663
|
+
length: lengthRaw,
|
|
664
|
+
language: languageRaw,
|
|
665
|
+
model: modelOverride,
|
|
666
|
+
includeContent: includeContentLog,
|
|
667
|
+
slides: Boolean(slidesSettings),
|
|
668
|
+
...(logSlidesSettings ? { slidesSettings: logSlidesSettings } : {}),
|
|
669
|
+
...(includeContentLog ? { diagnostics } : {}),
|
|
670
|
+
});
|
|
212
671
|
json(res, 200, { ok: true, id: session.id }, cors);
|
|
213
|
-
void (async () => {
|
|
672
|
+
void runWithProcessContext({ runId: session.id, source: 'summarize' }, async () => {
|
|
673
|
+
const slideLogState = {
|
|
674
|
+
startedAt: null,
|
|
675
|
+
requested: Boolean(slidesSettings),
|
|
676
|
+
cacheHit: false,
|
|
677
|
+
lastStatus: null,
|
|
678
|
+
statusCount: 0,
|
|
679
|
+
elapsedMs: null,
|
|
680
|
+
slidesCount: null,
|
|
681
|
+
ocrAvailable: null,
|
|
682
|
+
warnings: [],
|
|
683
|
+
};
|
|
214
684
|
try {
|
|
215
685
|
let emittedOutput = false;
|
|
216
686
|
const sink = {
|
|
217
687
|
writeChunk: (chunk) => {
|
|
218
688
|
emittedOutput = true;
|
|
219
|
-
|
|
689
|
+
if (includeContentLog) {
|
|
690
|
+
logSummaryText += chunk;
|
|
691
|
+
}
|
|
692
|
+
pushToSession(session, { event: 'chunk', data: { text: chunk } }, onSessionEvent);
|
|
220
693
|
},
|
|
221
694
|
onModelChosen: (modelId) => {
|
|
222
695
|
if (session.lastMeta.model === modelId)
|
|
@@ -225,35 +698,45 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
225
698
|
emitMeta(session, {
|
|
226
699
|
model: modelId,
|
|
227
700
|
modelLabel: formatModelLabelForDisplay(modelId),
|
|
228
|
-
});
|
|
701
|
+
}, onSessionEvent);
|
|
229
702
|
},
|
|
230
703
|
writeStatus: (text) => {
|
|
231
704
|
const clean = text.trim();
|
|
232
705
|
if (!clean)
|
|
233
706
|
return;
|
|
234
|
-
pushToSession(session, { event: 'status', data: { text: clean } });
|
|
707
|
+
pushToSession(session, { event: 'status', data: { text: clean } }, onSessionEvent);
|
|
235
708
|
},
|
|
236
709
|
writeMeta: (data) => {
|
|
710
|
+
if (typeof data.inputSummary === 'string') {
|
|
711
|
+
logInputSummary = data.inputSummary;
|
|
712
|
+
}
|
|
713
|
+
if (typeof data.summaryFromCache === 'boolean') {
|
|
714
|
+
logSummaryFromCache = data.summaryFromCache;
|
|
715
|
+
}
|
|
237
716
|
emitMeta(session, {
|
|
238
717
|
inputSummary: typeof data.inputSummary === 'string' ? data.inputSummary : null,
|
|
239
718
|
summaryFromCache: typeof data.summaryFromCache === 'boolean' ? data.summaryFromCache : null,
|
|
240
|
-
});
|
|
719
|
+
}, onSessionEvent);
|
|
241
720
|
},
|
|
242
721
|
};
|
|
243
722
|
const normalizedModelOverride = modelOverride && modelOverride.toLowerCase() !== 'auto' ? modelOverride : null;
|
|
244
723
|
const requestCache = noCache
|
|
245
724
|
? { ...cacheState, mode: 'bypass', store: null }
|
|
246
725
|
: cacheState;
|
|
247
|
-
|
|
248
|
-
firecrawlMode,
|
|
249
|
-
markdownMode,
|
|
250
|
-
preprocessMode,
|
|
251
|
-
youtubeMode,
|
|
252
|
-
timeoutMs,
|
|
253
|
-
retries,
|
|
254
|
-
maxOutputTokensArg,
|
|
255
|
-
};
|
|
726
|
+
let liveSlides = null;
|
|
256
727
|
const runWithMode = async (resolved) => {
|
|
728
|
+
if (resolved === 'url' && slideLogState.requested) {
|
|
729
|
+
slideLogState.startedAt = Date.now();
|
|
730
|
+
console.log(`[summarize-daemon] slides: start url=${pageUrl} (session=${session.id})`);
|
|
731
|
+
if (includeContentLog) {
|
|
732
|
+
requestLogger?.info({
|
|
733
|
+
event: 'slides.start',
|
|
734
|
+
url: pageUrl,
|
|
735
|
+
sessionId: session.id,
|
|
736
|
+
...(logSlidesSettings ? { settings: logSlidesSettings } : {}),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
257
740
|
return resolved === 'url'
|
|
258
741
|
? await streamSummaryForUrl({
|
|
259
742
|
env,
|
|
@@ -262,10 +745,123 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
262
745
|
promptOverride,
|
|
263
746
|
lengthRaw,
|
|
264
747
|
languageRaw,
|
|
748
|
+
format,
|
|
265
749
|
input: { url: pageUrl, title, maxCharacters },
|
|
266
750
|
sink,
|
|
267
751
|
cache: requestCache,
|
|
752
|
+
mediaCache,
|
|
268
753
|
overrides,
|
|
754
|
+
slides: slidesSettings,
|
|
755
|
+
hooks: {
|
|
756
|
+
...(includeContentLog
|
|
757
|
+
? {
|
|
758
|
+
onExtracted: (content) => {
|
|
759
|
+
logExtracted = content;
|
|
760
|
+
},
|
|
761
|
+
}
|
|
762
|
+
: {}),
|
|
763
|
+
onSlidesExtracted: (slides) => {
|
|
764
|
+
session.slides = slides;
|
|
765
|
+
slideLogState.slidesCount = slides.slides.length;
|
|
766
|
+
slideLogState.ocrAvailable = slides.ocrAvailable;
|
|
767
|
+
slideLogState.warnings = slides.warnings;
|
|
768
|
+
if (slideLogState.startedAt) {
|
|
769
|
+
slideLogState.elapsedMs = Date.now() - slideLogState.startedAt;
|
|
770
|
+
}
|
|
771
|
+
if (slideLogState.startedAt) {
|
|
772
|
+
const elapsedMs = Date.now() - slideLogState.startedAt;
|
|
773
|
+
console.log(`[summarize-daemon] slides: done count=${slides.slides.length} ocr=${slides.ocrAvailable} elapsedMs=${elapsedMs} warnings=${slides.warnings.join('; ')}`);
|
|
774
|
+
}
|
|
775
|
+
if (includeContentLog) {
|
|
776
|
+
requestLogger?.info({
|
|
777
|
+
event: 'slides.done',
|
|
778
|
+
url: pageUrl,
|
|
779
|
+
sessionId: session.id,
|
|
780
|
+
slidesCount: slides.slides.length,
|
|
781
|
+
ocrAvailable: slides.ocrAvailable,
|
|
782
|
+
elapsedMs: slideLogState.elapsedMs,
|
|
783
|
+
cacheHit: slideLogState.cacheHit,
|
|
784
|
+
warnings: slides.warnings,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
emitSlides(session, buildSlidesPayload({
|
|
788
|
+
slides,
|
|
789
|
+
port,
|
|
790
|
+
}), onSessionEvent);
|
|
791
|
+
},
|
|
792
|
+
onSlidesDone: (result) => {
|
|
793
|
+
emitSlidesDone(session, result, onSessionEvent);
|
|
794
|
+
},
|
|
795
|
+
onSlidesProgress: (text) => {
|
|
796
|
+
const clean = typeof text === 'string' ? text.trim() : '';
|
|
797
|
+
if (!clean)
|
|
798
|
+
return;
|
|
799
|
+
slideLogState.lastStatus = clean;
|
|
800
|
+
slideLogState.statusCount += 1;
|
|
801
|
+
if (clean.toLowerCase().includes('cached')) {
|
|
802
|
+
slideLogState.cacheHit = true;
|
|
803
|
+
}
|
|
804
|
+
const progressMatch = clean.match(/(\d+)%/);
|
|
805
|
+
const progress = progressMatch ? Number(progressMatch[1]) : null;
|
|
806
|
+
if (includeContentLog) {
|
|
807
|
+
requestLogger?.info({
|
|
808
|
+
event: 'slides.status',
|
|
809
|
+
url: pageUrl,
|
|
810
|
+
sessionId: session.id,
|
|
811
|
+
status: clean,
|
|
812
|
+
...(progress !== null ? { progress } : {}),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
emitSlidesStatus(session, clean, onSessionEvent);
|
|
816
|
+
},
|
|
817
|
+
onSlideChunk: (chunk) => {
|
|
818
|
+
const { slide, meta } = chunk;
|
|
819
|
+
if (slide == null ||
|
|
820
|
+
!meta?.slidesDir ||
|
|
821
|
+
!meta.sourceUrl ||
|
|
822
|
+
!meta.sourceId ||
|
|
823
|
+
!meta.sourceKind) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const nextSlides = liveSlides ?? {
|
|
827
|
+
sourceUrl: meta.sourceUrl,
|
|
828
|
+
sourceKind: meta.sourceKind,
|
|
829
|
+
sourceId: meta.sourceId,
|
|
830
|
+
slidesDir: meta.slidesDir,
|
|
831
|
+
sceneThreshold: 0,
|
|
832
|
+
autoTuneThreshold: false,
|
|
833
|
+
autoTune: {
|
|
834
|
+
enabled: false,
|
|
835
|
+
chosenThreshold: 0,
|
|
836
|
+
confidence: 0,
|
|
837
|
+
strategy: 'none',
|
|
838
|
+
},
|
|
839
|
+
maxSlides: 0,
|
|
840
|
+
minSlideDuration: 0,
|
|
841
|
+
ocrRequested: meta.ocrAvailable,
|
|
842
|
+
ocrAvailable: meta.ocrAvailable,
|
|
843
|
+
slides: [],
|
|
844
|
+
warnings: [],
|
|
845
|
+
};
|
|
846
|
+
liveSlides = nextSlides;
|
|
847
|
+
const existingIndex = nextSlides.slides.findIndex((item) => item.index === slide.index);
|
|
848
|
+
if (existingIndex >= 0) {
|
|
849
|
+
nextSlides.slides[existingIndex] = {
|
|
850
|
+
...nextSlides.slides[existingIndex],
|
|
851
|
+
...slide,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
nextSlides.slides.push(slide);
|
|
856
|
+
}
|
|
857
|
+
nextSlides.slides.sort((a, b) => a.index - b.index);
|
|
858
|
+
session.slides = nextSlides;
|
|
859
|
+
emitSlides(session, buildSlidesPayload({
|
|
860
|
+
slides: nextSlides,
|
|
861
|
+
port,
|
|
862
|
+
}), onSessionEvent);
|
|
863
|
+
},
|
|
864
|
+
},
|
|
269
865
|
})
|
|
270
866
|
: await streamSummaryForVisiblePage({
|
|
271
867
|
env,
|
|
@@ -274,9 +870,11 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
274
870
|
promptOverride,
|
|
275
871
|
lengthRaw,
|
|
276
872
|
languageRaw,
|
|
873
|
+
format,
|
|
277
874
|
input: { url: pageUrl, title, text: textContent, truncated },
|
|
278
875
|
sink,
|
|
279
876
|
cache: requestCache,
|
|
877
|
+
mediaCache,
|
|
280
878
|
overrides,
|
|
281
879
|
});
|
|
282
880
|
};
|
|
@@ -305,24 +903,304 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
305
903
|
emitMeta(session, {
|
|
306
904
|
model: result.usedModel,
|
|
307
905
|
modelLabel: formatModelLabelForDisplay(result.usedModel),
|
|
308
|
-
});
|
|
906
|
+
}, onSessionEvent);
|
|
309
907
|
}
|
|
310
|
-
pushToSession(session, { event: 'metrics', data: result.metrics });
|
|
311
|
-
pushToSession(session, { event: 'done', data: {} });
|
|
908
|
+
pushToSession(session, { event: 'metrics', data: result.metrics }, onSessionEvent);
|
|
909
|
+
pushToSession(session, { event: 'done', data: {} }, onSessionEvent);
|
|
910
|
+
requestLogger?.info({
|
|
911
|
+
event: 'summarize.done',
|
|
912
|
+
url: pageUrl,
|
|
913
|
+
mode,
|
|
914
|
+
model: result.usedModel,
|
|
915
|
+
elapsedMs: Date.now() - logStartedAt,
|
|
916
|
+
summaryFromCache: logSummaryFromCache,
|
|
917
|
+
inputSummary: logInputSummary,
|
|
918
|
+
...(includeContentLog && slideLogState.requested
|
|
919
|
+
? {
|
|
920
|
+
slides: {
|
|
921
|
+
requested: true,
|
|
922
|
+
cacheHit: slideLogState.cacheHit,
|
|
923
|
+
lastStatus: slideLogState.lastStatus,
|
|
924
|
+
statusCount: slideLogState.statusCount,
|
|
925
|
+
elapsedMs: slideLogState.elapsedMs,
|
|
926
|
+
slidesCount: slideLogState.slidesCount,
|
|
927
|
+
ocrAvailable: slideLogState.ocrAvailable,
|
|
928
|
+
warnings: slideLogState.warnings,
|
|
929
|
+
},
|
|
930
|
+
}
|
|
931
|
+
: {}),
|
|
932
|
+
...(includeContentLog && !logSummaryFromCache
|
|
933
|
+
? {
|
|
934
|
+
input: logInput,
|
|
935
|
+
extracted: logExtracted,
|
|
936
|
+
summary: logSummaryText,
|
|
937
|
+
}
|
|
938
|
+
: {}),
|
|
939
|
+
});
|
|
312
940
|
}
|
|
313
941
|
catch (error) {
|
|
314
942
|
const message = error instanceof Error ? error.message : String(error);
|
|
315
|
-
pushToSession(session, { event: 'error', data: { message } });
|
|
943
|
+
pushToSession(session, { event: 'error', data: { message } }, onSessionEvent);
|
|
944
|
+
if (session.slidesRequested && !session.slidesDone) {
|
|
945
|
+
emitSlidesDone(session, { ok: false, error: message }, onSessionEvent);
|
|
946
|
+
}
|
|
316
947
|
// Preserve full stack trace in daemon logs for debugging.
|
|
317
948
|
console.error('[summarize-daemon] summarize failed', error);
|
|
949
|
+
requestLogger?.error({
|
|
950
|
+
event: 'summarize.error',
|
|
951
|
+
url: pageUrl,
|
|
952
|
+
mode,
|
|
953
|
+
elapsedMs: Date.now() - logStartedAt,
|
|
954
|
+
summaryFromCache: logSummaryFromCache,
|
|
955
|
+
inputSummary: logInputSummary,
|
|
956
|
+
...(includeContentLog && slideLogState.requested
|
|
957
|
+
? {
|
|
958
|
+
slides: {
|
|
959
|
+
requested: true,
|
|
960
|
+
cacheHit: slideLogState.cacheHit,
|
|
961
|
+
lastStatus: slideLogState.lastStatus,
|
|
962
|
+
statusCount: slideLogState.statusCount,
|
|
963
|
+
elapsedMs: slideLogState.elapsedMs,
|
|
964
|
+
slidesCount: slideLogState.slidesCount,
|
|
965
|
+
ocrAvailable: slideLogState.ocrAvailable,
|
|
966
|
+
warnings: slideLogState.warnings,
|
|
967
|
+
},
|
|
968
|
+
}
|
|
969
|
+
: {}),
|
|
970
|
+
error: {
|
|
971
|
+
message,
|
|
972
|
+
stack: error instanceof Error ? error.stack : null,
|
|
973
|
+
},
|
|
974
|
+
...(includeContentLog && !logSummaryFromCache
|
|
975
|
+
? {
|
|
976
|
+
input: logInput,
|
|
977
|
+
extracted: logExtracted,
|
|
978
|
+
summary: logSummaryText || null,
|
|
979
|
+
}
|
|
980
|
+
: {}),
|
|
981
|
+
});
|
|
318
982
|
}
|
|
319
983
|
finally {
|
|
320
|
-
|
|
321
|
-
sessions.delete(session.id);
|
|
322
|
-
endSession(session);
|
|
323
|
-
}, 60_000).unref();
|
|
984
|
+
scheduleSessionCleanup({ session, sessions });
|
|
324
985
|
}
|
|
325
|
-
})
|
|
986
|
+
});
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (req.method === 'POST' && pathname === '/v1/agent') {
|
|
990
|
+
let body;
|
|
991
|
+
try {
|
|
992
|
+
body = await readJsonBody(req, 4_000_000);
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
996
|
+
json(res, 400, { ok: false, error: message }, cors);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (!body || typeof body !== 'object') {
|
|
1000
|
+
json(res, 400, { ok: false, error: 'invalid json' }, cors);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const obj = body;
|
|
1004
|
+
const pageUrl = typeof obj.url === 'string' ? obj.url.trim() : '';
|
|
1005
|
+
const pageTitle = typeof obj.title === 'string' ? obj.title.trim() : null;
|
|
1006
|
+
const pageContent = typeof obj.pageContent === 'string' ? obj.pageContent : '';
|
|
1007
|
+
const messages = obj.messages;
|
|
1008
|
+
const modelOverride = typeof obj.model === 'string' ? obj.model.trim() : null;
|
|
1009
|
+
const tools = Array.isArray(obj.tools)
|
|
1010
|
+
? obj.tools.filter((tool) => typeof tool === 'string')
|
|
1011
|
+
: [];
|
|
1012
|
+
const automationEnabled = Boolean(obj.automationEnabled);
|
|
1013
|
+
if (!pageUrl) {
|
|
1014
|
+
json(res, 400, { ok: false, error: 'missing url' }, cors);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const runId = `agent-${randomUUID()}`;
|
|
1018
|
+
const wantsJson = wantsJsonResponse(req, url);
|
|
1019
|
+
if (wantsJson) {
|
|
1020
|
+
try {
|
|
1021
|
+
const assistant = await runWithProcessContext({ runId, source: 'agent' }, async () => completeAgentResponse({
|
|
1022
|
+
env,
|
|
1023
|
+
pageUrl,
|
|
1024
|
+
pageTitle,
|
|
1025
|
+
pageContent,
|
|
1026
|
+
messages,
|
|
1027
|
+
modelOverride: modelOverride && modelOverride.toLowerCase() !== 'auto' ? modelOverride : null,
|
|
1028
|
+
tools,
|
|
1029
|
+
automationEnabled,
|
|
1030
|
+
}));
|
|
1031
|
+
json(res, 200, { ok: true, assistant }, cors);
|
|
1032
|
+
}
|
|
1033
|
+
catch (error) {
|
|
1034
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1035
|
+
console.error('[summarize-daemon] agent failed', error);
|
|
1036
|
+
json(res, 500, { ok: false, error: message }, cors);
|
|
1037
|
+
}
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
res.writeHead(200, {
|
|
1041
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
1042
|
+
'cache-control': 'no-cache',
|
|
1043
|
+
connection: 'keep-alive',
|
|
1044
|
+
'x-accel-buffering': 'no',
|
|
1045
|
+
...cors,
|
|
1046
|
+
});
|
|
1047
|
+
const controller = new AbortController();
|
|
1048
|
+
const abort = () => controller.abort();
|
|
1049
|
+
req.on('close', abort);
|
|
1050
|
+
res.on('close', abort);
|
|
1051
|
+
const writeEvent = (event) => {
|
|
1052
|
+
if (res.writableEnded)
|
|
1053
|
+
return;
|
|
1054
|
+
res.write(encodeSseEvent(event));
|
|
1055
|
+
};
|
|
1056
|
+
try {
|
|
1057
|
+
await runWithProcessContext({ runId, source: 'agent' }, async () => streamAgentResponse({
|
|
1058
|
+
env,
|
|
1059
|
+
pageUrl,
|
|
1060
|
+
pageTitle,
|
|
1061
|
+
pageContent,
|
|
1062
|
+
messages,
|
|
1063
|
+
modelOverride: modelOverride && modelOverride.toLowerCase() !== 'auto' ? modelOverride : null,
|
|
1064
|
+
tools,
|
|
1065
|
+
automationEnabled,
|
|
1066
|
+
onChunk: (text) => writeEvent({ event: 'chunk', data: { text } }),
|
|
1067
|
+
onAssistant: (assistant) => writeEvent({ event: 'assistant', data: assistant }),
|
|
1068
|
+
signal: controller.signal,
|
|
1069
|
+
}));
|
|
1070
|
+
writeEvent({ event: 'done', data: {} });
|
|
1071
|
+
res.end();
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
if (controller.signal.aborted)
|
|
1075
|
+
return;
|
|
1076
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1077
|
+
console.error('[summarize-daemon] agent failed', error);
|
|
1078
|
+
writeEvent({ event: 'error', data: { message } });
|
|
1079
|
+
writeEvent({ event: 'done', data: {} });
|
|
1080
|
+
res.end();
|
|
1081
|
+
}
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const slidesMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/slides$/);
|
|
1085
|
+
if (req.method === 'GET' && slidesMatch) {
|
|
1086
|
+
const id = slidesMatch[1];
|
|
1087
|
+
const session = id ? sessions.get(id) : null;
|
|
1088
|
+
if (!session || !session.slides) {
|
|
1089
|
+
json(res, 200, { ok: false, error: 'not found' }, cors);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
json(res, 200, { ok: true, slides: buildSlidesPayload({ slides: session.slides, port }) }, cors);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const slideImageMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/slides\/(\d+)$/);
|
|
1096
|
+
if (req.method === 'GET' && slideImageMatch) {
|
|
1097
|
+
const id = slideImageMatch[1];
|
|
1098
|
+
const index = Number(slideImageMatch[2]);
|
|
1099
|
+
const session = id ? sessions.get(id) : null;
|
|
1100
|
+
if (!session || !session.slides || !Number.isFinite(index)) {
|
|
1101
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const slide = session.slides.slides.find((item) => item.index === index);
|
|
1105
|
+
if (!slide) {
|
|
1106
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
try {
|
|
1110
|
+
const stat = await fs.stat(slide.imagePath);
|
|
1111
|
+
res.writeHead(200, {
|
|
1112
|
+
'content-type': 'image/png',
|
|
1113
|
+
'content-length': stat.size.toString(),
|
|
1114
|
+
'cache-control': 'no-cache',
|
|
1115
|
+
...cors,
|
|
1116
|
+
});
|
|
1117
|
+
const stream = createReadStream(slide.imagePath);
|
|
1118
|
+
stream.pipe(res);
|
|
1119
|
+
stream.on('error', () => res.end());
|
|
1120
|
+
}
|
|
1121
|
+
catch {
|
|
1122
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1123
|
+
}
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const stableSlideImageMatch = pathname.match(/^\/v1\/slides\/([^/]+)\/(\d+)$/);
|
|
1127
|
+
if (req.method === 'GET' && stableSlideImageMatch) {
|
|
1128
|
+
const sourceId = stableSlideImageMatch[1];
|
|
1129
|
+
const index = Number(stableSlideImageMatch[2]);
|
|
1130
|
+
if (!sourceId || !Number.isFinite(index) || index <= 0) {
|
|
1131
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const slidesRoot = path.resolve(resolveHomeDir(env), '.summarize', 'slides');
|
|
1135
|
+
const slidesDir = path.join(slidesRoot, sourceId);
|
|
1136
|
+
const payloadPath = path.join(slidesDir, 'slides.json');
|
|
1137
|
+
const resolveFromDisk = async () => {
|
|
1138
|
+
const raw = await fs.readFile(payloadPath, 'utf8').catch(() => null);
|
|
1139
|
+
if (raw) {
|
|
1140
|
+
try {
|
|
1141
|
+
const parsed = JSON.parse(raw);
|
|
1142
|
+
const slide = parsed?.slides?.find?.((item) => item?.index === index);
|
|
1143
|
+
if (slide?.imagePath) {
|
|
1144
|
+
const resolved = resolveSlideImagePath(slidesDir, slide.imagePath);
|
|
1145
|
+
if (resolved)
|
|
1146
|
+
return resolved;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
// fall through
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const prefix = `slide_${String(index).padStart(4, '0')}`;
|
|
1154
|
+
const entries = await fs.readdir(slidesDir).catch(() => null);
|
|
1155
|
+
if (!entries)
|
|
1156
|
+
return null;
|
|
1157
|
+
const candidates = entries
|
|
1158
|
+
.filter((name) => name.startsWith(prefix) && name.endsWith('.png'))
|
|
1159
|
+
.map((name) => path.join(slidesDir, name));
|
|
1160
|
+
if (candidates.length === 0)
|
|
1161
|
+
return null;
|
|
1162
|
+
let best = null;
|
|
1163
|
+
for (const filePath of candidates) {
|
|
1164
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
1165
|
+
if (!stat?.isFile())
|
|
1166
|
+
continue;
|
|
1167
|
+
const mtimeMs = stat.mtimeMs;
|
|
1168
|
+
if (!best || mtimeMs > best.mtimeMs)
|
|
1169
|
+
best = { filePath, mtimeMs };
|
|
1170
|
+
}
|
|
1171
|
+
return best?.filePath ?? null;
|
|
1172
|
+
};
|
|
1173
|
+
const filePath = await resolveFromDisk();
|
|
1174
|
+
if (!filePath) {
|
|
1175
|
+
// Return a tiny transparent PNG (placeholder) instead of 404 to avoid broken-image icons
|
|
1176
|
+
// while extraction is still running.
|
|
1177
|
+
const placeholder = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3kq0cAAAAASUVORK5CYII=', 'base64');
|
|
1178
|
+
res.writeHead(200, {
|
|
1179
|
+
'content-type': 'image/png',
|
|
1180
|
+
'content-length': placeholder.length.toString(),
|
|
1181
|
+
'cache-control': 'no-store',
|
|
1182
|
+
'x-summarize-slide-ready': '0',
|
|
1183
|
+
...cors,
|
|
1184
|
+
});
|
|
1185
|
+
res.end(placeholder);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
try {
|
|
1189
|
+
const stat = await fs.stat(filePath);
|
|
1190
|
+
res.writeHead(200, {
|
|
1191
|
+
'content-type': 'image/png',
|
|
1192
|
+
'content-length': stat.size.toString(),
|
|
1193
|
+
'cache-control': 'no-store',
|
|
1194
|
+
'x-summarize-slide-ready': '1',
|
|
1195
|
+
...cors,
|
|
1196
|
+
});
|
|
1197
|
+
const stream = createReadStream(filePath);
|
|
1198
|
+
stream.pipe(res);
|
|
1199
|
+
stream.on('error', () => res.end());
|
|
1200
|
+
}
|
|
1201
|
+
catch {
|
|
1202
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1203
|
+
}
|
|
326
1204
|
return;
|
|
327
1205
|
}
|
|
328
1206
|
const eventsMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/events$/);
|
|
@@ -344,8 +1222,93 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
344
1222
|
connection: 'keep-alive',
|
|
345
1223
|
});
|
|
346
1224
|
session.clients.add(res);
|
|
347
|
-
for (const
|
|
348
|
-
res.write(
|
|
1225
|
+
for (const entry of session.buffer) {
|
|
1226
|
+
res.write(encodeSseEvent(entry.event));
|
|
1227
|
+
}
|
|
1228
|
+
if (session.done) {
|
|
1229
|
+
res.end();
|
|
1230
|
+
session.clients.delete(res);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const keepalive = setInterval(() => {
|
|
1234
|
+
res.write(`: keepalive ${Date.now()}\n\n`);
|
|
1235
|
+
}, 15_000);
|
|
1236
|
+
keepalive.unref();
|
|
1237
|
+
res.on('close', () => {
|
|
1238
|
+
clearInterval(keepalive);
|
|
1239
|
+
session.clients.delete(res);
|
|
1240
|
+
});
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
const slidesEventsMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/slides\/events$/);
|
|
1244
|
+
if (req.method === 'GET' && slidesEventsMatch) {
|
|
1245
|
+
const id = slidesEventsMatch[1];
|
|
1246
|
+
if (!id) {
|
|
1247
|
+
json(res, 404, { ok: false }, cors);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const session = sessions.get(id);
|
|
1251
|
+
if (!session || !session.slidesRequested) {
|
|
1252
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
res.writeHead(200, {
|
|
1256
|
+
...cors,
|
|
1257
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
1258
|
+
'cache-control': 'no-cache, no-transform',
|
|
1259
|
+
connection: 'keep-alive',
|
|
1260
|
+
});
|
|
1261
|
+
session.slidesClients.add(res);
|
|
1262
|
+
for (const entry of session.slidesBuffer) {
|
|
1263
|
+
res.write(encodeSseEvent(entry.event));
|
|
1264
|
+
}
|
|
1265
|
+
const hasSlidesEvent = session.slidesBuffer.some((entry) => entry.event.event === 'slides');
|
|
1266
|
+
if (!hasSlidesEvent && session.slides) {
|
|
1267
|
+
res.write(encodeSseEvent({
|
|
1268
|
+
event: 'slides',
|
|
1269
|
+
data: buildSlidesPayload({ slides: session.slides, port }),
|
|
1270
|
+
}));
|
|
1271
|
+
}
|
|
1272
|
+
const hasStatusEvent = session.slidesBuffer.some((entry) => entry.event.event === 'status');
|
|
1273
|
+
if (!hasStatusEvent && session.slidesLastStatus) {
|
|
1274
|
+
res.write(encodeSseEvent({ event: 'status', data: { text: session.slidesLastStatus } }));
|
|
1275
|
+
}
|
|
1276
|
+
if (session.slidesDone) {
|
|
1277
|
+
res.end();
|
|
1278
|
+
session.slidesClients.delete(res);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const keepalive = setInterval(() => {
|
|
1282
|
+
res.write(`: keepalive ${Date.now()}\n\n`);
|
|
1283
|
+
}, 15_000);
|
|
1284
|
+
keepalive.unref();
|
|
1285
|
+
res.on('close', () => {
|
|
1286
|
+
clearInterval(keepalive);
|
|
1287
|
+
session.slidesClients.delete(res);
|
|
1288
|
+
});
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const refreshEventsMatch = pathname.match(/^\/v1\/refresh-free\/([^/]+)\/events$/);
|
|
1292
|
+
if (req.method === 'GET' && refreshEventsMatch) {
|
|
1293
|
+
const id = refreshEventsMatch[1];
|
|
1294
|
+
if (!id) {
|
|
1295
|
+
json(res, 404, { ok: false }, cors);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const session = refreshSessions.get(id);
|
|
1299
|
+
if (!session) {
|
|
1300
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
res.writeHead(200, {
|
|
1304
|
+
...cors,
|
|
1305
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
1306
|
+
'cache-control': 'no-cache, no-transform',
|
|
1307
|
+
connection: 'keep-alive',
|
|
1308
|
+
});
|
|
1309
|
+
session.clients.add(res);
|
|
1310
|
+
for (const entry of session.buffer) {
|
|
1311
|
+
res.write(encodeSseEvent(entry.event));
|
|
349
1312
|
}
|
|
350
1313
|
if (session.done) {
|
|
351
1314
|
res.end();
|
|
@@ -382,14 +1345,33 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
382
1345
|
try {
|
|
383
1346
|
await new Promise((resolve, reject) => {
|
|
384
1347
|
server.once('error', reject);
|
|
385
|
-
server.listen(port, DAEMON_HOST, () =>
|
|
1348
|
+
server.listen(port, DAEMON_HOST, () => {
|
|
1349
|
+
const address = server.address();
|
|
1350
|
+
const actualPort = address && typeof address === 'object' && typeof address.port === 'number'
|
|
1351
|
+
? address.port
|
|
1352
|
+
: port;
|
|
1353
|
+
onListening?.(actualPort);
|
|
1354
|
+
resolve();
|
|
1355
|
+
});
|
|
386
1356
|
});
|
|
387
1357
|
await new Promise((resolve) => {
|
|
388
|
-
|
|
1358
|
+
let resolved = false;
|
|
1359
|
+
const onStop = () => {
|
|
1360
|
+
if (resolved)
|
|
1361
|
+
return;
|
|
1362
|
+
resolved = true;
|
|
389
1363
|
server.close(() => resolve());
|
|
390
1364
|
};
|
|
391
|
-
process.once('SIGTERM',
|
|
392
|
-
process.once('SIGINT',
|
|
1365
|
+
process.once('SIGTERM', onStop);
|
|
1366
|
+
process.once('SIGINT', onStop);
|
|
1367
|
+
if (signal) {
|
|
1368
|
+
if (signal.aborted) {
|
|
1369
|
+
onStop();
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
signal.addEventListener('abort', onStop, { once: true });
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
393
1375
|
});
|
|
394
1376
|
}
|
|
395
1377
|
finally {
|