@juzi/wechaty 1.0.150 → 1.0.155
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/dist/cjs/src/package-json.js +4 -4
- package/dist/cjs/src/user-modules/voice.d.ts +12 -9
- package/dist/cjs/src/user-modules/voice.d.ts.map +1 -1
- package/dist/cjs/src/user-modules/voice.js +63 -11
- package/dist/cjs/src/user-modules/voice.js.map +1 -1
- package/dist/cjs/src/user-modules/voice.spec.d.ts +3 -0
- package/dist/cjs/src/user-modules/voice.spec.d.ts.map +1 -0
- package/dist/cjs/src/user-modules/voice.spec.js +155 -0
- package/dist/cjs/src/user-modules/voice.spec.js.map +1 -0
- package/dist/esm/src/package-json.js +4 -4
- package/dist/esm/src/user-modules/voice.d.ts +12 -9
- package/dist/esm/src/user-modules/voice.d.ts.map +1 -1
- package/dist/esm/src/user-modules/voice.js +63 -11
- package/dist/esm/src/user-modules/voice.js.map +1 -1
- package/dist/esm/src/user-modules/voice.spec.d.ts +3 -0
- package/dist/esm/src/user-modules/voice.spec.d.ts.map +1 -0
- package/dist/esm/src/user-modules/voice.spec.js +153 -0
- package/dist/esm/src/user-modules/voice.spec.js.map +1 -0
- package/package.json +3 -3
- package/src/package-json.ts +4 -4
- package/src/user-modules/voice.spec.ts +187 -0
- package/src/user-modules/voice.ts +67 -12
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.packageJson = exports.GIT_COMMIT_HASH = void 0;
|
|
4
|
-
exports.GIT_COMMIT_HASH = '
|
|
4
|
+
exports.GIT_COMMIT_HASH = '6ef3d89f2a44f5454d45c56ff45765c9eeca8a79';
|
|
5
5
|
exports.packageJson = {
|
|
6
6
|
"name": "@juzi/wechaty",
|
|
7
|
-
"version": "1.0.
|
|
7
|
+
"version": "1.0.155",
|
|
8
8
|
"description": "Wechaty is a RPA SDK for Chatbot Makers.",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"exports": {
|
|
@@ -113,7 +113,7 @@ exports.packageJson = {
|
|
|
113
113
|
},
|
|
114
114
|
"homepage": "https://github.com/wechaty/",
|
|
115
115
|
"dependencies": {
|
|
116
|
-
"@juzi/wechaty-puppet-service": "1.0.
|
|
116
|
+
"@juzi/wechaty-puppet-service": "1.0.119",
|
|
117
117
|
"clone-class": "^1.1.1",
|
|
118
118
|
"cmd-ts": "^0.10.0",
|
|
119
119
|
"cockatiel": "^2.0.2",
|
|
@@ -136,7 +136,7 @@ exports.packageJson = {
|
|
|
136
136
|
"@chatie/eslint-config": "^1.0.4",
|
|
137
137
|
"@chatie/semver": "^0.4.7",
|
|
138
138
|
"@chatie/tsconfig": "^4.6.3",
|
|
139
|
-
"@juzi/wechaty-puppet": "^1.0.
|
|
139
|
+
"@juzi/wechaty-puppet": "^1.0.143",
|
|
140
140
|
"@juzi/wechaty-puppet-mock": "^1.0.1",
|
|
141
141
|
"@swc/core": "1.3.44",
|
|
142
142
|
"@swc/helpers": "^0.3.6",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { FileBoxInterface } from 'file-box';
|
|
21
21
|
import type { Constructor } from 'clone-class';
|
|
22
|
+
import type * as PUPPET from '@juzi/wechaty-puppet';
|
|
22
23
|
declare const VoiceMixin_base: ((abstract new (...args: any[]) => {
|
|
23
24
|
readonly wechaty: import("../wechaty/wechaty-impl.js").WechatyInterface;
|
|
24
25
|
}) & {
|
|
@@ -34,8 +35,10 @@ declare class VoiceMixin extends VoiceMixin_base {
|
|
|
34
35
|
* Get the voice file (FileBox) via the puppet's dedicated `messageVoice` RPC.
|
|
35
36
|
*
|
|
36
37
|
* Falls back to the generic `messageFile` when the puppet does not implement
|
|
37
|
-
* `messageVoice`
|
|
38
|
-
*
|
|
38
|
+
* `messageVoice` — either the method is absent (old puppet built without it,
|
|
39
|
+
* caught by the `typeof` guard) or it rejects with an unsupported-API error.
|
|
40
|
+
* Keeps the behaviour compatible with the legacy "voice file via messageFile"
|
|
41
|
+
* path. A genuine runtime error from a working `messageVoice` is rethrown.
|
|
39
42
|
*/
|
|
40
43
|
file(): Promise<FileBoxInterface>;
|
|
41
44
|
/**
|
|
@@ -44,14 +47,14 @@ declare class VoiceMixin extends VoiceMixin_base {
|
|
|
44
47
|
* "confirmed no speech" (`noSpeech=true`) from "normal transcription".
|
|
45
48
|
*
|
|
46
49
|
* Falls back to reading the legacy `messagePayload().text` when the puppet
|
|
47
|
-
* does not implement `messageVoiceText`
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
+
* does not implement `messageVoiceText` — either the method is absent (old
|
|
51
|
+
* puppet built without it, caught by the `typeof` guard) or it rejects with
|
|
52
|
+
* an unsupported-API error. Old puppets put the ASR result into
|
|
53
|
+
* `payload.text`, so this keeps the behaviour compatible; `noSpeech` is
|
|
54
|
+
* `false` in the fallback. A genuine runtime error is rethrown (so the bot
|
|
55
|
+
* does not silently drop `noSpeech` and re-run its own paid ASR).
|
|
50
56
|
*/
|
|
51
|
-
text(): Promise<
|
|
52
|
-
text: string;
|
|
53
|
-
noSpeech: boolean;
|
|
54
|
-
}>;
|
|
57
|
+
text(): Promise<PUPPET.payloads.VoiceText>;
|
|
55
58
|
}
|
|
56
59
|
declare const VoiceImpl_base: {
|
|
57
60
|
new (...args: any[]): {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice.d.ts","sourceRoot":"","sources":["../../../../src/user-modules/voice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EACV,gBAAgB,EACjB,MAAwB,UAAU,CAAA;AACnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;;;;;;;;
|
|
1
|
+
{"version":3,"file":"voice.d.ts","sourceRoot":"","sources":["../../../../src/user-modules/voice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EACV,gBAAgB,EACjB,MAAwB,UAAU,CAAA;AACnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,KAAK,MAAM,MAAM,sBAAsB,CAAA;;;;;;;;AAsCnD,cAAM,UAAW,SAAQ,eAAoB;IAUlC,EAAE,EAAE,MAAM;IARnB,MAAM,CAAC,MAAM,CAAE,EAAE,EAAE,MAAM,GAAG,cAAc;gBAQjC,EAAE,EAAE,MAAM;IAMnB;;;;;;;;OAQG;IACG,IAAI,IAAK,OAAO,CAAC,gBAAgB,CAAC;IAqBxC;;;;;;;;;;;;OAYG;IACG,IAAI,IAAK,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;CAsBlD;;;;;;;AAED,cAAM,SAAU,SAAQ,cAA6C;CAAG;AACxE,UAAU,cAAe,SAAQ,SAAS;CAAI;AAE9C,aAAK,gBAAgB,GAAG,WAAW,CACjC,cAAc,EACd,OAAO,SAAS,CACjB,CAAA;AAED,YAAY,EACV,gBAAgB,EAChB,cAAc,GACf,CAAA;AACD,OAAO,EACL,SAAS,GACV,CAAA"}
|
|
@@ -4,6 +4,34 @@ exports.VoiceImpl = void 0;
|
|
|
4
4
|
const validation_js_1 = require("../user-mixins/validation.js");
|
|
5
5
|
const config_js_1 = require("../config.js");
|
|
6
6
|
const wechatify_js_1 = require("../user-mixins/wechatify.js");
|
|
7
|
+
/**
|
|
8
|
+
* gRPC status code for an RPC the server does not implement.
|
|
9
|
+
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
|
10
|
+
*/
|
|
11
|
+
const GRPC_STATUS_UNIMPLEMENTED = 12;
|
|
12
|
+
/**
|
|
13
|
+
* Whether `e` means the puppet / server does not implement the new voice RPC,
|
|
14
|
+
* i.e. it is safe to fall back to the legacy path.
|
|
15
|
+
*
|
|
16
|
+
* Transient errors (network / timeout / file expired / other gRPC codes) must
|
|
17
|
+
* NOT match here: otherwise the fallback silently masks real failures and — for
|
|
18
|
+
* `text()` — discards the `noSpeech` discriminator, making the bot re-run its
|
|
19
|
+
* own paid ASR on voices the puppet already confirmed as empty.
|
|
20
|
+
*
|
|
21
|
+
* Note: the "method does not exist at all" case (an old puppet built against a
|
|
22
|
+
* wechaty-puppet without the method) is detected up-front via a `typeof` guard
|
|
23
|
+
* at the call site, NOT here — matching the `'is not a function'` text would
|
|
24
|
+
* also swallow a genuine `TypeError` raised inside a working implementation.
|
|
25
|
+
*/
|
|
26
|
+
function isUnsupportedError(e) {
|
|
27
|
+
const err = e;
|
|
28
|
+
// old wechaty-puppet-service server without the new RPC → gRPC UNIMPLEMENTED
|
|
29
|
+
if (err?.code === GRPC_STATUS_UNIMPLEMENTED) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
// puppet implements the abstract method via throwUnsupportedError()
|
|
33
|
+
return (err?.message || '').includes('Unsupported API');
|
|
34
|
+
}
|
|
7
35
|
class VoiceMixin extends (0, wechatify_js_1.wechatifyMixinBase)() {
|
|
8
36
|
id;
|
|
9
37
|
static create(id) {
|
|
@@ -20,18 +48,29 @@ class VoiceMixin extends (0, wechatify_js_1.wechatifyMixinBase)() {
|
|
|
20
48
|
* Get the voice file (FileBox) via the puppet's dedicated `messageVoice` RPC.
|
|
21
49
|
*
|
|
22
50
|
* Falls back to the generic `messageFile` when the puppet does not implement
|
|
23
|
-
* `messageVoice`
|
|
24
|
-
*
|
|
51
|
+
* `messageVoice` — either the method is absent (old puppet built without it,
|
|
52
|
+
* caught by the `typeof` guard) or it rejects with an unsupported-API error.
|
|
53
|
+
* Keeps the behaviour compatible with the legacy "voice file via messageFile"
|
|
54
|
+
* path. A genuine runtime error from a working `messageVoice` is rethrown.
|
|
25
55
|
*/
|
|
26
56
|
async file() {
|
|
27
57
|
config_js_1.log.verbose('Voice', 'file() for id: "%s"', this.id);
|
|
58
|
+
const puppet = this.wechaty.puppet;
|
|
59
|
+
// puppet built against an old wechaty-puppet without the method at all
|
|
60
|
+
if (typeof puppet.messageVoice !== 'function') {
|
|
61
|
+
config_js_1.log.verbose('Voice', 'file() messageVoice() absent, fallback to messageFile()');
|
|
62
|
+
return puppet.messageFile(this.id);
|
|
63
|
+
}
|
|
28
64
|
try {
|
|
29
|
-
const fileBox = await
|
|
65
|
+
const fileBox = await puppet.messageVoice(this.id);
|
|
30
66
|
return fileBox;
|
|
31
67
|
}
|
|
32
68
|
catch (e) {
|
|
33
|
-
|
|
34
|
-
|
|
69
|
+
if (!isUnsupportedError(e)) {
|
|
70
|
+
throw e;
|
|
71
|
+
}
|
|
72
|
+
config_js_1.log.verbose('Voice', 'file() messageVoice() unsupported, fallback to messageFile(): %s', e.message);
|
|
73
|
+
const fileBox = await puppet.messageFile(this.id);
|
|
35
74
|
return fileBox;
|
|
36
75
|
}
|
|
37
76
|
}
|
|
@@ -41,19 +80,32 @@ class VoiceMixin extends (0, wechatify_js_1.wechatifyMixinBase)() {
|
|
|
41
80
|
* "confirmed no speech" (`noSpeech=true`) from "normal transcription".
|
|
42
81
|
*
|
|
43
82
|
* Falls back to reading the legacy `messagePayload().text` when the puppet
|
|
44
|
-
* does not implement `messageVoiceText`
|
|
45
|
-
*
|
|
46
|
-
*
|
|
83
|
+
* does not implement `messageVoiceText` — either the method is absent (old
|
|
84
|
+
* puppet built without it, caught by the `typeof` guard) or it rejects with
|
|
85
|
+
* an unsupported-API error. Old puppets put the ASR result into
|
|
86
|
+
* `payload.text`, so this keeps the behaviour compatible; `noSpeech` is
|
|
87
|
+
* `false` in the fallback. A genuine runtime error is rethrown (so the bot
|
|
88
|
+
* does not silently drop `noSpeech` and re-run its own paid ASR).
|
|
47
89
|
*/
|
|
48
90
|
async text() {
|
|
49
91
|
config_js_1.log.verbose('Voice', 'text() for id: "%s"', this.id);
|
|
92
|
+
const puppet = this.wechaty.puppet;
|
|
93
|
+
// puppet built against an old wechaty-puppet without the method at all
|
|
94
|
+
if (typeof puppet.messageVoiceText !== 'function') {
|
|
95
|
+
config_js_1.log.verbose('Voice', 'text() messageVoiceText() absent, fallback to messagePayload().text');
|
|
96
|
+
const payload = await puppet.messagePayload(this.id);
|
|
97
|
+
return { text: payload.text || '', noSpeech: false };
|
|
98
|
+
}
|
|
50
99
|
try {
|
|
51
|
-
const payload = await
|
|
100
|
+
const payload = await puppet.messageVoiceText(this.id);
|
|
52
101
|
return payload;
|
|
53
102
|
}
|
|
54
103
|
catch (e) {
|
|
55
|
-
|
|
56
|
-
|
|
104
|
+
if (!isUnsupportedError(e)) {
|
|
105
|
+
throw e;
|
|
106
|
+
}
|
|
107
|
+
config_js_1.log.verbose('Voice', 'text() messageVoiceText() unsupported, fallback to messagePayload().text: %s', e.message);
|
|
108
|
+
const payload = await puppet.messagePayload(this.id);
|
|
57
109
|
return { text: payload.text || '', noSpeech: false };
|
|
58
110
|
}
|
|
59
111
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice.js","sourceRoot":"","sources":["../../../../src/user-modules/voice.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"voice.js","sourceRoot":"","sources":["../../../../src/user-modules/voice.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.spec.d.ts","sourceRoot":"","sources":["../../../../src/user-modules/voice.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
/**
|
|
5
|
+
* Wechaty Chatbot SDK - https://github.com/wechaty/wechaty
|
|
6
|
+
*
|
|
7
|
+
* @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and
|
|
8
|
+
* Wechaty Contributors <https://github.com/wechaty>.
|
|
9
|
+
*
|
|
10
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
|
+
* you may not use this file except in compliance with the License.
|
|
12
|
+
* You may obtain a copy of the License at
|
|
13
|
+
*
|
|
14
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
15
|
+
*
|
|
16
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
17
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
+
* See the License for the specific language governing permissions and
|
|
20
|
+
* limitations under the License.
|
|
21
|
+
*
|
|
22
|
+
*/
|
|
23
|
+
const tstest_1 = require("tstest");
|
|
24
|
+
const file_box_1 = require("file-box");
|
|
25
|
+
const wechaty_puppet_mock_1 = require("@juzi/wechaty-puppet-mock");
|
|
26
|
+
const wechaty_builder_js_1 = require("../wechaty-builder.js");
|
|
27
|
+
const MESSAGE_ID = 'message-id-voice';
|
|
28
|
+
/**
|
|
29
|
+
* The fallback must trigger ONLY when the puppet does not implement the new
|
|
30
|
+
* RPC, never on transient failures: otherwise `text()` silently drops the
|
|
31
|
+
* `noSpeech` discriminator and makes the bot re-run its own paid ASR.
|
|
32
|
+
*
|
|
33
|
+
* The puppet is built & started once and reused across cases (`wechaty.Voice`
|
|
34
|
+
* is unavailable before `start()`). `messageVoice` / `messageVoiceText` are
|
|
35
|
+
* assigned directly (rather than via `sinon.stub(puppet, ...)`) because the
|
|
36
|
+
* installed wechaty-puppet-mock predates those methods, so stubbing a
|
|
37
|
+
* non-existent property would throw.
|
|
38
|
+
*/
|
|
39
|
+
const puppet = new wechaty_puppet_mock_1.PuppetMock();
|
|
40
|
+
const wechaty = wechaty_builder_js_1.WechatyBuilder.build({ puppet });
|
|
41
|
+
let started = false;
|
|
42
|
+
async function ensureStarted() {
|
|
43
|
+
if (!started) {
|
|
44
|
+
await wechaty.start();
|
|
45
|
+
started = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const voice = () => wechaty.Voice.create(MESSAGE_ID);
|
|
49
|
+
const UNSUPPORTED = new Error('Wechaty Puppet Unsupported API Error.');
|
|
50
|
+
(0, tstest_1.test)('Voice.file() returns the dedicated messageVoice result when supported', async (t) => {
|
|
51
|
+
await ensureStarted();
|
|
52
|
+
puppet.messageVoice = tstest_1.sinon.stub().resolves(file_box_1.FileBox.fromBuffer(Buffer.from('voice'), 'voice.silk'));
|
|
53
|
+
puppet.messageFile = tstest_1.sinon.stub().resolves(file_box_1.FileBox.fromBuffer(Buffer.from('file'), 'file.bin'));
|
|
54
|
+
const fileBox = await voice().file();
|
|
55
|
+
t.equal(fileBox.name, 'voice.silk', 'should return the messageVoice FileBox');
|
|
56
|
+
t.ok(puppet.messageVoice.calledOnceWith(MESSAGE_ID), 'should call messageVoice');
|
|
57
|
+
t.notOk(puppet.messageFile.called, 'should NOT fall back to messageFile when supported');
|
|
58
|
+
});
|
|
59
|
+
(0, tstest_1.test)('Voice.file() falls back to messageFile on unsupported error', async (t) => {
|
|
60
|
+
await ensureStarted();
|
|
61
|
+
puppet.messageVoice = tstest_1.sinon.stub().rejects(UNSUPPORTED);
|
|
62
|
+
puppet.messageFile = tstest_1.sinon.stub().resolves(file_box_1.FileBox.fromBuffer(Buffer.from('file'), 'file.bin'));
|
|
63
|
+
const fileBox = await voice().file();
|
|
64
|
+
t.equal(fileBox.name, 'file.bin', 'should fall back to messageFile');
|
|
65
|
+
t.ok(puppet.messageFile.calledOnceWith(MESSAGE_ID), 'should call messageFile on fallback');
|
|
66
|
+
});
|
|
67
|
+
(0, tstest_1.test)('Voice.file() falls back to messageFile when messageVoice is absent (old puppet)', async (t) => {
|
|
68
|
+
await ensureStarted();
|
|
69
|
+
puppet.messageVoice = undefined; // old puppet built without the method at all
|
|
70
|
+
puppet.messageFile = tstest_1.sinon.stub().resolves(file_box_1.FileBox.fromBuffer(Buffer.from('file'), 'file.bin'));
|
|
71
|
+
const fileBox = await voice().file();
|
|
72
|
+
t.equal(fileBox.name, 'file.bin', 'should fall back to messageFile via the typeof guard');
|
|
73
|
+
t.ok(puppet.messageFile.calledOnceWith(MESSAGE_ID), 'should call messageFile on fallback');
|
|
74
|
+
});
|
|
75
|
+
(0, tstest_1.test)('Voice.file() rethrows a transient error instead of falling back', async (t) => {
|
|
76
|
+
await ensureStarted();
|
|
77
|
+
puppet.messageVoice = tstest_1.sinon.stub().rejects(new Error('socket hang up'));
|
|
78
|
+
puppet.messageFile = tstest_1.sinon.stub().resolves(file_box_1.FileBox.fromBuffer(Buffer.from('file'), 'file.bin'));
|
|
79
|
+
await t.rejects(() => voice().file(), /socket hang up/, 'should rethrow the transient error');
|
|
80
|
+
t.notOk(puppet.messageFile.called, 'should NOT fall back on a transient error');
|
|
81
|
+
});
|
|
82
|
+
(0, tstest_1.test)('Voice.file() rethrows a genuine "is not a function" TypeError from a working messageVoice', async (t) => {
|
|
83
|
+
await ensureStarted();
|
|
84
|
+
// method exists but its implementation throws internally — must NOT be
|
|
85
|
+
// misclassified as unsupported just because the message says "is not a function"
|
|
86
|
+
puppet.messageVoice = tstest_1.sinon.stub().rejects(new TypeError('innerHelper.process is not a function'));
|
|
87
|
+
puppet.messageFile = tstest_1.sinon.stub().resolves(file_box_1.FileBox.fromBuffer(Buffer.from('file'), 'file.bin'));
|
|
88
|
+
await t.rejects(() => voice().file(), /is not a function/, 'should rethrow the internal TypeError');
|
|
89
|
+
t.notOk(puppet.messageFile.called, 'should NOT mask a real implementation bug');
|
|
90
|
+
});
|
|
91
|
+
(0, tstest_1.test)('Voice.text() returns the dedicated messageVoiceText payload when supported', async (t) => {
|
|
92
|
+
await ensureStarted();
|
|
93
|
+
puppet.messageVoiceText = tstest_1.sinon.stub().resolves({ text: 'hello', noSpeech: false });
|
|
94
|
+
puppet.messagePayload = tstest_1.sinon.stub().resolves({ text: 'legacy' });
|
|
95
|
+
const result = await voice().text();
|
|
96
|
+
t.same(result, { text: 'hello', noSpeech: false }, 'should return the messageVoiceText payload');
|
|
97
|
+
t.ok(puppet.messageVoiceText.calledOnceWith(MESSAGE_ID), 'should call messageVoiceText');
|
|
98
|
+
t.notOk(puppet.messagePayload.called, 'should NOT fall back to messagePayload when supported');
|
|
99
|
+
});
|
|
100
|
+
(0, tstest_1.test)('Voice.text() preserves noSpeech=true from the puppet (skip paid ASR)', async (t) => {
|
|
101
|
+
await ensureStarted();
|
|
102
|
+
puppet.messageVoiceText = tstest_1.sinon.stub().resolves({ text: '', noSpeech: true });
|
|
103
|
+
puppet.messagePayload = tstest_1.sinon.stub().resolves({ text: '' });
|
|
104
|
+
const result = await voice().text();
|
|
105
|
+
t.same(result, { text: '', noSpeech: true }, 'should return the puppet payload verbatim');
|
|
106
|
+
t.equal(result.noSpeech, true, 'should preserve noSpeech=true so the bot can skip its own ASR');
|
|
107
|
+
t.ok(puppet.messageVoiceText.calledOnceWith(MESSAGE_ID), 'should call messageVoiceText');
|
|
108
|
+
t.notOk(puppet.messagePayload.called, 'should NOT fall back when supported');
|
|
109
|
+
});
|
|
110
|
+
(0, tstest_1.test)('Voice.text() falls back to messagePayload().text on unsupported error', async (t) => {
|
|
111
|
+
await ensureStarted();
|
|
112
|
+
puppet.messageVoiceText = tstest_1.sinon.stub().rejects(UNSUPPORTED);
|
|
113
|
+
puppet.messagePayload = tstest_1.sinon.stub().resolves({ text: 'legacy asr' });
|
|
114
|
+
const result = await voice().text();
|
|
115
|
+
t.same(result, { text: 'legacy asr', noSpeech: false }, 'should fall back to legacy payload.text with noSpeech=false');
|
|
116
|
+
t.ok(puppet.messagePayload.calledOnceWith(MESSAGE_ID), 'should call messagePayload on fallback');
|
|
117
|
+
});
|
|
118
|
+
(0, tstest_1.test)('Voice.text() falls back on gRPC UNIMPLEMENTED (code 12)', async (t) => {
|
|
119
|
+
await ensureStarted();
|
|
120
|
+
const unimplemented = Object.assign(new Error('12 UNIMPLEMENTED'), { code: 12 });
|
|
121
|
+
puppet.messageVoiceText = tstest_1.sinon.stub().rejects(unimplemented);
|
|
122
|
+
puppet.messagePayload = tstest_1.sinon.stub().resolves({ text: 'legacy asr' });
|
|
123
|
+
const result = await voice().text();
|
|
124
|
+
t.same(result, { text: 'legacy asr', noSpeech: false }, 'should fall back when the server returns UNIMPLEMENTED');
|
|
125
|
+
t.ok(puppet.messagePayload.calledOnceWith(MESSAGE_ID), 'should call messagePayload on fallback');
|
|
126
|
+
});
|
|
127
|
+
(0, tstest_1.test)('Voice.text() falls back to messagePayload when messageVoiceText is absent (old puppet)', async (t) => {
|
|
128
|
+
await ensureStarted();
|
|
129
|
+
puppet.messageVoiceText = undefined; // old puppet built without the method at all
|
|
130
|
+
puppet.messagePayload = tstest_1.sinon.stub().resolves({ text: 'legacy asr' });
|
|
131
|
+
const result = await voice().text();
|
|
132
|
+
t.same(result, { text: 'legacy asr', noSpeech: false }, 'should fall back via the typeof guard with noSpeech=false');
|
|
133
|
+
t.ok(puppet.messagePayload.calledOnceWith(MESSAGE_ID), 'should call messagePayload on fallback');
|
|
134
|
+
});
|
|
135
|
+
(0, tstest_1.test)('Voice.text() rethrows a transient error instead of falling back', async (t) => {
|
|
136
|
+
await ensureStarted();
|
|
137
|
+
puppet.messageVoiceText = tstest_1.sinon.stub().rejects(new Error('ETIMEDOUT'));
|
|
138
|
+
puppet.messagePayload = tstest_1.sinon.stub().resolves({ text: 'legacy asr' });
|
|
139
|
+
await t.rejects(() => voice().text(), /ETIMEDOUT/, 'should rethrow the transient error');
|
|
140
|
+
t.notOk(puppet.messagePayload.called, 'should NOT fall back on a transient error (would drop noSpeech)');
|
|
141
|
+
});
|
|
142
|
+
(0, tstest_1.test)('Voice.text() rethrows a genuine "is not a function" TypeError from a working messageVoiceText', async (t) => {
|
|
143
|
+
await ensureStarted();
|
|
144
|
+
// regression: a real internal TypeError must NOT be downgraded — that would
|
|
145
|
+
// drop noSpeech and make the bot re-run its own paid ASR
|
|
146
|
+
puppet.messageVoiceText = tstest_1.sinon.stub().rejects(new TypeError('innerHelper.process is not a function'));
|
|
147
|
+
puppet.messagePayload = tstest_1.sinon.stub().resolves({ text: 'legacy asr' });
|
|
148
|
+
await t.rejects(() => voice().text(), /is not a function/, 'should rethrow the internal TypeError');
|
|
149
|
+
t.notOk(puppet.messagePayload.called, 'should NOT mask a real implementation bug');
|
|
150
|
+
});
|
|
151
|
+
(0, tstest_1.test)('teardown: stop the shared wechaty', async (t) => {
|
|
152
|
+
await wechaty.stop();
|
|
153
|
+
t.pass('stopped');
|
|
154
|
+
});
|
|
155
|
+
//# sourceMappingURL=voice.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.spec.js","sourceRoot":"","sources":["../../../../src/user-modules/voice.spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export const GIT_COMMIT_HASH = '
|
|
1
|
+
export const GIT_COMMIT_HASH = '6ef3d89f2a44f5454d45c56ff45765c9eeca8a79';
|
|
2
2
|
export const packageJson = {
|
|
3
3
|
"name": "@juzi/wechaty",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.155",
|
|
5
5
|
"description": "Wechaty is a RPA SDK for Chatbot Makers.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -110,7 +110,7 @@ export const packageJson = {
|
|
|
110
110
|
},
|
|
111
111
|
"homepage": "https://github.com/wechaty/",
|
|
112
112
|
"dependencies": {
|
|
113
|
-
"@juzi/wechaty-puppet-service": "1.0.
|
|
113
|
+
"@juzi/wechaty-puppet-service": "1.0.119",
|
|
114
114
|
"clone-class": "^1.1.1",
|
|
115
115
|
"cmd-ts": "^0.10.0",
|
|
116
116
|
"cockatiel": "^2.0.2",
|
|
@@ -133,7 +133,7 @@ export const packageJson = {
|
|
|
133
133
|
"@chatie/eslint-config": "^1.0.4",
|
|
134
134
|
"@chatie/semver": "^0.4.7",
|
|
135
135
|
"@chatie/tsconfig": "^4.6.3",
|
|
136
|
-
"@juzi/wechaty-puppet": "^1.0.
|
|
136
|
+
"@juzi/wechaty-puppet": "^1.0.143",
|
|
137
137
|
"@juzi/wechaty-puppet-mock": "^1.0.1",
|
|
138
138
|
"@swc/core": "1.3.44",
|
|
139
139
|
"@swc/helpers": "^0.3.6",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { FileBoxInterface } from 'file-box';
|
|
21
21
|
import type { Constructor } from 'clone-class';
|
|
22
|
+
import type * as PUPPET from '@juzi/wechaty-puppet';
|
|
22
23
|
declare const VoiceMixin_base: ((abstract new (...args: any[]) => {
|
|
23
24
|
readonly wechaty: import("../wechaty/wechaty-impl.js").WechatyInterface;
|
|
24
25
|
}) & {
|
|
@@ -34,8 +35,10 @@ declare class VoiceMixin extends VoiceMixin_base {
|
|
|
34
35
|
* Get the voice file (FileBox) via the puppet's dedicated `messageVoice` RPC.
|
|
35
36
|
*
|
|
36
37
|
* Falls back to the generic `messageFile` when the puppet does not implement
|
|
37
|
-
* `messageVoice`
|
|
38
|
-
*
|
|
38
|
+
* `messageVoice` — either the method is absent (old puppet built without it,
|
|
39
|
+
* caught by the `typeof` guard) or it rejects with an unsupported-API error.
|
|
40
|
+
* Keeps the behaviour compatible with the legacy "voice file via messageFile"
|
|
41
|
+
* path. A genuine runtime error from a working `messageVoice` is rethrown.
|
|
39
42
|
*/
|
|
40
43
|
file(): Promise<FileBoxInterface>;
|
|
41
44
|
/**
|
|
@@ -44,14 +47,14 @@ declare class VoiceMixin extends VoiceMixin_base {
|
|
|
44
47
|
* "confirmed no speech" (`noSpeech=true`) from "normal transcription".
|
|
45
48
|
*
|
|
46
49
|
* Falls back to reading the legacy `messagePayload().text` when the puppet
|
|
47
|
-
* does not implement `messageVoiceText`
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
+
* does not implement `messageVoiceText` — either the method is absent (old
|
|
51
|
+
* puppet built without it, caught by the `typeof` guard) or it rejects with
|
|
52
|
+
* an unsupported-API error. Old puppets put the ASR result into
|
|
53
|
+
* `payload.text`, so this keeps the behaviour compatible; `noSpeech` is
|
|
54
|
+
* `false` in the fallback. A genuine runtime error is rethrown (so the bot
|
|
55
|
+
* does not silently drop `noSpeech` and re-run its own paid ASR).
|
|
50
56
|
*/
|
|
51
|
-
text(): Promise<
|
|
52
|
-
text: string;
|
|
53
|
-
noSpeech: boolean;
|
|
54
|
-
}>;
|
|
57
|
+
text(): Promise<PUPPET.payloads.VoiceText>;
|
|
55
58
|
}
|
|
56
59
|
declare const VoiceImpl_base: {
|
|
57
60
|
new (...args: any[]): {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice.d.ts","sourceRoot":"","sources":["../../../../src/user-modules/voice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EACV,gBAAgB,EACjB,MAAwB,UAAU,CAAA;AACnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;;;;;;;;
|
|
1
|
+
{"version":3,"file":"voice.d.ts","sourceRoot":"","sources":["../../../../src/user-modules/voice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EACV,gBAAgB,EACjB,MAAwB,UAAU,CAAA;AACnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,KAAK,MAAM,MAAM,sBAAsB,CAAA;;;;;;;;AAsCnD,cAAM,UAAW,SAAQ,eAAoB;IAUlC,EAAE,EAAE,MAAM;IARnB,MAAM,CAAC,MAAM,CAAE,EAAE,EAAE,MAAM,GAAG,cAAc;gBAQjC,EAAE,EAAE,MAAM;IAMnB;;;;;;;;OAQG;IACG,IAAI,IAAK,OAAO,CAAC,gBAAgB,CAAC;IAqBxC;;;;;;;;;;;;OAYG;IACG,IAAI,IAAK,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;CAsBlD;;;;;;;AAED,cAAM,SAAU,SAAQ,cAA6C;CAAG;AACxE,UAAU,cAAe,SAAQ,SAAS;CAAI;AAE9C,aAAK,gBAAgB,GAAG,WAAW,CACjC,cAAc,EACd,OAAO,SAAS,CACjB,CAAA;AAED,YAAY,EACV,gBAAgB,EAChB,cAAc,GACf,CAAA;AACD,OAAO,EACL,SAAS,GACV,CAAA"}
|
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import { validationMixin } from '../user-mixins/validation.js';
|
|
2
2
|
import { log } from '../config.js';
|
|
3
3
|
import { wechatifyMixinBase, } from '../user-mixins/wechatify.js';
|
|
4
|
+
/**
|
|
5
|
+
* gRPC status code for an RPC the server does not implement.
|
|
6
|
+
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
|
7
|
+
*/
|
|
8
|
+
const GRPC_STATUS_UNIMPLEMENTED = 12;
|
|
9
|
+
/**
|
|
10
|
+
* Whether `e` means the puppet / server does not implement the new voice RPC,
|
|
11
|
+
* i.e. it is safe to fall back to the legacy path.
|
|
12
|
+
*
|
|
13
|
+
* Transient errors (network / timeout / file expired / other gRPC codes) must
|
|
14
|
+
* NOT match here: otherwise the fallback silently masks real failures and — for
|
|
15
|
+
* `text()` — discards the `noSpeech` discriminator, making the bot re-run its
|
|
16
|
+
* own paid ASR on voices the puppet already confirmed as empty.
|
|
17
|
+
*
|
|
18
|
+
* Note: the "method does not exist at all" case (an old puppet built against a
|
|
19
|
+
* wechaty-puppet without the method) is detected up-front via a `typeof` guard
|
|
20
|
+
* at the call site, NOT here — matching the `'is not a function'` text would
|
|
21
|
+
* also swallow a genuine `TypeError` raised inside a working implementation.
|
|
22
|
+
*/
|
|
23
|
+
function isUnsupportedError(e) {
|
|
24
|
+
const err = e;
|
|
25
|
+
// old wechaty-puppet-service server without the new RPC → gRPC UNIMPLEMENTED
|
|
26
|
+
if (err?.code === GRPC_STATUS_UNIMPLEMENTED) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
// puppet implements the abstract method via throwUnsupportedError()
|
|
30
|
+
return (err?.message || '').includes('Unsupported API');
|
|
31
|
+
}
|
|
4
32
|
class VoiceMixin extends wechatifyMixinBase() {
|
|
5
33
|
id;
|
|
6
34
|
static create(id) {
|
|
@@ -17,18 +45,29 @@ class VoiceMixin extends wechatifyMixinBase() {
|
|
|
17
45
|
* Get the voice file (FileBox) via the puppet's dedicated `messageVoice` RPC.
|
|
18
46
|
*
|
|
19
47
|
* Falls back to the generic `messageFile` when the puppet does not implement
|
|
20
|
-
* `messageVoice`
|
|
21
|
-
*
|
|
48
|
+
* `messageVoice` — either the method is absent (old puppet built without it,
|
|
49
|
+
* caught by the `typeof` guard) or it rejects with an unsupported-API error.
|
|
50
|
+
* Keeps the behaviour compatible with the legacy "voice file via messageFile"
|
|
51
|
+
* path. A genuine runtime error from a working `messageVoice` is rethrown.
|
|
22
52
|
*/
|
|
23
53
|
async file() {
|
|
24
54
|
log.verbose('Voice', 'file() for id: "%s"', this.id);
|
|
55
|
+
const puppet = this.wechaty.puppet;
|
|
56
|
+
// puppet built against an old wechaty-puppet without the method at all
|
|
57
|
+
if (typeof puppet.messageVoice !== 'function') {
|
|
58
|
+
log.verbose('Voice', 'file() messageVoice() absent, fallback to messageFile()');
|
|
59
|
+
return puppet.messageFile(this.id);
|
|
60
|
+
}
|
|
25
61
|
try {
|
|
26
|
-
const fileBox = await
|
|
62
|
+
const fileBox = await puppet.messageVoice(this.id);
|
|
27
63
|
return fileBox;
|
|
28
64
|
}
|
|
29
65
|
catch (e) {
|
|
30
|
-
|
|
31
|
-
|
|
66
|
+
if (!isUnsupportedError(e)) {
|
|
67
|
+
throw e;
|
|
68
|
+
}
|
|
69
|
+
log.verbose('Voice', 'file() messageVoice() unsupported, fallback to messageFile(): %s', e.message);
|
|
70
|
+
const fileBox = await puppet.messageFile(this.id);
|
|
32
71
|
return fileBox;
|
|
33
72
|
}
|
|
34
73
|
}
|
|
@@ -38,19 +77,32 @@ class VoiceMixin extends wechatifyMixinBase() {
|
|
|
38
77
|
* "confirmed no speech" (`noSpeech=true`) from "normal transcription".
|
|
39
78
|
*
|
|
40
79
|
* Falls back to reading the legacy `messagePayload().text` when the puppet
|
|
41
|
-
* does not implement `messageVoiceText`
|
|
42
|
-
*
|
|
43
|
-
*
|
|
80
|
+
* does not implement `messageVoiceText` — either the method is absent (old
|
|
81
|
+
* puppet built without it, caught by the `typeof` guard) or it rejects with
|
|
82
|
+
* an unsupported-API error. Old puppets put the ASR result into
|
|
83
|
+
* `payload.text`, so this keeps the behaviour compatible; `noSpeech` is
|
|
84
|
+
* `false` in the fallback. A genuine runtime error is rethrown (so the bot
|
|
85
|
+
* does not silently drop `noSpeech` and re-run its own paid ASR).
|
|
44
86
|
*/
|
|
45
87
|
async text() {
|
|
46
88
|
log.verbose('Voice', 'text() for id: "%s"', this.id);
|
|
89
|
+
const puppet = this.wechaty.puppet;
|
|
90
|
+
// puppet built against an old wechaty-puppet without the method at all
|
|
91
|
+
if (typeof puppet.messageVoiceText !== 'function') {
|
|
92
|
+
log.verbose('Voice', 'text() messageVoiceText() absent, fallback to messagePayload().text');
|
|
93
|
+
const payload = await puppet.messagePayload(this.id);
|
|
94
|
+
return { text: payload.text || '', noSpeech: false };
|
|
95
|
+
}
|
|
47
96
|
try {
|
|
48
|
-
const payload = await
|
|
97
|
+
const payload = await puppet.messageVoiceText(this.id);
|
|
49
98
|
return payload;
|
|
50
99
|
}
|
|
51
100
|
catch (e) {
|
|
52
|
-
|
|
53
|
-
|
|
101
|
+
if (!isUnsupportedError(e)) {
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
log.verbose('Voice', 'text() messageVoiceText() unsupported, fallback to messagePayload().text: %s', e.message);
|
|
105
|
+
const payload = await puppet.messagePayload(this.id);
|
|
54
106
|
return { text: payload.text || '', noSpeech: false };
|
|
55
107
|
}
|
|
56
108
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice.js","sourceRoot":"","sources":["../../../../src/user-modules/voice.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"voice.js","sourceRoot":"","sources":["../../../../src/user-modules/voice.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.spec.d.ts","sourceRoot":"","sources":["../../../../src/user-modules/voice.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
|
|
2
|
+
/**
|
|
3
|
+
* Wechaty Chatbot SDK - https://github.com/wechaty/wechaty
|
|
4
|
+
*
|
|
5
|
+
* @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and
|
|
6
|
+
* Wechaty Contributors <https://github.com/wechaty>.
|
|
7
|
+
*
|
|
8
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
* you may not use this file except in compliance with the License.
|
|
10
|
+
* You may obtain a copy of the License at
|
|
11
|
+
*
|
|
12
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
*
|
|
14
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
* See the License for the specific language governing permissions and
|
|
18
|
+
* limitations under the License.
|
|
19
|
+
*
|
|
20
|
+
*/
|
|
21
|
+
import { test, sinon, } from 'tstest';
|
|
22
|
+
import { FileBox } from 'file-box';
|
|
23
|
+
import { PuppetMock } from '@juzi/wechaty-puppet-mock';
|
|
24
|
+
import { WechatyBuilder } from '../wechaty-builder.js';
|
|
25
|
+
const MESSAGE_ID = 'message-id-voice';
|
|
26
|
+
/**
|
|
27
|
+
* The fallback must trigger ONLY when the puppet does not implement the new
|
|
28
|
+
* RPC, never on transient failures: otherwise `text()` silently drops the
|
|
29
|
+
* `noSpeech` discriminator and makes the bot re-run its own paid ASR.
|
|
30
|
+
*
|
|
31
|
+
* The puppet is built & started once and reused across cases (`wechaty.Voice`
|
|
32
|
+
* is unavailable before `start()`). `messageVoice` / `messageVoiceText` are
|
|
33
|
+
* assigned directly (rather than via `sinon.stub(puppet, ...)`) because the
|
|
34
|
+
* installed wechaty-puppet-mock predates those methods, so stubbing a
|
|
35
|
+
* non-existent property would throw.
|
|
36
|
+
*/
|
|
37
|
+
const puppet = new PuppetMock();
|
|
38
|
+
const wechaty = WechatyBuilder.build({ puppet });
|
|
39
|
+
let started = false;
|
|
40
|
+
async function ensureStarted() {
|
|
41
|
+
if (!started) {
|
|
42
|
+
await wechaty.start();
|
|
43
|
+
started = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const voice = () => wechaty.Voice.create(MESSAGE_ID);
|
|
47
|
+
const UNSUPPORTED = new Error('Wechaty Puppet Unsupported API Error.');
|
|
48
|
+
test('Voice.file() returns the dedicated messageVoice result when supported', async (t) => {
|
|
49
|
+
await ensureStarted();
|
|
50
|
+
puppet.messageVoice = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('voice'), 'voice.silk'));
|
|
51
|
+
puppet.messageFile = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('file'), 'file.bin'));
|
|
52
|
+
const fileBox = await voice().file();
|
|
53
|
+
t.equal(fileBox.name, 'voice.silk', 'should return the messageVoice FileBox');
|
|
54
|
+
t.ok(puppet.messageVoice.calledOnceWith(MESSAGE_ID), 'should call messageVoice');
|
|
55
|
+
t.notOk(puppet.messageFile.called, 'should NOT fall back to messageFile when supported');
|
|
56
|
+
});
|
|
57
|
+
test('Voice.file() falls back to messageFile on unsupported error', async (t) => {
|
|
58
|
+
await ensureStarted();
|
|
59
|
+
puppet.messageVoice = sinon.stub().rejects(UNSUPPORTED);
|
|
60
|
+
puppet.messageFile = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('file'), 'file.bin'));
|
|
61
|
+
const fileBox = await voice().file();
|
|
62
|
+
t.equal(fileBox.name, 'file.bin', 'should fall back to messageFile');
|
|
63
|
+
t.ok(puppet.messageFile.calledOnceWith(MESSAGE_ID), 'should call messageFile on fallback');
|
|
64
|
+
});
|
|
65
|
+
test('Voice.file() falls back to messageFile when messageVoice is absent (old puppet)', async (t) => {
|
|
66
|
+
await ensureStarted();
|
|
67
|
+
puppet.messageVoice = undefined; // old puppet built without the method at all
|
|
68
|
+
puppet.messageFile = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('file'), 'file.bin'));
|
|
69
|
+
const fileBox = await voice().file();
|
|
70
|
+
t.equal(fileBox.name, 'file.bin', 'should fall back to messageFile via the typeof guard');
|
|
71
|
+
t.ok(puppet.messageFile.calledOnceWith(MESSAGE_ID), 'should call messageFile on fallback');
|
|
72
|
+
});
|
|
73
|
+
test('Voice.file() rethrows a transient error instead of falling back', async (t) => {
|
|
74
|
+
await ensureStarted();
|
|
75
|
+
puppet.messageVoice = sinon.stub().rejects(new Error('socket hang up'));
|
|
76
|
+
puppet.messageFile = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('file'), 'file.bin'));
|
|
77
|
+
await t.rejects(() => voice().file(), /socket hang up/, 'should rethrow the transient error');
|
|
78
|
+
t.notOk(puppet.messageFile.called, 'should NOT fall back on a transient error');
|
|
79
|
+
});
|
|
80
|
+
test('Voice.file() rethrows a genuine "is not a function" TypeError from a working messageVoice', async (t) => {
|
|
81
|
+
await ensureStarted();
|
|
82
|
+
// method exists but its implementation throws internally — must NOT be
|
|
83
|
+
// misclassified as unsupported just because the message says "is not a function"
|
|
84
|
+
puppet.messageVoice = sinon.stub().rejects(new TypeError('innerHelper.process is not a function'));
|
|
85
|
+
puppet.messageFile = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('file'), 'file.bin'));
|
|
86
|
+
await t.rejects(() => voice().file(), /is not a function/, 'should rethrow the internal TypeError');
|
|
87
|
+
t.notOk(puppet.messageFile.called, 'should NOT mask a real implementation bug');
|
|
88
|
+
});
|
|
89
|
+
test('Voice.text() returns the dedicated messageVoiceText payload when supported', async (t) => {
|
|
90
|
+
await ensureStarted();
|
|
91
|
+
puppet.messageVoiceText = sinon.stub().resolves({ text: 'hello', noSpeech: false });
|
|
92
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy' });
|
|
93
|
+
const result = await voice().text();
|
|
94
|
+
t.same(result, { text: 'hello', noSpeech: false }, 'should return the messageVoiceText payload');
|
|
95
|
+
t.ok(puppet.messageVoiceText.calledOnceWith(MESSAGE_ID), 'should call messageVoiceText');
|
|
96
|
+
t.notOk(puppet.messagePayload.called, 'should NOT fall back to messagePayload when supported');
|
|
97
|
+
});
|
|
98
|
+
test('Voice.text() preserves noSpeech=true from the puppet (skip paid ASR)', async (t) => {
|
|
99
|
+
await ensureStarted();
|
|
100
|
+
puppet.messageVoiceText = sinon.stub().resolves({ text: '', noSpeech: true });
|
|
101
|
+
puppet.messagePayload = sinon.stub().resolves({ text: '' });
|
|
102
|
+
const result = await voice().text();
|
|
103
|
+
t.same(result, { text: '', noSpeech: true }, 'should return the puppet payload verbatim');
|
|
104
|
+
t.equal(result.noSpeech, true, 'should preserve noSpeech=true so the bot can skip its own ASR');
|
|
105
|
+
t.ok(puppet.messageVoiceText.calledOnceWith(MESSAGE_ID), 'should call messageVoiceText');
|
|
106
|
+
t.notOk(puppet.messagePayload.called, 'should NOT fall back when supported');
|
|
107
|
+
});
|
|
108
|
+
test('Voice.text() falls back to messagePayload().text on unsupported error', async (t) => {
|
|
109
|
+
await ensureStarted();
|
|
110
|
+
puppet.messageVoiceText = sinon.stub().rejects(UNSUPPORTED);
|
|
111
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy asr' });
|
|
112
|
+
const result = await voice().text();
|
|
113
|
+
t.same(result, { text: 'legacy asr', noSpeech: false }, 'should fall back to legacy payload.text with noSpeech=false');
|
|
114
|
+
t.ok(puppet.messagePayload.calledOnceWith(MESSAGE_ID), 'should call messagePayload on fallback');
|
|
115
|
+
});
|
|
116
|
+
test('Voice.text() falls back on gRPC UNIMPLEMENTED (code 12)', async (t) => {
|
|
117
|
+
await ensureStarted();
|
|
118
|
+
const unimplemented = Object.assign(new Error('12 UNIMPLEMENTED'), { code: 12 });
|
|
119
|
+
puppet.messageVoiceText = sinon.stub().rejects(unimplemented);
|
|
120
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy asr' });
|
|
121
|
+
const result = await voice().text();
|
|
122
|
+
t.same(result, { text: 'legacy asr', noSpeech: false }, 'should fall back when the server returns UNIMPLEMENTED');
|
|
123
|
+
t.ok(puppet.messagePayload.calledOnceWith(MESSAGE_ID), 'should call messagePayload on fallback');
|
|
124
|
+
});
|
|
125
|
+
test('Voice.text() falls back to messagePayload when messageVoiceText is absent (old puppet)', async (t) => {
|
|
126
|
+
await ensureStarted();
|
|
127
|
+
puppet.messageVoiceText = undefined; // old puppet built without the method at all
|
|
128
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy asr' });
|
|
129
|
+
const result = await voice().text();
|
|
130
|
+
t.same(result, { text: 'legacy asr', noSpeech: false }, 'should fall back via the typeof guard with noSpeech=false');
|
|
131
|
+
t.ok(puppet.messagePayload.calledOnceWith(MESSAGE_ID), 'should call messagePayload on fallback');
|
|
132
|
+
});
|
|
133
|
+
test('Voice.text() rethrows a transient error instead of falling back', async (t) => {
|
|
134
|
+
await ensureStarted();
|
|
135
|
+
puppet.messageVoiceText = sinon.stub().rejects(new Error('ETIMEDOUT'));
|
|
136
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy asr' });
|
|
137
|
+
await t.rejects(() => voice().text(), /ETIMEDOUT/, 'should rethrow the transient error');
|
|
138
|
+
t.notOk(puppet.messagePayload.called, 'should NOT fall back on a transient error (would drop noSpeech)');
|
|
139
|
+
});
|
|
140
|
+
test('Voice.text() rethrows a genuine "is not a function" TypeError from a working messageVoiceText', async (t) => {
|
|
141
|
+
await ensureStarted();
|
|
142
|
+
// regression: a real internal TypeError must NOT be downgraded — that would
|
|
143
|
+
// drop noSpeech and make the bot re-run its own paid ASR
|
|
144
|
+
puppet.messageVoiceText = sinon.stub().rejects(new TypeError('innerHelper.process is not a function'));
|
|
145
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy asr' });
|
|
146
|
+
await t.rejects(() => voice().text(), /is not a function/, 'should rethrow the internal TypeError');
|
|
147
|
+
t.notOk(puppet.messagePayload.called, 'should NOT mask a real implementation bug');
|
|
148
|
+
});
|
|
149
|
+
test('teardown: stop the shared wechaty', async (t) => {
|
|
150
|
+
await wechaty.stop();
|
|
151
|
+
t.pass('stopped');
|
|
152
|
+
});
|
|
153
|
+
//# sourceMappingURL=voice.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.spec.js","sourceRoot":"","sources":["../../../../src/user-modules/voice.spec.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EACL,IAAI,EACJ,KAAK,GACN,MAAkB,QAAQ,CAAA;AAE3B,OAAO,EAAE,OAAO,EAAE,MAAa,UAAU,CAAA;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAU,2BAA2B,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAEtD,MAAM,UAAU,GAAG,kBAAkB,CAAA;AAErC;;;;;;;;;;GAUG;AACH,MAAM,MAAM,GAAI,IAAI,UAAU,EAAS,CAAA;AACvC,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;AAEhD,IAAI,OAAO,GAAG,KAAK,CAAA;AACnB,KAAK,UAAU,aAAa;IAC1B,IAAI,CAAC,OAAO,EAAE;QACZ,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA;QACrB,OAAO,GAAG,IAAI,CAAA;KACf;AACH,CAAC;AAED,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;AAEpD,MAAM,WAAW,GAAG,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;AAEtE,IAAI,CAAC,uEAAuE,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IACtF,MAAM,aAAa,EAAE,CAAA;IACrB,MAAM,CAAC,YAAY,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,CAAC,CAAA;IACnG,MAAM,CAAC,WAAW,GAAI,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC,CAAA;IAEhG,MAAM,OAAO,GAAG,MAAM,KAAK,EAAE,CAAC,IAAI,EAAE,CAAA;IACpC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,YAAY,EAAE,wCAAwC,CAAC,CAAA;IAC7E,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,0BAA0B,CAAC,CAAA;IAChF,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,oDAAoD,CAAC,CAAA;AAC1F,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,6DAA6D,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IAC5E,MAAM,aAAa,EAAE,CAAA;IACrB,MAAM,CAAC,YAAY,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IACvD,MAAM,CAAC,WAAW,GAAI,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC,CAAA;IAEhG,MAAM,OAAO,GAAG,MAAM,KAAK,EAAE,CAAC,IAAI,EAAE,CAAA;IACpC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,iCAAiC,CAAC,CAAA;IACpE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,qCAAqC,CAAC,CAAA;AAC5F,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,iFAAiF,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IAChG,MAAM,aAAa,EAAE,CAAA;IACrB,MAAM,CAAC,YAAY,GAAG,SAAS,CAAA,CAAC,6CAA6C;IAC7E,MAAM,CAAC,WAAW,GAAI,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC,CAAA;IAEhG,MAAM,OAAO,GAAG,MAAM,KAAK,EAAE,CAAC,IAAI,EAAE,CAAA;IACpC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,sDAAsD,CAAC,CAAA;IACzF,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,qCAAqC,CAAC,CAAA;AAC5F,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,iEAAiE,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IAChF,MAAM,aAAa,EAAE,CAAA;IACrB,MAAM,CAAC,YAAY,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAA;IACvE,MAAM,CAAC,WAAW,GAAI,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC,CAAA;IAEhG,MAAM,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,gBAAgB,EAAE,oCAAoC,CAAC,CAAA;IAC7F,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,2CAA2C,CAAC,CAAA;AACjF,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,2FAA2F,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IAC1G,MAAM,aAAa,EAAE,CAAA;IACrB,uEAAuE;IACvE,iFAAiF;IACjF,MAAM,CAAC,YAAY,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,SAAS,CAAC,uCAAuC,CAAC,CAAC,CAAA;IAClG,MAAM,CAAC,WAAW,GAAI,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC,CAAA;IAEhG,MAAM,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,mBAAmB,EAAE,uCAAuC,CAAC,CAAA;IACnG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,2CAA2C,CAAC,CAAA;AACjF,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,4EAA4E,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IAC3F,MAAM,aAAa,EAAE,CAAA;IACrB,MAAM,CAAC,gBAAgB,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAA;IACnF,MAAM,CAAC,cAAc,GAAK,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAA6B,CAAC,CAAA;IAE9F,MAAM,MAAM,GAAG,MAAM,KAAK,EAAE,CAAC,IAAI,EAAE,CAAA;IACnC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,4CAA4C,CAAC,CAAA;IAChG,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,8BAA8B,CAAC,CAAA;IACxF,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,uDAAuD,CAAC,CAAA;AAChG,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,sEAAsE,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IACrF,MAAM,aAAa,EAAE,CAAA;IACrB,MAAM,CAAC,gBAAgB,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAC7E,MAAM,CAAC,cAAc,GAAK,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,EAAE,EAA6B,CAAC,CAAA;IAExF,MAAM,MAAM,GAAG,MAAM,KAAK,EAAE,CAAC,IAAI,EAAE,CAAA;IACnC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,2CAA2C,CAAC,CAAA;IACzF,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,+DAA+D,CAAC,CAAA;IAC/F,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,8BAA8B,CAAC,CAAA;IACxF,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,qCAAqC,CAAC,CAAA;AAC9E,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,uEAAuE,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IACtF,MAAM,aAAa,EAAE,CAAA;IACrB,MAAM,CAAC,gBAAgB,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAC3D,MAAM,CAAC,cAAc,GAAK,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,YAAY,EAA6B,CAAC,CAAA;IAElG,MAAM,MAAM,GAAG,MAAM,KAAK,EAAE,CAAC,IAAI,EAAE,CAAA;IACnC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,6DAA6D,CAAC,CAAA;IACtH,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,wCAAwC,CAAC,CAAA;AAClG,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,yDAAyD,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IACxE,MAAM,aAAa,EAAE,CAAA;IACrB,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,kBAAkB,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;IAChF,MAAM,CAAC,gBAAgB,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;IAC7D,MAAM,CAAC,cAAc,GAAK,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,YAAY,EAA6B,CAAC,CAAA;IAElG,MAAM,MAAM,GAAG,MAAM,KAAK,EAAE,CAAC,IAAI,EAAE,CAAA;IACnC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,wDAAwD,CAAC,CAAA;IACjH,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,wCAAwC,CAAC,CAAA;AAClG,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,wFAAwF,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IACvG,MAAM,aAAa,EAAE,CAAA;IACrB,MAAM,CAAC,gBAAgB,GAAG,SAAS,CAAA,CAAC,6CAA6C;IACjF,MAAM,CAAC,cAAc,GAAK,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,YAAY,EAA6B,CAAC,CAAA;IAElG,MAAM,MAAM,GAAG,MAAM,KAAK,EAAE,CAAC,IAAI,EAAE,CAAA;IACnC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,2DAA2D,CAAC,CAAA;IACpH,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,wCAAwC,CAAC,CAAA;AAClG,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,iEAAiE,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IAChF,MAAM,aAAa,EAAE,CAAA;IACrB,MAAM,CAAC,gBAAgB,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,CAAA;IACtE,MAAM,CAAC,cAAc,GAAK,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,YAAY,EAA6B,CAAC,CAAA;IAElG,MAAM,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,oCAAoC,CAAC,CAAA;IACxF,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,iEAAiE,CAAC,CAAA;AAC1G,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,+FAA+F,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IAC9G,MAAM,aAAa,EAAE,CAAA;IACrB,4EAA4E;IAC5E,yDAAyD;IACzD,MAAM,CAAC,gBAAgB,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,SAAS,CAAC,uCAAuC,CAAC,CAAC,CAAA;IACtG,MAAM,CAAC,cAAc,GAAK,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,YAAY,EAA6B,CAAC,CAAA;IAElG,MAAM,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,mBAAmB,EAAE,uCAAuC,CAAC,CAAA;IACnG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,2CAA2C,CAAC,CAAA;AACpF,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,mCAAmC,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IAClD,MAAM,OAAO,CAAC,IAAI,EAAE,CAAA;IACpB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;AACnB,CAAC,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juzi/wechaty",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.155",
|
|
4
4
|
"description": "Wechaty is a RPA SDK for Chatbot Makers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
},
|
|
110
110
|
"homepage": "https://github.com/wechaty/",
|
|
111
111
|
"dependencies": {
|
|
112
|
-
"@juzi/wechaty-puppet-service": "1.0.
|
|
112
|
+
"@juzi/wechaty-puppet-service": "1.0.119",
|
|
113
113
|
"clone-class": "^1.1.1",
|
|
114
114
|
"cmd-ts": "^0.10.0",
|
|
115
115
|
"cockatiel": "^2.0.2",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"@chatie/eslint-config": "^1.0.4",
|
|
133
133
|
"@chatie/semver": "^0.4.7",
|
|
134
134
|
"@chatie/tsconfig": "^4.6.3",
|
|
135
|
-
"@juzi/wechaty-puppet": "^1.0.
|
|
135
|
+
"@juzi/wechaty-puppet": "^1.0.143",
|
|
136
136
|
"@juzi/wechaty-puppet-mock": "^1.0.1",
|
|
137
137
|
"@swc/core": "1.3.44",
|
|
138
138
|
"@swc/helpers": "^0.3.6",
|
package/src/package-json.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { PackageJsonWechaty } from './config'
|
|
6
6
|
|
|
7
|
-
export const GIT_COMMIT_HASH: string = '
|
|
7
|
+
export const GIT_COMMIT_HASH: string = '6ef3d89f2a44f5454d45c56ff45765c9eeca8a79'
|
|
8
8
|
export const packageJson: PackageJsonWechaty = {
|
|
9
9
|
"name": "@juzi/wechaty",
|
|
10
|
-
"version": "1.0.
|
|
10
|
+
"version": "1.0.155",
|
|
11
11
|
"description": "Wechaty is a RPA SDK for Chatbot Makers.",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"exports": {
|
|
@@ -116,7 +116,7 @@ export const packageJson: PackageJsonWechaty = {
|
|
|
116
116
|
},
|
|
117
117
|
"homepage": "https://github.com/wechaty/",
|
|
118
118
|
"dependencies": {
|
|
119
|
-
"@juzi/wechaty-puppet-service": "1.0.
|
|
119
|
+
"@juzi/wechaty-puppet-service": "1.0.119",
|
|
120
120
|
"clone-class": "^1.1.1",
|
|
121
121
|
"cmd-ts": "^0.10.0",
|
|
122
122
|
"cockatiel": "^2.0.2",
|
|
@@ -139,7 +139,7 @@ export const packageJson: PackageJsonWechaty = {
|
|
|
139
139
|
"@chatie/eslint-config": "^1.0.4",
|
|
140
140
|
"@chatie/semver": "^0.4.7",
|
|
141
141
|
"@chatie/tsconfig": "^4.6.3",
|
|
142
|
-
"@juzi/wechaty-puppet": "^1.0.
|
|
142
|
+
"@juzi/wechaty-puppet": "^1.0.143",
|
|
143
143
|
"@juzi/wechaty-puppet-mock": "^1.0.1",
|
|
144
144
|
"@swc/core": "1.3.44",
|
|
145
145
|
"@swc/helpers": "^0.3.6",
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
|
|
2
|
+
/**
|
|
3
|
+
* Wechaty Chatbot SDK - https://github.com/wechaty/wechaty
|
|
4
|
+
*
|
|
5
|
+
* @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and
|
|
6
|
+
* Wechaty Contributors <https://github.com/wechaty>.
|
|
7
|
+
*
|
|
8
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
* you may not use this file except in compliance with the License.
|
|
10
|
+
* You may obtain a copy of the License at
|
|
11
|
+
*
|
|
12
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
*
|
|
14
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
* See the License for the specific language governing permissions and
|
|
18
|
+
* limitations under the License.
|
|
19
|
+
*
|
|
20
|
+
*/
|
|
21
|
+
import {
|
|
22
|
+
test,
|
|
23
|
+
sinon,
|
|
24
|
+
} from 'tstest'
|
|
25
|
+
|
|
26
|
+
import { FileBox } from 'file-box'
|
|
27
|
+
import type * as PUPPET from '@juzi/wechaty-puppet'
|
|
28
|
+
import { PuppetMock } from '@juzi/wechaty-puppet-mock'
|
|
29
|
+
import { WechatyBuilder } from '../wechaty-builder.js'
|
|
30
|
+
|
|
31
|
+
const MESSAGE_ID = 'message-id-voice'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The fallback must trigger ONLY when the puppet does not implement the new
|
|
35
|
+
* RPC, never on transient failures: otherwise `text()` silently drops the
|
|
36
|
+
* `noSpeech` discriminator and makes the bot re-run its own paid ASR.
|
|
37
|
+
*
|
|
38
|
+
* The puppet is built & started once and reused across cases (`wechaty.Voice`
|
|
39
|
+
* is unavailable before `start()`). `messageVoice` / `messageVoiceText` are
|
|
40
|
+
* assigned directly (rather than via `sinon.stub(puppet, ...)`) because the
|
|
41
|
+
* installed wechaty-puppet-mock predates those methods, so stubbing a
|
|
42
|
+
* non-existent property would throw.
|
|
43
|
+
*/
|
|
44
|
+
const puppet = new PuppetMock() as any
|
|
45
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
46
|
+
|
|
47
|
+
let started = false
|
|
48
|
+
async function ensureStarted () {
|
|
49
|
+
if (!started) {
|
|
50
|
+
await wechaty.start()
|
|
51
|
+
started = true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const voice = () => wechaty.Voice.create(MESSAGE_ID)
|
|
56
|
+
|
|
57
|
+
const UNSUPPORTED = new Error('Wechaty Puppet Unsupported API Error.')
|
|
58
|
+
|
|
59
|
+
test('Voice.file() returns the dedicated messageVoice result when supported', async t => {
|
|
60
|
+
await ensureStarted()
|
|
61
|
+
puppet.messageVoice = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('voice'), 'voice.silk'))
|
|
62
|
+
puppet.messageFile = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('file'), 'file.bin'))
|
|
63
|
+
|
|
64
|
+
const fileBox = await voice().file()
|
|
65
|
+
t.equal(fileBox.name, 'voice.silk', 'should return the messageVoice FileBox')
|
|
66
|
+
t.ok(puppet.messageVoice.calledOnceWith(MESSAGE_ID), 'should call messageVoice')
|
|
67
|
+
t.notOk(puppet.messageFile.called, 'should NOT fall back to messageFile when supported')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('Voice.file() falls back to messageFile on unsupported error', async t => {
|
|
71
|
+
await ensureStarted()
|
|
72
|
+
puppet.messageVoice = sinon.stub().rejects(UNSUPPORTED)
|
|
73
|
+
puppet.messageFile = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('file'), 'file.bin'))
|
|
74
|
+
|
|
75
|
+
const fileBox = await voice().file()
|
|
76
|
+
t.equal(fileBox.name, 'file.bin', 'should fall back to messageFile')
|
|
77
|
+
t.ok(puppet.messageFile.calledOnceWith(MESSAGE_ID), 'should call messageFile on fallback')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('Voice.file() falls back to messageFile when messageVoice is absent (old puppet)', async t => {
|
|
81
|
+
await ensureStarted()
|
|
82
|
+
puppet.messageVoice = undefined // old puppet built without the method at all
|
|
83
|
+
puppet.messageFile = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('file'), 'file.bin'))
|
|
84
|
+
|
|
85
|
+
const fileBox = await voice().file()
|
|
86
|
+
t.equal(fileBox.name, 'file.bin', 'should fall back to messageFile via the typeof guard')
|
|
87
|
+
t.ok(puppet.messageFile.calledOnceWith(MESSAGE_ID), 'should call messageFile on fallback')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('Voice.file() rethrows a transient error instead of falling back', async t => {
|
|
91
|
+
await ensureStarted()
|
|
92
|
+
puppet.messageVoice = sinon.stub().rejects(new Error('socket hang up'))
|
|
93
|
+
puppet.messageFile = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('file'), 'file.bin'))
|
|
94
|
+
|
|
95
|
+
await t.rejects(() => voice().file(), /socket hang up/, 'should rethrow the transient error')
|
|
96
|
+
t.notOk(puppet.messageFile.called, 'should NOT fall back on a transient error')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('Voice.file() rethrows a genuine "is not a function" TypeError from a working messageVoice', async t => {
|
|
100
|
+
await ensureStarted()
|
|
101
|
+
// method exists but its implementation throws internally — must NOT be
|
|
102
|
+
// misclassified as unsupported just because the message says "is not a function"
|
|
103
|
+
puppet.messageVoice = sinon.stub().rejects(new TypeError('innerHelper.process is not a function'))
|
|
104
|
+
puppet.messageFile = sinon.stub().resolves(FileBox.fromBuffer(Buffer.from('file'), 'file.bin'))
|
|
105
|
+
|
|
106
|
+
await t.rejects(() => voice().file(), /is not a function/, 'should rethrow the internal TypeError')
|
|
107
|
+
t.notOk(puppet.messageFile.called, 'should NOT mask a real implementation bug')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('Voice.text() returns the dedicated messageVoiceText payload when supported', async t => {
|
|
111
|
+
await ensureStarted()
|
|
112
|
+
puppet.messageVoiceText = sinon.stub().resolves({ text: 'hello', noSpeech: false })
|
|
113
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy' } as PUPPET.payloads.Message)
|
|
114
|
+
|
|
115
|
+
const result = await voice().text()
|
|
116
|
+
t.same(result, { text: 'hello', noSpeech: false }, 'should return the messageVoiceText payload')
|
|
117
|
+
t.ok(puppet.messageVoiceText.calledOnceWith(MESSAGE_ID), 'should call messageVoiceText')
|
|
118
|
+
t.notOk(puppet.messagePayload.called, 'should NOT fall back to messagePayload when supported')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('Voice.text() preserves noSpeech=true from the puppet (skip paid ASR)', async t => {
|
|
122
|
+
await ensureStarted()
|
|
123
|
+
puppet.messageVoiceText = sinon.stub().resolves({ text: '', noSpeech: true })
|
|
124
|
+
puppet.messagePayload = sinon.stub().resolves({ text: '' } as PUPPET.payloads.Message)
|
|
125
|
+
|
|
126
|
+
const result = await voice().text()
|
|
127
|
+
t.same(result, { text: '', noSpeech: true }, 'should return the puppet payload verbatim')
|
|
128
|
+
t.equal(result.noSpeech, true, 'should preserve noSpeech=true so the bot can skip its own ASR')
|
|
129
|
+
t.ok(puppet.messageVoiceText.calledOnceWith(MESSAGE_ID), 'should call messageVoiceText')
|
|
130
|
+
t.notOk(puppet.messagePayload.called, 'should NOT fall back when supported')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('Voice.text() falls back to messagePayload().text on unsupported error', async t => {
|
|
134
|
+
await ensureStarted()
|
|
135
|
+
puppet.messageVoiceText = sinon.stub().rejects(UNSUPPORTED)
|
|
136
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy asr' } as PUPPET.payloads.Message)
|
|
137
|
+
|
|
138
|
+
const result = await voice().text()
|
|
139
|
+
t.same(result, { text: 'legacy asr', noSpeech: false }, 'should fall back to legacy payload.text with noSpeech=false')
|
|
140
|
+
t.ok(puppet.messagePayload.calledOnceWith(MESSAGE_ID), 'should call messagePayload on fallback')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('Voice.text() falls back on gRPC UNIMPLEMENTED (code 12)', async t => {
|
|
144
|
+
await ensureStarted()
|
|
145
|
+
const unimplemented = Object.assign(new Error('12 UNIMPLEMENTED'), { code: 12 })
|
|
146
|
+
puppet.messageVoiceText = sinon.stub().rejects(unimplemented)
|
|
147
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy asr' } as PUPPET.payloads.Message)
|
|
148
|
+
|
|
149
|
+
const result = await voice().text()
|
|
150
|
+
t.same(result, { text: 'legacy asr', noSpeech: false }, 'should fall back when the server returns UNIMPLEMENTED')
|
|
151
|
+
t.ok(puppet.messagePayload.calledOnceWith(MESSAGE_ID), 'should call messagePayload on fallback')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('Voice.text() falls back to messagePayload when messageVoiceText is absent (old puppet)', async t => {
|
|
155
|
+
await ensureStarted()
|
|
156
|
+
puppet.messageVoiceText = undefined // old puppet built without the method at all
|
|
157
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy asr' } as PUPPET.payloads.Message)
|
|
158
|
+
|
|
159
|
+
const result = await voice().text()
|
|
160
|
+
t.same(result, { text: 'legacy asr', noSpeech: false }, 'should fall back via the typeof guard with noSpeech=false')
|
|
161
|
+
t.ok(puppet.messagePayload.calledOnceWith(MESSAGE_ID), 'should call messagePayload on fallback')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('Voice.text() rethrows a transient error instead of falling back', async t => {
|
|
165
|
+
await ensureStarted()
|
|
166
|
+
puppet.messageVoiceText = sinon.stub().rejects(new Error('ETIMEDOUT'))
|
|
167
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy asr' } as PUPPET.payloads.Message)
|
|
168
|
+
|
|
169
|
+
await t.rejects(() => voice().text(), /ETIMEDOUT/, 'should rethrow the transient error')
|
|
170
|
+
t.notOk(puppet.messagePayload.called, 'should NOT fall back on a transient error (would drop noSpeech)')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('Voice.text() rethrows a genuine "is not a function" TypeError from a working messageVoiceText', async t => {
|
|
174
|
+
await ensureStarted()
|
|
175
|
+
// regression: a real internal TypeError must NOT be downgraded — that would
|
|
176
|
+
// drop noSpeech and make the bot re-run its own paid ASR
|
|
177
|
+
puppet.messageVoiceText = sinon.stub().rejects(new TypeError('innerHelper.process is not a function'))
|
|
178
|
+
puppet.messagePayload = sinon.stub().resolves({ text: 'legacy asr' } as PUPPET.payloads.Message)
|
|
179
|
+
|
|
180
|
+
await t.rejects(() => voice().text(), /is not a function/, 'should rethrow the internal TypeError')
|
|
181
|
+
t.notOk(puppet.messagePayload.called, 'should NOT mask a real implementation bug')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('teardown: stop the shared wechaty', async t => {
|
|
185
|
+
await wechaty.stop()
|
|
186
|
+
t.pass('stopped')
|
|
187
|
+
})
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
FileBoxInterface,
|
|
22
22
|
} from 'file-box'
|
|
23
23
|
import type { Constructor } from 'clone-class'
|
|
24
|
+
import type * as PUPPET from '@juzi/wechaty-puppet'
|
|
24
25
|
import { validationMixin } from '../user-mixins/validation.js'
|
|
25
26
|
import { log } from '../config.js'
|
|
26
27
|
|
|
@@ -28,6 +29,36 @@ import {
|
|
|
28
29
|
wechatifyMixinBase,
|
|
29
30
|
} from '../user-mixins/wechatify.js'
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* gRPC status code for an RPC the server does not implement.
|
|
34
|
+
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
|
35
|
+
*/
|
|
36
|
+
const GRPC_STATUS_UNIMPLEMENTED = 12
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether `e` means the puppet / server does not implement the new voice RPC,
|
|
40
|
+
* i.e. it is safe to fall back to the legacy path.
|
|
41
|
+
*
|
|
42
|
+
* Transient errors (network / timeout / file expired / other gRPC codes) must
|
|
43
|
+
* NOT match here: otherwise the fallback silently masks real failures and — for
|
|
44
|
+
* `text()` — discards the `noSpeech` discriminator, making the bot re-run its
|
|
45
|
+
* own paid ASR on voices the puppet already confirmed as empty.
|
|
46
|
+
*
|
|
47
|
+
* Note: the "method does not exist at all" case (an old puppet built against a
|
|
48
|
+
* wechaty-puppet without the method) is detected up-front via a `typeof` guard
|
|
49
|
+
* at the call site, NOT here — matching the `'is not a function'` text would
|
|
50
|
+
* also swallow a genuine `TypeError` raised inside a working implementation.
|
|
51
|
+
*/
|
|
52
|
+
function isUnsupportedError (e: unknown): boolean {
|
|
53
|
+
const err = e as { code?: number | string; message?: string } | undefined
|
|
54
|
+
// old wechaty-puppet-service server without the new RPC → gRPC UNIMPLEMENTED
|
|
55
|
+
if (err?.code === GRPC_STATUS_UNIMPLEMENTED) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
// puppet implements the abstract method via throwUnsupportedError()
|
|
59
|
+
return (err?.message || '').includes('Unsupported API')
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
class VoiceMixin extends wechatifyMixinBase() {
|
|
32
63
|
|
|
33
64
|
static create (id: string): VoiceInterface {
|
|
@@ -48,17 +79,28 @@ class VoiceMixin extends wechatifyMixinBase() {
|
|
|
48
79
|
* Get the voice file (FileBox) via the puppet's dedicated `messageVoice` RPC.
|
|
49
80
|
*
|
|
50
81
|
* Falls back to the generic `messageFile` when the puppet does not implement
|
|
51
|
-
* `messageVoice`
|
|
52
|
-
*
|
|
82
|
+
* `messageVoice` — either the method is absent (old puppet built without it,
|
|
83
|
+
* caught by the `typeof` guard) or it rejects with an unsupported-API error.
|
|
84
|
+
* Keeps the behaviour compatible with the legacy "voice file via messageFile"
|
|
85
|
+
* path. A genuine runtime error from a working `messageVoice` is rethrown.
|
|
53
86
|
*/
|
|
54
87
|
async file (): Promise<FileBoxInterface> {
|
|
55
88
|
log.verbose('Voice', 'file() for id: "%s"', this.id)
|
|
89
|
+
const puppet = this.wechaty.puppet
|
|
90
|
+
// puppet built against an old wechaty-puppet without the method at all
|
|
91
|
+
if (typeof (puppet as { messageVoice?: unknown }).messageVoice !== 'function') {
|
|
92
|
+
log.verbose('Voice', 'file() messageVoice() absent, fallback to messageFile()')
|
|
93
|
+
return puppet.messageFile(this.id)
|
|
94
|
+
}
|
|
56
95
|
try {
|
|
57
|
-
const fileBox = await
|
|
96
|
+
const fileBox = await puppet.messageVoice(this.id)
|
|
58
97
|
return fileBox
|
|
59
98
|
} catch (e) {
|
|
60
|
-
|
|
61
|
-
|
|
99
|
+
if (!isUnsupportedError(e)) {
|
|
100
|
+
throw e
|
|
101
|
+
}
|
|
102
|
+
log.verbose('Voice', 'file() messageVoice() unsupported, fallback to messageFile(): %s', (e as Error).message)
|
|
103
|
+
const fileBox = await puppet.messageFile(this.id)
|
|
62
104
|
return fileBox
|
|
63
105
|
}
|
|
64
106
|
}
|
|
@@ -69,18 +111,31 @@ class VoiceMixin extends wechatifyMixinBase() {
|
|
|
69
111
|
* "confirmed no speech" (`noSpeech=true`) from "normal transcription".
|
|
70
112
|
*
|
|
71
113
|
* Falls back to reading the legacy `messagePayload().text` when the puppet
|
|
72
|
-
* does not implement `messageVoiceText`
|
|
73
|
-
*
|
|
74
|
-
*
|
|
114
|
+
* does not implement `messageVoiceText` — either the method is absent (old
|
|
115
|
+
* puppet built without it, caught by the `typeof` guard) or it rejects with
|
|
116
|
+
* an unsupported-API error. Old puppets put the ASR result into
|
|
117
|
+
* `payload.text`, so this keeps the behaviour compatible; `noSpeech` is
|
|
118
|
+
* `false` in the fallback. A genuine runtime error is rethrown (so the bot
|
|
119
|
+
* does not silently drop `noSpeech` and re-run its own paid ASR).
|
|
75
120
|
*/
|
|
76
|
-
async text (): Promise<
|
|
121
|
+
async text (): Promise<PUPPET.payloads.VoiceText> {
|
|
77
122
|
log.verbose('Voice', 'text() for id: "%s"', this.id)
|
|
123
|
+
const puppet = this.wechaty.puppet
|
|
124
|
+
// puppet built against an old wechaty-puppet without the method at all
|
|
125
|
+
if (typeof (puppet as { messageVoiceText?: unknown }).messageVoiceText !== 'function') {
|
|
126
|
+
log.verbose('Voice', 'text() messageVoiceText() absent, fallback to messagePayload().text')
|
|
127
|
+
const payload = await puppet.messagePayload(this.id)
|
|
128
|
+
return { text: payload.text || '', noSpeech: false }
|
|
129
|
+
}
|
|
78
130
|
try {
|
|
79
|
-
const payload = await
|
|
131
|
+
const payload = await puppet.messageVoiceText(this.id)
|
|
80
132
|
return payload
|
|
81
133
|
} catch (e) {
|
|
82
|
-
|
|
83
|
-
|
|
134
|
+
if (!isUnsupportedError(e)) {
|
|
135
|
+
throw e
|
|
136
|
+
}
|
|
137
|
+
log.verbose('Voice', 'text() messageVoiceText() unsupported, fallback to messagePayload().text: %s', (e as Error).message)
|
|
138
|
+
const payload = await puppet.messagePayload(this.id)
|
|
84
139
|
return { text: payload.text || '', noSpeech: false }
|
|
85
140
|
}
|
|
86
141
|
}
|