@steipete/summarize 0.11.1 → 0.12.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 +38 -1
- package/README.md +63 -17
- package/dist/cli.js +1 -1
- package/dist/esm/cache-keys.js +75 -0
- package/dist/esm/cache-keys.js.map +1 -0
- package/dist/esm/cache-slides-cleanup.js +47 -0
- package/dist/esm/cache-slides-cleanup.js.map +1 -0
- package/dist/esm/cache.js +14 -91
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/config/env.js +49 -0
- package/dist/esm/config/env.js.map +1 -0
- package/dist/esm/config/model.js +193 -0
- package/dist/esm/config/model.js.map +1 -0
- package/dist/esm/config/parse-helpers.js +50 -0
- package/dist/esm/config/parse-helpers.js.map +1 -0
- package/dist/esm/config/read.js +83 -0
- package/dist/esm/config/read.js.map +1 -0
- package/dist/esm/config/sections.js +438 -0
- package/dist/esm/config/sections.js.map +1 -0
- package/dist/esm/config/types.js +2 -0
- package/dist/esm/config/types.js.map +1 -0
- package/dist/esm/config.js +24 -807
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/content/asset.js +2 -2
- package/dist/esm/content/asset.js.map +1 -1
- package/dist/esm/daemon/agent-model.js +235 -0
- package/dist/esm/daemon/agent-model.js.map +1 -0
- package/dist/esm/daemon/agent-request.js +87 -0
- package/dist/esm/daemon/agent-request.js.map +1 -0
- package/dist/esm/daemon/agent.js +42 -243
- package/dist/esm/daemon/agent.js.map +1 -1
- package/dist/esm/daemon/chat.js +69 -8
- package/dist/esm/daemon/chat.js.map +1 -1
- package/dist/esm/daemon/cli.js +21 -4
- package/dist/esm/daemon/cli.js.map +1 -1
- package/dist/esm/daemon/config.js +65 -9
- package/dist/esm/daemon/config.js.map +1 -1
- package/dist/esm/daemon/env-snapshot.js +4 -0
- package/dist/esm/daemon/env-snapshot.js.map +1 -1
- package/dist/esm/daemon/flow-context.js +8 -1
- package/dist/esm/daemon/flow-context.js.map +1 -1
- package/dist/esm/daemon/models.js +16 -0
- package/dist/esm/daemon/models.js.map +1 -1
- package/dist/esm/daemon/process-registry.js.map +1 -1
- package/dist/esm/daemon/server-admin-routes.js +134 -0
- package/dist/esm/daemon/server-admin-routes.js.map +1 -0
- package/dist/esm/daemon/server-agent-route.js +104 -0
- package/dist/esm/daemon/server-agent-route.js.map +1 -0
- package/dist/esm/daemon/server-http.js +89 -0
- package/dist/esm/daemon/server-http.js.map +1 -0
- package/dist/esm/daemon/server-session-routes.js +209 -0
- package/dist/esm/daemon/server-session-routes.js.map +1 -0
- package/dist/esm/daemon/server-session.js +118 -0
- package/dist/esm/daemon/server-session.js.map +1 -0
- package/dist/esm/daemon/server-sse.js +28 -0
- package/dist/esm/daemon/server-sse.js.map +1 -0
- package/dist/esm/daemon/server-summarize-execution.js +357 -0
- package/dist/esm/daemon/server-summarize-execution.js.map +1 -0
- package/dist/esm/daemon/server-summarize-request.js +119 -0
- package/dist/esm/daemon/server-summarize-request.js.map +1 -0
- package/dist/esm/daemon/server.js +72 -1121
- package/dist/esm/daemon/server.js.map +1 -1
- package/dist/esm/daemon/summarize-progress.js +1 -1
- package/dist/esm/daemon/summarize-progress.js.map +1 -1
- package/dist/esm/daemon/summarize.js.map +1 -1
- package/dist/esm/llm/cli-exec.js +75 -0
- package/dist/esm/llm/cli-exec.js.map +1 -0
- package/dist/esm/llm/cli-provider-output.js +191 -0
- package/dist/esm/llm/cli-provider-output.js.map +1 -0
- package/dist/esm/llm/cli.js +3 -212
- package/dist/esm/llm/cli.js.map +1 -1
- package/dist/esm/llm/generate-text-document.js +109 -0
- package/dist/esm/llm/generate-text-document.js.map +1 -0
- package/dist/esm/llm/generate-text-shared.js +102 -0
- package/dist/esm/llm/generate-text-shared.js.map +1 -0
- package/dist/esm/llm/generate-text-stream.js +258 -0
- package/dist/esm/llm/generate-text-stream.js.map +1 -0
- package/dist/esm/llm/generate-text.js +145 -480
- package/dist/esm/llm/generate-text.js.map +1 -1
- package/dist/esm/llm/model-id.js +21 -20
- package/dist/esm/llm/model-id.js.map +1 -1
- package/dist/esm/llm/provider-capabilities.js +2 -0
- package/dist/esm/llm/provider-capabilities.js.map +1 -0
- package/dist/esm/llm/provider-profile.js +142 -0
- package/dist/esm/llm/provider-profile.js.map +1 -0
- package/dist/esm/llm/providers/google.js +42 -5
- package/dist/esm/llm/providers/google.js.map +1 -1
- package/dist/esm/llm/providers/models.js +13 -0
- package/dist/esm/llm/providers/models.js.map +1 -1
- package/dist/esm/llm/providers/openai.js.map +1 -1
- package/dist/esm/llm/transcript-to-markdown.js.map +1 -1
- package/dist/esm/model-auto-cli.js +89 -0
- package/dist/esm/model-auto-cli.js.map +1 -0
- package/dist/esm/model-auto-rules.js +86 -0
- package/dist/esm/model-auto-rules.js.map +1 -0
- package/dist/esm/model-auto.js +10 -245
- package/dist/esm/model-auto.js.map +1 -1
- package/dist/esm/model-spec.js +23 -17
- package/dist/esm/model-spec.js.map +1 -1
- package/dist/esm/refresh-free.js +1 -1
- package/dist/esm/refresh-free.js.map +1 -1
- package/dist/esm/run/attachments.js +1 -1
- package/dist/esm/run/attachments.js.map +1 -1
- package/dist/esm/run/bird/exec.js +23 -0
- package/dist/esm/run/bird/exec.js.map +1 -0
- package/dist/esm/run/bird/media.js +171 -0
- package/dist/esm/run/bird/media.js.map +1 -0
- package/dist/esm/run/bird/parse.js +82 -0
- package/dist/esm/run/bird/parse.js.map +1 -0
- package/dist/esm/run/bird/types.js +2 -0
- package/dist/esm/run/bird/types.js.map +1 -0
- package/dist/esm/run/bird.js +86 -144
- package/dist/esm/run/bird.js.map +1 -1
- package/dist/esm/run/cache-state.js.map +1 -1
- package/dist/esm/run/constants.js +2 -1
- package/dist/esm/run/constants.js.map +1 -1
- package/dist/esm/run/env.js +3 -0
- package/dist/esm/run/env.js.map +1 -1
- package/dist/esm/run/finish-line-labels.js +76 -0
- package/dist/esm/run/finish-line-labels.js.map +1 -0
- package/dist/esm/run/finish-line-lengths.js +96 -0
- package/dist/esm/run/finish-line-lengths.js.map +1 -0
- package/dist/esm/run/finish-line.js +3 -169
- package/dist/esm/run/finish-line.js.map +1 -1
- package/dist/esm/run/flows/asset/extract.js.map +1 -1
- package/dist/esm/run/flows/asset/input.js +1 -1
- package/dist/esm/run/flows/asset/input.js.map +1 -1
- package/dist/esm/run/flows/asset/media.js +19 -10
- package/dist/esm/run/flows/asset/media.js.map +1 -1
- package/dist/esm/run/flows/asset/output.js.map +1 -1
- package/dist/esm/run/flows/asset/preprocess.js.map +1 -1
- package/dist/esm/run/flows/asset/summary-attempts.js +109 -0
- package/dist/esm/run/flows/asset/summary-attempts.js.map +1 -0
- package/dist/esm/run/flows/asset/summary.js +19 -107
- package/dist/esm/run/flows/asset/summary.js.map +1 -1
- package/dist/esm/run/flows/url/extract.js +7 -4
- package/dist/esm/run/flows/url/extract.js.map +1 -1
- package/dist/esm/run/flows/url/flow-progress.js +119 -0
- package/dist/esm/run/flows/url/flow-progress.js.map +1 -0
- package/dist/esm/run/flows/url/flow.js +22 -93
- package/dist/esm/run/flows/url/flow.js.map +1 -1
- package/dist/esm/run/flows/url/markdown.js +21 -3
- package/dist/esm/run/flows/url/markdown.js.map +1 -1
- package/dist/esm/run/flows/url/progress-status.js +56 -0
- package/dist/esm/run/flows/url/progress-status.js.map +1 -0
- package/dist/esm/run/flows/url/slides-output-render.js +78 -0
- package/dist/esm/run/flows/url/slides-output-render.js.map +1 -0
- package/dist/esm/run/flows/url/slides-output-state.js +86 -0
- package/dist/esm/run/flows/url/slides-output-state.js.map +1 -0
- package/dist/esm/run/flows/url/slides-output-stream.js +271 -0
- package/dist/esm/run/flows/url/slides-output-stream.js.map +1 -0
- package/dist/esm/run/flows/url/slides-output.js +29 -422
- package/dist/esm/run/flows/url/slides-output.js.map +1 -1
- package/dist/esm/run/flows/url/slides-text-markdown.js +431 -0
- package/dist/esm/run/flows/url/slides-text-markdown.js.map +1 -0
- package/dist/esm/run/flows/url/slides-text-transcript.js +199 -0
- package/dist/esm/run/flows/url/slides-text-transcript.js.map +1 -0
- package/dist/esm/run/flows/url/slides-text-types.js +2 -0
- package/dist/esm/run/flows/url/slides-text-types.js.map +1 -0
- package/dist/esm/run/flows/url/slides-text.js +2 -627
- package/dist/esm/run/flows/url/slides-text.js.map +1 -1
- package/dist/esm/run/flows/url/summary-finish.js +34 -0
- package/dist/esm/run/flows/url/summary-finish.js.map +1 -0
- package/dist/esm/run/flows/url/summary-json.js +32 -0
- package/dist/esm/run/flows/url/summary-json.js.map +1 -0
- package/dist/esm/run/flows/url/summary-prompt.js +147 -0
- package/dist/esm/run/flows/url/summary-prompt.js.map +1 -0
- package/dist/esm/run/flows/url/summary-resolution.js +320 -0
- package/dist/esm/run/flows/url/summary-resolution.js.map +1 -0
- package/dist/esm/run/flows/url/summary-timestamps.js +136 -0
- package/dist/esm/run/flows/url/summary-timestamps.js.map +1 -0
- package/dist/esm/run/flows/url/summary.js +49 -543
- package/dist/esm/run/flows/url/summary.js.map +1 -1
- package/dist/esm/run/help.js +9 -3
- package/dist/esm/run/help.js.map +1 -1
- package/dist/esm/run/markdown-transforms.js +89 -0
- package/dist/esm/run/markdown-transforms.js.map +1 -0
- package/dist/esm/run/markdown.js +1 -96
- package/dist/esm/run/markdown.js.map +1 -1
- package/dist/esm/run/run-env.js +28 -7
- package/dist/esm/run/run-env.js.map +1 -1
- package/dist/esm/run/run-settings-parse.js +73 -0
- package/dist/esm/run/run-settings-parse.js.map +1 -0
- package/dist/esm/run/run-settings.js +1 -72
- package/dist/esm/run/run-settings.js.map +1 -1
- package/dist/esm/run/runner-contexts.js +116 -0
- package/dist/esm/run/runner-contexts.js.map +1 -0
- package/dist/esm/run/runner-execution.js +62 -0
- package/dist/esm/run/runner-execution.js.map +1 -0
- package/dist/esm/run/runner-flags.js +97 -0
- package/dist/esm/run/runner-flags.js.map +1 -0
- package/dist/esm/run/runner-setup.js +109 -0
- package/dist/esm/run/runner-setup.js.map +1 -0
- package/dist/esm/run/runner-slides.js +38 -0
- package/dist/esm/run/runner-slides.js.map +1 -0
- package/dist/esm/run/runner.js +99 -390
- package/dist/esm/run/runner.js.map +1 -1
- package/dist/esm/run/slides-render.js +5 -2
- package/dist/esm/run/slides-render.js.map +1 -1
- package/dist/esm/run/stdin-temp-file.js +1 -1
- package/dist/esm/run/stdin-temp-file.js.map +1 -1
- package/dist/esm/run/streaming.js +1 -0
- package/dist/esm/run/streaming.js.map +1 -1
- package/dist/esm/run/summary-engine.js +26 -10
- package/dist/esm/run/summary-engine.js.map +1 -1
- package/dist/esm/run/summary-llm.js +2 -1
- package/dist/esm/run/summary-llm.js.map +1 -1
- package/dist/esm/run/terminal.js +4 -1
- package/dist/esm/run/terminal.js.map +1 -1
- package/dist/esm/run/transcriber-cli.js +1 -1
- package/dist/esm/run/transcriber-cli.js.map +1 -1
- package/dist/esm/slides/download.js +242 -0
- package/dist/esm/slides/download.js.map +1 -0
- package/dist/esm/slides/extract-finalize.js +98 -0
- package/dist/esm/slides/extract-finalize.js.map +1 -0
- package/dist/esm/slides/extract.js +64 -1621
- package/dist/esm/slides/extract.js.map +1 -1
- package/dist/esm/slides/frame-extraction.js +372 -0
- package/dist/esm/slides/frame-extraction.js.map +1 -0
- package/dist/esm/slides/ingest.js +167 -0
- package/dist/esm/slides/ingest.js.map +1 -0
- package/dist/esm/slides/ocr.js +91 -0
- package/dist/esm/slides/ocr.js.map +1 -0
- package/dist/esm/slides/process.js +218 -0
- package/dist/esm/slides/process.js.map +1 -0
- package/dist/esm/slides/scene-detection.js +387 -0
- package/dist/esm/slides/scene-detection.js.map +1 -0
- package/dist/esm/slides/source-id.js +42 -0
- package/dist/esm/slides/source-id.js.map +1 -0
- package/dist/esm/tty/progress/fetch-html.js.map +1 -1
- package/dist/esm/tty/progress/transcript.js +21 -8
- package/dist/esm/tty/progress/transcript.js.map +1 -1
- package/dist/esm/tty/spinner.js +8 -2
- package/dist/esm/tty/spinner.js.map +1 -1
- package/dist/esm/tty/website-progress.js +5 -3
- package/dist/esm/tty/website-progress.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/types/cache-keys.d.ts +44 -0
- package/dist/types/cache-slides-cleanup.d.ts +1 -0
- package/dist/types/cache.d.ts +1 -9
- package/dist/types/config/env.d.ts +6 -0
- package/dist/types/config/model.d.ts +3 -0
- package/dist/types/config/parse-helpers.d.ts +7 -0
- package/dist/types/config/read.d.ts +2 -0
- package/dist/types/config/sections.d.ts +33 -0
- package/dist/types/config/types.d.ts +230 -0
- package/dist/types/config.d.ts +3 -209
- package/dist/types/costs.d.ts +1 -1
- package/dist/types/daemon/agent-model.d.ts +40 -0
- package/dist/types/daemon/agent-request.d.ts +14 -0
- package/dist/types/daemon/chat.d.ts +3 -1
- package/dist/types/daemon/config.d.ts +13 -2
- package/dist/types/daemon/env-snapshot.d.ts +1 -1
- package/dist/types/daemon/flow-context.d.ts +1 -1
- package/dist/types/daemon/models.d.ts +1 -0
- package/dist/types/daemon/server-admin-routes.d.ts +22 -0
- package/dist/types/daemon/server-agent-route.d.ts +9 -0
- package/dist/types/daemon/server-http.d.ts +10 -0
- package/dist/types/daemon/server-session-routes.d.ts +11 -0
- package/dist/types/daemon/server-session.d.ts +52 -0
- package/dist/types/daemon/server-sse.d.ts +12 -0
- package/dist/types/daemon/server-summarize-execution.d.ts +70 -0
- package/dist/types/daemon/server-summarize-request.d.ts +36 -0
- package/dist/types/daemon/server.d.ts +3 -4
- package/dist/types/daemon/summarize.d.ts +1 -1
- package/dist/types/llm/cli-exec.d.ts +13 -0
- package/dist/types/llm/cli-provider-output.d.ts +16 -0
- package/dist/types/llm/generate-text-document.d.ts +34 -0
- package/dist/types/llm/generate-text-shared.d.ts +25 -0
- package/dist/types/llm/generate-text-stream.d.ts +26 -0
- package/dist/types/llm/generate-text.d.ts +6 -26
- package/dist/types/llm/html-to-markdown.d.ts +1 -1
- package/dist/types/llm/model-id.d.ts +1 -1
- package/dist/types/llm/provider-capabilities.d.ts +2 -0
- package/dist/types/llm/provider-profile.d.ts +31 -0
- package/dist/types/llm/providers/google.d.ts +6 -0
- package/dist/types/llm/providers/models.d.ts +5 -0
- package/dist/types/llm/transcript-to-markdown.d.ts +1 -1
- package/dist/types/model-auto-cli.d.ts +15 -0
- package/dist/types/model-auto-rules.d.ts +7 -0
- package/dist/types/model-auto.d.ts +5 -7
- package/dist/types/model-spec.d.ts +2 -2
- package/dist/types/run/attachments.d.ts +2 -2
- package/dist/types/run/bird/exec.d.ts +1 -0
- package/dist/types/run/bird/media.d.ts +3 -0
- package/dist/types/run/bird/parse.d.ts +3 -0
- package/dist/types/run/bird/types.d.ts +18 -0
- package/dist/types/run/bird.d.ts +12 -17
- package/dist/types/run/cache-state.d.ts +1 -1
- package/dist/types/run/constants.d.ts +2 -1
- package/dist/types/run/env.d.ts +1 -0
- package/dist/types/run/finish-line-labels.d.ts +29 -0
- package/dist/types/run/finish-line-lengths.d.ts +23 -0
- package/dist/types/run/finish-line.d.ts +2 -52
- package/dist/types/run/flows/asset/extract.d.ts +1 -1
- package/dist/types/run/flows/asset/input.d.ts +1 -1
- package/dist/types/run/flows/asset/preprocess.d.ts +1 -1
- package/dist/types/run/flows/asset/summary-attempts.d.ts +24 -0
- package/dist/types/run/flows/asset/summary.d.ts +6 -2
- package/dist/types/run/flows/url/flow-progress.d.ts +41 -0
- package/dist/types/run/flows/url/markdown.d.ts +2 -2
- package/dist/types/run/flows/url/progress-status.d.ts +16 -0
- package/dist/types/run/flows/url/slides-output-render.d.ts +43 -0
- package/dist/types/run/flows/url/slides-output-state.d.ts +21 -0
- package/dist/types/run/flows/url/slides-output-stream.d.ts +18 -0
- package/dist/types/run/flows/url/slides-output.d.ts +2 -17
- package/dist/types/run/flows/url/slides-text-markdown.d.ts +46 -0
- package/dist/types/run/flows/url/slides-text-transcript.d.ts +36 -0
- package/dist/types/run/flows/url/slides-text-types.d.ts +8 -0
- package/dist/types/run/flows/url/slides-text.d.ts +3 -87
- package/dist/types/run/flows/url/summary-finish.d.ts +16 -0
- package/dist/types/run/flows/url/summary-json.d.ts +51 -0
- package/dist/types/run/flows/url/summary-prompt.d.ts +22 -0
- package/dist/types/run/flows/url/summary-resolution.d.ts +31 -0
- package/dist/types/run/flows/url/summary-timestamps.d.ts +11 -0
- package/dist/types/run/flows/url/types.d.ts +4 -0
- package/dist/types/run/markdown-transforms.d.ts +3 -0
- package/dist/types/run/run-context.d.ts +4 -0
- package/dist/types/run/run-env.d.ts +4 -0
- package/dist/types/run/run-settings-parse.d.ts +5 -0
- package/dist/types/run/runner-contexts.d.ts +62 -0
- package/dist/types/run/runner-execution.d.ts +57 -0
- package/dist/types/run/runner-flags.d.ts +41 -0
- package/dist/types/run/runner-setup.d.ts +21 -0
- package/dist/types/run/runner-slides.d.ts +8 -0
- package/dist/types/run/streaming.d.ts +1 -1
- package/dist/types/run/summary-engine.d.ts +8 -4
- package/dist/types/run/summary-llm.d.ts +4 -3
- package/dist/types/run/terminal.d.ts +2 -0
- package/dist/types/run/types.d.ts +2 -2
- package/dist/types/slides/download.d.ts +29 -0
- package/dist/types/slides/extract-finalize.d.ts +57 -0
- package/dist/types/slides/extract.d.ts +1 -7
- package/dist/types/slides/frame-extraction.d.ts +38 -0
- package/dist/types/slides/ingest.d.ts +47 -0
- package/dist/types/slides/ocr.d.ts +5 -0
- package/dist/types/slides/process.d.ts +22 -0
- package/dist/types/slides/scene-detection.d.ts +75 -0
- package/dist/types/slides/source-id.d.ts +2 -0
- package/dist/types/version.d.ts +1 -1
- package/docs/_config.yml +1 -0
- package/docs/agent.md +3 -2
- package/docs/assets/site.css +134 -2
- package/docs/cache.md +2 -1
- package/docs/chrome-extension.md +12 -4
- package/docs/cli.md +2 -2
- package/docs/config.md +11 -4
- package/docs/index.html +5 -0
- package/docs/llm.md +5 -2
- package/docs/manual-tests.md +3 -0
- package/docs/media.md +3 -1
- package/docs/model-auto.md +2 -2
- package/docs/model-provider-resolution.md +57 -0
- package/docs/site/index.html +5 -0
- package/docs/slides-rendering-flow.md +46 -0
- package/docs/slides.md +5 -5
- package/docs/smoketest.md +1 -1
- package/docs/transcript-provider-flow.md +66 -0
- package/docs/website.md +1 -0
- package/docs/youtube.md +4 -2
- package/package.json +11 -11
|
@@ -1,140 +1,27 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { createReadStream, promises as fs } from "node:fs";
|
|
3
2
|
import http from "node:http";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { Writable } from "node:stream";
|
|
6
5
|
import { loadSummarizeConfig } from "../config.js";
|
|
7
6
|
import { createDaemonLogger } from "../logging/daemon.js";
|
|
8
|
-
import {
|
|
7
|
+
import { setProcessObserver } from "../processes.js";
|
|
9
8
|
import { refreshFree } from "../refresh-free.js";
|
|
10
9
|
import { createCacheStateFromConfig, refreshCacheStoreIfMissing } from "../run/cache-state.js";
|
|
11
10
|
import { resolveExecutableInPath } from "../run/env.js";
|
|
12
|
-
import { formatModelLabelForDisplay } from "../run/finish-line.js";
|
|
13
11
|
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";
|
|
17
12
|
import { resolvePackageVersion } from "../version.js";
|
|
18
|
-
import {
|
|
19
|
-
import { resolveAutoDaemonMode } from "./auto-mode.js";
|
|
13
|
+
import { daemonConfigTokens } from "./config.js";
|
|
20
14
|
import { DAEMON_HOST, DAEMON_PORT_DEFAULT } from "./constants.js";
|
|
21
15
|
import { resolveDaemonLogPaths } from "./launchd.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
res.end(body);
|
|
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
|
-
}
|
|
67
|
-
function text(res, status, body, headers) {
|
|
68
|
-
const out = body.endsWith("\n") ? body : `${body}\n`;
|
|
69
|
-
res.writeHead(status, {
|
|
70
|
-
"content-type": "text/plain; charset=utf-8",
|
|
71
|
-
"content-length": Buffer.byteLength(out).toString(),
|
|
72
|
-
...headers,
|
|
73
|
-
});
|
|
74
|
-
res.end(out);
|
|
75
|
-
}
|
|
76
|
-
function resolveOriginHeader(req) {
|
|
77
|
-
const origin = req.headers.origin;
|
|
78
|
-
if (typeof origin !== "string")
|
|
79
|
-
return null;
|
|
80
|
-
if (!origin.trim())
|
|
81
|
-
return null;
|
|
82
|
-
return origin;
|
|
83
|
-
}
|
|
84
|
-
function corsHeaders(origin) {
|
|
85
|
-
if (!origin)
|
|
86
|
-
return {};
|
|
87
|
-
return {
|
|
88
|
-
"access-control-allow-origin": origin,
|
|
89
|
-
"access-control-allow-credentials": "true",
|
|
90
|
-
"access-control-allow-headers": "authorization, content-type",
|
|
91
|
-
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
92
|
-
// Chrome Private Network Access (PNA): allow requests to localhost from secure contexts.
|
|
93
|
-
// Without this, extensions often fail with a generic "Failed to fetch".
|
|
94
|
-
"access-control-allow-private-network": "true",
|
|
95
|
-
"access-control-max-age": "600",
|
|
96
|
-
vary: "Origin",
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function readBearerToken(req) {
|
|
100
|
-
const header = req.headers.authorization;
|
|
101
|
-
if (typeof header !== "string")
|
|
102
|
-
return null;
|
|
103
|
-
const m = header.match(/^Bearer\s+(.+)\s*$/i);
|
|
104
|
-
return m?.[1]?.trim() || null;
|
|
105
|
-
}
|
|
106
|
-
async function readJsonBody(req, maxBytes) {
|
|
107
|
-
const chunks = [];
|
|
108
|
-
let total = 0;
|
|
109
|
-
for await (const chunk of req) {
|
|
110
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
111
|
-
total += buf.byteLength;
|
|
112
|
-
if (total > maxBytes)
|
|
113
|
-
throw new Error(`Body too large (>${maxBytes} bytes)`);
|
|
114
|
-
chunks.push(buf);
|
|
115
|
-
}
|
|
116
|
-
const text = Buffer.concat(chunks).toString("utf8");
|
|
117
|
-
return JSON.parse(text);
|
|
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
|
-
}
|
|
16
|
+
import { ProcessRegistry } from "./process-registry.js";
|
|
17
|
+
import { handleAdminRoutes } from "./server-admin-routes.js";
|
|
18
|
+
import { handleAgentRoute } from "./server-agent-route.js";
|
|
19
|
+
import { json, readBearerToken, readCorsHeaders, text, } from "./server-http.js";
|
|
20
|
+
import { handleSessionRoutes } from "./server-session-routes.js";
|
|
21
|
+
import { createSession, endSession, pushToSession, } from "./server-session.js";
|
|
22
|
+
import { executeSummarizeSession, handleExtractOnlySummarizeRequest, toExtractOnlySlidesPayload, } from "./server-summarize-execution.js";
|
|
23
|
+
import { parseSummarizeRequest } from "./server-summarize-request.js";
|
|
24
|
+
export { corsHeaders, isTrustedOrigin } from "./server-http.js";
|
|
138
25
|
function createLineWriter(onLine) {
|
|
139
26
|
let buffer = "";
|
|
140
27
|
return new Writable({
|
|
@@ -159,168 +46,12 @@ function createLineWriter(onLine) {
|
|
|
159
46
|
},
|
|
160
47
|
});
|
|
161
48
|
}
|
|
162
|
-
function createSession() {
|
|
163
|
-
return {
|
|
164
|
-
id: randomUUID(),
|
|
165
|
-
createdAtMs: Date.now(),
|
|
166
|
-
buffer: [],
|
|
167
|
-
bufferBytes: 0,
|
|
168
|
-
done: false,
|
|
169
|
-
clients: new Set(),
|
|
170
|
-
slidesBuffer: [],
|
|
171
|
-
slidesBufferBytes: 0,
|
|
172
|
-
slidesClients: new Set(),
|
|
173
|
-
slidesDone: false,
|
|
174
|
-
slidesRequested: false,
|
|
175
|
-
slidesLastStatus: null,
|
|
176
|
-
lastMeta: { model: null, modelLabel: null, inputSummary: null, summaryFromCache: null },
|
|
177
|
-
slides: null,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
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);
|
|
187
|
-
for (const res of session.clients) {
|
|
188
|
-
res.write(encoded);
|
|
189
|
-
}
|
|
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;
|
|
200
|
-
}
|
|
201
|
-
if (evt.event === "done" || evt.event === "error") {
|
|
202
|
-
session.done = true;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
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) {
|
|
229
|
-
const next = { ...session.lastMeta, ...patch };
|
|
230
|
-
if (next.model === session.lastMeta.model &&
|
|
231
|
-
next.modelLabel === session.lastMeta.modelLabel &&
|
|
232
|
-
next.inputSummary === session.lastMeta.inputSummary &&
|
|
233
|
-
next.summaryFromCache === session.lastMeta.summaryFromCache) {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
session.lastMeta = 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
49
|
function resolveToolPath(binary, env, explicitEnvKey) {
|
|
297
50
|
const explicit = explicitEnvKey && typeof env[explicitEnvKey] === "string" ? env[explicitEnvKey]?.trim() : "";
|
|
298
51
|
if (explicit)
|
|
299
52
|
return resolveExecutableInPath(explicit, env);
|
|
300
53
|
return resolveExecutableInPath(binary, env);
|
|
301
54
|
}
|
|
302
|
-
function endSession(session) {
|
|
303
|
-
for (const res of session.clients) {
|
|
304
|
-
res.end();
|
|
305
|
-
}
|
|
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();
|
|
323
|
-
}
|
|
324
55
|
export function buildHealthPayload(importMetaUrl) {
|
|
325
56
|
return { ok: true, pid: process.pid, version: resolvePackageVersion(importMetaUrl) };
|
|
326
57
|
}
|
|
@@ -347,8 +78,7 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
347
78
|
let activeRefreshSessionId = null;
|
|
348
79
|
const server = http.createServer((req, res) => {
|
|
349
80
|
void (async () => {
|
|
350
|
-
const
|
|
351
|
-
const cors = corsHeaders(origin);
|
|
81
|
+
const cors = readCorsHeaders(req);
|
|
352
82
|
if (req.method === "OPTIONS") {
|
|
353
83
|
res.writeHead(204, cors);
|
|
354
84
|
res.end();
|
|
@@ -361,107 +91,26 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
361
91
|
return;
|
|
362
92
|
}
|
|
363
93
|
const token = readBearerToken(req);
|
|
364
|
-
const authed = token
|
|
94
|
+
const authed = token ? daemonConfigTokens(config).includes(token) : false;
|
|
365
95
|
if (pathname.startsWith("/v1/") && !authed) {
|
|
366
96
|
json(res, 401, { ok: false, error: "unauthorized" }, cors);
|
|
367
97
|
return;
|
|
368
98
|
}
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
}
|
|
443
|
-
if (req.method === "GET" && pathname === "/v1/models") {
|
|
444
|
-
const result = await buildModelPickerOptions({
|
|
445
|
-
env,
|
|
446
|
-
envForRun: env,
|
|
447
|
-
configForCli: summarizeConfig,
|
|
448
|
-
fetchImpl,
|
|
449
|
-
});
|
|
450
|
-
json(res, 200, result, cors);
|
|
451
|
-
return;
|
|
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);
|
|
99
|
+
if (await handleAdminRoutes({
|
|
100
|
+
req,
|
|
101
|
+
res,
|
|
102
|
+
url,
|
|
103
|
+
pathname,
|
|
104
|
+
cors,
|
|
105
|
+
env,
|
|
106
|
+
fetchImpl,
|
|
107
|
+
summarizeConfig,
|
|
108
|
+
daemonLogger,
|
|
109
|
+
daemonLogFile,
|
|
110
|
+
daemonLogPaths,
|
|
111
|
+
processRegistry,
|
|
112
|
+
resolveToolPath,
|
|
113
|
+
})) {
|
|
465
114
|
return;
|
|
466
115
|
}
|
|
467
116
|
if (req.method === "POST" && pathname === "/v1/refresh-free") {
|
|
@@ -469,7 +118,7 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
469
118
|
json(res, 200, { ok: true, id: activeRefreshSessionId, running: true }, cors);
|
|
470
119
|
return;
|
|
471
120
|
}
|
|
472
|
-
const session = createSession();
|
|
121
|
+
const session = createSession(() => randomUUID());
|
|
473
122
|
refreshSessions.set(session.id, session);
|
|
474
123
|
activeRefreshSessionId = session.id;
|
|
475
124
|
json(res, 200, { ok: true, id: session.id }, cors);
|
|
@@ -503,99 +152,28 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
503
152
|
}
|
|
504
153
|
if (req.method === "POST" && pathname === "/v1/summarize") {
|
|
505
154
|
await refreshCacheStoreIfMissing({ cacheState, transcriptNamespace: "yt:auto" });
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
json(res, 400, { ok: false, error: message }, cors);
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
if (!body || typeof body !== "object") {
|
|
516
|
-
json(res, 400, { ok: false, error: "invalid json" }, cors);
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
const obj = body;
|
|
520
|
-
const pageUrl = typeof obj.url === "string" ? obj.url.trim() : "";
|
|
521
|
-
const title = typeof obj.title === "string" ? obj.title.trim() : null;
|
|
522
|
-
const textContent = typeof obj.text === "string" ? obj.text : "";
|
|
523
|
-
const truncated = Boolean(obj.truncated);
|
|
524
|
-
const modelOverride = typeof obj.model === "string" ? obj.model.trim() : null;
|
|
525
|
-
const lengthRaw = typeof obj.length === "string" ? obj.length.trim() : "";
|
|
526
|
-
const languageRaw = typeof obj.language === "string" ? obj.language.trim() : "";
|
|
527
|
-
const promptRaw = typeof obj.prompt === "string" ? obj.prompt : "";
|
|
528
|
-
const promptOverride = promptRaw.trim() || null;
|
|
529
|
-
const noCache = Boolean(obj.noCache);
|
|
530
|
-
const extractOnly = Boolean(obj.extractOnly);
|
|
531
|
-
const modeRaw = typeof obj.mode === "string" ? obj.mode.trim().toLowerCase() : "";
|
|
532
|
-
const mode = modeRaw === "url" ? "url" : modeRaw === "page" ? "page" : "auto";
|
|
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
|
-
autoCliFallback: obj.autoCliFallback,
|
|
553
|
-
autoCliOrder: obj.autoCliOrder,
|
|
554
|
-
magicCliAuto: obj.magicCliAuto,
|
|
555
|
-
magicCliOrder: obj.magicCliOrder,
|
|
155
|
+
const request = await parseSummarizeRequest({
|
|
156
|
+
req,
|
|
157
|
+
res,
|
|
158
|
+
cors,
|
|
159
|
+
env,
|
|
160
|
+
resolveToolPath,
|
|
556
161
|
});
|
|
557
|
-
|
|
558
|
-
const diagnostics = parseDiagnostics(obj.diagnostics);
|
|
559
|
-
const includeContentLog = daemonLogger.enabled && diagnostics.includeContent;
|
|
560
|
-
const hasText = Boolean(textContent.trim());
|
|
561
|
-
if (!pageUrl || !/^https?:\/\//i.test(pageUrl)) {
|
|
562
|
-
json(res, 400, { ok: false, error: "missing url" }, cors);
|
|
162
|
+
if (!request) {
|
|
563
163
|
return;
|
|
564
164
|
}
|
|
165
|
+
const { pageUrl, title, textContent, truncated, modelOverride, lengthRaw, languageRaw, promptOverride, noCache, extractOnly, mode, maxCharacters, format, overrides, slidesSettings, diagnostics, hasText, } = request;
|
|
166
|
+
const includeContentLog = daemonLogger.enabled && diagnostics.includeContent;
|
|
565
167
|
if (extractOnly) {
|
|
566
|
-
if (mode === "page") {
|
|
567
|
-
json(res, 400, { ok: false, error: "extractOnly requires mode=url" }, cors);
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
168
|
try {
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
: cacheState;
|
|
574
|
-
const runId = randomUUID();
|
|
575
|
-
const { extracted, slides } = await runWithProcessContext({ runId, source: "extract" }, async () => extractContentForUrl({
|
|
169
|
+
const { extracted, slides } = await handleExtractOnlySummarizeRequest({
|
|
170
|
+
request,
|
|
576
171
|
env,
|
|
577
172
|
fetchImpl,
|
|
578
|
-
|
|
579
|
-
cache: requestCache,
|
|
173
|
+
cacheState,
|
|
580
174
|
mediaCache,
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
slides: slidesSettings,
|
|
584
|
-
}));
|
|
585
|
-
const slidesPayload = slides && slides.slides.length > 0
|
|
586
|
-
? {
|
|
587
|
-
sourceUrl: slides.sourceUrl,
|
|
588
|
-
sourceId: slides.sourceId,
|
|
589
|
-
sourceKind: slides.sourceKind,
|
|
590
|
-
ocrAvailable: slides.ocrAvailable,
|
|
591
|
-
slides: slides.slides.map((slide) => ({
|
|
592
|
-
index: slide.index,
|
|
593
|
-
timestamp: slide.timestamp,
|
|
594
|
-
ocrText: slide.ocrText ?? null,
|
|
595
|
-
ocrConfidence: slide.ocrConfidence ?? null,
|
|
596
|
-
})),
|
|
597
|
-
}
|
|
598
|
-
: null;
|
|
175
|
+
});
|
|
176
|
+
const slidesPayload = toExtractOnlySlidesPayload(slides);
|
|
599
177
|
json(res, 200, {
|
|
600
178
|
ok: true,
|
|
601
179
|
extracted: {
|
|
@@ -624,11 +202,7 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
624
202
|
}
|
|
625
203
|
return;
|
|
626
204
|
}
|
|
627
|
-
|
|
628
|
-
json(res, 400, { ok: false, error: "missing text" }, cors);
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
const session = createSession();
|
|
205
|
+
const session = createSession(() => randomUUID());
|
|
632
206
|
session.slidesRequested = Boolean(slidesSettings);
|
|
633
207
|
sessions.set(session.id, session);
|
|
634
208
|
const requestLogger = daemonLogger.getSubLogger("daemon.summarize", {
|
|
@@ -673,666 +247,43 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
673
247
|
...(includeContentLog ? { diagnostics } : {}),
|
|
674
248
|
});
|
|
675
249
|
json(res, 200, { ok: true, id: session.id }, cors);
|
|
676
|
-
void
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
emittedOutput = true;
|
|
693
|
-
if (includeContentLog) {
|
|
694
|
-
logSummaryText += chunk;
|
|
695
|
-
}
|
|
696
|
-
pushToSession(session, { event: "chunk", data: { text: chunk } }, onSessionEvent);
|
|
697
|
-
},
|
|
698
|
-
onModelChosen: (modelId) => {
|
|
699
|
-
if (session.lastMeta.model === modelId)
|
|
700
|
-
return;
|
|
701
|
-
emittedOutput = true;
|
|
702
|
-
emitMeta(session, {
|
|
703
|
-
model: modelId,
|
|
704
|
-
modelLabel: formatModelLabelForDisplay(modelId),
|
|
705
|
-
}, onSessionEvent);
|
|
706
|
-
},
|
|
707
|
-
writeStatus: (text) => {
|
|
708
|
-
const clean = text.trim();
|
|
709
|
-
if (!clean)
|
|
710
|
-
return;
|
|
711
|
-
pushToSession(session, { event: "status", data: { text: clean } }, onSessionEvent);
|
|
712
|
-
},
|
|
713
|
-
writeMeta: (data) => {
|
|
714
|
-
if (typeof data.inputSummary === "string") {
|
|
715
|
-
logInputSummary = data.inputSummary;
|
|
716
|
-
}
|
|
717
|
-
if (typeof data.summaryFromCache === "boolean") {
|
|
718
|
-
logSummaryFromCache = data.summaryFromCache;
|
|
719
|
-
}
|
|
720
|
-
emitMeta(session, {
|
|
721
|
-
inputSummary: typeof data.inputSummary === "string" ? data.inputSummary : null,
|
|
722
|
-
summaryFromCache: typeof data.summaryFromCache === "boolean" ? data.summaryFromCache : null,
|
|
723
|
-
}, onSessionEvent);
|
|
724
|
-
},
|
|
725
|
-
};
|
|
726
|
-
const normalizedModelOverride = modelOverride && modelOverride.toLowerCase() !== "auto" ? modelOverride : null;
|
|
727
|
-
const requestCache = noCache
|
|
728
|
-
? { ...cacheState, mode: "bypass", store: null }
|
|
729
|
-
: cacheState;
|
|
730
|
-
let liveSlides = null;
|
|
731
|
-
const runWithMode = async (resolved) => {
|
|
732
|
-
if (resolved === "url" && slideLogState.requested) {
|
|
733
|
-
slideLogState.startedAt = Date.now();
|
|
734
|
-
console.log(`[summarize-daemon] slides: start url=${pageUrl} (session=${session.id})`);
|
|
735
|
-
if (includeContentLog) {
|
|
736
|
-
requestLogger?.info({
|
|
737
|
-
event: "slides.start",
|
|
738
|
-
url: pageUrl,
|
|
739
|
-
sessionId: session.id,
|
|
740
|
-
...(logSlidesSettings ? { settings: logSlidesSettings } : {}),
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
return resolved === "url"
|
|
745
|
-
? await streamSummaryForUrl({
|
|
746
|
-
env,
|
|
747
|
-
fetchImpl,
|
|
748
|
-
modelOverride: normalizedModelOverride,
|
|
749
|
-
promptOverride,
|
|
750
|
-
lengthRaw,
|
|
751
|
-
languageRaw,
|
|
752
|
-
format,
|
|
753
|
-
input: { url: pageUrl, title, maxCharacters },
|
|
754
|
-
sink,
|
|
755
|
-
cache: requestCache,
|
|
756
|
-
mediaCache,
|
|
757
|
-
overrides,
|
|
758
|
-
slides: slidesSettings,
|
|
759
|
-
hooks: {
|
|
760
|
-
...(includeContentLog
|
|
761
|
-
? {
|
|
762
|
-
onExtracted: (content) => {
|
|
763
|
-
logExtracted = content;
|
|
764
|
-
},
|
|
765
|
-
}
|
|
766
|
-
: {}),
|
|
767
|
-
onSlidesExtracted: (slides) => {
|
|
768
|
-
session.slides = slides;
|
|
769
|
-
slideLogState.slidesCount = slides.slides.length;
|
|
770
|
-
slideLogState.ocrAvailable = slides.ocrAvailable;
|
|
771
|
-
slideLogState.warnings = slides.warnings;
|
|
772
|
-
if (slideLogState.startedAt) {
|
|
773
|
-
slideLogState.elapsedMs = Date.now() - slideLogState.startedAt;
|
|
774
|
-
}
|
|
775
|
-
if (slideLogState.startedAt) {
|
|
776
|
-
const elapsedMs = Date.now() - slideLogState.startedAt;
|
|
777
|
-
console.log(`[summarize-daemon] slides: done count=${slides.slides.length} ocr=${slides.ocrAvailable} elapsedMs=${elapsedMs} warnings=${slides.warnings.join("; ")}`);
|
|
778
|
-
}
|
|
779
|
-
if (includeContentLog) {
|
|
780
|
-
requestLogger?.info({
|
|
781
|
-
event: "slides.done",
|
|
782
|
-
url: pageUrl,
|
|
783
|
-
sessionId: session.id,
|
|
784
|
-
slidesCount: slides.slides.length,
|
|
785
|
-
ocrAvailable: slides.ocrAvailable,
|
|
786
|
-
elapsedMs: slideLogState.elapsedMs,
|
|
787
|
-
cacheHit: slideLogState.cacheHit,
|
|
788
|
-
warnings: slides.warnings,
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
emitSlides(session, buildSlidesPayload({
|
|
792
|
-
slides,
|
|
793
|
-
port,
|
|
794
|
-
}), onSessionEvent);
|
|
795
|
-
},
|
|
796
|
-
onSlidesDone: (result) => {
|
|
797
|
-
emitSlidesDone(session, result, onSessionEvent);
|
|
798
|
-
},
|
|
799
|
-
onSlidesProgress: (text) => {
|
|
800
|
-
const clean = typeof text === "string" ? text.trim() : "";
|
|
801
|
-
if (!clean)
|
|
802
|
-
return;
|
|
803
|
-
slideLogState.lastStatus = clean;
|
|
804
|
-
slideLogState.statusCount += 1;
|
|
805
|
-
if (clean.toLowerCase().includes("cached")) {
|
|
806
|
-
slideLogState.cacheHit = true;
|
|
807
|
-
}
|
|
808
|
-
const progressMatch = clean.match(/(\d+)%/);
|
|
809
|
-
const progress = progressMatch ? Number(progressMatch[1]) : null;
|
|
810
|
-
if (includeContentLog) {
|
|
811
|
-
requestLogger?.info({
|
|
812
|
-
event: "slides.status",
|
|
813
|
-
url: pageUrl,
|
|
814
|
-
sessionId: session.id,
|
|
815
|
-
status: clean,
|
|
816
|
-
...(progress !== null ? { progress } : {}),
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
emitSlidesStatus(session, clean, onSessionEvent);
|
|
820
|
-
},
|
|
821
|
-
onSlideChunk: (chunk) => {
|
|
822
|
-
const { slide, meta } = chunk;
|
|
823
|
-
if (slide == null ||
|
|
824
|
-
!meta?.slidesDir ||
|
|
825
|
-
!meta.sourceUrl ||
|
|
826
|
-
!meta.sourceId ||
|
|
827
|
-
!meta.sourceKind) {
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
const nextSlides = liveSlides ?? {
|
|
831
|
-
sourceUrl: meta.sourceUrl,
|
|
832
|
-
sourceKind: meta.sourceKind,
|
|
833
|
-
sourceId: meta.sourceId,
|
|
834
|
-
slidesDir: meta.slidesDir,
|
|
835
|
-
sceneThreshold: 0,
|
|
836
|
-
autoTuneThreshold: false,
|
|
837
|
-
autoTune: {
|
|
838
|
-
enabled: false,
|
|
839
|
-
chosenThreshold: 0,
|
|
840
|
-
confidence: 0,
|
|
841
|
-
strategy: "none",
|
|
842
|
-
},
|
|
843
|
-
maxSlides: 0,
|
|
844
|
-
minSlideDuration: 0,
|
|
845
|
-
ocrRequested: meta.ocrAvailable,
|
|
846
|
-
ocrAvailable: meta.ocrAvailable,
|
|
847
|
-
slides: [],
|
|
848
|
-
warnings: [],
|
|
849
|
-
};
|
|
850
|
-
liveSlides = nextSlides;
|
|
851
|
-
const existingIndex = nextSlides.slides.findIndex((item) => item.index === slide.index);
|
|
852
|
-
if (existingIndex >= 0) {
|
|
853
|
-
nextSlides.slides[existingIndex] = {
|
|
854
|
-
...nextSlides.slides[existingIndex],
|
|
855
|
-
...slide,
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
else {
|
|
859
|
-
nextSlides.slides.push(slide);
|
|
860
|
-
}
|
|
861
|
-
nextSlides.slides.sort((a, b) => a.index - b.index);
|
|
862
|
-
session.slides = nextSlides;
|
|
863
|
-
emitSlides(session, buildSlidesPayload({
|
|
864
|
-
slides: nextSlides,
|
|
865
|
-
port,
|
|
866
|
-
}), onSessionEvent);
|
|
867
|
-
},
|
|
868
|
-
},
|
|
869
|
-
})
|
|
870
|
-
: await streamSummaryForVisiblePage({
|
|
871
|
-
env,
|
|
872
|
-
fetchImpl,
|
|
873
|
-
modelOverride: normalizedModelOverride,
|
|
874
|
-
promptOverride,
|
|
875
|
-
lengthRaw,
|
|
876
|
-
languageRaw,
|
|
877
|
-
format,
|
|
878
|
-
input: { url: pageUrl, title, text: textContent, truncated },
|
|
879
|
-
sink,
|
|
880
|
-
cache: requestCache,
|
|
881
|
-
mediaCache,
|
|
882
|
-
overrides,
|
|
883
|
-
});
|
|
884
|
-
};
|
|
885
|
-
const result = await (async () => {
|
|
886
|
-
if (mode !== "auto")
|
|
887
|
-
return runWithMode(mode);
|
|
888
|
-
const { primary, fallback } = resolveAutoDaemonMode({ url: pageUrl, hasText });
|
|
889
|
-
try {
|
|
890
|
-
return await runWithMode(primary);
|
|
891
|
-
}
|
|
892
|
-
catch (error) {
|
|
893
|
-
if (!fallback || emittedOutput)
|
|
894
|
-
throw error;
|
|
895
|
-
sink.writeStatus?.("Primary failed. Trying fallback…");
|
|
896
|
-
try {
|
|
897
|
-
return await runWithMode(fallback);
|
|
898
|
-
}
|
|
899
|
-
catch (fallbackError) {
|
|
900
|
-
const primaryMessage = error instanceof Error ? error.message : String(error);
|
|
901
|
-
const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
902
|
-
throw new Error(`Auto mode failed.\nPrimary (${primary}): ${primaryMessage}\nFallback (${fallback}): ${fallbackMessage}`);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
})();
|
|
906
|
-
if (!session.lastMeta.model) {
|
|
907
|
-
emitMeta(session, {
|
|
908
|
-
model: result.usedModel,
|
|
909
|
-
modelLabel: formatModelLabelForDisplay(result.usedModel),
|
|
910
|
-
}, onSessionEvent);
|
|
911
|
-
}
|
|
912
|
-
pushToSession(session, { event: "metrics", data: result.metrics }, onSessionEvent);
|
|
913
|
-
pushToSession(session, { event: "done", data: {} }, onSessionEvent);
|
|
914
|
-
requestLogger?.info({
|
|
915
|
-
event: "summarize.done",
|
|
916
|
-
url: pageUrl,
|
|
917
|
-
mode,
|
|
918
|
-
model: result.usedModel,
|
|
919
|
-
elapsedMs: Date.now() - logStartedAt,
|
|
920
|
-
summaryFromCache: logSummaryFromCache,
|
|
921
|
-
inputSummary: logInputSummary,
|
|
922
|
-
...(includeContentLog && slideLogState.requested
|
|
923
|
-
? {
|
|
924
|
-
slides: {
|
|
925
|
-
requested: true,
|
|
926
|
-
cacheHit: slideLogState.cacheHit,
|
|
927
|
-
lastStatus: slideLogState.lastStatus,
|
|
928
|
-
statusCount: slideLogState.statusCount,
|
|
929
|
-
elapsedMs: slideLogState.elapsedMs,
|
|
930
|
-
slidesCount: slideLogState.slidesCount,
|
|
931
|
-
ocrAvailable: slideLogState.ocrAvailable,
|
|
932
|
-
warnings: slideLogState.warnings,
|
|
933
|
-
},
|
|
934
|
-
}
|
|
935
|
-
: {}),
|
|
936
|
-
...(includeContentLog && !logSummaryFromCache
|
|
937
|
-
? {
|
|
938
|
-
input: logInput,
|
|
939
|
-
extracted: logExtracted,
|
|
940
|
-
summary: logSummaryText,
|
|
941
|
-
}
|
|
942
|
-
: {}),
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
catch (error) {
|
|
946
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
947
|
-
pushToSession(session, { event: "error", data: { message } }, onSessionEvent);
|
|
948
|
-
if (session.slidesRequested && !session.slidesDone) {
|
|
949
|
-
emitSlidesDone(session, { ok: false, error: message }, onSessionEvent);
|
|
950
|
-
}
|
|
951
|
-
// Preserve full stack trace in daemon logs for debugging.
|
|
952
|
-
console.error("[summarize-daemon] summarize failed", error);
|
|
953
|
-
requestLogger?.error({
|
|
954
|
-
event: "summarize.error",
|
|
955
|
-
url: pageUrl,
|
|
956
|
-
mode,
|
|
957
|
-
elapsedMs: Date.now() - logStartedAt,
|
|
958
|
-
summaryFromCache: logSummaryFromCache,
|
|
959
|
-
inputSummary: logInputSummary,
|
|
960
|
-
...(includeContentLog && slideLogState.requested
|
|
961
|
-
? {
|
|
962
|
-
slides: {
|
|
963
|
-
requested: true,
|
|
964
|
-
cacheHit: slideLogState.cacheHit,
|
|
965
|
-
lastStatus: slideLogState.lastStatus,
|
|
966
|
-
statusCount: slideLogState.statusCount,
|
|
967
|
-
elapsedMs: slideLogState.elapsedMs,
|
|
968
|
-
slidesCount: slideLogState.slidesCount,
|
|
969
|
-
ocrAvailable: slideLogState.ocrAvailable,
|
|
970
|
-
warnings: slideLogState.warnings,
|
|
971
|
-
},
|
|
972
|
-
}
|
|
973
|
-
: {}),
|
|
974
|
-
error: {
|
|
975
|
-
message,
|
|
976
|
-
stack: error instanceof Error ? error.stack : null,
|
|
977
|
-
},
|
|
978
|
-
...(includeContentLog && !logSummaryFromCache
|
|
979
|
-
? {
|
|
980
|
-
input: logInput,
|
|
981
|
-
extracted: logExtracted,
|
|
982
|
-
summary: logSummaryText || null,
|
|
983
|
-
}
|
|
984
|
-
: {}),
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
finally {
|
|
988
|
-
scheduleSessionCleanup({ session, sessions });
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
if (req.method === "POST" && pathname === "/v1/agent") {
|
|
994
|
-
let body;
|
|
995
|
-
try {
|
|
996
|
-
body = await readJsonBody(req, 4_000_000);
|
|
997
|
-
}
|
|
998
|
-
catch (error) {
|
|
999
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1000
|
-
json(res, 400, { ok: false, error: message }, cors);
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
if (!body || typeof body !== "object") {
|
|
1004
|
-
json(res, 400, { ok: false, error: "invalid json" }, cors);
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
const obj = body;
|
|
1008
|
-
const pageUrl = typeof obj.url === "string" ? obj.url.trim() : "";
|
|
1009
|
-
const pageTitle = typeof obj.title === "string" ? obj.title.trim() : null;
|
|
1010
|
-
const pageContent = typeof obj.pageContent === "string" ? obj.pageContent : "";
|
|
1011
|
-
const messages = obj.messages;
|
|
1012
|
-
const modelOverride = typeof obj.model === "string" ? obj.model.trim() : null;
|
|
1013
|
-
const tools = Array.isArray(obj.tools)
|
|
1014
|
-
? obj.tools.filter((tool) => typeof tool === "string")
|
|
1015
|
-
: [];
|
|
1016
|
-
const automationEnabled = Boolean(obj.automationEnabled);
|
|
1017
|
-
if (!pageUrl) {
|
|
1018
|
-
json(res, 400, { ok: false, error: "missing url" }, cors);
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
const runId = `agent-${randomUUID()}`;
|
|
1022
|
-
const wantsJson = wantsJsonResponse(req, url);
|
|
1023
|
-
if (wantsJson) {
|
|
1024
|
-
try {
|
|
1025
|
-
const assistant = await runWithProcessContext({ runId, source: "agent" }, async () => completeAgentResponse({
|
|
1026
|
-
env,
|
|
1027
|
-
pageUrl,
|
|
1028
|
-
pageTitle,
|
|
1029
|
-
pageContent,
|
|
1030
|
-
messages,
|
|
1031
|
-
modelOverride: modelOverride && modelOverride.toLowerCase() !== "auto" ? modelOverride : null,
|
|
1032
|
-
tools,
|
|
1033
|
-
automationEnabled,
|
|
1034
|
-
}));
|
|
1035
|
-
json(res, 200, { ok: true, assistant }, cors);
|
|
1036
|
-
}
|
|
1037
|
-
catch (error) {
|
|
1038
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1039
|
-
console.error("[summarize-daemon] agent failed", error);
|
|
1040
|
-
json(res, 500, { ok: false, error: message }, cors);
|
|
1041
|
-
}
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
res.writeHead(200, {
|
|
1045
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
1046
|
-
"cache-control": "no-cache",
|
|
1047
|
-
connection: "keep-alive",
|
|
1048
|
-
"x-accel-buffering": "no",
|
|
1049
|
-
...cors,
|
|
1050
|
-
});
|
|
1051
|
-
const controller = new AbortController();
|
|
1052
|
-
const abort = () => controller.abort();
|
|
1053
|
-
req.on("close", abort);
|
|
1054
|
-
res.on("close", abort);
|
|
1055
|
-
const writeEvent = (event) => {
|
|
1056
|
-
if (res.writableEnded)
|
|
1057
|
-
return;
|
|
1058
|
-
res.write(encodeSseEvent(event));
|
|
1059
|
-
};
|
|
1060
|
-
try {
|
|
1061
|
-
await runWithProcessContext({ runId, source: "agent" }, async () => streamAgentResponse({
|
|
1062
|
-
env,
|
|
1063
|
-
pageUrl,
|
|
1064
|
-
pageTitle,
|
|
1065
|
-
pageContent,
|
|
1066
|
-
messages,
|
|
1067
|
-
modelOverride: modelOverride && modelOverride.toLowerCase() !== "auto" ? modelOverride : null,
|
|
1068
|
-
tools,
|
|
1069
|
-
automationEnabled,
|
|
1070
|
-
onChunk: (text) => writeEvent({ event: "chunk", data: { text } }),
|
|
1071
|
-
onAssistant: (assistant) => writeEvent({ event: "assistant", data: assistant }),
|
|
1072
|
-
signal: controller.signal,
|
|
1073
|
-
}));
|
|
1074
|
-
writeEvent({ event: "done", data: {} });
|
|
1075
|
-
res.end();
|
|
1076
|
-
}
|
|
1077
|
-
catch (error) {
|
|
1078
|
-
if (controller.signal.aborted)
|
|
1079
|
-
return;
|
|
1080
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1081
|
-
console.error("[summarize-daemon] agent failed", error);
|
|
1082
|
-
writeEvent({ event: "error", data: { message } });
|
|
1083
|
-
writeEvent({ event: "done", data: {} });
|
|
1084
|
-
res.end();
|
|
1085
|
-
}
|
|
1086
|
-
return;
|
|
1087
|
-
}
|
|
1088
|
-
const slidesMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/slides$/);
|
|
1089
|
-
if (req.method === "GET" && slidesMatch) {
|
|
1090
|
-
const id = slidesMatch[1];
|
|
1091
|
-
const session = id ? sessions.get(id) : null;
|
|
1092
|
-
if (!session || !session.slides) {
|
|
1093
|
-
json(res, 200, { ok: false, error: "not found" }, cors);
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
json(res, 200, { ok: true, slides: buildSlidesPayload({ slides: session.slides, port }) }, cors);
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
const slideImageMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/slides\/(\d+)$/);
|
|
1100
|
-
if (req.method === "GET" && slideImageMatch) {
|
|
1101
|
-
const id = slideImageMatch[1];
|
|
1102
|
-
const index = Number(slideImageMatch[2]);
|
|
1103
|
-
const session = id ? sessions.get(id) : null;
|
|
1104
|
-
if (!session || !session.slides || !Number.isFinite(index)) {
|
|
1105
|
-
json(res, 404, { ok: false, error: "not found" }, cors);
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
const slide = session.slides.slides.find((item) => item.index === index);
|
|
1109
|
-
if (!slide) {
|
|
1110
|
-
json(res, 404, { ok: false, error: "not found" }, cors);
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
try {
|
|
1114
|
-
const stat = await fs.stat(slide.imagePath);
|
|
1115
|
-
res.writeHead(200, {
|
|
1116
|
-
"content-type": "image/png",
|
|
1117
|
-
"content-length": stat.size.toString(),
|
|
1118
|
-
"cache-control": "no-cache",
|
|
1119
|
-
...cors,
|
|
1120
|
-
});
|
|
1121
|
-
const stream = createReadStream(slide.imagePath);
|
|
1122
|
-
stream.pipe(res);
|
|
1123
|
-
stream.on("error", () => res.end());
|
|
1124
|
-
}
|
|
1125
|
-
catch {
|
|
1126
|
-
json(res, 404, { ok: false, error: "not found" }, cors);
|
|
1127
|
-
}
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
const stableSlideImageMatch = pathname.match(/^\/v1\/slides\/([^/]+)\/(\d+)$/);
|
|
1131
|
-
if (req.method === "GET" && stableSlideImageMatch) {
|
|
1132
|
-
const sourceId = stableSlideImageMatch[1];
|
|
1133
|
-
const index = Number(stableSlideImageMatch[2]);
|
|
1134
|
-
if (!sourceId || !Number.isFinite(index) || index <= 0) {
|
|
1135
|
-
json(res, 404, { ok: false, error: "not found" }, cors);
|
|
1136
|
-
return;
|
|
1137
|
-
}
|
|
1138
|
-
const slidesRoot = path.resolve(resolveHomeDir(env), ".summarize", "slides");
|
|
1139
|
-
const slidesDir = path.join(slidesRoot, sourceId);
|
|
1140
|
-
const payloadPath = path.join(slidesDir, "slides.json");
|
|
1141
|
-
const resolveFromDisk = async () => {
|
|
1142
|
-
const raw = await fs.readFile(payloadPath, "utf8").catch(() => null);
|
|
1143
|
-
if (raw) {
|
|
1144
|
-
try {
|
|
1145
|
-
const parsed = JSON.parse(raw);
|
|
1146
|
-
const slide = parsed?.slides?.find?.((item) => item?.index === index);
|
|
1147
|
-
if (slide?.imagePath) {
|
|
1148
|
-
const resolved = resolveSlideImagePath(slidesDir, slide.imagePath);
|
|
1149
|
-
if (resolved)
|
|
1150
|
-
return resolved;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
catch {
|
|
1154
|
-
// fall through
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
const prefix = `slide_${String(index).padStart(4, "0")}`;
|
|
1158
|
-
const entries = await fs.readdir(slidesDir).catch(() => null);
|
|
1159
|
-
if (!entries)
|
|
1160
|
-
return null;
|
|
1161
|
-
const candidates = entries
|
|
1162
|
-
.filter((name) => name.startsWith(prefix) && name.endsWith(".png"))
|
|
1163
|
-
.map((name) => path.join(slidesDir, name));
|
|
1164
|
-
if (candidates.length === 0)
|
|
1165
|
-
return null;
|
|
1166
|
-
let best = null;
|
|
1167
|
-
for (const filePath of candidates) {
|
|
1168
|
-
const stat = await fs.stat(filePath).catch(() => null);
|
|
1169
|
-
if (!stat?.isFile())
|
|
1170
|
-
continue;
|
|
1171
|
-
const mtimeMs = stat.mtimeMs;
|
|
1172
|
-
if (!best || mtimeMs > best.mtimeMs)
|
|
1173
|
-
best = { filePath, mtimeMs };
|
|
1174
|
-
}
|
|
1175
|
-
return best?.filePath ?? null;
|
|
1176
|
-
};
|
|
1177
|
-
const filePath = await resolveFromDisk();
|
|
1178
|
-
if (!filePath) {
|
|
1179
|
-
// Return a tiny transparent PNG (placeholder) instead of 404 to avoid broken-image icons
|
|
1180
|
-
// while extraction is still running.
|
|
1181
|
-
const placeholder = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3kq0cAAAAASUVORK5CYII=", "base64");
|
|
1182
|
-
res.writeHead(200, {
|
|
1183
|
-
"content-type": "image/png",
|
|
1184
|
-
"content-length": placeholder.length.toString(),
|
|
1185
|
-
"cache-control": "no-store",
|
|
1186
|
-
"x-summarize-slide-ready": "0",
|
|
1187
|
-
...cors,
|
|
1188
|
-
});
|
|
1189
|
-
res.end(placeholder);
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
|
-
try {
|
|
1193
|
-
const stat = await fs.stat(filePath);
|
|
1194
|
-
res.writeHead(200, {
|
|
1195
|
-
"content-type": "image/png",
|
|
1196
|
-
"content-length": stat.size.toString(),
|
|
1197
|
-
"cache-control": "no-store",
|
|
1198
|
-
"x-summarize-slide-ready": "1",
|
|
1199
|
-
...cors,
|
|
1200
|
-
});
|
|
1201
|
-
const stream = createReadStream(filePath);
|
|
1202
|
-
stream.pipe(res);
|
|
1203
|
-
stream.on("error", () => res.end());
|
|
1204
|
-
}
|
|
1205
|
-
catch {
|
|
1206
|
-
json(res, 404, { ok: false, error: "not found" }, cors);
|
|
1207
|
-
}
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
const eventsMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/events$/);
|
|
1211
|
-
if (req.method === "GET" && eventsMatch) {
|
|
1212
|
-
const id = eventsMatch[1];
|
|
1213
|
-
if (!id) {
|
|
1214
|
-
json(res, 404, { ok: false }, cors);
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
const session = sessions.get(id);
|
|
1218
|
-
if (!session) {
|
|
1219
|
-
json(res, 404, { ok: false, error: "not found" }, cors);
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
res.writeHead(200, {
|
|
1223
|
-
...cors,
|
|
1224
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
1225
|
-
"cache-control": "no-cache, no-transform",
|
|
1226
|
-
connection: "keep-alive",
|
|
1227
|
-
});
|
|
1228
|
-
session.clients.add(res);
|
|
1229
|
-
for (const entry of session.buffer) {
|
|
1230
|
-
res.write(encodeSseEvent(entry.event));
|
|
1231
|
-
}
|
|
1232
|
-
if (session.done) {
|
|
1233
|
-
res.end();
|
|
1234
|
-
session.clients.delete(res);
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
const keepalive = setInterval(() => {
|
|
1238
|
-
res.write(`: keepalive ${Date.now()}\n\n`);
|
|
1239
|
-
}, 15_000);
|
|
1240
|
-
keepalive.unref();
|
|
1241
|
-
res.on("close", () => {
|
|
1242
|
-
clearInterval(keepalive);
|
|
1243
|
-
session.clients.delete(res);
|
|
250
|
+
void executeSummarizeSession({
|
|
251
|
+
session,
|
|
252
|
+
request,
|
|
253
|
+
env,
|
|
254
|
+
fetchImpl,
|
|
255
|
+
cacheState,
|
|
256
|
+
mediaCache,
|
|
257
|
+
port,
|
|
258
|
+
onSessionEvent,
|
|
259
|
+
requestLogger,
|
|
260
|
+
includeContentLog,
|
|
261
|
+
logStartedAt,
|
|
262
|
+
logInput,
|
|
263
|
+
logSlidesSettings,
|
|
264
|
+
sessions,
|
|
265
|
+
refreshSessions,
|
|
1244
266
|
});
|
|
1245
267
|
return;
|
|
1246
268
|
}
|
|
1247
|
-
|
|
1248
|
-
if (req.method === "GET" && slidesEventsMatch) {
|
|
1249
|
-
const id = slidesEventsMatch[1];
|
|
1250
|
-
if (!id) {
|
|
1251
|
-
json(res, 404, { ok: false }, cors);
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
const session = sessions.get(id);
|
|
1255
|
-
if (!session || !session.slidesRequested) {
|
|
1256
|
-
json(res, 404, { ok: false, error: "not found" }, cors);
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
res.writeHead(200, {
|
|
1260
|
-
...cors,
|
|
1261
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
1262
|
-
"cache-control": "no-cache, no-transform",
|
|
1263
|
-
connection: "keep-alive",
|
|
1264
|
-
});
|
|
1265
|
-
session.slidesClients.add(res);
|
|
1266
|
-
for (const entry of session.slidesBuffer) {
|
|
1267
|
-
res.write(encodeSseEvent(entry.event));
|
|
1268
|
-
}
|
|
1269
|
-
const hasSlidesEvent = session.slidesBuffer.some((entry) => entry.event.event === "slides");
|
|
1270
|
-
if (!hasSlidesEvent && session.slides) {
|
|
1271
|
-
res.write(encodeSseEvent({
|
|
1272
|
-
event: "slides",
|
|
1273
|
-
data: buildSlidesPayload({ slides: session.slides, port }),
|
|
1274
|
-
}));
|
|
1275
|
-
}
|
|
1276
|
-
const hasStatusEvent = session.slidesBuffer.some((entry) => entry.event.event === "status");
|
|
1277
|
-
if (!hasStatusEvent && session.slidesLastStatus) {
|
|
1278
|
-
res.write(encodeSseEvent({ event: "status", data: { text: session.slidesLastStatus } }));
|
|
1279
|
-
}
|
|
1280
|
-
if (session.slidesDone) {
|
|
1281
|
-
res.end();
|
|
1282
|
-
session.slidesClients.delete(res);
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
const keepalive = setInterval(() => {
|
|
1286
|
-
res.write(`: keepalive ${Date.now()}\n\n`);
|
|
1287
|
-
}, 15_000);
|
|
1288
|
-
keepalive.unref();
|
|
1289
|
-
res.on("close", () => {
|
|
1290
|
-
clearInterval(keepalive);
|
|
1291
|
-
session.slidesClients.delete(res);
|
|
1292
|
-
});
|
|
269
|
+
if (await handleAgentRoute({ req, res, url, cors, env, createRunId: randomUUID })) {
|
|
1293
270
|
return;
|
|
1294
271
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
res.writeHead(200, {
|
|
1308
|
-
...cors,
|
|
1309
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
1310
|
-
"cache-control": "no-cache, no-transform",
|
|
1311
|
-
connection: "keep-alive",
|
|
1312
|
-
});
|
|
1313
|
-
session.clients.add(res);
|
|
1314
|
-
for (const entry of session.buffer) {
|
|
1315
|
-
res.write(encodeSseEvent(entry.event));
|
|
1316
|
-
}
|
|
1317
|
-
if (session.done) {
|
|
1318
|
-
res.end();
|
|
1319
|
-
session.clients.delete(res);
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
const keepalive = setInterval(() => {
|
|
1323
|
-
res.write(`: keepalive ${Date.now()}\n\n`);
|
|
1324
|
-
}, 15_000);
|
|
1325
|
-
keepalive.unref();
|
|
1326
|
-
res.on("close", () => {
|
|
1327
|
-
clearInterval(keepalive);
|
|
1328
|
-
session.clients.delete(res);
|
|
1329
|
-
});
|
|
272
|
+
if (await handleSessionRoutes({
|
|
273
|
+
req,
|
|
274
|
+
res,
|
|
275
|
+
pathname,
|
|
276
|
+
cors,
|
|
277
|
+
env,
|
|
278
|
+
port,
|
|
279
|
+
sessions,
|
|
280
|
+
refreshSessions,
|
|
281
|
+
})) {
|
|
1330
282
|
return;
|
|
1331
283
|
}
|
|
1332
284
|
text(res, 404, "Not found", cors);
|
|
1333
285
|
})().catch((error) => {
|
|
1334
|
-
const
|
|
1335
|
-
const cors = corsHeaders(origin);
|
|
286
|
+
const cors = readCorsHeaders(req);
|
|
1336
287
|
const message = error instanceof Error ? error.message : String(error);
|
|
1337
288
|
if (!res.headersSent) {
|
|
1338
289
|
json(res, 500, { ok: false, error: message }, cors);
|