@nanhara/hara 0.122.2 → 0.122.4
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 +108 -0
- package/README.md +15 -2
- package/dist/agent/limits.js +51 -0
- package/dist/agent/loop.js +367 -112
- package/dist/agent/repeat-guard.js +49 -12
- package/dist/checkpoints.js +9 -0
- package/dist/cli.js +2 -1
- package/dist/config.js +10 -2
- package/dist/context/agents-md.js +21 -2
- package/dist/context/mentions.js +84 -1
- package/dist/context/workspace-scope.js +49 -0
- package/dist/cron/deliver.js +61 -38
- package/dist/cron/runner.js +423 -65
- package/dist/cron/schedule.js +23 -7
- package/dist/cron/store.js +709 -43
- package/dist/fs-walk.js +279 -7
- package/dist/fs-write.js +9 -0
- package/dist/gateway/feishu.js +351 -64
- package/dist/gateway/flows-pending.js +31 -17
- package/dist/gateway/flows.js +306 -31
- package/dist/gateway/outbound-files.js +20 -6
- package/dist/gateway/runtime-state.js +1485 -0
- package/dist/gateway/serve.js +550 -242
- package/dist/gateway/sessions.js +3 -3
- package/dist/gateway/telegram.js +279 -29
- package/dist/gateway/tts.js +182 -38
- package/dist/hooks.js +22 -22
- package/dist/index.js +466 -158
- package/dist/memory/store.js +8 -5
- package/dist/org/planner.js +11 -6
- package/dist/org/projects.js +3 -3
- package/dist/process-identity.js +52 -0
- package/dist/providers/bounded-turn.js +42 -0
- package/dist/recall.js +69 -1
- package/dist/runtime.js +24 -0
- package/dist/sandbox.js +54 -4
- package/dist/search/embed.js +13 -2
- package/dist/search/hybrid.js +6 -3
- package/dist/search/semindex.js +87 -5
- package/dist/security/guardian.js +3 -15
- package/dist/security/permissions.js +11 -0
- package/dist/serve/server.js +70 -42
- package/dist/serve/sessions.js +4 -3
- package/dist/sync-sleep.js +46 -0
- package/dist/tools/agent.js +1 -1
- package/dist/tools/ask_user.js +5 -1
- package/dist/tools/builtin.js +5 -2
- package/dist/tools/codebase.js +67 -18
- package/dist/tools/computer.js +149 -68
- package/dist/tools/cron.js +72 -18
- package/dist/tools/edit.js +3 -2
- package/dist/tools/external_agent.js +66 -12
- package/dist/tools/memory.js +25 -7
- package/dist/tools/patch.js +11 -2
- package/dist/tools/registry.js +16 -1
- package/dist/tools/search.js +68 -9
- package/dist/tools/send.js +1 -1
- package/dist/tools/skill.js +1 -1
- package/dist/tools/task.js +3 -3
- package/dist/tools/web.js +43 -13
- package/dist/tui/App.js +93 -25
- package/dist/vision.js +5 -6
- package/package.json +2 -2
- package/runtime-bootstrap.cjs +22 -0
package/dist/gateway/tts.js
CHANGED
|
@@ -11,8 +11,18 @@
|
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import { writeFileSync } from "node:fs";
|
|
14
|
+
import { chmodSync, lstatSync, rmSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { terminateSubprocessTree } from "../security/subprocess-env.js";
|
|
17
|
+
const DEFAULT_TTS_TIMEOUT_MS = 60_000;
|
|
18
|
+
const MAX_TTS_TIMEOUT_MS = 120_000;
|
|
19
|
+
/** TTS cannot opt out of a deadline. Small values remain available for deterministic health checks/tests. */
|
|
20
|
+
export function ttsTimeoutMs(value = process.env.HARA_TTS_TIMEOUT_MS) {
|
|
21
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
22
|
+
return Number.isFinite(parsed) && parsed > 0
|
|
23
|
+
? Math.max(50, Math.min(Math.floor(parsed), MAX_TTS_TIMEOUT_MS))
|
|
24
|
+
: DEFAULT_TTS_TIMEOUT_MS;
|
|
25
|
+
}
|
|
16
26
|
/** Read TTS settings from the environment (fully configurable — nothing hardcoded). */
|
|
17
27
|
export function ttsConfigFromEnv(env = process.env) {
|
|
18
28
|
return {
|
|
@@ -22,6 +32,8 @@ export function ttsConfigFromEnv(env = process.env) {
|
|
|
22
32
|
baseURL: (env.HARA_TTS_BASE_URL || "").trim(),
|
|
23
33
|
apiKey: (env.HARA_TTS_API_KEY || "").trim(),
|
|
24
34
|
cmd: (env.HARA_TTS_CMD || "").trim(),
|
|
35
|
+
// Passing an explicit fixture env must not accidentally fall back to the live process environment.
|
|
36
|
+
timeoutMs: ttsTimeoutMs(env.HARA_TTS_TIMEOUT_MS ?? Number.NaN),
|
|
25
37
|
};
|
|
26
38
|
}
|
|
27
39
|
/** Normalize a reply for speech: collapse whitespace, drop code fences, cap length (long audio is unwanted). */
|
|
@@ -32,38 +44,113 @@ export function ttsCleanText(text, max = 1200) {
|
|
|
32
44
|
.trim()
|
|
33
45
|
.slice(0, max);
|
|
34
46
|
}
|
|
35
|
-
function
|
|
36
|
-
return new
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
function abortReason(signal) {
|
|
48
|
+
return signal.reason instanceof Error ? signal.reason : new Error("TTS cancelled");
|
|
49
|
+
}
|
|
50
|
+
function throwIfAborted(signal) {
|
|
51
|
+
if (signal.aborted)
|
|
52
|
+
throw abortReason(signal);
|
|
53
|
+
}
|
|
54
|
+
/** Race even an API implementation that ignores AbortSignal, while still passing the signal to its fetch. */
|
|
55
|
+
function withAbort(task, signal) {
|
|
56
|
+
if (signal.aborted)
|
|
57
|
+
return Promise.reject(abortReason(signal));
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
let settled = false;
|
|
60
|
+
const finish = (fn) => {
|
|
61
|
+
if (settled)
|
|
62
|
+
return;
|
|
63
|
+
settled = true;
|
|
64
|
+
signal.removeEventListener("abort", onAbort);
|
|
65
|
+
fn();
|
|
66
|
+
};
|
|
67
|
+
const onAbort = () => finish(() => reject(abortReason(signal)));
|
|
68
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
69
|
+
task.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Run local TTS in its own process group. Cancellation escalates TERM→KILL for the whole descendant tree. */
|
|
73
|
+
function run(cmd, args, signal, stdin) {
|
|
74
|
+
throwIfAborted(signal);
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const processGroup = process.platform !== "win32";
|
|
77
|
+
let child;
|
|
78
|
+
try {
|
|
79
|
+
child = spawn(cmd, args, {
|
|
80
|
+
stdio: [stdin != null ? "pipe" : "ignore", "ignore", "ignore"],
|
|
81
|
+
detached: processGroup,
|
|
82
|
+
windowsHide: true,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
resolve(false);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let settled = false;
|
|
90
|
+
let stopping = false;
|
|
91
|
+
let cancelTermination;
|
|
92
|
+
const settle = (ok, error) => {
|
|
93
|
+
if (settled)
|
|
94
|
+
return;
|
|
95
|
+
settled = true;
|
|
96
|
+
signal.removeEventListener("abort", stop);
|
|
97
|
+
// Cancel only the API fallback. If TERM closed the direct child while a descendant survived, the helper's
|
|
98
|
+
// referenced force timer still kills the owned process group after the grace period.
|
|
99
|
+
cancelTermination?.();
|
|
100
|
+
child.stdin?.destroy();
|
|
101
|
+
if (error)
|
|
102
|
+
reject(error);
|
|
103
|
+
else
|
|
104
|
+
resolve(ok);
|
|
105
|
+
};
|
|
106
|
+
const stop = () => {
|
|
107
|
+
if (settled || stopping)
|
|
108
|
+
return;
|
|
109
|
+
stopping = true;
|
|
110
|
+
cancelTermination = terminateSubprocessTree(child, {
|
|
111
|
+
processGroup,
|
|
112
|
+
graceMs: 250,
|
|
113
|
+
fallbackMs: 250,
|
|
114
|
+
onFallback: () => settle(false, abortReason(signal)),
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
child.once("error", () => settle(false, stopping ? abortReason(signal) : undefined));
|
|
118
|
+
child.once("close", (code) => settle(code === 0, stopping ? abortReason(signal) : undefined));
|
|
119
|
+
child.stdin?.on("error", () => { });
|
|
120
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
121
|
+
if (signal.aborted)
|
|
122
|
+
stop();
|
|
123
|
+
if (stdin != null && !stopping)
|
|
124
|
+
child.stdin?.end(stdin);
|
|
42
125
|
});
|
|
43
126
|
}
|
|
44
127
|
// local: macOS `say` → m4a (AAC). Zero config; HARA_TTS_VOICE picks the voice (default Tingting / zh).
|
|
45
|
-
async function sayTts(text, out, cfg) {
|
|
46
|
-
return run("say", ["-v", cfg.voice || "Tingting", "-o", out, "--file-format", "m4af", "--data-format", "aac", text]);
|
|
128
|
+
async function sayTts(text, out, cfg, signal) {
|
|
129
|
+
return run("say", ["-v", cfg.voice || "Tingting", "-o", out, "--file-format", "m4af", "--data-format", "aac", text], signal);
|
|
47
130
|
}
|
|
48
131
|
// API: OpenAI-compatible /audio/speech. Works against OpenAI, Aliyun DashScope (compatible-mode), or a local
|
|
49
132
|
// server exposing the same shape — set HARA_TTS_BASE_URL + HARA_TTS_API_KEY + HARA_TTS_MODEL + HARA_TTS_VOICE.
|
|
50
|
-
async function openaiTts(text, out, cfg) {
|
|
133
|
+
async function openaiTts(text, out, cfg, signal) {
|
|
51
134
|
if (!cfg.apiKey && !cfg.baseURL)
|
|
52
135
|
return false; // not configured → unavailable
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
136
|
+
return withAbort((async () => {
|
|
137
|
+
const { default: OpenAI } = (await import("openai"));
|
|
138
|
+
throwIfAborted(signal);
|
|
139
|
+
const client = new OpenAI({ baseURL: cfg.baseURL || undefined, apiKey: cfg.apiKey || "x" });
|
|
140
|
+
const res = await client.audio.speech.create({ model: cfg.model || "tts-1", voice: cfg.voice || "alloy", input: text }, { signal });
|
|
141
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
142
|
+
throwIfAborted(signal);
|
|
143
|
+
if (!buf.length)
|
|
144
|
+
return false;
|
|
145
|
+
writeFileSync(out, buf, { flag: "wx", mode: 0o600 });
|
|
146
|
+
return true;
|
|
147
|
+
})(), signal);
|
|
61
148
|
}
|
|
62
149
|
// local: a configurable command (e.g. VoxCPM). `{out}` → output path; the text is piped on stdin.
|
|
63
|
-
async function cmdTts(text, out, cfg) {
|
|
150
|
+
async function cmdTts(text, out, cfg, signal) {
|
|
64
151
|
if (!cfg.cmd)
|
|
65
152
|
return false;
|
|
66
|
-
return run("sh", ["-c", cfg.cmd.replace(/\{out\}/g, out)], text);
|
|
153
|
+
return run("sh", ["-c", cfg.cmd.replace(/\{out\}/g, out)], signal, text);
|
|
67
154
|
}
|
|
68
155
|
const PROVIDERS = {
|
|
69
156
|
say: sayTts,
|
|
@@ -71,30 +158,87 @@ const PROVIDERS = {
|
|
|
71
158
|
cmd: cmdTts,
|
|
72
159
|
};
|
|
73
160
|
const EXT = { say: "m4a", openai: "mp3", cmd: "wav" };
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
161
|
+
function isAbortSignal(value) {
|
|
162
|
+
return Boolean(value && typeof value.addEventListener === "function" && typeof value.aborted === "boolean");
|
|
163
|
+
}
|
|
164
|
+
function safeProviderError(error, cfg) {
|
|
165
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
for (const [secret, replacement] of [[cfg.apiKey, "[redacted]"], [cfg.baseURL, "[redacted-url]"]]) {
|
|
167
|
+
if (secret)
|
|
168
|
+
message = message.split(secret).join(replacement);
|
|
169
|
+
}
|
|
170
|
+
return message;
|
|
171
|
+
}
|
|
172
|
+
function secureCompletedAudio(path) {
|
|
173
|
+
try {
|
|
174
|
+
const stat = lstatSync(path);
|
|
175
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size <= 0)
|
|
176
|
+
return false;
|
|
177
|
+
chmodSync(path, 0o600);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Synthesize `text` to a temp audio file; returns its path, or null on provider/deadline failure. The legacy
|
|
185
|
+
* `synthesize(text, config)` form remains valid; gateway callers use `synthesize(text, signal, config?)`. */
|
|
186
|
+
export async function synthesize(text, signalOrConfig, explicitConfig) {
|
|
187
|
+
const parentSignal = isAbortSignal(signalOrConfig) ? signalOrConfig : undefined;
|
|
188
|
+
if (parentSignal?.aborted)
|
|
189
|
+
throw abortReason(parentSignal);
|
|
77
190
|
const clean = ttsCleanText(text);
|
|
78
191
|
if (!clean)
|
|
79
192
|
return null;
|
|
193
|
+
const cfg = isAbortSignal(signalOrConfig)
|
|
194
|
+
? (explicitConfig ?? ttsConfigFromEnv())
|
|
195
|
+
: (explicitConfig ?? signalOrConfig ?? ttsConfigFromEnv());
|
|
196
|
+
const deadline = new AbortController();
|
|
197
|
+
const timeoutMs = ttsTimeoutMs(cfg.timeoutMs);
|
|
198
|
+
const timeoutError = new Error(`TTS timed out after ${timeoutMs}ms`);
|
|
199
|
+
const timeout = setTimeout(() => deadline.abort(timeoutError), timeoutMs);
|
|
200
|
+
const cancel = () => deadline.abort(parentSignal?.reason instanceof Error ? parentSignal.reason : new Error("TTS cancelled"));
|
|
201
|
+
parentSignal?.addEventListener("abort", cancel, { once: true });
|
|
80
202
|
const provider = PROVIDERS[cfg.provider] ? cfg.provider : "say";
|
|
81
|
-
const out = join(tmpdir(), `hara-tts-${randomUUID()
|
|
203
|
+
const out = join(tmpdir(), `hara-tts-${randomUUID()}.${EXT[provider] || "wav"}`);
|
|
82
204
|
try {
|
|
83
|
-
if (await PROVIDERS[provider](clean, out, cfg))
|
|
84
|
-
return out;
|
|
85
|
-
}
|
|
86
|
-
catch (e) {
|
|
87
|
-
console.error(`tts(${provider}): ${e?.message ?? e}`);
|
|
88
|
-
}
|
|
89
|
-
if (provider !== "say") {
|
|
90
205
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return o2; // graceful fallback to local
|
|
206
|
+
if (await PROVIDERS[provider](clean, out, cfg, deadline.signal) && secureCompletedAudio(out))
|
|
207
|
+
return out;
|
|
94
208
|
}
|
|
95
|
-
catch {
|
|
96
|
-
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (parentSignal?.aborted)
|
|
211
|
+
throw abortReason(parentSignal);
|
|
212
|
+
if (deadline.signal.aborted) {
|
|
213
|
+
console.error(`tts(${provider}): ${timeoutError.message}`);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
console.error(`tts(${provider}): ${safeProviderError(error, cfg)}`);
|
|
97
217
|
}
|
|
218
|
+
rmSync(out, { force: true });
|
|
219
|
+
if (provider !== "say" && !deadline.signal.aborted) {
|
|
220
|
+
const fallback = join(tmpdir(), `hara-tts-${randomUUID()}.m4a`);
|
|
221
|
+
try {
|
|
222
|
+
if (await sayTts(clean, fallback, cfg, deadline.signal) && secureCompletedAudio(fallback))
|
|
223
|
+
return fallback; // graceful local fallback
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (parentSignal?.aborted)
|
|
227
|
+
throw abortReason(parentSignal);
|
|
228
|
+
if (deadline.signal.aborted)
|
|
229
|
+
console.error(`tts(say): ${timeoutError.message}`);
|
|
230
|
+
else
|
|
231
|
+
console.error(`tts(say): ${safeProviderError(error, cfg)}`);
|
|
232
|
+
}
|
|
233
|
+
rmSync(fallback, { force: true });
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
clearTimeout(timeout);
|
|
239
|
+
parentSignal?.removeEventListener("abort", cancel);
|
|
240
|
+
// Returned paths are owned by the caller. Every other partial/late provider output is removed here.
|
|
241
|
+
if (deadline.signal.aborted)
|
|
242
|
+
rmSync(out, { force: true });
|
|
98
243
|
}
|
|
99
|
-
return null;
|
|
100
244
|
}
|
package/dist/hooks.js
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
// PreToolUse runs BEFORE a tool: non-zero, timeout, signal, or launch failure BLOCKS the call.
|
|
3
3
|
// PostToolUse runs AFTER: observe-only (format, log, notify). Configured in config.json `hooks` + contributed
|
|
4
4
|
// by plugins. The command receives {tool, payload} as JSON on stdin + HARA_TOOL_NAME in the env.
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
5
|
import { resolve } from "node:path";
|
|
7
6
|
import { loadConfig } from "./config.js";
|
|
8
7
|
import { pluginHooks } from "./plugins/plugins.js";
|
|
9
|
-
import { redactToolSubprocessOutput
|
|
10
|
-
import {
|
|
8
|
+
import { redactToolSubprocessOutput } from "./security/subprocess-env.js";
|
|
9
|
+
import { runShell } from "./sandbox.js";
|
|
11
10
|
const cache = new Map();
|
|
12
11
|
export function resetHooksCache() {
|
|
13
12
|
cache.clear();
|
|
@@ -42,41 +41,42 @@ export function hasHooks(cwd = process.cwd()) {
|
|
|
42
41
|
return !!(h.PreToolUse?.length || h.PostToolUse?.length);
|
|
43
42
|
}
|
|
44
43
|
/** Run hooks for an event matching `toolName`. Any abnormal PreToolUse termination fails closed;
|
|
45
|
-
* PostToolUse remains observe-only.
|
|
46
|
-
|
|
44
|
+
* PostToolUse remains observe-only. Every child owns a cancellable process group; no hook may hold the
|
|
45
|
+
* event loop past the parent run deadline, and an aborted batch never starts its next command. */
|
|
46
|
+
export async function runHooks(event, toolName, payload, cwd, timeoutMs = 30_000, signal) {
|
|
47
47
|
for (const h of merged(cwd)[event] ?? []) {
|
|
48
|
+
if (signal?.aborted) {
|
|
49
|
+
return event === "PreToolUse"
|
|
50
|
+
? { block: true, message: "⛔ blocked because the agent run was cancelled before the PreToolUse hook completed" }
|
|
51
|
+
: { block: false, message: "" };
|
|
52
|
+
}
|
|
48
53
|
if (!matches(h.matcher, toolName))
|
|
49
54
|
continue;
|
|
50
|
-
let r;
|
|
51
55
|
try {
|
|
52
56
|
// Hooks are user/plugin-configured external code. Route them through the same command preflight and
|
|
53
57
|
// macOS protected-read mask as Bash, even though hooks remain observe-only after a tool has run.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
cwd,
|
|
57
|
-
input: JSON.stringify({ tool: toolName, payload }),
|
|
58
|
-
encoding: "utf8",
|
|
58
|
+
await runShell(h.command, cwd, "off", {
|
|
59
|
+
signal,
|
|
59
60
|
timeout: timeoutMs,
|
|
60
|
-
|
|
61
|
+
maxBuffer: 256 * 1024,
|
|
62
|
+
input: JSON.stringify({ tool: toolName, payload }),
|
|
63
|
+
env: { HARA_TOOL_NAME: toolName },
|
|
61
64
|
});
|
|
62
65
|
}
|
|
63
66
|
catch (error) {
|
|
64
67
|
if (event === "PreToolUse") {
|
|
65
|
-
const
|
|
66
|
-
|
|
68
|
+
const failure = error;
|
|
69
|
+
const output = redactToolSubprocessOutput(`${failure.stdout ?? ""}${failure.stderr ?? ""}`.trim());
|
|
70
|
+
const detail = redactToolSubprocessOutput(failure.message || String(error));
|
|
71
|
+
return {
|
|
72
|
+
block: true,
|
|
73
|
+
message: `⛔ blocked by a PreToolUse hook${output ? `: ${output}` : detail ? ` (${detail})` : ""}`,
|
|
74
|
+
};
|
|
67
75
|
}
|
|
68
76
|
// PostToolUse is best-effort: a policy-blocked or broken observer must never rewrite the result of a
|
|
69
77
|
// tool that already completed. In particular, do not retry it through an unsandboxed fallback shell.
|
|
70
78
|
continue;
|
|
71
79
|
}
|
|
72
|
-
// A hook is allowed to ignore stdin. On Linux, a command that exits successfully before Node finishes
|
|
73
|
-
// writing `input` can report EPIPE in r.error *alongside status=0*. The wait status is authoritative in
|
|
74
|
-
// that case; true launch/write failures still have status=null, while timeouts/signals remain blocked.
|
|
75
|
-
if (event === "PreToolUse" && (r.status !== 0 || !!r.signal)) {
|
|
76
|
-
const output = redactToolSubprocessOutput((String(r.stdout ?? "") + String(r.stderr ?? "")).trim());
|
|
77
|
-
const failure = redactToolSubprocessOutput(r.error?.message || (r.signal ? `terminated by ${r.signal}` : `exit ${r.status ?? "unknown"}`));
|
|
78
|
-
return { block: true, message: `⛔ blocked by a PreToolUse hook${output ? `: ${output}` : ` (${failure})`}` };
|
|
79
|
-
}
|
|
80
80
|
}
|
|
81
81
|
return { block: false, message: "" };
|
|
82
82
|
}
|