rol-websocket-channel 1.1.2 → 1.1.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/MQTT-API /{351/205/215/345/257/271/344/277/256/346/224/271.md" → 346/226/260/345/242/236/346/226/207/344/273/266/345/212/237/350/203/275.md" } +2 -2
- package/index.ts +44 -2
- package/package.json +1 -1
- package/src/admin/methods/sessions-extended.ts +12 -4
- package/src/admin/methods/sessions.ts +26 -7
- package/src/admin/methods/skills.ts +49 -11
- package/src/admin/methods/system.ts +5 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# OpenClaw MQTT API 文档
|
|
1
|
+
# OpenClaw MQTT API 文档 (新增文件功能)
|
|
2
2
|
|
|
3
3
|
## 消息格式
|
|
4
4
|
|
|
@@ -1671,7 +1671,7 @@ openclaw admin-bridge pair <key> --endpoint https://api.deotaland.ai
|
|
|
1671
1671
|
|
|
1672
1672
|
MQTT 请求示例:
|
|
1673
1673
|
|
|
1674
|
-
|
|
1674
|
+
```json
|
|
1675
1675
|
{
|
|
1676
1676
|
"type": "artifactsList",
|
|
1677
1677
|
"trace_id": "artifact-list-before-download-001",
|
package/index.ts
CHANGED
|
@@ -507,7 +507,15 @@ async function handleIncomingMessage(
|
|
|
507
507
|
timestamp: Date.now(),
|
|
508
508
|
};
|
|
509
509
|
|
|
510
|
-
|
|
510
|
+
// 根据 source_type 修改 topic 末尾的 #
|
|
511
|
+
let targetTopic = mqttTopic;
|
|
512
|
+
const sourceType = innerData?.source_type;
|
|
513
|
+
if (targetTopic.endsWith("#")) {
|
|
514
|
+
const replacement = sourceType === "device" ? "device" : "bot";
|
|
515
|
+
targetTopic = targetTopic.slice(0, -1) + replacement;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
conn.ws.publish(targetTopic, JSON.stringify(replyMessage));
|
|
511
519
|
},
|
|
512
520
|
onError: (err: Error) => {
|
|
513
521
|
log?.error(`[rol-websocket-channel] Delivery error: ${err.message}`);
|
|
@@ -528,6 +536,7 @@ async function handleCustomMessageType(
|
|
|
528
536
|
accountId: string,
|
|
529
537
|
mqttTopic: string,
|
|
530
538
|
): Promise<void> {
|
|
539
|
+
const isSkillInstallFlow = msgType === "skillsInstallFromClawHub" || msgType === "skillsUpdateFromClawHub";
|
|
531
540
|
const response: any = {
|
|
532
541
|
type: "receiver",
|
|
533
542
|
trace_id: traceId,
|
|
@@ -535,6 +544,12 @@ async function handleCustomMessageType(
|
|
|
535
544
|
timestamp: Date.now(),
|
|
536
545
|
};
|
|
537
546
|
|
|
547
|
+
if (isSkillInstallFlow) {
|
|
548
|
+
console.log(
|
|
549
|
+
`[rol-websocket-channel] custom message start: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, accountId=${accountId}, topic=${mqttTopic}`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
538
553
|
const handlerMethod = (messageHandler as any)[msgType];
|
|
539
554
|
if (typeof handlerMethod === "function") {
|
|
540
555
|
try {
|
|
@@ -553,15 +568,34 @@ async function handleCustomMessageType(
|
|
|
553
568
|
response.data = methodResult.result;
|
|
554
569
|
if (!methodResult.ok) {
|
|
555
570
|
response.error = methodResult.error?.message || "Unknown error";
|
|
571
|
+
if (isSkillInstallFlow) {
|
|
572
|
+
console.error(
|
|
573
|
+
`[rol-websocket-channel] custom message failed: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, error=${response.error}, detail=${JSON.stringify(methodResult.error?.data ?? {})}`,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
} else if (isSkillInstallFlow) {
|
|
577
|
+
console.log(
|
|
578
|
+
`[rol-websocket-channel] custom message success: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}`,
|
|
579
|
+
);
|
|
556
580
|
}
|
|
557
581
|
} else {
|
|
558
582
|
// 旧格式:直接返回数据
|
|
559
583
|
response.success = true;
|
|
560
584
|
response.data = methodResult;
|
|
585
|
+
if (isSkillInstallFlow) {
|
|
586
|
+
console.log(
|
|
587
|
+
`[rol-websocket-channel] custom message success: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
561
590
|
}
|
|
562
591
|
} catch (handlerErr: any) {
|
|
563
592
|
response.success = false;
|
|
564
593
|
response.error = handlerErr.message;
|
|
594
|
+
if (isSkillInstallFlow) {
|
|
595
|
+
console.error(
|
|
596
|
+
`[rol-websocket-channel] custom message threw: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, error=${handlerErr?.message ?? String(handlerErr)}`,
|
|
597
|
+
);
|
|
598
|
+
}
|
|
565
599
|
}
|
|
566
600
|
} else {
|
|
567
601
|
response.success = false;
|
|
@@ -570,7 +604,15 @@ async function handleCustomMessageType(
|
|
|
570
604
|
|
|
571
605
|
const conn = ConnectionManager.getGlobalConnection();
|
|
572
606
|
if (conn && conn.ws && conn.ws.connected) {
|
|
573
|
-
|
|
607
|
+
// 根据 source_type 修改 topic 末尾的 #
|
|
608
|
+
let targetTopic = mqttTopic;
|
|
609
|
+
const sourceType = innerData?.source_type;
|
|
610
|
+
if (targetTopic.endsWith("#")) {
|
|
611
|
+
const replacement = sourceType === "device" ? "device" : "bot";
|
|
612
|
+
targetTopic = targetTopic.slice(0, -1) + replacement;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
conn.ws.publish(targetTopic, JSON.stringify(response));
|
|
574
616
|
}
|
|
575
617
|
}
|
|
576
618
|
|
package/package.json
CHANGED
|
@@ -21,6 +21,9 @@ export const getSession: MethodHandler = async (
|
|
|
21
21
|
context
|
|
22
22
|
): Promise<JsonValue> => {
|
|
23
23
|
const objectParams = isObject(params) ? params : {};
|
|
24
|
+
const agentId = typeof objectParams.agentId === 'string' && objectParams.agentId.trim()
|
|
25
|
+
? objectParams.agentId.trim()
|
|
26
|
+
: undefined;
|
|
24
27
|
const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
|
|
25
28
|
const requestedLimit = typeof objectParams.limit === 'number' ? objectParams.limit : MAX_SESSION_MESSAGES;
|
|
26
29
|
const requestedOffset = typeof objectParams.offset === 'number' ? objectParams.offset : 0;
|
|
@@ -34,11 +37,11 @@ export const getSession: MethodHandler = async (
|
|
|
34
37
|
);
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
const session = await findSessionRecord(context, sessionId);
|
|
40
|
+
const session = await findSessionRecord(context, sessionId, agentId);
|
|
38
41
|
if (!session) {
|
|
39
42
|
throw new JsonRpcException(
|
|
40
43
|
JSON_RPC_ERRORS.invalidParams,
|
|
41
|
-
`Session not found: ${sessionId}`
|
|
44
|
+
agentId ? `Session not found: ${sessionId} for agent ${agentId}` : `Session not found: ${sessionId}`
|
|
42
45
|
);
|
|
43
46
|
}
|
|
44
47
|
|
|
@@ -52,6 +55,7 @@ export const getSession: MethodHandler = async (
|
|
|
52
55
|
const messages = await readSessionMessages(sessionFile, limit, offset);
|
|
53
56
|
|
|
54
57
|
return {
|
|
58
|
+
agentId: session.agentId,
|
|
55
59
|
agentName: session.agentName,
|
|
56
60
|
sessionId: session.sessionId ?? sessionId,
|
|
57
61
|
sessionKey: session.sessionKey,
|
|
@@ -76,6 +80,9 @@ export const prepareMessage: MethodHandler = async (
|
|
|
76
80
|
context
|
|
77
81
|
): Promise<JsonValue> => {
|
|
78
82
|
const objectParams = isObject(params) ? params : {};
|
|
83
|
+
const agentId = typeof objectParams.agentId === 'string' && objectParams.agentId.trim()
|
|
84
|
+
? objectParams.agentId.trim()
|
|
85
|
+
: undefined;
|
|
79
86
|
const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
|
|
80
87
|
const message = typeof objectParams.message === 'string' ? objectParams.message : null;
|
|
81
88
|
const attachedSkills = Array.isArray(objectParams.attachedSkills) ? objectParams.attachedSkills : [];
|
|
@@ -94,15 +101,16 @@ export const prepareMessage: MethodHandler = async (
|
|
|
94
101
|
);
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
const session = await findSessionRecord(context, sessionId);
|
|
104
|
+
const session = await findSessionRecord(context, sessionId, agentId);
|
|
98
105
|
if (!session) {
|
|
99
106
|
throw new JsonRpcException(
|
|
100
107
|
JSON_RPC_ERRORS.invalidParams,
|
|
101
|
-
`Session not found: ${sessionId}`
|
|
108
|
+
agentId ? `Session not found: ${sessionId} for agent ${agentId}` : `Session not found: ${sessionId}`
|
|
102
109
|
);
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
const messageData: any = {
|
|
113
|
+
agentId: session.agentId,
|
|
106
114
|
agentName: session.agentName,
|
|
107
115
|
sessionId: session.sessionId ?? sessionId,
|
|
108
116
|
sessionKey: session.sessionKey,
|
|
@@ -26,6 +26,7 @@ interface SessionsIndexEntry {
|
|
|
26
26
|
type SessionsIndex = Record<string, SessionsIndexEntry>;
|
|
27
27
|
|
|
28
28
|
export interface AgentSessionRecord {
|
|
29
|
+
agentId: string;
|
|
29
30
|
agentName: string;
|
|
30
31
|
sessionKey: string;
|
|
31
32
|
sessionId: string | null;
|
|
@@ -41,17 +42,24 @@ export interface AgentSessionRecord {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export const listSessions: MethodHandler = async (
|
|
44
|
-
|
|
45
|
+
params,
|
|
45
46
|
context
|
|
46
47
|
): Promise<JsonValue> => {
|
|
47
|
-
const
|
|
48
|
+
const objectParams = isObject(params) ? params : {};
|
|
49
|
+
const agentId = typeof objectParams.agentId === 'string' && objectParams.agentId.trim()
|
|
50
|
+
? objectParams.agentId.trim()
|
|
51
|
+
: undefined;
|
|
52
|
+
const items = await listAllAgentSessions(context, agentId);
|
|
48
53
|
return {
|
|
49
54
|
count: items.length,
|
|
50
55
|
items
|
|
51
56
|
};
|
|
52
57
|
};
|
|
53
58
|
|
|
54
|
-
export async function listAllAgentSessions(
|
|
59
|
+
export async function listAllAgentSessions(
|
|
60
|
+
context: MethodContext,
|
|
61
|
+
agentId?: string
|
|
62
|
+
): Promise<AgentSessionRecord[]> {
|
|
55
63
|
const agentsRoot = path.join(context.openclawRoot, 'agents');
|
|
56
64
|
if (!(await pathExists(agentsRoot))) {
|
|
57
65
|
return [];
|
|
@@ -66,6 +74,10 @@ export async function listAllAgentSessions(context: MethodContext): Promise<Agen
|
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
const agentName = agentEntry.name;
|
|
77
|
+
if (agentId && agentName !== agentId) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
const sessionsPath = path.join(agentsRoot, agentName, 'sessions', 'sessions.json');
|
|
70
82
|
if (!(await pathExists(sessionsPath))) {
|
|
71
83
|
continue;
|
|
@@ -75,6 +87,7 @@ export async function listAllAgentSessions(context: MethodContext): Promise<Agen
|
|
|
75
87
|
for (const [sessionKey, entry] of Object.entries(sessions)) {
|
|
76
88
|
const sessionId = entry.sessionId ?? extractSessionIdFromFile(entry.sessionFile) ?? null;
|
|
77
89
|
items.push({
|
|
90
|
+
agentId: agentName,
|
|
78
91
|
agentName,
|
|
79
92
|
sessionKey,
|
|
80
93
|
sessionId,
|
|
@@ -98,20 +111,26 @@ export async function listAllAgentSessions(context: MethodContext): Promise<Agen
|
|
|
98
111
|
|
|
99
112
|
export async function findSessionRecord(
|
|
100
113
|
context: MethodContext,
|
|
101
|
-
sessionIdOrKey: string
|
|
114
|
+
sessionIdOrKey: string,
|
|
115
|
+
agentId?: string
|
|
102
116
|
): Promise<AgentSessionRecord | null> {
|
|
103
|
-
const sessions = await listAllAgentSessions(context);
|
|
117
|
+
const sessions = await listAllAgentSessions(context, agentId);
|
|
104
118
|
return sessions.find((item) => item.sessionId === sessionIdOrKey || item.sessionKey === sessionIdOrKey) ?? null;
|
|
105
119
|
}
|
|
106
120
|
|
|
107
121
|
export async function resolveSessionFile(
|
|
108
122
|
context: MethodContext,
|
|
109
|
-
sessionIdOrKey: string
|
|
123
|
+
sessionIdOrKey: string,
|
|
124
|
+
agentId?: string
|
|
110
125
|
): Promise<string | null> {
|
|
111
|
-
const session = await findSessionRecord(context, sessionIdOrKey);
|
|
126
|
+
const session = await findSessionRecord(context, sessionIdOrKey, agentId);
|
|
112
127
|
return session?.sessionFilePath ?? null;
|
|
113
128
|
}
|
|
114
129
|
|
|
130
|
+
function isObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
|
|
131
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
|
|
115
134
|
function extractSessionIdFromFile(sessionFile: string | undefined): string | null {
|
|
116
135
|
if (!sessionFile) {
|
|
117
136
|
return null;
|
|
@@ -143,7 +143,7 @@ export const searchClawHubSkills: MethodHandler = async (params, context): Promi
|
|
|
143
143
|
const objectParams = isObject(params) ? params : {};
|
|
144
144
|
const query = typeof objectParams.query === 'string' ? objectParams.query.trim() : '';
|
|
145
145
|
const args = query ? ['skills', 'search', query, '--json'] : ['skills', 'search', '--json'];
|
|
146
|
-
const result = await runOpenClawSkillCommand(args, context.
|
|
146
|
+
const result = await runOpenClawSkillCommand(args, context.openclawRoot, context.openclawRoot);
|
|
147
147
|
|
|
148
148
|
return {
|
|
149
149
|
ok: true,
|
|
@@ -155,7 +155,7 @@ export const searchClawHubSkills: MethodHandler = async (params, context): Promi
|
|
|
155
155
|
export const installSkillFromClawHub: MethodHandler = async (params, context): Promise<JsonValue> => {
|
|
156
156
|
const objectParams = expectObject(params) as unknown as ClawHubSkillParams;
|
|
157
157
|
const slug = expectString(objectParams.slug, 'slug');
|
|
158
|
-
const result = await runOpenClawSkillCommand(['skills', 'install', slug], context.
|
|
158
|
+
const result = await runOpenClawSkillCommand(['skills', 'install', slug], context.openclawRoot, context.openclawRoot);
|
|
159
159
|
|
|
160
160
|
return {
|
|
161
161
|
ok: true,
|
|
@@ -167,7 +167,7 @@ export const installSkillFromClawHub: MethodHandler = async (params, context): P
|
|
|
167
167
|
export const updateSkillFromClawHub: MethodHandler = async (params, context): Promise<JsonValue> => {
|
|
168
168
|
const objectParams = expectObject(params) as unknown as ClawHubSkillParams;
|
|
169
169
|
const slug = expectString(objectParams.slug, 'slug');
|
|
170
|
-
const result = await runOpenClawSkillCommand(['skills', 'update', slug], context.
|
|
170
|
+
const result = await runOpenClawSkillCommand(['skills', 'update', slug], context.openclawRoot, context.openclawRoot);
|
|
171
171
|
|
|
172
172
|
return {
|
|
173
173
|
ok: true,
|
|
@@ -233,27 +233,42 @@ async function npmPack(packageSpec: string, cwd: string): Promise<string> {
|
|
|
233
233
|
|
|
234
234
|
async function runOpenClawSkillCommand(
|
|
235
235
|
args: string[],
|
|
236
|
-
cwd: string
|
|
236
|
+
cwd: string,
|
|
237
|
+
openclawRoot?: string
|
|
237
238
|
): Promise<{ stdout: string; stderr: string; parsed: JsonValue | null }> {
|
|
238
239
|
const command = process.env.OPENCLAW_BIN || 'openclaw';
|
|
239
|
-
const options = buildOpenClawExecOptions(cwd);
|
|
240
|
+
const options = buildOpenClawExecOptions(cwd, openclawRoot);
|
|
241
|
+
const openclawBin = process.env.OPENCLAW_BIN || '';
|
|
242
|
+
const openclawHome = options.env?.OPENCLAW_HOME || '';
|
|
243
|
+
|
|
244
|
+
console.log(
|
|
245
|
+
`[skills] exec start: command=${command}, args=${JSON.stringify(args)}, cwd=${cwd}, OPENCLAW_BIN=${openclawBin}, OPENCLAW_HOME=${openclawHome}`
|
|
246
|
+
);
|
|
240
247
|
|
|
241
248
|
try {
|
|
242
249
|
const { stdout, stderr } = await execFileAsync(command, args, options);
|
|
250
|
+
console.log(
|
|
251
|
+
`[skills] exec success: command=${command}, args=${JSON.stringify(args)}, stdoutLength=${stdout.length}, stderrLength=${stderr.length}`
|
|
252
|
+
);
|
|
243
253
|
return {
|
|
244
254
|
stdout,
|
|
245
255
|
stderr,
|
|
246
256
|
parsed: parseJsonOutput(stdout)
|
|
247
257
|
};
|
|
248
258
|
} catch (err: any) {
|
|
259
|
+
const stdout = typeof err?.stdout === 'string' ? err.stdout : '';
|
|
260
|
+
const stderr = typeof err?.stderr === 'string' ? err.stderr : '';
|
|
261
|
+
console.error(
|
|
262
|
+
`[skills] exec failed: command=${command}, args=${JSON.stringify(args)}, cwd=${cwd}, OPENCLAW_BIN=${openclawBin}, OPENCLAW_HOME=${openclawHome}, stdout=${JSON.stringify(stdout)}, stderr=${JSON.stringify(stderr)}`
|
|
263
|
+
);
|
|
249
264
|
throw new JsonRpcException(
|
|
250
265
|
JSON_RPC_ERRORS.internalError,
|
|
251
266
|
`OpenClaw skill command failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
252
267
|
{
|
|
253
268
|
command,
|
|
254
269
|
args,
|
|
255
|
-
stdout
|
|
256
|
-
stderr
|
|
270
|
+
stdout,
|
|
271
|
+
stderr
|
|
257
272
|
}
|
|
258
273
|
);
|
|
259
274
|
}
|
|
@@ -274,7 +289,7 @@ function parseJsonOutput(stdout: string): JsonValue | null {
|
|
|
274
289
|
|
|
275
290
|
async function queryOpenClawSkills(context: MethodContext): Promise<unknown[]> {
|
|
276
291
|
const command = process.env.OPENCLAW_BIN || 'openclaw';
|
|
277
|
-
const options = buildOpenClawExecOptions(context.
|
|
292
|
+
const options = buildOpenClawExecOptions(context.openclawRoot, context.openclawRoot);
|
|
278
293
|
|
|
279
294
|
try {
|
|
280
295
|
const { stdout } = await execFileAsync(command, ['skills', 'list', '--json'], options);
|
|
@@ -300,15 +315,38 @@ async function queryOpenClawSkills(context: MethodContext): Promise<unknown[]> {
|
|
|
300
315
|
}
|
|
301
316
|
}
|
|
302
317
|
|
|
303
|
-
function buildOpenClawExecOptions(
|
|
318
|
+
function buildOpenClawExecOptions(
|
|
319
|
+
cwd: string,
|
|
320
|
+
openclawRoot?: string
|
|
321
|
+
): { cwd: string; shell?: boolean; env: NodeJS.ProcessEnv } {
|
|
322
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
323
|
+
const openclawHome = resolveOpenClawHomeForCli(openclawRoot);
|
|
324
|
+
if (openclawHome) {
|
|
325
|
+
env.OPENCLAW_HOME = openclawHome;
|
|
326
|
+
}
|
|
327
|
+
|
|
304
328
|
if (process.platform === 'win32') {
|
|
305
329
|
return {
|
|
306
330
|
cwd,
|
|
307
|
-
shell: true
|
|
331
|
+
shell: true,
|
|
332
|
+
env
|
|
308
333
|
};
|
|
309
334
|
}
|
|
310
335
|
|
|
311
|
-
return { cwd };
|
|
336
|
+
return { cwd, env };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function resolveOpenClawHomeForCli(openclawRoot?: string): string | undefined {
|
|
340
|
+
if (!openclawRoot) {
|
|
341
|
+
return process.env.OPENCLAW_HOME;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const resolvedRoot = path.resolve(openclawRoot);
|
|
345
|
+
if (path.basename(resolvedRoot) === '.openclaw') {
|
|
346
|
+
return path.dirname(resolvedRoot);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return process.env.OPENCLAW_HOME;
|
|
312
350
|
}
|
|
313
351
|
|
|
314
352
|
function normalizeCliSkill(
|
|
@@ -136,9 +136,9 @@ export const logs: MethodHandler = async (params: any, context: MethodContext):
|
|
|
136
136
|
return { ok: false, error: `No .log files found in directory: ${logDir}` };
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
const limit =
|
|
139
|
+
const limit = 10;
|
|
140
140
|
const maxBytes = params?.maxBytes ?? 250000;
|
|
141
|
-
|
|
141
|
+
const offset = undefined;
|
|
142
142
|
|
|
143
143
|
// 获取所有候选日志的详细信息并排序(对应 ls -t)
|
|
144
144
|
const fileStats = await Promise.all(
|
|
@@ -180,7 +180,9 @@ export const logs: MethodHandler = async (params: any, context: MethodContext):
|
|
|
180
180
|
const content = buffer.toString('utf-8');
|
|
181
181
|
const rawLines = content.split(/\r?\n/).filter(line => line.trim().length > 0);
|
|
182
182
|
|
|
183
|
-
const resultLines = typeof offset === 'number'
|
|
183
|
+
const resultLines = typeof offset === 'number'
|
|
184
|
+
? rawLines
|
|
185
|
+
: rawLines.slice(-limit).reverse();
|
|
184
186
|
|
|
185
187
|
return {
|
|
186
188
|
ok: true,
|