:3
This commit is contained in:
@@ -124,5 +124,9 @@ Models are addressed as `"driver:model"` (split on the first `:`).
|
|||||||
**visible** `[data-copy-message]` button as end-of-response (toolbars exist
|
**visible** `[data-copy-message]` button as end-of-response (toolbars exist
|
||||||
hidden in the DOM during streaming — visibility is the real signal).
|
hidden in the DOM during streaming — visibility is the real signal).
|
||||||
Navigation uses domcontentloaded + retries (chatgpt.com stalls sometimes).
|
Navigation uses domcontentloaded + retries (chatgpt.com stalls sometimes).
|
||||||
POC — ToS-risky, off unless `PUPPETEER_ENABLED=true`. Smoke test:
|
POC — ToS-risky, off unless `PUPPETEER_ENABLED=true`. Headless validated
|
||||||
|
(`PUPPETEER_HEADLESS=true`, see `scripts/poc-chatgpt.ts --headless`); on
|
||||||
|
NixOS use `services.kisassistant.enableBrowserDriver` for a headless
|
||||||
|
Firefox in the service, or wrap it all in a declarative `containers.*`
|
||||||
|
(see `docs/NIXOS-HOSTING.md`). Smoke test:
|
||||||
`bun scripts/poc-chatgpt.ts` (headed, `--probe` for a state report).
|
`bun scripts/poc-chatgpt.ts` (headed, `--probe` for a state report).
|
||||||
|
|||||||
@@ -129,6 +129,66 @@ tool (`POST /api/tools/ocr`, image attachments) works out of the box.
|
|||||||
> `services.kisassistant.extraEnv = { MISTRAL_ENABLED = "true"; };` plus a
|
> `services.kisassistant.extraEnv = { MISTRAL_ENABLED = "true"; };` plus a
|
||||||
> drop-in containing the key.
|
> drop-in containing the key.
|
||||||
|
|
||||||
|
### ChatGPT web driver (`enableBrowserDriver`)
|
||||||
|
|
||||||
|
The `puppeteer-openai` driver answers through the ChatGPT web UI: a headless
|
||||||
|
Firefox from the nix store runs **inside the service** and drives
|
||||||
|
`chatgpt.com` via puppeteer-core (WebDriver BiDi). Anonymous sessions work —
|
||||||
|
no ChatGPT login needed — and every browser launch gets a throwaway profile
|
||||||
|
(mkdtemp under `/tmp`, deleted on exit).
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.kisassistant = {
|
||||||
|
enable = true;
|
||||||
|
# …
|
||||||
|
enableBrowserDriver = true; # adds firefox to the service PATH and sets
|
||||||
|
}; # PUPPETEER_ENABLED/HEADLESS/EXECUTABLE
|
||||||
|
```
|
||||||
|
|
||||||
|
Overridable through `extraEnv` (values win over the module defaults):
|
||||||
|
`PUPPETEER_LOGIN_TIMEOUT_S` (default 300), `PUPPETEER_RESPONSE_TIMEOUT_S`
|
||||||
|
(default 300). The browser only starts on the **first request** that uses the
|
||||||
|
`puppeteer-openai:chatgpt` model — boot cost stays zero otherwise.
|
||||||
|
|
||||||
|
**Full isolation: run the whole app in a declarative NixOS container.**
|
||||||
|
The driver launches Firefox as a subprocess, so the browser must live in the
|
||||||
|
same namespace as the backend — the cleanest isolation is an nspawn
|
||||||
|
container wrapping the entire service:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
containers.kisassistant = {
|
||||||
|
autoStart = true;
|
||||||
|
privateNetwork = true;
|
||||||
|
hostAddress = "10.99.0.1";
|
||||||
|
localAddress = "10.99.0.2";
|
||||||
|
config = { config, pkgs, ... }: {
|
||||||
|
imports = [ kisassistant.nixosModules.default ];
|
||||||
|
services.kisassistant = {
|
||||||
|
enable = true;
|
||||||
|
package = kisassistant.packages.x86_64-linux.default;
|
||||||
|
host = "0.0.0.0";
|
||||||
|
enableBrowserDriver = true;
|
||||||
|
passwordFile = "/var/lib/kisassistant-password"; # provision inside
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Host: nginx proxies to the container (enableNginx stays unset/false)
|
||||||
|
services.nginx.virtualHosts."chat.kisakay.com" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://10.99.0.2:3000";
|
||||||
|
extraConfig = "proxy_buffering off;";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Headless mode needs no display server anywhere (validated on CI-less builds:
|
||||||
|
cookie banner accepted, message sent, response streamed). If ChatGPT ever
|
||||||
|
starts headless-shaming, fall back to `PUPPETEER_HEADLESS=false` inside the
|
||||||
|
container plus an `Xvfb` service — same driver, virtual display.
|
||||||
|
|
||||||
## 5. TLS reverse proxy (`enableNginx`)
|
## 5. TLS reverse proxy (`enableNginx`)
|
||||||
|
|
||||||
The conventional switch is `enableNginx` — a tri-state:
|
The conventional switch is `enableNginx` — a tri-state:
|
||||||
@@ -228,4 +288,5 @@ After dependency bumps the first build may fail with a `bunDepsHash` mismatch
|
|||||||
| `ollamaHost` | str, `http://localhost:11434` | Ollama endpoint |
|
| `ollamaHost` | str, `http://localhost:11434` | Ollama endpoint |
|
||||||
| `domain` | null \| str | nginx vhost name, required when `enableNginx = true` |
|
| `domain` | null \| str | nginx vhost name, required when `enableNginx = true` |
|
||||||
| `enableNginx` | null \| bool, `null` | `true`/`false` force the bundled reverse proxy on/off; `null` = nginx iff `domain` set (legacy) |
|
| `enableNginx` | null \| bool, `null` | `true`/`false` force the bundled reverse proxy on/off; `null` = nginx iff `domain` set (legacy) |
|
||||||
|
| `enableBrowserDriver` | bool, `false` | headless Firefox in the service for the `puppeteer-openai` ChatGPT web driver (sets `PUPPETEER_ENABLED`/`PUPPETEER_HEADLESS`/`PUPPETEER_EXECUTABLE`, overridable via `extraEnv`) |
|
||||||
| `extraEnv` | attrs of str, `{}` | extra environment (non-secret values only) |
|
| `extraEnv` | attrs of str, `{}` | extra environment (non-secret values only) |
|
||||||
|
|||||||
@@ -53,19 +53,36 @@
|
|||||||
cp -r ../public/. $out/
|
cp -r ../public/. $out/
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
# Backend runtime deps from bun.lock (fixed-output, has network).
|
||||||
|
# Gives the package its node_modules (nodemailer, puppeteer-core, …).
|
||||||
|
# If the build fails with a hash mismatch, paste the "got: sha256-…"
|
||||||
|
# value from the error into `outputHash` of `backendDeps` below.
|
||||||
|
backendDeps = pkgs.stdenv.mkDerivation {
|
||||||
|
pname = "kisassistant-backend-bun-deps";
|
||||||
|
inherit version;
|
||||||
|
src = ./.;
|
||||||
|
nativeBuildInputs = [ pkgs.bun ];
|
||||||
|
buildPhase = ''
|
||||||
|
export HOME=$TMPDIR
|
||||||
|
export BUN_INSTALL_CACHE_DIR=$TMPDIR/bun-cache
|
||||||
|
bun install --frozen-lockfile --ignore-scripts
|
||||||
|
'';
|
||||||
|
installPhase = ''
|
||||||
|
cp -r node_modules $out
|
||||||
|
'';
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHashMode = "recursive";
|
||||||
|
outputHash = "sha256-gQmtcFBDcGeMGIZf1VaMG6mlxJ6r08e9iHelS7wvYZ8=";
|
||||||
|
};
|
||||||
in pkgs.stdenv.mkDerivation {
|
in pkgs.stdenv.mkDerivation {
|
||||||
pname = "kisassistant";
|
pname = "kisassistant";
|
||||||
inherit version;
|
inherit version;
|
||||||
src = ./.;
|
src = ./.;
|
||||||
nativeBuildInputs = [ pkgs.bun ];
|
|
||||||
buildPhase = ''
|
|
||||||
export HOME=$TMPDIR
|
|
||||||
# Backend has zero runtime deps; install only for @types/bun (typecheck).
|
|
||||||
bun install --frozen-lockfile 2>/dev/null || bun install --no-save || true
|
|
||||||
'';
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
mkdir -p $out/share/kisassistant
|
mkdir -p $out/share/kisassistant
|
||||||
cp -r src package.json tsconfig.json .env.example $out/share/kisassistant/
|
cp -r src package.json tsconfig.json .env.example $out/share/kisassistant/
|
||||||
|
cp -a ${backendDeps} $out/share/kisassistant/node_modules
|
||||||
|
chmod -R u+w $out/share/kisassistant/node_modules
|
||||||
cp -r ${frontend} $out/share/kisassistant/public
|
cp -r ${frontend} $out/share/kisassistant/public
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
cat > $out/bin/kisassistant <<EOF
|
cat > $out/bin/kisassistant <<EOF
|
||||||
|
|||||||
@@ -90,17 +90,17 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid min-h-full place-items-center overflow-y-auto bg-gradient-to-br from-stone-100 via-stone-50 to-accent-100/60 p-6 dark:from-zinc-950 dark:via-zinc-950 dark:to-accent-950/40">
|
<div className="grid min-h-full place-items-center overflow-y-auto bg-gradient-to-br from-stone-100 via-stone-50 to-accent-100/60 p-4 dark:from-zinc-950 dark:via-zinc-950 dark:to-accent-950/40">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-md">
|
||||||
<div className="rounded-[2rem] border border-white/60 bg-white/80 p-8 shadow-2xl shadow-accent-900/5 backdrop-blur-xl dark:border-zinc-700/60 dark:bg-zinc-900/80">
|
<div className="rounded-[2rem] border border-white/60 bg-white/80 px-6 py-6 shadow-2xl shadow-accent-900/5 backdrop-blur-xl sm:px-8 dark:border-zinc-700/60 dark:bg-zinc-900/80">
|
||||||
<div className="mb-2 flex flex-col items-center text-center">
|
<div className="mb-1 flex flex-col items-center text-center">
|
||||||
<FlowerMark size={68} dynamic={false} className="mb-4 drop-shadow-lg" />
|
<FlowerMark size={52} dynamic={false} className="mb-2 drop-shadow-lg" />
|
||||||
<h1 className="text-2xl font-bold tracking-tight">{t("login.welcome")}</h1>
|
<h1 className="text-2xl font-bold tracking-tight">{t("login.welcome")}</h1>
|
||||||
<p className="mt-1 text-sm opacity-60">{t("login.subtitle")}</p>
|
<p className="mt-0.5 text-sm opacity-60">{t("login.subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{totpToken ? (
|
{totpToken ? (
|
||||||
<form onSubmit={submitTotp} className="mt-6 space-y-3">
|
<form onSubmit={submitTotp} className="mt-4 space-y-2.5">
|
||||||
<p className="text-center text-sm opacity-70">
|
<p className="text-center text-sm opacity-70">
|
||||||
{t("login.totpIntro", { user: totpUsername })}
|
{t("login.totpIntro", { user: totpUsername })}
|
||||||
</p>
|
</p>
|
||||||
@@ -135,7 +135,7 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={submit} className="mt-6 space-y-3">
|
<form onSubmit={submit} className="mt-4 space-y-2.5">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<User size={17} className="pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 text-stone-400 dark:text-zinc-500" />
|
<User size={17} className="pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 text-stone-400 dark:text-zinc-500" />
|
||||||
<Input
|
<Input
|
||||||
@@ -173,7 +173,7 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
<div className="mt-4 flex items-center gap-3 text-xs opacity-40">
|
<div className="mt-3 flex items-center gap-3 text-xs opacity-40">
|
||||||
<span className="h-px flex-1 bg-current" />
|
<span className="h-px flex-1 bg-current" />
|
||||||
{t("login.newHere")}
|
{t("login.newHere")}
|
||||||
<span className="h-px flex-1 bg-current" />
|
<span className="h-px flex-1 bg-current" />
|
||||||
@@ -181,7 +181,7 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="mt-3 w-full"
|
className="mt-2 w-full"
|
||||||
disabled={!registrationOn}
|
disabled={!registrationOn}
|
||||||
title={registrationOn ? t("login.regTipOn") : t("login.regTipOff")}
|
title={registrationOn ? t("login.regTipOn") : t("login.regTipOff")}
|
||||||
onClick={() => navigate("/register")}
|
onClick={() => navigate("/register")}
|
||||||
@@ -195,7 +195,7 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="mt-3 w-full"
|
className="mt-2 w-full"
|
||||||
onClick={() => setRequestOpen(true)}
|
onClick={() => setRequestOpen(true)}
|
||||||
>
|
>
|
||||||
<MessageSquareText size={16} />
|
<MessageSquareText size={16} />
|
||||||
@@ -204,21 +204,21 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
|||||||
)}
|
)}
|
||||||
<AccessRequestModal open={requestOpen} onClose={() => setRequestOpen(false)} />
|
<AccessRequestModal open={requestOpen} onClose={() => setRequestOpen(false)} />
|
||||||
{recoveryOn && (
|
{recoveryOn && (
|
||||||
<button onClick={() => setRecoverOpen(true)} className="mt-3 w-full text-center text-sm opacity-60 transition hover:opacity-100 hover:underline">
|
<button onClick={() => setRecoverOpen(true)} className="mt-2 w-full text-center text-sm opacity-60 transition hover:opacity-100 hover:underline">
|
||||||
{t("login.forgot")}
|
{t("login.forgot")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<RecoverDialog open={recoverOpen} onClose={() => setRecoverOpen(false)} from={recoveryFrom} />
|
<RecoverDialog open={recoverOpen} onClose={() => setRecoverOpen(false)} from={recoveryFrom} />
|
||||||
|
|
||||||
<div className="mt-4 flex justify-center">
|
<div className="mt-3 flex justify-center">
|
||||||
<LangPicker />
|
<LangPicker />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="mt-6 space-y-2 border-t border-stone-200/70 pt-5 dark:border-zinc-800">
|
<ul className="mt-4 grid grid-cols-1 gap-2 border-t border-stone-200/70 pt-4 sm:grid-cols-2 dark:border-zinc-800">
|
||||||
{PERKS.map((p) => (
|
{PERKS.map((p) => (
|
||||||
<li key={p.text} className="flex items-center gap-2.5 text-[13px] opacity-70">
|
<li key={p.text} className="flex items-center gap-2 text-xs opacity-70">
|
||||||
<span className="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-accent-600/10 text-accent-600 dark:text-accent-400">
|
<span className="grid h-6 w-6 shrink-0 place-items-center rounded-full bg-accent-600/10 text-accent-600 dark:text-accent-400">
|
||||||
<p.icon size={14} />
|
<p.icon size={13} />
|
||||||
</span>
|
</span>
|
||||||
{p.text}
|
{p.text}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -54,16 +54,16 @@ export function RegisterPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid min-h-full place-items-center overflow-y-auto bg-gradient-to-br from-stone-100 via-stone-50 to-accent-100/60 p-6 dark:from-zinc-950 dark:via-zinc-950 dark:to-accent-950/40">
|
<div className="grid min-h-full place-items-center overflow-y-auto bg-gradient-to-br from-stone-100 via-stone-50 to-accent-100/60 p-4 dark:from-zinc-950 dark:via-zinc-950 dark:to-accent-950/40">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-md">
|
||||||
<div className="rounded-[2rem] border border-white/60 bg-white/80 p-8 shadow-2xl shadow-accent-900/5 backdrop-blur-xl dark:border-zinc-700/60 dark:bg-zinc-900/80">
|
<div className="rounded-[2rem] border border-white/60 bg-white/80 px-6 py-6 shadow-2xl shadow-accent-900/5 backdrop-blur-xl sm:px-8 dark:border-zinc-700/60 dark:bg-zinc-900/80">
|
||||||
<div className="mb-2 flex flex-col items-center text-center">
|
<div className="mb-1 flex flex-col items-center text-center">
|
||||||
<FlowerMark size={68} dynamic={false} className="mb-4 drop-shadow-lg" />
|
<FlowerMark size={52} dynamic={false} className="mb-2 drop-shadow-lg" />
|
||||||
<h1 className="text-2xl font-bold tracking-tight">{t("dlg.registerTitle")}</h1>
|
<h1 className="text-2xl font-bold tracking-tight">{t("dlg.registerTitle")}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{key ? (
|
{key ? (
|
||||||
<div className="mt-6 space-y-4">
|
<div className="mt-4 space-y-3">
|
||||||
<p className="text-center text-sm opacity-80">
|
<p className="text-center text-sm opacity-80">
|
||||||
{t("dlg.regWelcome", { user: username.trim().toLowerCase() })}
|
{t("dlg.regWelcome", { user: username.trim().toLowerCase() })}
|
||||||
</p>
|
</p>
|
||||||
@@ -76,28 +76,30 @@ export function RegisterPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={submit} className="mt-6 space-y-3">
|
<form onSubmit={submit} className="mt-4 space-y-2.5">
|
||||||
<Field label={t("common.username")}>
|
<div className="grid grid-cols-2 gap-2.5">
|
||||||
<Input
|
<Field label={t("common.username")}>
|
||||||
variant="soft"
|
<Input
|
||||||
value={username}
|
variant="soft"
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
value={username}
|
||||||
placeholder="alice"
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
maxLength={32}
|
placeholder="alice"
|
||||||
autoComplete="username"
|
maxLength={32}
|
||||||
required
|
autoComplete="username"
|
||||||
/>
|
required
|
||||||
</Field>
|
/>
|
||||||
<Field label={t("common.displayName")}>
|
</Field>
|
||||||
<Input
|
<Field label={t("common.displayName")}>
|
||||||
variant="soft"
|
<Input
|
||||||
value={displayName}
|
variant="soft"
|
||||||
onChange={(e) => setDisplayName(e.target.value)}
|
value={displayName}
|
||||||
placeholder="Alice"
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
maxLength={60}
|
placeholder="Alice"
|
||||||
autoComplete="nickname"
|
maxLength={60}
|
||||||
/>
|
autoComplete="nickname"
|
||||||
</Field>
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
<Field label={t("dlg.emailOpt")}>
|
<Field label={t("dlg.emailOpt")}>
|
||||||
<Input
|
<Input
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@@ -131,13 +133,13 @@ export function RegisterPage() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/login")}
|
onClick={() => navigate("/login")}
|
||||||
className="mt-4 inline-flex w-full items-center justify-center gap-1.5 text-center text-sm opacity-60 transition hover:opacity-100 hover:underline"
|
className="mt-3 inline-flex w-full items-center justify-center gap-1.5 text-center text-sm opacity-60 transition hover:opacity-100 hover:underline"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={15} />
|
<ArrowLeft size={15} />
|
||||||
{t("common.backToLogin")}
|
{t("common.backToLogin")}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="mt-4 flex justify-center">
|
<div className="mt-3 flex justify-center">
|
||||||
<LangPicker />
|
<LangPicker />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+17
-2
@@ -44,6 +44,16 @@ in {
|
|||||||
- `null` (default): legacy behaviour — nginx is configured iff `domain` is set.
|
- `null` (default): legacy behaviour — nginx is configured iff `domain` is set.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
enableBrowserDriver = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Enable the ChatGPT web driver (puppeteer-openai): headless Firefox
|
||||||
|
from the nix store runs inside the service and drives chatgpt.com.
|
||||||
|
Sets PUPPETEER_ENABLED/PUPPETEER_HEADLESS/PUPPETEER_EXECUTABLE
|
||||||
|
(still overridable via extraEnv).
|
||||||
|
'';
|
||||||
|
};
|
||||||
extraEnv = lib.mkOption {
|
extraEnv = lib.mkOption {
|
||||||
type = lib.types.attrsOf lib.types.str;
|
type = lib.types.attrsOf lib.types.str;
|
||||||
default = {};
|
default = {};
|
||||||
@@ -83,6 +93,10 @@ in {
|
|||||||
CDN_DIR = "${cfg.dataDir}/cdn";
|
CDN_DIR = "${cfg.dataDir}/cdn";
|
||||||
DRIVER_DEBUG = "false";
|
DRIVER_DEBUG = "false";
|
||||||
OLLAMA_HOST = cfg.ollamaHost;
|
OLLAMA_HOST = cfg.ollamaHost;
|
||||||
|
} // lib.optionalAttrs cfg.enableBrowserDriver {
|
||||||
|
PUPPETEER_ENABLED = "true";
|
||||||
|
PUPPETEER_HEADLESS = "true";
|
||||||
|
PUPPETEER_EXECUTABLE = "${pkgs.firefox}/bin/firefox";
|
||||||
} // cfg.extraEnv;
|
} // cfg.extraEnv;
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
@@ -93,8 +107,9 @@ in {
|
|||||||
} // lib.optionalAttrs (cfg.passwordFile != null) {
|
} // lib.optionalAttrs (cfg.passwordFile != null) {
|
||||||
LoadCredential = "app-password:${cfg.passwordFile}";
|
LoadCredential = "app-password:${cfg.passwordFile}";
|
||||||
};
|
};
|
||||||
# tesseract binary for the OCR platform tool (see TESSERACT_BIN).
|
# tesseract binary for the OCR platform tool (see TESSERACT_BIN);
|
||||||
path = [ pkgs.tesseract ];
|
# firefox for the ChatGPT web driver (enableBrowserDriver).
|
||||||
|
path = [ pkgs.tesseract ] ++ lib.optionals cfg.enableBrowserDriver [ pkgs.firefox ];
|
||||||
script = ''
|
script = ''
|
||||||
${lib.optionalString (cfg.passwordFile != null) ''
|
${lib.optionalString (cfg.passwordFile != null) ''
|
||||||
export APP_PASSWORD="$(cat "$CREDENTIALS_DIRECTORY/app-password")"
|
export APP_PASSWORD="$(cat "$CREDENTIALS_DIRECTORY/app-password")"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ if (!Number.isFinite(responseTimeoutS) || responseTimeoutS <= 0) {
|
|||||||
|
|
||||||
const engine = new ChatGPTBrowserEngine({
|
const engine = new ChatGPTBrowserEngine({
|
||||||
executablePath,
|
executablePath,
|
||||||
headless: false,
|
headless: flag("--headless"),
|
||||||
loginTimeoutMs: 600_000,
|
loginTimeoutMs: 600_000,
|
||||||
responseTimeoutMs: responseTimeoutS * 1000,
|
responseTimeoutMs: responseTimeoutS * 1000,
|
||||||
});
|
});
|
||||||
@@ -63,7 +63,7 @@ try {
|
|||||||
step(info.composer ? `composer ready: ${info.composer}` : "composer NOT found — login wall or challenge page");
|
step(info.composer ? `composer ready: ${info.composer}` : "composer NOT found — login wall or challenge page");
|
||||||
} else {
|
} else {
|
||||||
const message = option("--message") ?? DEFAULT_MESSAGE;
|
const message = option("--message") ?? DEFAULT_MESSAGE;
|
||||||
step(`launching Firefox (${executablePath}) — temporary profile, deleted on exit`);
|
step(`launching Firefox (${executablePath}${flag("--headless") ? ", headless" : ""}) — temporary profile, deleted on exit`);
|
||||||
let streamed = "";
|
let streamed = "";
|
||||||
const final = await engine.send(message, {
|
const final = await engine.send(message, {
|
||||||
onLoginHint: () => step("login required: log in inside the Firefox window (waiting up to 10 min)"),
|
onLoginHint: () => step("login required: log in inside the Firefox window (waiting up to 10 min)"),
|
||||||
|
|||||||
Reference in New Issue
Block a user