alexa-ai 2.3.0 → 2.5.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 +46 -0
- package/README.md +60 -0
- package/examples/diagnose.js +17 -17
- package/examples/text2img-standalone.js +15 -15
- package/package.json +1 -1
- package/src/AlexaAI.js +56 -6
- package/src/core/Config.js +16 -0
- package/src/core/DeepAIClient.js +78 -28
- package/src/services/ImageDescriber.js +1 -1
- package/src/services/PromptBuilder.js +1 -0
- package/src/services/ResponseFormatter.js +17 -0
- package/test/run-tests.js +91 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,52 @@ All notable changes to `alexa-ai` are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.5.0] — 2026-09-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`proxy` option / `DEEPAI_PROXY`** — route `/api/*` calls through a
|
|
11
|
+
residential or mobile proxy (`http(s)://` / `socks5://`). Honoured by the
|
|
12
|
+
curl transports (`-x`); `transport: 'auto'` drops global fetch from the
|
|
13
|
+
chain when a proxy is set. This is what makes anonymous image generation
|
|
14
|
+
work from a VPS / data-center IP.
|
|
15
|
+
- **`englishOnly` (default on)** — the persona instructs English-only replies;
|
|
16
|
+
any CJK/Kana/Hangul in an answer triggers one translation re-ask and, if
|
|
17
|
+
that fails, script stripping with a plain-English fallback sentence.
|
|
18
|
+
- `DEEPAI_LOGIN_REQUIRED` error for login-gated models ("model only available
|
|
19
|
+
to logged in users") — no key rotation, anonymous retry or transport
|
|
20
|
+
cascade is attempted for them.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **`detectNsfw()` on free keys** — `nsfw-detector` has no free/anonymous
|
|
24
|
+
tier, so after the API refusal the engine asks the vision model for a
|
|
25
|
+
safety score (`via: 'chat'`) and otherwise returns `DEEPAI_PRO_REQUIRED`
|
|
26
|
+
with an explanatory message.
|
|
27
|
+
- When script stripping empties the reply, a fixed English sentence is sent
|
|
28
|
+
instead of the original non-English text.
|
|
29
|
+
|
|
30
|
+
## [2.4.0] — 2026-09-07
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- **`detectNsfw()` (and every other `/api/*` helper) now works on free keys.**
|
|
34
|
+
A registered key refused with "Pro members in good standing" gets one
|
|
35
|
+
anonymous browser-dialect retry (`anonymousApiFallback`, on by default) —
|
|
36
|
+
the same mechanism that made `generateImage()` work. Applies to
|
|
37
|
+
`generateImage`, `editImage`, `upscaleImage`, `colorizeImage`,
|
|
38
|
+
`detectNsfw`, `summarizeText` and `deepai.runApi()`.
|
|
39
|
+
- `detectNsfw()` dropped its `opts` argument instead of passing it through.
|
|
40
|
+
- CLI output of the bundled examples is plain ASCII now — emoji and
|
|
41
|
+
typographic dashes rendered as mojibake in Windows consoles with a
|
|
42
|
+
non-UTF-8 codepage.
|
|
43
|
+
- When both the registered and the anonymous attempt fail, the error message
|
|
44
|
+
reports both refusals.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- `generateImage()` route 1 carries the browser fields into its built-in
|
|
48
|
+
anonymous retry, so a Pro refusal recovers directly with `via: 'api'`.
|
|
49
|
+
- Client media helpers accept an `options` argument (third/fourth parameter)
|
|
50
|
+
and default their anonymous dialect per endpoint (`generation_source=img`
|
|
51
|
+
for model pages; none for image-editor/torch-srgan, matching the site).
|
|
52
|
+
|
|
7
53
|
## [2.3.0] — 2026-09-07
|
|
8
54
|
|
|
9
55
|
### Added
|
package/README.md
CHANGED
|
@@ -894,6 +894,66 @@ github.com/lexiforest/curl-impersonate/releases). Quota and auth errors are
|
|
|
894
894
|
never re-driven through other transports. Run `node examples/diagnose.js` to
|
|
895
895
|
see which transport your network accepts.
|
|
896
896
|
|
|
897
|
+
**`anonymousApiFallback` (2.4.0).** Every `/api/*` helper — `generateImage`,
|
|
898
|
+
`editImage`, `upscaleImage`, `colorizeImage`, `detectNsfw`, `summarizeText`
|
|
899
|
+
and plain `deepai.runApi()` — automatically retries once with a fresh
|
|
900
|
+
anonymous key (browser dialect per endpoint) when the registered key is
|
|
901
|
+
refused with "Pro members in good standing". This is what the website does
|
|
902
|
+
for free visitors, so all of these now work on free keys from a residential
|
|
903
|
+
IP. Disable globally with `anonymousApiFallback: false` in the constructor.
|
|
904
|
+
On failure both refusals are reported (`<registered refusal> | anonymous
|
|
905
|
+
retry: <anonymous refusal>`).
|
|
906
|
+
|
|
907
|
+
**Windows consoles.** CLI output from the bundled examples is plain ASCII on
|
|
908
|
+
purpose — emoji and typographic dashes render as garbage (Chinese-looking
|
|
909
|
+
mojibake) in cmd.exe/PowerShell with a non-UTF-8 codepage.
|
|
910
|
+
|
|
911
|
+
**Running on a VPS / server IP.** DeepAI refuses anonymous `/api/*` generation
|
|
912
|
+
from data-center IPs ("Please try this model on deepai.org"), so image
|
|
913
|
+
generation that works at home fails on a VPS. Two options:
|
|
914
|
+
|
|
915
|
+
1. `proxy` option (2.5.0) — route the `/api/*` calls through a
|
|
916
|
+
residential/mobile proxy; the curl transports honour
|
|
917
|
+
`proxy: 'socks5://host:port'` / `http://host:port` (or `DEEPAI_PROXY`).
|
|
918
|
+
Global fetch cannot use the proxy, so `auto` switches to curl automatically
|
|
919
|
+
when one is configured.
|
|
920
|
+
2. A DeepAI **Pro key** — works from any IP, no proxy needed.
|
|
921
|
+
|
|
922
|
+
**English-only replies (`englishOnly`, 2.5.0, on by default).** The persona
|
|
923
|
+
now instructs the model to answer in English only; if a reply still contains
|
|
924
|
+
CJK/Kana/Hangul, the engine re-asks once for an English version and strips any
|
|
925
|
+
remaining script as a last resort. Disable with `englishOnly: false`.
|
|
926
|
+
|
|
927
|
+
**`detectNsfw()` on a free key.** The `nsfw-detector` model has **no free or
|
|
928
|
+
anonymous tier** on DeepAI (no try-it on its model page; registered free keys
|
|
929
|
+
get the Pro refusal). 2.5.0 therefore: (a) tries the API, (b) on a plan
|
|
930
|
+
refusal asks the vision model for a score instead (`via: 'chat'` — works when
|
|
931
|
+
the key can see images), (c) otherwise returns `error: 'DEEPAI_PRO_REQUIRED'`
|
|
932
|
+
with a clear message. A Pro key makes the real model work.
|
|
933
|
+
|
|
934
|
+
**Always check `result.ok` before sending media.** On failure every helper
|
|
935
|
+
returns `url: null` — passing that straight to Baileys'
|
|
936
|
+
`prepareWAMessageMedia` crashes the bot with
|
|
937
|
+
`TypeError: Cannot read properties of undefined (reading 'toString')`. Guard
|
|
938
|
+
the bot side:
|
|
939
|
+
|
|
940
|
+
```js
|
|
941
|
+
const r = await ai.generateImage(prompt);
|
|
942
|
+
if (!r.ok || !r.url) {
|
|
943
|
+
await sock.sendMessage(jid, { text: `Image failed: ${r.message || r.error}` });
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
await sock.sendMessage(jid, { image: { url: r.url } });
|
|
948
|
+
} catch (e) {
|
|
949
|
+
console.error('send failed', e);
|
|
950
|
+
}
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
Also register `process.on('uncaughtException', …)` and
|
|
954
|
+
`process.on('unhandledRejection', …)` handlers in the bot entrypoint so one
|
|
955
|
+
bad send can never kill the whole process.
|
|
956
|
+
|
|
897
957
|
**`generateImage()` returns `{ ok: false, error: 'DEEPAI_QUOTA_EXCEEDED' }`**
|
|
898
958
|
`/api/text2img` is Pro-only for registered keys ("APIs are only available for
|
|
899
959
|
Pro members in good standing"), the anonymous browser-shaped retry was
|
package/examples/diagnose.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
/**
|
|
4
|
-
* diagnose.js
|
|
4
|
+
* diagnose.js - DeepAI text2img connectivity diagnostic.
|
|
5
5
|
*
|
|
6
6
|
* Run ON THE MACHINE where the browser playground works:
|
|
7
7
|
*
|
|
@@ -93,19 +93,19 @@ function runCurl(binary, impersonate) {
|
|
|
93
93
|
|
|
94
94
|
function label(t) {
|
|
95
95
|
const b = String(t.body).slice(0, 140).replace(/\s+/g, ' ');
|
|
96
|
-
if (t.status === 200 && /share_url|output_url/.test(t.body)) return
|
|
97
|
-
if (/valid Api-Key/i.test(b)) return
|
|
98
|
-
if (/try this model/i.test(b)) return
|
|
99
|
-
if (/Pro members/i.test(b)) return
|
|
100
|
-
if (/try it exceeded/i.test(b)) return
|
|
101
|
-
return
|
|
96
|
+
if (t.status === 200 && /share_url|output_url/.test(t.body)) return `[OK] SUCCESS - image generated (HTTP 200)`;
|
|
97
|
+
if (/valid Api-Key/i.test(b)) return `[FAIL] key rejected (unexpected - fresh key was used)`;
|
|
98
|
+
if (/try this model/i.test(b)) return `[FAIL] transport refused ("Please try this model on deepai.org")`;
|
|
99
|
+
if (/Pro members/i.test(b)) return `[FAIL] account-level refusal (needs a Pro key)`;
|
|
100
|
+
if (/try it exceeded/i.test(b)) return `[WARN] free quota exhausted for this device/IP - retry later or change --device-id`;
|
|
101
|
+
return `[FAIL] HTTP ${t.status}: ${b}`;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
(async () => {
|
|
105
|
-
console.log(`DeepAI text2img diagnostic
|
|
105
|
+
console.log(`DeepAI text2img diagnostic - device ${deviceId.slice(0, 8)}...\n`);
|
|
106
106
|
const rows = [];
|
|
107
|
-
try { rows.push(['1. Node fetch (library default)', label(await viaFetch())]); } catch (e) { rows.push(['1. Node fetch',
|
|
108
|
-
try { rows.push(['2. system curl', label(runCurl('curl', false))]); } catch (e) { rows.push(['2. system curl',
|
|
107
|
+
try { rows.push(['1. Node fetch (library default)', label(await viaFetch())]); } catch (e) { rows.push(['1. Node fetch', `[WARN] ${e.message}`]); }
|
|
108
|
+
try { rows.push(['2. system curl', label(runCurl('curl', false))]); } catch (e) { rows.push(['2. system curl', `[WARN] ${e.message.split('\n')[0]}`]); }
|
|
109
109
|
|
|
110
110
|
let impBin = opt.imp;
|
|
111
111
|
if (!impBin) {
|
|
@@ -115,20 +115,20 @@ function label(t) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
if (impBin) {
|
|
118
|
-
try { rows.push([`3. curl-impersonate (${impBin})`, label(runCurl(impBin, true))]); } catch (e) { rows.push(['3. curl-impersonate',
|
|
118
|
+
try { rows.push([`3. curl-impersonate (${impBin})`, label(runCurl(impBin, true))]); } catch (e) { rows.push(['3. curl-impersonate', `[WARN] ${e.message.split('\n')[0]}`]); }
|
|
119
119
|
} else {
|
|
120
|
-
rows.push(['3. curl-impersonate', '
|
|
120
|
+
rows.push(['3. curl-impersonate', '[SKIP] skipped - binary not found (see README for install)']);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
for (const [name, v] of rows) console.log(`${name.padEnd(34)} ${v}`);
|
|
124
124
|
console.log(`
|
|
125
125
|
Reading the results:
|
|
126
|
-
- A
|
|
126
|
+
- A [OK] on ANY line -> that transport works; use it (library: transport option,
|
|
127
127
|
standalone: --transport).
|
|
128
|
-
-
|
|
128
|
+
- [FAIL] on line 1 only -> non-browser TLS stack refused; use 'curl' or
|
|
129
129
|
'impersonate'.
|
|
130
|
-
-
|
|
131
|
-
-
|
|
130
|
+
- [FAIL] on lines 1+2, [OK] on 3 -> strict browser-TLS matching; use 'impersonate'.
|
|
131
|
+
- [FAIL] everywhere -> the IP is refused for anonymous generation, or the free
|
|
132
132
|
quota is exhausted. Compare with the browser: DevTools
|
|
133
|
-
|
|
133
|
+
-> Network -> generate -> text2img request -> Response.`);
|
|
134
134
|
})();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
/**
|
|
4
|
-
* text2img-standalone.js
|
|
4
|
+
* text2img-standalone.js - zero-dependency DeepAI text-to-image CLI.
|
|
5
5
|
*
|
|
6
|
-
* Speaks the anonymous browser dialect: a fresh single-use `tryit
|
|
6
|
+
* Speaks the anonymous browser dialect: a fresh single-use `tryit-...` key
|
|
7
7
|
* (hashed over the User-Agent) per run, browser-identical headers and a
|
|
8
8
|
* multipart/form-data body, plus a stable `deepai_device_id` cookie.
|
|
9
9
|
*
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Transports: 'fetch' (default, Node fetch) | 'curl' (system curl) |
|
|
20
20
|
* 'impersonate' (curl-impersonate binary, Chrome TLS profile). Some
|
|
21
|
-
* networks serve non-browser TLS stacks a refusal
|
|
21
|
+
* networks serve non-browser TLS stacks a refusal - if 'fetch' fails with
|
|
22
22
|
* "Please try this model on deepai.org", try 'curl', then 'impersonate'.
|
|
23
23
|
*
|
|
24
24
|
* Requires Node.js 18+. Anonymous generation is refused from
|
|
@@ -27,20 +27,20 @@
|
|
|
27
27
|
|
|
28
28
|
const API_URL = 'https://api.deepai.org/api/text2img';
|
|
29
29
|
const SALT = 'hackers_become_a_little_stinkier_every_time_they_hack';
|
|
30
|
-
// Keep this EXACT string in sync with the User-Agent header below
|
|
30
|
+
// Keep this EXACT string in sync with the User-Agent header below - the key
|
|
31
31
|
// hash is computed over it and the server recomputes it from the request.
|
|
32
32
|
const USER_AGENT =
|
|
33
33
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
|
|
34
34
|
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
36
|
// Deterministic key hash (see DeepAIClient._islandHash in the engine).
|
|
37
|
-
// The integer/bit-level behaviour is intentional
|
|
37
|
+
// The integer/bit-level behaviour is intentional - do not simplify it.
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
39
|
function islandHash(input) {
|
|
40
40
|
const a = [];
|
|
41
41
|
for (let b = 0; 64 > b; ) a[b] = 0 | (4294967296 * Math.sin(++b % Math.PI));
|
|
42
42
|
let d, e, f, g = [(d = 1732584193), (e = 4023233417), ~d, ~e], h = [];
|
|
43
|
-
const l = unescape(encodeURI(input)) + '
|
|
43
|
+
const l = unescape(encodeURI(input)) + '\u0080';
|
|
44
44
|
let k = l.length;
|
|
45
45
|
let c = (--k / 4 + 2) | 15;
|
|
46
46
|
for (h[--c] = 8 * k; ~k; ) h[k >> 2] |= l.charCodeAt(k) << (8 * k--);
|
|
@@ -120,19 +120,19 @@ async function main() {
|
|
|
120
120
|
Accept: '*/*',
|
|
121
121
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
122
122
|
Cookie: `deepai_device_id=${opts.deviceId}`,
|
|
123
|
-
// NOTE: do NOT set Content-Type yourself
|
|
123
|
+
// NOTE: do NOT set Content-Type yourself - undici adds the multipart
|
|
124
124
|
// boundary. A manual Content-Type without the boundary is rejected.
|
|
125
125
|
};
|
|
126
126
|
|
|
127
127
|
console.log(`Prompt : ${opts.prompt}`);
|
|
128
|
-
console.log(`Key : ${opts.key ? '(registered key
|
|
128
|
+
console.log(`Key : ${opts.key ? '(registered key - needs Pro)' : apiKey + ' (fresh, single-use)'}`);
|
|
129
129
|
console.log('POST : ' + API_URL);
|
|
130
130
|
|
|
131
131
|
let res;
|
|
132
132
|
if (opts.transport === 'curl' || opts.transport === 'impersonate') {
|
|
133
133
|
const { execFile } = require('child_process');
|
|
134
134
|
const binary = opts.transport === 'impersonate' ? (opts.imp || 'curl-impersonate') : 'curl';
|
|
135
|
-
// the chrome136 profile sends its own Mac Chrome UA
|
|
135
|
+
// the chrome136 profile sends its own Mac Chrome UA - the anonymous
|
|
136
136
|
// key hash must be derived from exactly that UA
|
|
137
137
|
const profileUa = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';
|
|
138
138
|
const apiKey = opts.key || freshTryItKey(opts.transport === 'impersonate' ? profileUa : USER_AGENT);
|
|
@@ -169,10 +169,10 @@ async function main() {
|
|
|
169
169
|
console.error(`\nFAILED HTTP ${res.status}`);
|
|
170
170
|
console.error(raw.slice(0, 500));
|
|
171
171
|
const s = String(data?.status || data?.err || '');
|
|
172
|
-
if (/valid Api-Key/i.test(s)) console.error('\n
|
|
173
|
-
else if (/try this model on deepai\.org/i.test(s)) console.error('\n
|
|
174
|
-
else if (/Pro members/i.test(s)) console.error('\n
|
|
175
|
-
else if (/try it exceeded/i.test(s)) console.error('\n
|
|
172
|
+
if (/valid Api-Key/i.test(s)) console.error('\n-> The key is invalid or already used. Keys are single-use and must be hashed for the exact User-Agent sent; this script mints a fresh one each run.');
|
|
173
|
+
else if (/try this model on deepai\.org/i.test(s)) console.error('\n-> DeepAI is refusing anonymous generation from your IP (datacenter/VPN) or the Origin header is missing. Run from a residential IP and keep the Origin/Referer headers.');
|
|
174
|
+
else if (/Pro members/i.test(s)) console.error('\n-> Your registered key is on the free plan; /api/* needs Pro. Use the anonymous mode (omit --key) or upgrade.');
|
|
175
|
+
else if (/try it exceeded/i.test(s)) console.error('\n-> Free quota for this device/IP is exhausted. Try a different --device-id or wait for the reset.');
|
|
176
176
|
process.exit(1);
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -190,10 +190,10 @@ async function main() {
|
|
|
190
190
|
require('fs').writeFileSync(out, buf);
|
|
191
191
|
console.log(`Saved : ${out} (${(buf.length / 1024).toFixed(1)} KB)`);
|
|
192
192
|
} else {
|
|
193
|
-
console.log(`(download skipped: HTTP ${img.status}
|
|
193
|
+
console.log(`(download skipped: HTTP ${img.status} - open the URL above in a browser)`);
|
|
194
194
|
}
|
|
195
195
|
} catch (e) {
|
|
196
|
-
console.log(`(download failed: ${e.message}
|
|
196
|
+
console.log(`(download failed: ${e.message} - open the URL above in a browser)`);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alexa-ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "AI engine for the Alexa WhatsApp bot: DeepAI-powered chat with PostgreSQL-backed long-term memory, cross-chat identity (@lid <-> phone), vision/OCR, image generation and web search.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/src/AlexaAI.js
CHANGED
|
@@ -418,6 +418,26 @@ class AlexaAI {
|
|
|
418
418
|
// Guarantee no @MEMORY remnant ever reaches WhatsApp.
|
|
419
419
|
if (/@\s*MEMORY/i.test(finalText)) finalText = MemoryExtractor.strip(finalText);
|
|
420
420
|
|
|
421
|
+
// English-only replies: if the model answered in another script,
|
|
422
|
+
// re-ask once for an English version and strip any remainder.
|
|
423
|
+
if (this.config.englishOnly && ResponseFormatter.hasNonEnglish(finalText)) {
|
|
424
|
+
let repaired = null;
|
|
425
|
+
try {
|
|
426
|
+
repaired = await this.client.chat([
|
|
427
|
+
{ role: 'user', content: `Rewrite the following in plain English only, keeping the same meaning and formatting. Output only the English text and nothing else:\n\n${finalText}` },
|
|
428
|
+
]);
|
|
429
|
+
} catch {
|
|
430
|
+
repaired = null;
|
|
431
|
+
}
|
|
432
|
+
if (repaired && repaired.trim() && !ResponseFormatter.hasNonEnglish(repaired)) {
|
|
433
|
+
finalText = repaired.trim();
|
|
434
|
+
} else {
|
|
435
|
+
finalText =
|
|
436
|
+
ResponseFormatter.stripNonEnglishScripts(finalText) ||
|
|
437
|
+
'Sorry, I could not phrase that in English. Please ask me again.';
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
421
441
|
if (!finalText.trim()) {
|
|
422
442
|
finalText = 'Sorry, I did not quite catch that. Could you say it again?';
|
|
423
443
|
}
|
|
@@ -642,10 +662,14 @@ class AlexaAI {
|
|
|
642
662
|
// size/quality fields).
|
|
643
663
|
if (!chatToolOnly) {
|
|
644
664
|
try {
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
665
|
+
const browserFields = AlexaAI._browserImageFields(aspectRatio || '1:1', apiFields);
|
|
666
|
+
const extra = this.client.usingTryItKey ? browserFields : apiFields;
|
|
667
|
+
const data = await this.client.text2img(text, extra, {
|
|
668
|
+
signal,
|
|
669
|
+
// used when a registered key is refused and runApi()
|
|
670
|
+
// retries anonymously
|
|
671
|
+
anonymousExtraFields: browserFields,
|
|
672
|
+
});
|
|
649
673
|
const url = AlexaAI._outputUrl(data);
|
|
650
674
|
if (url) return { ok: true, url, id: data.id || null, error: null, via: 'api', raw: data };
|
|
651
675
|
errors.push('text2img: no output_url in response');
|
|
@@ -784,10 +808,36 @@ class AlexaAI {
|
|
|
784
808
|
if (!field) return { ...AlexaAI._mediaError('detectNsfw', 'NSFW_FAILED'), score: null, nsfw: null };
|
|
785
809
|
const threshold = typeof opts.threshold === 'number' ? opts.threshold : 0.7;
|
|
786
810
|
try {
|
|
787
|
-
const data = await this.client.detectNsfw(field);
|
|
811
|
+
const data = await this.client.detectNsfw(field, {}, opts);
|
|
788
812
|
const score = typeof data?.output?.nsfw_score === 'number' ? data.output.nsfw_score : null;
|
|
789
|
-
return { ok: true, score, nsfw: score == null ? null : score >= threshold, error: null, raw: data };
|
|
813
|
+
return { ok: true, score, nsfw: score == null ? null : score >= threshold, error: null, via: 'api', raw: data };
|
|
790
814
|
} catch (err) {
|
|
815
|
+
// The dedicated model is Pro/login-only. As a best effort, ask
|
|
816
|
+
// the vision model for a safety score instead — works whenever
|
|
817
|
+
// the key can see images.
|
|
818
|
+
if (err.code === 'DEEPAI_LOGIN_REQUIRED' || err.code === 'DEEPAI_QUOTA_EXCEEDED') {
|
|
819
|
+
try {
|
|
820
|
+
const media = Media.normalize(image);
|
|
821
|
+
if (media) {
|
|
822
|
+
const judged = await this.vision.describe(media,
|
|
823
|
+
'Rate how sexually explicit this image is. Reply with ONLY a single decimal number between 0 (completely safe) and 1 (explicit), with no other text.');
|
|
824
|
+
const m = /(?:0\.\d+|1(?:\.0+)?|0|1)/.exec(String(judged.description || judged.text || ''));
|
|
825
|
+
if (judged.ok && m) {
|
|
826
|
+
const score = Number(m[0]);
|
|
827
|
+
return { ok: true, score, nsfw: score >= threshold, error: null, via: 'chat' };
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
} catch {
|
|
831
|
+
/* fall through to the honest refusal */
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
ok: false,
|
|
835
|
+
score: null,
|
|
836
|
+
nsfw: null,
|
|
837
|
+
error: 'DEEPAI_PRO_REQUIRED',
|
|
838
|
+
message: 'nsfw-detector is only available to DeepAI Pro keys (it has no free/anonymous tier), and the vision fallback could not run on this key.',
|
|
839
|
+
};
|
|
840
|
+
}
|
|
791
841
|
return { ok: false, score: null, nsfw: null, error: err.code || 'NSFW_FAILED', message: err.message };
|
|
792
842
|
}
|
|
793
843
|
}
|
package/src/core/Config.js
CHANGED
|
@@ -126,6 +126,18 @@ class Config {
|
|
|
126
126
|
this.curlImpersonatePath =
|
|
127
127
|
opts.curlImpersonatePath || process.env.DEEPAI_CURL_IMPERSONATE || null;
|
|
128
128
|
this.curlImpersonateTarget = opts.curlImpersonateTarget || 'chrome136';
|
|
129
|
+
// Optional proxy for /api/* calls (http/https/socks5 URL). Applied to
|
|
130
|
+
// the curl transports via -x; global fetch cannot honour it without
|
|
131
|
+
// the undici package, so 'auto' skips fetch entirely when this is set.
|
|
132
|
+
// Useful when the bot runs on a server IP that DeepAI refuses for
|
|
133
|
+
// anonymous generation.
|
|
134
|
+
this.proxy = opts.proxy || process.env.DEEPAI_PROXY || null;
|
|
135
|
+
// ---- /api/* anonymous fallback ----------------------------------------
|
|
136
|
+
// When a registered key is refused ("Pro members in good standing"),
|
|
137
|
+
// retry once with a fresh anonymous key in the browser dialect
|
|
138
|
+
// (options.anonymousExtraFields). Mirrors how the website keeps
|
|
139
|
+
// working for free visitors. Disable with anonymousApiFallback:false.
|
|
140
|
+
this.anonymousApiFallback = opts.anonymousApiFallback !== false;
|
|
129
141
|
|
|
130
142
|
// ---- Anonymous device identity ---------------------------------------
|
|
131
143
|
// Stable device identifier sent as the `deepai_device_id` cookie.
|
|
@@ -207,6 +219,10 @@ class Config {
|
|
|
207
219
|
opts.pool || {}
|
|
208
220
|
);
|
|
209
221
|
|
|
222
|
+
// Replies must be plain English: any CJK/Kana/Hangul in a model
|
|
223
|
+
// answer triggers one translation re-ask, then script stripping.
|
|
224
|
+
this.englishOnly = opts.englishOnly !== false;
|
|
225
|
+
|
|
210
226
|
this.debug = Boolean(opts.debug);
|
|
211
227
|
this.logger = opts.logger || console;
|
|
212
228
|
|
package/src/core/DeepAIClient.js
CHANGED
|
@@ -615,9 +615,48 @@ class DeepAIClient {
|
|
|
615
615
|
* @returns {Promise<object>} e.g. `{ id, output_url }`
|
|
616
616
|
*/
|
|
617
617
|
async runApi(name, fields = {}, options = {}) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
618
|
+
try {
|
|
619
|
+
const url = `${this.config.url('api')}/${String(name).replace(/^\/+/, '')}`;
|
|
620
|
+
const entries = this._buildApiFields(fields, options);
|
|
621
|
+
return await this._apiFormRequest(url, entries, options);
|
|
622
|
+
} catch (err) {
|
|
623
|
+
// A registered key refused for plan reasons ("Pro members in
|
|
624
|
+
// good standing") gets one anonymous browser-shaped retry, the
|
|
625
|
+
// same way the website keeps serving free visitors.
|
|
626
|
+
if (
|
|
627
|
+
err instanceof QuotaExceededError &&
|
|
628
|
+
this.config.anonymousApiFallback !== false &&
|
|
629
|
+
!options._anonymous &&
|
|
630
|
+
!this.usingTryItKey
|
|
631
|
+
) {
|
|
632
|
+
const anonFields = { ...(options.anonymousExtraFields || {}), ...fields };
|
|
633
|
+
if (this.config.debug) this.log.warn?.(`[AlexaAI] ${name} refused for the registered key; retrying anonymously`);
|
|
634
|
+
return this._runAnonymousApi(name, anonFields, options).catch((anonErr) => {
|
|
635
|
+
anonErr.message = `${err.message} | anonymous retry: ${anonErr.message}`;
|
|
636
|
+
throw anonErr;
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
throw err;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Run one `/api/<name>` call with a one-shot anonymous key regardless of
|
|
645
|
+
* the configured key. The active keys are swapped out for the duration
|
|
646
|
+
* of the call and restored afterwards.
|
|
647
|
+
* @private
|
|
648
|
+
*/
|
|
649
|
+
async _runAnonymousApi(name, fields, options = {}) {
|
|
650
|
+
const previousKeys = this._keys;
|
|
651
|
+
const previousIndex = this._keyIndex;
|
|
652
|
+
this._keys = [DeepAIClient.generateTryItKey(this.config.userAgent)];
|
|
653
|
+
this._keyIndex = 0;
|
|
654
|
+
try {
|
|
655
|
+
return await this.runApi(name, fields, { ...options, _anonymous: true });
|
|
656
|
+
} finally {
|
|
657
|
+
this._keys = previousKeys;
|
|
658
|
+
this._keyIndex = previousIndex;
|
|
659
|
+
}
|
|
621
660
|
}
|
|
622
661
|
|
|
623
662
|
/**
|
|
@@ -702,7 +741,8 @@ class DeepAIClient {
|
|
|
702
741
|
if (t === 'fetch') return ['fetch'];
|
|
703
742
|
if (t === 'curl') return ['curl'];
|
|
704
743
|
if (t === 'impersonate') return ['impersonate'];
|
|
705
|
-
|
|
744
|
+
// A proxy can only be honoured by the curl transports.
|
|
745
|
+
const chain = this.config.proxy ? ['curl'] : ['fetch', 'curl'];
|
|
706
746
|
if (await DeepAIClient.resolveImpersonateBinary(this.config)) chain.push('impersonate');
|
|
707
747
|
return chain;
|
|
708
748
|
}
|
|
@@ -754,6 +794,7 @@ class DeepAIClient {
|
|
|
754
794
|
: this.apiKey;
|
|
755
795
|
|
|
756
796
|
const args = impersonate ? ['--impersonate', this.config.curlImpersonateTarget] : [];
|
|
797
|
+
if (this.config.proxy) args.push('-x', this.config.proxy);
|
|
757
798
|
args.push(
|
|
758
799
|
url,
|
|
759
800
|
'-sS', '--compressed',
|
|
@@ -849,47 +890,45 @@ class DeepAIClient {
|
|
|
849
890
|
}
|
|
850
891
|
|
|
851
892
|
/**
|
|
852
|
-
* Run a classic `/api/<name>` call with a one-shot anonymous
|
|
853
|
-
* regardless of the configured key
|
|
854
|
-
* registered key is refused ("Pro members only"); a fresh key is minted
|
|
855
|
-
* for this single request.
|
|
893
|
+
* Run a classic `/api/<name>` call with a one-shot anonymous key,
|
|
894
|
+
* regardless of the configured key (public wrapper).
|
|
856
895
|
*/
|
|
857
896
|
async runApiWithTryItKey(name, fields = {}, options = {}) {
|
|
858
|
-
|
|
859
|
-
const previousIndex = this._keyIndex;
|
|
860
|
-
this._keys = [DeepAIClient.generateTryItKey(this.config.userAgent)];
|
|
861
|
-
this._keyIndex = 0;
|
|
862
|
-
try {
|
|
863
|
-
return await this.runApi(name, fields, options);
|
|
864
|
-
} finally {
|
|
865
|
-
this._keys = previousKeys;
|
|
866
|
-
this._keyIndex = previousIndex;
|
|
867
|
-
}
|
|
897
|
+
return this._runAnonymousApi(name, { ...(options.anonymousExtraFields || {}), ...fields }, { ...options, _anonymous: true });
|
|
868
898
|
}
|
|
869
899
|
|
|
870
900
|
/** Prompt-driven image edit (`/api/image-editor`). */
|
|
871
|
-
async editImage(image, text, extra = {}) {
|
|
872
|
-
return this.runApi(STANDARD_APIS.imageEditor, { image, text, ...extra });
|
|
901
|
+
async editImage(image, text, extra = {}, options = {}) {
|
|
902
|
+
return this.runApi(STANDARD_APIS.imageEditor, { image, text, ...extra }, options);
|
|
873
903
|
}
|
|
874
904
|
|
|
875
905
|
/** 4x upscale (`/api/torch-srgan`). */
|
|
876
|
-
async upscaleImage(image, extra = {}) {
|
|
877
|
-
return this.runApi(STANDARD_APIS.superResolution, { image, ...extra });
|
|
906
|
+
async upscaleImage(image, extra = {}, options = {}) {
|
|
907
|
+
return this.runApi(STANDARD_APIS.superResolution, { image, ...extra }, options);
|
|
878
908
|
}
|
|
879
909
|
|
|
880
910
|
/** Colourise a black-and-white photo (`/api/colorizer`). */
|
|
881
|
-
async colorizeImage(image, extra = {}) {
|
|
882
|
-
return this.runApi(STANDARD_APIS.colorizer, { image, ...extra }
|
|
911
|
+
async colorizeImage(image, extra = {}, options = {}) {
|
|
912
|
+
return this.runApi(STANDARD_APIS.colorizer, { image, ...extra }, {
|
|
913
|
+
anonymousExtraFields: { generation_source: 'img' },
|
|
914
|
+
...options,
|
|
915
|
+
});
|
|
883
916
|
}
|
|
884
917
|
|
|
885
918
|
/** NSFW score (`/api/nsfw-detector`). */
|
|
886
|
-
async detectNsfw(image, extra = {}) {
|
|
887
|
-
return this.runApi(STANDARD_APIS.nsfwDetector, { image, ...extra }
|
|
919
|
+
async detectNsfw(image, extra = {}, options = {}) {
|
|
920
|
+
return this.runApi(STANDARD_APIS.nsfwDetector, { image, ...extra }, {
|
|
921
|
+
anonymousExtraFields: { generation_source: 'img' },
|
|
922
|
+
...options,
|
|
923
|
+
});
|
|
888
924
|
}
|
|
889
925
|
|
|
890
926
|
/** Abstractive summary (`/api/summarization`). */
|
|
891
|
-
async summarize(text, extra = {}) {
|
|
892
|
-
return this.runApi(STANDARD_APIS.summarization, { text, ...extra }
|
|
927
|
+
async summarize(text, extra = {}, options = {}) {
|
|
928
|
+
return this.runApi(STANDARD_APIS.summarization, { text, ...extra }, {
|
|
929
|
+
anonymousExtraFields: { generation_source: 'img' },
|
|
930
|
+
...options,
|
|
931
|
+
});
|
|
893
932
|
}
|
|
894
933
|
|
|
895
934
|
/** Sentiment labels (`/api/sentiment-analysis`). */
|
|
@@ -1035,6 +1074,17 @@ class DeepAIClient {
|
|
|
1035
1074
|
const msg = statusMessage || DeepAIClient._detectJsonStatus(body) || `HTTP ${status}`;
|
|
1036
1075
|
const lowered = String(msg).toLowerCase();
|
|
1037
1076
|
|
|
1077
|
+
// Login-gated models cannot be recovered by key rotation, anonymous
|
|
1078
|
+
// retries or another transport — report them as their own error.
|
|
1079
|
+
if (lowered.includes('model only available to logged in users')) {
|
|
1080
|
+
return new DeepAIError(`DeepAI refused the request: ${msg}`, {
|
|
1081
|
+
code: 'DEEPAI_LOGIN_REQUIRED',
|
|
1082
|
+
status,
|
|
1083
|
+
body,
|
|
1084
|
+
retryable: false,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1038
1088
|
const quotaHints = [
|
|
1039
1089
|
'quota exceeded',
|
|
1040
1090
|
'try it exceeded',
|
|
@@ -154,7 +154,7 @@ class ImageDescriber {
|
|
|
154
154
|
sawRefusal = true;
|
|
155
155
|
this._modelsRefused.add(model);
|
|
156
156
|
if (this.config.debug) {
|
|
157
|
-
this.log.warn?.(`[AlexaAI] ${model} cannot see attachments
|
|
157
|
+
this.log.warn?.(`[AlexaAI] ${model} cannot see attachments - trying the next model`);
|
|
158
158
|
}
|
|
159
159
|
continue;
|
|
160
160
|
}
|
|
@@ -128,6 +128,7 @@ class PromptBuilder {
|
|
|
128
128
|
`You are ${assistantName}, a warm, friendly female WhatsApp assistant created by ${creator}.`,
|
|
129
129
|
`Your name is exactly "${assistantName}" — never a variant such as "${assistantName} Mini" or "${assistantName} AI".`,
|
|
130
130
|
'Never mention DeepAI, ChatGPT, OpenAI, GPT, Llama, Gemini or any model/company name, and never call yourself a language model.',
|
|
131
|
+
'You always reply in plain English only. Never answer in Chinese, Japanese, Korean or any other non-Latin script.',
|
|
131
132
|
'Use WhatsApp formatting only: *bold*, _italic_, ~strike~, `code`. Never use ** or markdown headers.',
|
|
132
133
|
'You have a permanent memory database: facts you are given about a person are things you genuinely remember, in private chats and in every group. Never claim you cannot remember.',
|
|
133
134
|
'Append new personal facts at the very end as @MEMORY: {"key": "value"} and never mention that tag.',
|
|
@@ -19,6 +19,23 @@
|
|
|
19
19
|
* Fenced code blocks are protected and restored verbatim.
|
|
20
20
|
*/
|
|
21
21
|
class ResponseFormatter {
|
|
22
|
+
// CJK ideographs, Kana, Hangul, fullwidth forms — the scripts that must
|
|
23
|
+
// never appear in an English-only reply.
|
|
24
|
+
static NON_ENGLISH_RE = /[\u2E80-\u2EFF\u3000-\u303F\u3040-\u30FF\u3130-\u318F\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF\uFF00-\uFFEF]/;
|
|
25
|
+
|
|
26
|
+
/** True when the text contains CJK/Kana/Hangul/fullwidth characters. */
|
|
27
|
+
static hasNonEnglish(text) {
|
|
28
|
+
return ResponseFormatter.NON_ENGLISH_RE.test(String(text || ''));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Remove CJK/Kana/Hangul/fullwidth runs (last-resort cleanup). */
|
|
32
|
+
static stripNonEnglishScripts(text) {
|
|
33
|
+
return String(text || '')
|
|
34
|
+
.replace(/([\u2E80-\u2EFF\u3000-\u303F\u3040-\u30FF\u3130-\u318F\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF\uFF00-\uFFEF])+/g, ' ')
|
|
35
|
+
.replace(/ {2,}/g, ' ')
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
/**
|
|
23
40
|
* @param {string} reply
|
|
24
41
|
* @returns {string}
|
package/test/run-tests.js
CHANGED
|
@@ -695,7 +695,7 @@ section('DeepAIClient — the whole endpoint surface (mocked transport)');
|
|
|
695
695
|
const ai = new AlexaAI({ key: '11111111-2222-3333-4444-555555555555', postgresUrl: 'postgres://u:p@localhost/db', autoMigrate: false });
|
|
696
696
|
const result = await ai.generateImage('a cute orange cat', { aspectRatio: '16:9' });
|
|
697
697
|
global.fetch = realFetch;
|
|
698
|
-
ok('generateImage recovers
|
|
698
|
+
ok('generateImage recovers through the built-in anonymous fallback', result.ok === true && result.via === 'api');
|
|
699
699
|
const anon = calls.find((c) => DeepAIClient.isTryItKey(c.key));
|
|
700
700
|
ok(
|
|
701
701
|
'anonymous retry carries the browser fields',
|
|
@@ -707,6 +707,67 @@ section('DeepAIClient — the whole endpoint surface (mocked transport)');
|
|
|
707
707
|
anon.form.quality === 'true'
|
|
708
708
|
);
|
|
709
709
|
ok('share_url is preferred over output_url', result.url === 'https://deepai.org/generated-image.png');
|
|
710
|
+
|
|
711
|
+
// detectNsfw: same Pro refusal, same anonymous recovery
|
|
712
|
+
calls.length = 0;
|
|
713
|
+
global.fetch = async (url, init = {}) => {
|
|
714
|
+
const form = {};
|
|
715
|
+
for (const [k, v] of init.body.entries()) form[k] = v;
|
|
716
|
+
calls.push({ url, key: init.headers['api-key'], form });
|
|
717
|
+
const first = calls.filter((c) => /nsfw-detector$/.test(c.url)).length === 1;
|
|
718
|
+
return {
|
|
719
|
+
status: first ? 402 : 200,
|
|
720
|
+
headers: { get: () => 'application/json' },
|
|
721
|
+
text: async () =>
|
|
722
|
+
first
|
|
723
|
+
? JSON.stringify({ status: 'APIs are only available for Pro members in good standing' })
|
|
724
|
+
: JSON.stringify({ id: 'n1', output: { nsfw_score: 0.13 } }),
|
|
725
|
+
};
|
|
726
|
+
};
|
|
727
|
+
const nsfw = await ai.detectNsfw(Buffer.from('fake-image-bytes'));
|
|
728
|
+
global.fetch = realFetch;
|
|
729
|
+
ok('detectNsfw recovers through the anonymous fallback', nsfw.ok === true && nsfw.score === 0.13 && nsfw.nsfw === false);
|
|
730
|
+
const anonNsfw = calls.find((c) => DeepAIClient.isTryItKey(c.key));
|
|
731
|
+
ok('anonymous nsfw retry uses the model-page dialect', anonNsfw && anonNsfw.form.generation_source === 'img');
|
|
732
|
+
|
|
733
|
+
// anonymousApiFallback:false keeps the refusal (no /api retry; the
|
|
734
|
+
// vision fallback may still run but makes no /api/nsfw-detector call)
|
|
735
|
+
const strict = new AlexaAI({ key: '11111111-2222-3333-4444-555555555555', postgresUrl: 'postgres://u:p@localhost/db', autoMigrate: false, anonymousApiFallback: false });
|
|
736
|
+
let apiCalls = 0;
|
|
737
|
+
global.fetch = async (url, init = {}) => {
|
|
738
|
+
if (/nsfw-detector$/.test(String(url))) apiCalls++;
|
|
739
|
+
return { status: 402, headers: { get: () => 'application/json' }, text: async () => JSON.stringify({ status: 'APIs are only available for Pro members in good standing' }) };
|
|
740
|
+
};
|
|
741
|
+
const refused = await strict.detectNsfw(Buffer.from('fake-image-bytes'));
|
|
742
|
+
global.fetch = realFetch;
|
|
743
|
+
ok('anonymousApiFallback:false surfaces the refusal', refused.ok === false && refused.error === 'DEEPAI_PRO_REQUIRED');
|
|
744
|
+
ok('no anonymous retry was made', apiCalls === 1);
|
|
745
|
+
|
|
746
|
+
// login-gated models map to DEEPAI_LOGIN_REQUIRED and never rotate keys
|
|
747
|
+
{
|
|
748
|
+
const cfg2 = new Config({ key: 'tryit-1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', keys: ['tryit-1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'tryit-2-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'], postgresUrl: 'postgres://u:p@localhost/db', maxRetries: 0 });
|
|
749
|
+
const c2 = new DeepAIClient(cfg2);
|
|
750
|
+
const seen2 = [];
|
|
751
|
+
global.fetch = async (url, init = {}) => {
|
|
752
|
+
seen2.push(init.headers['api-key']);
|
|
753
|
+
return { status: 401, headers: { get: () => 'application/json' }, text: async () => JSON.stringify({ status: 'model only available to logged in users' }) };
|
|
754
|
+
};
|
|
755
|
+
let loginErr = null;
|
|
756
|
+
try { await c2.runApi('nsfw-detector', { image: 'https://x/y.png' }); } catch (e) { loginErr = e; }
|
|
757
|
+
global.fetch = realFetch;
|
|
758
|
+
ok('login-gated model raises DEEPAI_LOGIN_REQUIRED', loginErr?.code === 'DEEPAI_LOGIN_REQUIRED');
|
|
759
|
+
ok('no key rotation on login-gated models', seen2.length === 1);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// the vision fallback answers when the model refuses for plan reasons
|
|
763
|
+
{
|
|
764
|
+
const ai2 = new AlexaAI({ key: '11111111-2222-3333-4444-555555555555', postgresUrl: 'postgres://u:p@localhost/db', autoMigrate: false });
|
|
765
|
+
ai2.vision = { describe: async () => ({ ok: true, description: '0.85' }) };
|
|
766
|
+
global.fetch = async () => ({ status: 402, headers: { get: () => 'application/json' }, text: async () => JSON.stringify({ status: 'APIs are only available for Pro members in good standing' }) });
|
|
767
|
+
const judged = await ai2.detectNsfw(Buffer.from('fake'), { threshold: 0.7 });
|
|
768
|
+
global.fetch = realFetch;
|
|
769
|
+
ok('detectNsfw falls back to the vision model', judged.ok === true && judged.via === 'chat' && judged.score === 0.85 && judged.nsfw === true);
|
|
770
|
+
}
|
|
710
771
|
}
|
|
711
772
|
|
|
712
773
|
{
|
|
@@ -794,6 +855,22 @@ section('DeepAIClient — the whole endpoint surface (mocked transport)');
|
|
|
794
855
|
ok('fetch-only transport never shells out', curlCalled === false);
|
|
795
856
|
}
|
|
796
857
|
|
|
858
|
+
// 5. a proxy pins the chain to the curl transports and passes -x
|
|
859
|
+
{
|
|
860
|
+
let call = null;
|
|
861
|
+
DeepAIClient.execCurl = async (bin, args) => { call = { bin, args }; return '{"id":"p1","share_url":"https://deepai.org/p.png"}\n200'; };
|
|
862
|
+
const client = new DeepAIClient(new Config({
|
|
863
|
+
key: 'k', postgresUrl: 'postgres://u:p@localhost/db',
|
|
864
|
+
proxy: 'socks5://127.0.0.1:9050', maxRetries: 0,
|
|
865
|
+
}));
|
|
866
|
+
const chain = await client._transportChain();
|
|
867
|
+
ok('proxy removes fetch from the transport chain', !chain.includes('fetch') && chain.includes('curl'));
|
|
868
|
+
const data = await client.runApi('text2img', { text: 'cat' });
|
|
869
|
+
ok('proxied request succeeds through curl', data.share_url === 'https://deepai.org/p.png');
|
|
870
|
+
const xi = call.args.indexOf('-x');
|
|
871
|
+
ok('curl receives the proxy flag', xi !== -1 && call.args[xi + 1] === 'socks5://127.0.0.1:9050');
|
|
872
|
+
}
|
|
873
|
+
|
|
797
874
|
global.fetch = realFetch;
|
|
798
875
|
DeepAIClient.execCurl = realExecCurl;
|
|
799
876
|
DeepAIClient._impersonateCache = realCache;
|
|
@@ -890,6 +967,19 @@ async function endToEndTests() {
|
|
|
890
967
|
deepai.push('Sure! DeepAI can help you with that.');
|
|
891
968
|
const leak = await ai.chat({ message: 'can you help me?', userId: '78151912841263@lid' });
|
|
892
969
|
ok('vendor name never ships', !/deepai/i.test(leak.text));
|
|
970
|
+
|
|
971
|
+
// 7. A non-English reply is re-asked in English.
|
|
972
|
+
deepai.push('你好!很高兴认识你,Nimal。');
|
|
973
|
+
deepai.push('Hello! Very nice to meet you, Nimal.');
|
|
974
|
+
const zh = await ai.chat({ message: 'hi again', userId: '78151912841263@lid' });
|
|
975
|
+
check('Chinese reply is re-asked in English', zh.text, 'Hello! Very nice to meet you, Nimal.');
|
|
976
|
+
|
|
977
|
+
// 8. When the re-ask also fails, the CJK is stripped (or replaced).
|
|
978
|
+
deepai.push('这是一段完全中文的回答。');
|
|
979
|
+
deepai.push('还是中文。');
|
|
980
|
+
const zh2 = await ai.chat({ message: 'hello', userId: '78151912841263@lid' });
|
|
981
|
+
ok('unrecoverable reply carries no CJK', !/[\u4E00-\u9FFF]/.test(zh2.text));
|
|
982
|
+
ok('unrecoverable reply is non-empty', zh2.text.trim().length > 0);
|
|
893
983
|
} finally {
|
|
894
984
|
deepai.restore();
|
|
895
985
|
}
|