diff --git a/.github/readme.md b/.github/readme.md index 36d387166..e40bc2d47 100644 --- a/.github/readme.md +++ b/.github/readme.md @@ -276,6 +276,46 @@ In order to enable viewing your keys by clicking a button in the API block: 1. Set the value of `allowKeysExposure` to `true` in `config.yaml` file. 2. Restart the SillyTavern server. +## Command-line arguments + +You can pass command-line arguments to SillyTavern server startup to override some settings in `config.yaml`. + +### Examples + +```shell +node server.js --port 8000 --listen false +# or +npm run start -- --port 8000 --listen false +# or (Windows only) +Start.bat --port 8000 --listen false +``` + +### Supported arguments + +| Option | Description | Type | Default | +|-------------------------|------------------------------------------------------------------------------------------------------|----------|------------------------------| +| `--version` | Show version number | boolean | | +| `--enableIPv6` | Enables IPv6. | boolean | false | +| `--enableIPv4` | Enables IPv4. | boolean | true | +| `--port` | Sets the port under which SillyTavern will run. If not provided falls back to yaml config 'port'. | number | 8000 | +| `--dnsPreferIPv6` | Prefers IPv6 for dns. If not provided falls back to yaml config 'preferIPv6'. | boolean | false | +| `--autorun` | Automatically launch SillyTavern in the browser. If not provided falls back to yaml config 'autorun'.| boolean | false | +| `--autorunHostname` | The autorun hostname, probably best left on 'auto'. | string | null | +| `--autorunPortOverride` | Overrides the port for autorun. | string | null | +| `--listen` | SillyTavern is listening on all network interfaces. If not provided falls back to yaml config 'listen'.| boolean | false | +| `--corsProxy` | Enables CORS proxy. If not provided falls back to yaml config 'enableCorsProxy'. | boolean | false | +| `--disableCsrf` | Disables CSRF protection | boolean | null | +| `--ssl` | Enables SSL | boolean | false | +| `--certPath` | Path to your certificate file. | string | "certs/cert.pem" | +| `--keyPath` | Path to your private key file. | string | "certs/privkey.pem" | +| `--whitelist` | Enables whitelist mode | boolean | null | +| `--dataRoot` | Root directory for data storage | string | null | +| `--avoidLocalhost` | Avoids using 'localhost' for autorun in auto mode. | boolean | null | +| `--basicAuthMode` | Enables basic authentication | boolean | null | +| `--requestProxyEnabled` | Enables a use of proxy for outgoing requests | boolean | null | +| `--requestProxyUrl` | Request proxy URL (HTTP or SOCKS protocols) | string | null | +| `--requestProxyBypass` | Request proxy bypass list (space separated list of hosts) | array | null | + ## Remote connections Most often this is for people who want to use SillyTavern on their mobile phones while their PC runs the ST server on the same wifi network. diff --git a/default/config.yaml b/default/config.yaml index 58db04a49..5f3e0ce9a 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -37,6 +37,16 @@ basicAuthUser: password: "password" # Enables CORS proxy middleware enableCorsProxy: false +# -- REQUEST PROXY CONFIGURATION -- +requestProxy: + # If a proxy is enabled, all outgoing HTTP/HTTPS requests will be routed through it. + enabled: false + # Proxy URL. Possible protocols: http, https, socks, socks5, socks4, pac + url: "socks5://username:password@example.com:1080" + # Proxy bypass list. Requests to these hosts won't be routed through the proxy. + bypass: + - localhost + - 127.0.0.1 # Enable multi-user mode enableUserAccounts: false # Enable discreet login mode: hides user list on the login screen diff --git a/package-lock.json b/package-lock.json index 58e0034e6..ca83e8446 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "cookie-session": "^2.1.0", "cors": "^2.8.5", "csrf-csrf": "^2.2.3", - "express": "^4.19.2", + "express": "^4.20.0", "form-data": "^4.0.0", "google-translate-api-browser": "^3.0.1", "he": "^1.2.0", @@ -40,6 +40,7 @@ "png-chunk-text": "^1.0.0", "png-chunks-encode": "^1.0.0", "png-chunks-extract": "^1.0.0", + "proxy-agent": "^6.4.0", "rate-limiter-flexible": "^5.0.0", "response-time": "^2.3.2", "sanitize-filename": "^1.6.3", @@ -941,6 +942,12 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -1080,6 +1087,41 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/agentkeepalive": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", @@ -1382,6 +1424,18 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", @@ -1445,6 +1499,15 @@ ], "license": "MIT" }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bing-translate-api": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-2.9.1.tgz", @@ -2250,6 +2313,15 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2316,6 +2388,20 @@ "node": ">=8" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2528,6 +2614,27 @@ "node": ">=0.8.0" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", @@ -2683,6 +2790,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -2713,7 +2833,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -2723,7 +2842,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -3071,6 +3189,20 @@ "node": ">= 0.6" } }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3130,6 +3262,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/gifwrap": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", @@ -3369,6 +3539,42 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -3382,6 +3588,42 @@ "node": ">=10.19.0" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -3485,6 +3727,19 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ip-matching": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ip-matching/-/ip-matching-2.1.2.tgz", @@ -3669,6 +3924,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3761,6 +4022,18 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -4053,6 +4326,15 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -4323,6 +4605,61 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -4618,6 +4955,57 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5006,36 +5394,6 @@ "node": ">= 0.8.0" } }, - "node_modules/serve-static/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static/node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5165,6 +5523,83 @@ "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==", "license": "MIT" }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -5378,6 +5813,12 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, "node_modules/tsscmp": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", @@ -5437,6 +5878,15 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 93c240873..87d7fa508 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "cookie-session": "^2.1.0", "cors": "^2.8.5", "csrf-csrf": "^2.2.3", - "express": "^4.19.2", + "express": "^4.20.0", "form-data": "^4.0.0", "google-translate-api-browser": "^3.0.1", "he": "^1.2.0", @@ -30,6 +30,7 @@ "png-chunk-text": "^1.0.0", "png-chunks-encode": "^1.0.0", "png-chunks-extract": "^1.0.0", + "proxy-agent": "^6.4.0", "rate-limiter-flexible": "^5.0.0", "response-time": "^2.3.2", "sanitize-filename": "^1.6.3", @@ -62,6 +63,9 @@ }, "node-fetch": { "whatwg-url": "^14.0.0" + }, + "express": { + "send": "^0.19.0" } }, "name": "sillytavern", diff --git a/public/index.html b/public/index.html index a3f89daec..939994e29 100644 --- a/public/index.html +++ b/public/index.html @@ -6479,8 +6479,8 @@ + - diff --git a/public/lib/object-hasown.js b/public/lib/object-hasown.js deleted file mode 100644 index 9b7792e88..000000000 --- a/public/lib/object-hasown.js +++ /dev/null @@ -1,4 +0,0 @@ -// Polyfill for old Safari versions -if (!Object.hasOwn) { - Object.hasOwn = function (obj, prop) { return obj.hasOwnProperty(prop); } -} diff --git a/public/lib/polyfill.js b/public/lib/polyfill.js new file mode 100644 index 000000000..cef73b4e5 --- /dev/null +++ b/public/lib/polyfill.js @@ -0,0 +1,19 @@ +// Polyfills for old Safari versions +if (!Object.hasOwn) { + Object.hasOwn = function (obj, prop) { return obj.hasOwnProperty(prop); } +} + +if (!Array.prototype.findLastIndex) { + Array.prototype.findLastIndex = function (callback, thisArg) { + for (let i = this.length - 1; i >= 0; i--) { + if (callback.call(thisArg, this[i], i, this)) return i; + } + return -1; + }; +} + +if (!Array.prototype.toSorted) { + Array.prototype.toSorted = function (compareFunction) { + return this.slice().sort(compareFunction); + }; +} diff --git a/public/script.js b/public/script.js index 9a4178bff..2f82f8d88 100644 --- a/public/script.js +++ b/public/script.js @@ -7472,7 +7472,7 @@ export function showSwipeButtons() { const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`); const swipeId = chat[chat.length - 1].swipe_id; - var swipesCounterHTML = (`${(swipeId + 1)}/${(chat[chat.length - 1].swipes.length)}`); + const swipeCounterText = (`${(swipeId + 1)}\u200B/\u200b${(chat[chat.length - 1].swipes.length)}`); if (swipeId !== undefined && (chat[chat.length - 1].swipes.length > 1 || swipeId > 0)) { currentMessage.children('.swipe_left').css('display', 'flex'); @@ -7489,7 +7489,7 @@ export function showSwipeButtons() { } //console.log(swipesCounterHTML); - $('.swipes-counter').html(swipesCounterHTML); + $('.swipes-counter').text(swipeCounterText); //console.log(swipeId); //console.log(chat[chat.length - 1].swipes.length); @@ -7878,13 +7878,19 @@ async function createOrEditCharacter(e) { formData.set('avatar', convertedFile); } - if ($('#form_create').attr('actiontype') == 'createcharacter') { - if (String($('#character_name_pole').val()).length > 0) { - if (is_group_generating || is_send_press) { - toastr.error('Cannot create characters while generating. Stop the request and try again.', 'Creation aborted'); - throw new Error('Cannot import character while generating'); - } + const headers = getRequestHeaders(); + delete headers['Content-Type']; + if ($('#form_create').attr('actiontype') == 'createcharacter') { + if (String($('#character_name_pole').val()).length === 0) { + toastr.error('Name is required'); + return; + } + if (is_group_generating || is_send_press) { + toastr.error('Cannot create characters while generating. Stop the request and try again.', 'Creation aborted'); + return; + } + try { //if the character name text area isn't empty (only posible when creating a new character) let url = '/api/characters/create'; @@ -7899,142 +7905,133 @@ async function createOrEditCharacter(e) { formData.append('extensions', JSON.stringify(create_save.extensions)); - await jQuery.ajax({ - type: 'POST', - url: url, - data: formData, - beforeSend: function () { - $('#create_button').attr('disabled', String(true)); - $('#create_button').attr('value', '⏳'); - }, - cache: false, - contentType: false, - processData: false, - success: async function (html) { - $('#character_cross').trigger('click'); //closes the advanced character editing popup - const fields = [ - { id: '#character_name_pole', callback: value => create_save.name = value }, - { id: '#description_textarea', callback: value => create_save.description = value }, - { id: '#creator_notes_textarea', callback: value => create_save.creator_notes = value }, - { id: '#character_version_textarea', callback: value => create_save.character_version = value }, - { id: '#post_history_instructions_textarea', callback: value => create_save.post_history_instructions = value }, - { id: '#system_prompt_textarea', callback: value => create_save.system_prompt = value }, - { id: '#tags_textarea', callback: value => create_save.tags = value }, - { id: '#creator_textarea', callback: value => create_save.creator = value }, - { id: '#personality_textarea', callback: value => create_save.personality = value }, - { id: '#firstmessage_textarea', callback: value => create_save.first_message = value }, - { id: '#talkativeness_slider', callback: value => create_save.talkativeness = value, defaultValue: talkativeness_default }, - { id: '#scenario_pole', callback: value => create_save.scenario = value }, - { id: '#depth_prompt_prompt', callback: value => create_save.depth_prompt_prompt = value }, - { id: '#depth_prompt_depth', callback: value => create_save.depth_prompt_depth = value, defaultValue: depth_prompt_depth_default }, - { id: '#depth_prompt_role', callback: value => create_save.depth_prompt_role = value, defaultValue: depth_prompt_role_default }, - { id: '#mes_example_textarea', callback: value => create_save.mes_example = value }, - { id: '#character_json_data', callback: () => { } }, - { id: '#alternate_greetings_template', callback: value => create_save.alternate_greetings = value, defaultValue: [] }, - { id: '#character_world', callback: value => create_save.world = value }, - { id: '#_character_extensions_fake', callback: value => create_save.extensions = {} }, - ]; - - fields.forEach(field => { - const fieldValue = field.defaultValue !== undefined ? field.defaultValue : ''; - $(field.id).val(fieldValue); - field.callback && field.callback(fieldValue); - }); - - $('#character_popup-button-h3').text('Create character'); - - create_save.avatar = ''; - - $('#create_button').removeAttr('disabled'); - $('#add_avatar_button').replaceWith( - $('#add_avatar_button').val('').clone(true), - ); - - $('#create_button').attr('value', '✅'); - let oldSelectedChar = null; - if (this_chid !== undefined) { - oldSelectedChar = characters[this_chid].avatar; - } - - console.log(`new avatar id: ${html}`); - createTagMapFromList('#tagList', html); - await getCharacters(); - - select_rm_info('char_create', html, oldSelectedChar); - - crop_data = undefined; - }, - error: function (jqXHR, exception) { - $('#create_button').removeAttr('disabled'); - }, + const fetchResult = await fetch(url, { + method: 'POST', + headers: headers, + body: formData, + cache: 'no-cache', }); - } else { - toastr.error('Name is required'); + + if (!fetchResult.ok) { + throw new Error('Fetch result is not ok'); + } + + const avatarId = await fetchResult.text(); + + $('#character_cross').trigger('click'); //closes the advanced character editing popup + const fields = [ + { id: '#character_name_pole', callback: value => create_save.name = value }, + { id: '#description_textarea', callback: value => create_save.description = value }, + { id: '#creator_notes_textarea', callback: value => create_save.creator_notes = value }, + { id: '#character_version_textarea', callback: value => create_save.character_version = value }, + { id: '#post_history_instructions_textarea', callback: value => create_save.post_history_instructions = value }, + { id: '#system_prompt_textarea', callback: value => create_save.system_prompt = value }, + { id: '#tags_textarea', callback: value => create_save.tags = value }, + { id: '#creator_textarea', callback: value => create_save.creator = value }, + { id: '#personality_textarea', callback: value => create_save.personality = value }, + { id: '#firstmessage_textarea', callback: value => create_save.first_message = value }, + { id: '#talkativeness_slider', callback: value => create_save.talkativeness = value, defaultValue: talkativeness_default }, + { id: '#scenario_pole', callback: value => create_save.scenario = value }, + { id: '#depth_prompt_prompt', callback: value => create_save.depth_prompt_prompt = value }, + { id: '#depth_prompt_depth', callback: value => create_save.depth_prompt_depth = value, defaultValue: depth_prompt_depth_default }, + { id: '#depth_prompt_role', callback: value => create_save.depth_prompt_role = value, defaultValue: depth_prompt_role_default }, + { id: '#mes_example_textarea', callback: value => create_save.mes_example = value }, + { id: '#character_json_data', callback: () => { } }, + { id: '#alternate_greetings_template', callback: value => create_save.alternate_greetings = value, defaultValue: [] }, + { id: '#character_world', callback: value => create_save.world = value }, + { id: '#_character_extensions_fake', callback: value => create_save.extensions = {} }, + ]; + + fields.forEach(field => { + const fieldValue = field.defaultValue !== undefined ? field.defaultValue : ''; + $(field.id).val(fieldValue); + field.callback && field.callback(fieldValue); + }); + + $('#character_popup-button-h3').text('Create character'); + + create_save.avatar = ''; + + $('#add_avatar_button').replaceWith( + $('#add_avatar_button').val('').clone(true), + ); + + let oldSelectedChar = null; + if (this_chid !== undefined) { + oldSelectedChar = characters[this_chid].avatar; + } + + console.log(`new avatar id: ${avatarId}`); + createTagMapFromList('#tagList', avatarId); + await getCharacters(); + + select_rm_info('char_create', avatarId, oldSelectedChar); + + crop_data = undefined; + + } catch (error) { + console.error('Error creating character', error); + toastr.error('Failed to create character'); } } else { - let url = '/api/characters/edit'; + try { + let url = '/api/characters/edit'; - if (crop_data != undefined) { - url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`; - } - - formData.delete('alternate_greetings'); - const chid = $('.open_alternate_greetings').data('chid'); - if (chid && Array.isArray(characters[chid]?.data?.alternate_greetings)) { - for (const value of characters[chid].data.alternate_greetings) { - formData.append('alternate_greetings', value); + if (crop_data != undefined) { + url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`; } - } - await jQuery.ajax({ - type: 'POST', - url: url, - data: formData, - beforeSend: function () { - $('#create_button').attr('disabled', String(true)); - $('#create_button').attr('value', 'Save'); - }, - cache: false, - contentType: false, - processData: false, - success: async function (html) { - $('#create_button').removeAttr('disabled'); - - await getOneCharacter(formData.get('avatar_url')); - favsToHotswap(); // Update fav state - - $('#add_avatar_button').replaceWith( - $('#add_avatar_button').val('').clone(true), - ); - $('#create_button').attr('value', 'Save'); - crop_data = undefined; - eventSource.emit(event_types.CHARACTER_EDITED, { detail: { id: this_chid, character: characters[this_chid] } }); - - // Recreate the chat if it hasn't been used at least once (i.e. with continue). - const message = getFirstMessage(); - const shouldRegenerateMessage = - !isNewChat && - message.mes && - !selected_group && - !chat_metadata['tainted'] && - (chat.length === 0 || (chat.length === 1 && !chat[0].is_user && !chat[0].is_system)); - - if (shouldRegenerateMessage) { - chat.splice(0, chat.length, message); - const messageId = (chat.length - 1); - await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId); - await clearChat(); - await printMessages(); - await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId); - await saveChatConditional(); + formData.delete('alternate_greetings'); + const chid = $('.open_alternate_greetings').data('chid'); + if (chid && Array.isArray(characters[chid]?.data?.alternate_greetings)) { + for (const value of characters[chid].data.alternate_greetings) { + formData.append('alternate_greetings', value); } - }, - error: function (jqXHR, exception) { - $('#create_button').removeAttr('disabled'); - console.log('Error! Either a file with the same name already existed, or the image file provided was in an invalid format. Double check that the image is not a webp.'); - toastr.error('Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.'); - }, - }); + } + + const fetchResult = await fetch(url, { + method: 'POST', + headers: headers, + body: formData, + cache: 'no-cache', + }); + + if (!fetchResult.ok) { + throw new Error('Fetch result is not ok'); + } + + await getOneCharacter(formData.get('avatar_url')); + favsToHotswap(); // Update fav state + + $('#add_avatar_button').replaceWith( + $('#add_avatar_button').val('').clone(true), + ); + $('#create_button').attr('value', 'Save'); + crop_data = undefined; + await eventSource.emit(event_types.CHARACTER_EDITED, { detail: { id: this_chid, character: characters[this_chid] } }); + + // Recreate the chat if it hasn't been used at least once (i.e. with continue). + const message = getFirstMessage(); + const shouldRegenerateMessage = + !isNewChat && + message.mes && + !selected_group && + !chat_metadata['tainted'] && + (chat.length === 0 || (chat.length === 1 && !chat[0].is_user && !chat[0].is_system)); + + if (shouldRegenerateMessage) { + chat.splice(0, chat.length, message); + const messageId = (chat.length - 1); + await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId); + await clearChat(); + await printMessages(); + await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId); + await saveChatConditional(); + } + } catch (error) { + console.log(error); + toastr.error('Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.'); + } } } @@ -8526,7 +8523,7 @@ async function selectContextCallback(args, name) { async function selectInstructCallback(args, name) { if (!name) { - return power_user.instruct.preset; + return power_user.instruct.enabled || isTrueBoolean(args?.forceGet) ? power_user.instruct.preset : ''; } const quiet = isTrueBoolean(args?.quiet); @@ -9227,6 +9224,13 @@ jQuery(async function () { defaultValue: 'false', enumList: commonEnumProviders.boolean('trueFalse')(), }), + SlashCommandNamedArgument.fromProps({ + name: 'forceGet', + description: 'Force getting a name even if instruct mode is disabled', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'false', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ @@ -9237,7 +9241,8 @@ jQuery(async function () { ], helpString: `
- Selects instruct mode template by name. Gets the current instruct template if no name is provided. + Selects instruct mode template by name. Enables instruct mode if not already enabled. + Gets the current instruct template if no name is provided and instruct mode is enabled or forceGet=true is passed.
Example: diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index aa494c366..68f001359 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -24,6 +24,7 @@ import { main_api, name1, name2, + neutralCharacterName, reloadCurrentChat, removeMacros, renameCharacter, @@ -427,6 +428,7 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'ask', callback: askCharacter, + returns: 'the generated text', namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'name', @@ -438,7 +440,7 @@ export function initDefaultSlashCommands() { ], unnamedArgumentList: [ new SlashCommandArgument( - 'prompt', [ARGUMENT_TYPE.STRING], true, false, + 'prompt', [ARGUMENT_TYPE.STRING], false, false, ), ], helpString: 'Asks a specified character card a prompt. Character name must be provided in a named argument.', @@ -2467,44 +2469,40 @@ async function askCharacter(args, text) { // Not supported in group chats // TODO: Maybe support group chats? if (selected_group) { - toastr.error('Cannot run this command in a group chat!'); - return ''; - } - - if (!text) { - console.warn('WARN: No text provided for /ask command'); - toastr.warning('No text provided for /ask command'); + toastr.error('Cannot run /ask command in a group chat!'); return ''; } let name = ''; - let mesText = ''; if (args?.name) { name = args.name.trim(); - mesText = text.trim(); - if (!name && !mesText) { - toastr.warning('You must specify a name and text to ask.'); + if (!name) { + toastr.warning('You must specify a name of the character to ask.'); return ''; } } - mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND); - const prevChId = this_chid; // Find the character - const chId = characters.findIndex((e) => e.name === name); + const chId = characters.findIndex((e) => e.name === name || e.avatar === name); if (!characters[chId] || chId === -1) { toastr.error('Character not found.'); return ''; } + if (text) { + const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND); + // Sending a message implicitly saves the chat, so this needs to be done before changing the character + // Otherwise, a corruption will occur + await sendMessageAsUser(mesText, ''); + } + // Override character and send a user message setCharacterId(String(chId)); - // TODO: Maybe look up by filename instead of name const character = characters[chId]; let force_avatar, original_avatar; @@ -2519,9 +2517,11 @@ async function askCharacter(args, text) { setCharacterName(character.name); - await sendMessageAsUser(mesText, ''); - const restoreCharacter = () => { + if (String(this_chid) !== String(chId)) { + return; + } + setCharacterId(prevChId); setCharacterName(characters[prevChId].name); @@ -2532,23 +2532,27 @@ async function askCharacter(args, text) { lastMessage.force_avatar = force_avatar; lastMessage.original_avatar = original_avatar; } - - // Kill this callback once the event fires - eventSource.removeListener(event_types.CHARACTER_MESSAGE_RENDERED, restoreCharacter); }; - // Run generate and restore previous character on error + let askResult = ''; + + // Run generate and restore previous character try { + eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter); toastr.info(`Asking ${character.name} something...`); - await Generate('ask_command'); - } catch { + askResult = await Generate('ask_command'); + } catch (error) { restoreCharacter(); + console.error('Error running /ask command', error); + } finally { + if (String(this_chid) === String(prevChId)) { + await saveChatConditional(); + } else { + toastr.error('It is strongly recommended to reload the page.', 'Something went wrong'); + } } - // Restore previous character once message renders - // Hack for generate - eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, restoreCharacter); - return ''; + return askResult; } async function hideMessageCallback(_, arg) { @@ -3129,7 +3133,13 @@ export async function sendMessageAs(args, text) { const character = characters.find(x => x.avatar === name) ?? characters.find(x => x.name === name); let force_avatar, original_avatar; - if (character && character.avatar !== 'none') { + const chatCharacter = this_chid !== undefined ? characters[this_chid] : null; + const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName; + + if (chatCharacter === character || isNeutralCharacter) { + // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars + } + else if (character && character.avatar !== 'none') { force_avatar = getThumbnailUrl('avatar', character.avatar); original_avatar = character.avatar; } diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index c6876482b..5612f47b5 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -1,4 +1,4 @@ -import { chat_metadata, characters, substituteParams, chat, extension_prompt_roles, extension_prompt_types } from '../../script.js'; +import { chat_metadata, characters, substituteParams, chat, extension_prompt_roles, extension_prompt_types, name2, neutralCharacterName } from '../../script.js'; import { extension_settings } from '../extensions.js'; import { getGroupMembers, groups } from '../group-chats.js'; import { power_user } from '../power-user.js'; @@ -6,7 +6,9 @@ import { searchCharByName, getTagsList, tags } from '../tags.js'; import { world_names } from '../world-info.js'; import { SlashCommandClosure } from './SlashCommandClosure.js'; import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js'; -import { SlashCommandScope } from "./SlashCommandScope.js"; + +/** @typedef {import('./SlashCommandExecutor.js').SlashCommandExecutor} SlashCommandExecutor */ +/** @typedef {import('./SlashCommandScope.js').SlashCommandScope} SlashCommandScope */ /** * A collection of regularly used enum icons @@ -140,7 +142,7 @@ export const commonEnumProviders = { * @param {...('global'|'local'|'scope'|'all')} type - The type of variables to include in the array. Can be 'all', 'global', or 'local'. * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]} */ - variables: (...type) => (executor, scope) => { + variables: (...type) => (_, scope) => { const types = type.flat(); const isAll = types.includes('all'); return [ @@ -160,6 +162,7 @@ export const commonEnumProviders = { return [ ...['all', 'character'].includes(mode) ? characters.map(char => new SlashCommandEnumValue(char.name, null, enumTypes.name, enumIcons.character)) : [], ...['all', 'group'].includes(mode) ? groups.map(group => new SlashCommandEnumValue(group.name, null, enumTypes.qr, enumIcons.group)) : [], + ...(name2 === neutralCharacterName) ? [new SlashCommandEnumValue(neutralCharacterName, null, enumTypes.name, '🥸')] : [], ]; }, @@ -184,7 +187,7 @@ export const commonEnumProviders = { * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show * @returns {() => SlashCommandEnumValue[]} */ - tagsForChar: (mode = 'all') => (/** @type {import('./SlashCommandExecutor.js').SlashCommandExecutor} */ executor) => { + tagsForChar: (mode = 'all') => (/** @type {SlashCommandExecutor} */ executor) => { // Try to see if we can find the char during execution to filter down the tags list some more. Otherwise take all tags. const charName = executor.namedArgumentList.find(it => it.name == 'name')?.value; if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures'); @@ -202,13 +205,13 @@ export const commonEnumProviders = { * @param {object} [options={}] - Optional arguments * @param {boolean} [options.allowIdAfter=false] - Whether to add an enum option for the new message id after the last message * @param {boolean} [options.allowVars=false] - Whether to add enum option for variable names - * @returns {() => SlashCommandEnumValue[]} + * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]} */ - messages: ({ allowIdAfter = false, allowVars = false } = {}) => () => { + messages: ({ allowIdAfter = false, allowVars = false } = {}) => (_, scope) => { return [ ...chat.map((message, index) => new SlashCommandEnumValue(String(index), `${message.name}: ${message.mes}`, enumTypes.number, message.is_user ? enumIcons.user : message.is_system ? enumIcons.system : enumIcons.assistant)), ...allowIdAfter ? [new SlashCommandEnumValue(String(chat.length), '>> After Last Message >>', enumTypes.enum, '➕')] : [], - ...allowVars ? commonEnumProviders.variables('all')() : [], + ...allowVars ? commonEnumProviders.variables('all')(_, scope) : [], ]; }, diff --git a/public/style.css b/public/style.css index cf6ebde60..21387cfd4 100644 --- a/public/style.css +++ b/public/style.css @@ -993,7 +993,7 @@ body .panelControlBar { padding: 0; font-family: var(--mainFontFamily); font-weight: 400; - align-self: self-end; + align-self: center; } .swipe_left { @@ -5463,4 +5463,4 @@ body:not(.movingUI) .drawer-content.maximized { #AdvancedFormatting .autoSetHeight { overflow-wrap: anywhere; -} \ No newline at end of file +} diff --git a/server.js b/server.js index a32b9da3d..b9fff9bc7 100644 --- a/server.js +++ b/server.js @@ -37,6 +37,7 @@ util.inspect.defaultOptions.depth = 4; const userModule = require('./src/users'); const basicAuthMiddleware = require('./src/middleware/basicAuth'); const whitelistMiddleware = require('./src/middleware/whitelist'); +const initRequestProxy = require('./src/request-proxy'); const contentManager = require('./src/endpoints/content-manager'); const { getVersion, @@ -75,6 +76,10 @@ const DEFAULT_AVOID_LOCALHOST = false; const DEFAULT_AUTORUN_HOSTNAME = 'auto'; const DEFAULT_AUTORUN_PORT = -1; +const DEFAULT_PROXY_ENABLED = false; +const DEFAULT_PROXY_URL = ''; +const DEFAULT_PROXY_BYPASS = []; + const cliArguments = yargs(hideBin(process.argv)) .usage('Usage: [options]') .option('enableIPv6', { @@ -145,6 +150,18 @@ const cliArguments = yargs(hideBin(process.argv)) type: 'boolean', default: null, describe: 'Enables basic authentication', + }).option('requestProxyEnabled', { + type: 'boolean', + default: null, + describe: 'Enables a use of proxy for outgoing requests', + }).option('requestProxyUrl', { + type: 'string', + default: null, + describe: 'Request proxy URL (HTTP or SOCKS protocols)', + }).option('requestProxyBypass', { + type: 'array', + default: null, + describe: 'Request proxy bypass list (space separated list of hosts)', }).parseSync(); // change all relative paths @@ -181,6 +198,10 @@ const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST); +const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED); +const proxyUrl = cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', DEFAULT_PROXY_URL); +const proxyBypass = cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', DEFAULT_PROXY_BYPASS); + if (dnsPreferIPv6) { // Set default DNS resolution order to IPv6 first dns.setDefaultResultOrder('ipv6first'); @@ -662,6 +683,9 @@ const preSetupTasks = async function () { console.error('Uncaught exception:', err); exitProcess(); }); + + // Add request proxy. + initRequestProxy({ enabled: proxyEnabled, url: proxyUrl, bypass: proxyBypass }); }; /** diff --git a/src/request-proxy.js b/src/request-proxy.js new file mode 100644 index 000000000..815fa76a3 --- /dev/null +++ b/src/request-proxy.js @@ -0,0 +1,56 @@ +const http = require('node:http'); +const https = require('node:https'); + +const { isValidUrl, color } = require('./util.js'); + +const LOG_HEADER = '[Request Proxy]'; + +/** + * Initialize request proxy. + * @param {ProxySettings} settings Proxy settings. + * @typedef {object} ProxySettings + * @property {boolean} enabled Whether proxy is enabled. + * @property {string} url Proxy URL. + * @property {string[]} bypass List of URLs to bypass proxy. + */ +function initRequestProxy({ enabled, url, bypass }) { + try { + const { ProxyAgent } = require('proxy-agent'); + + // No proxy is enabled, so return + if (!enabled) { + return; + } + + if (!url) { + console.error(color.red(LOG_HEADER), 'No proxy URL provided'); + return; + } + + if (!isValidUrl(url)) { + console.error(color.red(LOG_HEADER), 'Invalid proxy URL provided'); + return; + } + + // ProxyAgent uses proxy-from-env under the hood + // Reference: https://github.com/Rob--W/proxy-from-env + process.env.all_proxy = url; + + + if (Array.isArray(bypass) && bypass.length > 0) { + process.env.no_proxy = bypass.join(','); + } + + const proxyAgent = new ProxyAgent(); + http.globalAgent = proxyAgent; + https.globalAgent = proxyAgent; + + console.log(); + console.log(color.green(LOG_HEADER), 'Proxy URL is used:', color.blue(url)); + console.log(); + } catch (error) { + console.error(color.red(LOG_HEADER), 'Failed to initialize request proxy:', error); + } +} + +module.exports = initRequestProxy;