alexa-ai 2.4.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 +23 -0
- package/README.md +46 -0
- package/package.json +1 -1
- package/src/AlexaAI.js +47 -1
- package/src/core/Config.js +10 -0
- package/src/core/DeepAIClient.js +14 -1
- package/src/services/PromptBuilder.js +1 -0
- package/src/services/ResponseFormatter.js +17 -0
- package/test/run-tests.js +61 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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
|
+
|
|
7
30
|
## [2.4.0] — 2026-09-07
|
|
8
31
|
|
|
9
32
|
### Fixed
|
package/README.md
CHANGED
|
@@ -908,6 +908,52 @@ retry: <anonymous refusal>`).
|
|
|
908
908
|
purpose — emoji and typographic dashes render as garbage (Chinese-looking
|
|
909
909
|
mojibake) in cmd.exe/PowerShell with a non-UTF-8 codepage.
|
|
910
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
|
+
|
|
911
957
|
**`generateImage()` returns `{ ok: false, error: 'DEEPAI_QUOTA_EXCEEDED' }`**
|
|
912
958
|
`/api/text2img` is Pro-only for registered keys ("APIs are only available for
|
|
913
959
|
Pro members in good standing"), the anonymous browser-shaped retry was
|
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
|
}
|
|
@@ -790,8 +810,34 @@ class AlexaAI {
|
|
|
790
810
|
try {
|
|
791
811
|
const data = await this.client.detectNsfw(field, {}, opts);
|
|
792
812
|
const score = typeof data?.output?.nsfw_score === 'number' ? data.output.nsfw_score : null;
|
|
793
|
-
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 };
|
|
794
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
|
+
}
|
|
795
841
|
return { ok: false, score: null, nsfw: null, error: err.code || 'NSFW_FAILED', message: err.message };
|
|
796
842
|
}
|
|
797
843
|
}
|
package/src/core/Config.js
CHANGED
|
@@ -126,6 +126,12 @@ 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;
|
|
129
135
|
// ---- /api/* anonymous fallback ----------------------------------------
|
|
130
136
|
// When a registered key is refused ("Pro members in good standing"),
|
|
131
137
|
// retry once with a fresh anonymous key in the browser dialect
|
|
@@ -213,6 +219,10 @@ class Config {
|
|
|
213
219
|
opts.pool || {}
|
|
214
220
|
);
|
|
215
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
|
+
|
|
216
226
|
this.debug = Boolean(opts.debug);
|
|
217
227
|
this.logger = opts.logger || console;
|
|
218
228
|
|
package/src/core/DeepAIClient.js
CHANGED
|
@@ -741,7 +741,8 @@ class DeepAIClient {
|
|
|
741
741
|
if (t === 'fetch') return ['fetch'];
|
|
742
742
|
if (t === 'curl') return ['curl'];
|
|
743
743
|
if (t === 'impersonate') return ['impersonate'];
|
|
744
|
-
|
|
744
|
+
// A proxy can only be honoured by the curl transports.
|
|
745
|
+
const chain = this.config.proxy ? ['curl'] : ['fetch', 'curl'];
|
|
745
746
|
if (await DeepAIClient.resolveImpersonateBinary(this.config)) chain.push('impersonate');
|
|
746
747
|
return chain;
|
|
747
748
|
}
|
|
@@ -793,6 +794,7 @@ class DeepAIClient {
|
|
|
793
794
|
: this.apiKey;
|
|
794
795
|
|
|
795
796
|
const args = impersonate ? ['--impersonate', this.config.curlImpersonateTarget] : [];
|
|
797
|
+
if (this.config.proxy) args.push('-x', this.config.proxy);
|
|
796
798
|
args.push(
|
|
797
799
|
url,
|
|
798
800
|
'-sS', '--compressed',
|
|
@@ -1072,6 +1074,17 @@ class DeepAIClient {
|
|
|
1072
1074
|
const msg = statusMessage || DeepAIClient._detectJsonStatus(body) || `HTTP ${status}`;
|
|
1073
1075
|
const lowered = String(msg).toLowerCase();
|
|
1074
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
|
+
|
|
1075
1088
|
const quotaHints = [
|
|
1076
1089
|
'quota exceeded',
|
|
1077
1090
|
'try it exceeded',
|
|
@@ -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
|
@@ -730,18 +730,44 @@ section('DeepAIClient — the whole endpoint surface (mocked transport)');
|
|
|
730
730
|
const anonNsfw = calls.find((c) => DeepAIClient.isTryItKey(c.key));
|
|
731
731
|
ok('anonymous nsfw retry uses the model-page dialect', anonNsfw && anonNsfw.form.generation_source === 'img');
|
|
732
732
|
|
|
733
|
-
// anonymousApiFallback:false keeps the refusal
|
|
733
|
+
// anonymousApiFallback:false keeps the refusal (no /api retry; the
|
|
734
|
+
// vision fallback may still run but makes no /api/nsfw-detector call)
|
|
734
735
|
const strict = new AlexaAI({ key: '11111111-2222-3333-4444-555555555555', postgresUrl: 'postgres://u:p@localhost/db', autoMigrate: false, anonymousApiFallback: false });
|
|
735
|
-
|
|
736
|
-
let fetchCalls = 0;
|
|
736
|
+
let apiCalls = 0;
|
|
737
737
|
global.fetch = async (url, init = {}) => {
|
|
738
|
-
|
|
738
|
+
if (/nsfw-detector$/.test(String(url))) apiCalls++;
|
|
739
739
|
return { status: 402, headers: { get: () => 'application/json' }, text: async () => JSON.stringify({ status: 'APIs are only available for Pro members in good standing' }) };
|
|
740
740
|
};
|
|
741
741
|
const refused = await strict.detectNsfw(Buffer.from('fake-image-bytes'));
|
|
742
742
|
global.fetch = realFetch;
|
|
743
|
-
ok('anonymousApiFallback:false surfaces the refusal', refused.ok === false && refused.error === '
|
|
744
|
-
ok('no anonymous retry was made',
|
|
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
|
+
}
|
|
745
771
|
}
|
|
746
772
|
|
|
747
773
|
{
|
|
@@ -829,6 +855,22 @@ section('DeepAIClient — the whole endpoint surface (mocked transport)');
|
|
|
829
855
|
ok('fetch-only transport never shells out', curlCalled === false);
|
|
830
856
|
}
|
|
831
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
|
+
|
|
832
874
|
global.fetch = realFetch;
|
|
833
875
|
DeepAIClient.execCurl = realExecCurl;
|
|
834
876
|
DeepAIClient._impersonateCache = realCache;
|
|
@@ -925,6 +967,19 @@ async function endToEndTests() {
|
|
|
925
967
|
deepai.push('Sure! DeepAI can help you with that.');
|
|
926
968
|
const leak = await ai.chat({ message: 'can you help me?', userId: '78151912841263@lid' });
|
|
927
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);
|
|
928
983
|
} finally {
|
|
929
984
|
deepai.restore();
|
|
930
985
|
}
|