aamp-openclaw-plugin 0.1.19 → 0.1.21
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/bin/aamp-openclaw-plugin.mjs +174 -42
- package/dist/file-store.js +35 -9
- package/dist/file-store.js.map +3 -3
- package/dist/index.js +316 -14989
- package/dist/index.js.map +4 -4
- package/openclaw.plugin.json +1 -0
- package/package.json +4 -3
|
@@ -11,6 +11,36 @@ import { fileURLToPath } from 'node:url'
|
|
|
11
11
|
const PLUGIN_ID = 'aamp-openclaw-plugin'
|
|
12
12
|
const DEFAULT_AAMP_HOST = 'https://meshmail.ai'
|
|
13
13
|
const DEFAULT_CREDENTIALS_FILE = '~/.openclaw/extensions/aamp-openclaw-plugin/.credentials.json'
|
|
14
|
+
const CODING_TOOL_ALLOWLIST = [
|
|
15
|
+
'read',
|
|
16
|
+
'write',
|
|
17
|
+
'edit',
|
|
18
|
+
'apply_patch',
|
|
19
|
+
'exec',
|
|
20
|
+
'process',
|
|
21
|
+
'web_search',
|
|
22
|
+
'web_fetch',
|
|
23
|
+
'memory_search',
|
|
24
|
+
'memory_get',
|
|
25
|
+
'sessions_list',
|
|
26
|
+
'sessions_history',
|
|
27
|
+
'sessions_send',
|
|
28
|
+
'sessions_spawn',
|
|
29
|
+
'sessions_yield',
|
|
30
|
+
'subagents',
|
|
31
|
+
'session_status',
|
|
32
|
+
'cron',
|
|
33
|
+
'image',
|
|
34
|
+
'image_generate',
|
|
35
|
+
]
|
|
36
|
+
const AAMP_PLUGIN_TOOL_ALLOWLIST = [
|
|
37
|
+
'aamp_send_result',
|
|
38
|
+
'aamp_send_help',
|
|
39
|
+
'aamp_pending_tasks',
|
|
40
|
+
'aamp_dispatch_task',
|
|
41
|
+
'aamp_check_protocol',
|
|
42
|
+
'aamp_download_attachment',
|
|
43
|
+
]
|
|
14
44
|
|
|
15
45
|
function resolveOpenClawHome() {
|
|
16
46
|
return process.env.OPENCLAW_HOME?.trim() || join(homedir(), '.openclaw')
|
|
@@ -46,7 +76,7 @@ function normalizeBaseUrl(url) {
|
|
|
46
76
|
return `https://${url.replace(/\/$/, '')}`
|
|
47
77
|
}
|
|
48
78
|
|
|
49
|
-
function ensurePluginConfig(config, pluginConfig) {
|
|
79
|
+
function ensurePluginConfig(config, pluginConfig, options = {}) {
|
|
50
80
|
const next = config && typeof config === 'object' ? structuredClone(config) : {}
|
|
51
81
|
if (!next.plugins || typeof next.plugins !== 'object') next.plugins = {}
|
|
52
82
|
if (!Array.isArray(next.plugins.allow)) next.plugins.allow = []
|
|
@@ -76,9 +106,65 @@ function ensurePluginConfig(config, pluginConfig) {
|
|
|
76
106
|
delete next.plugins.entries.aamp
|
|
77
107
|
}
|
|
78
108
|
|
|
109
|
+
next.tools = ensureAampToolAllowlist(next.tools, options)
|
|
110
|
+
|
|
111
|
+
return next
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ensureAampToolAllowlist(toolsConfig, options = {}) {
|
|
115
|
+
const next = toolsConfig && typeof toolsConfig === 'object' ? structuredClone(toolsConfig) : {}
|
|
116
|
+
const existingAllow = Array.isArray(next.allow) ? next.allow.filter((value) => typeof value === 'string' && value.trim()) : []
|
|
117
|
+
const includeCodingBaseline = options.includeCodingBaseline === true
|
|
118
|
+
|
|
119
|
+
const mergedAllow = [
|
|
120
|
+
...existingAllow,
|
|
121
|
+
...(includeCodingBaseline ? CODING_TOOL_ALLOWLIST : []),
|
|
122
|
+
...AAMP_PLUGIN_TOOL_ALLOWLIST,
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
next.allow = Array.from(new Set(mergedAllow))
|
|
126
|
+
|
|
79
127
|
return next
|
|
80
128
|
}
|
|
81
129
|
|
|
130
|
+
function planToolPolicyUpdate(toolsConfig, options = {}) {
|
|
131
|
+
const current = toolsConfig && typeof toolsConfig === 'object' ? structuredClone(toolsConfig) : {}
|
|
132
|
+
const existingAllow = Array.isArray(current.allow) ? current.allow.filter((value) => typeof value === 'string' && value.trim()) : []
|
|
133
|
+
const includeCodingBaseline = options.includeCodingBaseline === true
|
|
134
|
+
const missingAampTools = AAMP_PLUGIN_TOOL_ALLOWLIST.filter((tool) => !existingAllow.includes(tool))
|
|
135
|
+
const currentProfile = typeof current.profile === 'string' ? current.profile : undefined
|
|
136
|
+
const missingCodingTools = includeCodingBaseline
|
|
137
|
+
? CODING_TOOL_ALLOWLIST.filter((tool) => !existingAllow.includes(tool))
|
|
138
|
+
: []
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
current,
|
|
142
|
+
missingAampTools,
|
|
143
|
+
missingCodingTools,
|
|
144
|
+
needsAnyChange: missingAampTools.length > 0 || missingCodingTools.length > 0,
|
|
145
|
+
needsNonPluginChange: missingCodingTools.length > 0,
|
|
146
|
+
currentProfile,
|
|
147
|
+
next: ensureAampToolAllowlist(current, { includeCodingBaseline }),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function currentToolPolicySummary(plan) {
|
|
152
|
+
const lines = []
|
|
153
|
+
if (plan.currentProfile) {
|
|
154
|
+
lines.push(` current tools.profile: ${plan.currentProfile}`)
|
|
155
|
+
} else {
|
|
156
|
+
lines.push(` current tools.profile: (none)`)
|
|
157
|
+
}
|
|
158
|
+
lines.push(` current tools.allow count: ${Array.isArray(plan.current.allow) ? plan.current.allow.length : 0}`)
|
|
159
|
+
if (plan.missingAampTools.length > 0) {
|
|
160
|
+
lines.push(` missing AAMP tools: ${plan.missingAampTools.join(', ')}`)
|
|
161
|
+
}
|
|
162
|
+
if (plan.needsNonPluginChange) {
|
|
163
|
+
lines.push(` additional core tools to add: ${plan.missingCodingTools.join(', ')}`)
|
|
164
|
+
}
|
|
165
|
+
return lines.join('\n')
|
|
166
|
+
}
|
|
167
|
+
|
|
82
168
|
function parseDispatchContextRules(raw) {
|
|
83
169
|
const trimmed = raw.trim()
|
|
84
170
|
if (!trimmed) return undefined
|
|
@@ -121,9 +207,34 @@ function copyIntoDir(src, dest) {
|
|
|
121
207
|
cpSync(src, dest, { recursive: true, force: true })
|
|
122
208
|
}
|
|
123
209
|
|
|
210
|
+
function ensureBuiltArtifacts(packageRoot) {
|
|
211
|
+
const entryFile = join(packageRoot, 'dist', 'index.js')
|
|
212
|
+
if (existsSync(entryFile)) return
|
|
213
|
+
|
|
214
|
+
const result = spawnSync('npm', ['run', 'build'], {
|
|
215
|
+
cwd: packageRoot,
|
|
216
|
+
encoding: 'utf-8',
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
if (result.error) {
|
|
220
|
+
throw new Error(`Failed to build plugin artifacts: ${result.error.message}`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (result.status !== 0) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Failed to build plugin artifacts: ${(result.stderr || result.stdout || `exit code ${result.status}`).trim()}`,
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!existsSync(entryFile)) {
|
|
230
|
+
throw new Error(`Plugin build completed but ${entryFile} is still missing`)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
124
234
|
function installPluginFiles(credentialsFile = DEFAULT_CREDENTIALS_FILE) {
|
|
125
235
|
const extensionDir = resolveExtensionDir()
|
|
126
236
|
const packageRoot = packageRootFromEntry(fileURLToPath(import.meta.url))
|
|
237
|
+
ensureBuiltArtifacts(packageRoot)
|
|
127
238
|
const packageJson = readJsonFile(join(packageRoot, 'package.json'))
|
|
128
239
|
const credentialsPath = expandHome(credentialsFile)
|
|
129
240
|
const existingCredentials = existsSync(credentialsPath)
|
|
@@ -142,6 +253,18 @@ function installPluginFiles(credentialsFile = DEFAULT_CREDENTIALS_FILE) {
|
|
|
142
253
|
|
|
143
254
|
writeJsonFile(join(extensionDir, 'package.json'), packageJson)
|
|
144
255
|
|
|
256
|
+
const dependencyPackages = ['ws', 'nodemailer']
|
|
257
|
+
const nodeModulesDir = join(extensionDir, 'node_modules')
|
|
258
|
+
mkdirSync(nodeModulesDir, { recursive: true })
|
|
259
|
+
|
|
260
|
+
for (const dep of dependencyPackages) {
|
|
261
|
+
const depRoot = join(packageRoot, 'node_modules', dep)
|
|
262
|
+
if (!existsSync(depRoot)) {
|
|
263
|
+
throw new Error(`Missing dependency directory: ${depRoot}`)
|
|
264
|
+
}
|
|
265
|
+
copyIntoDir(depRoot, join(nodeModulesDir, dep))
|
|
266
|
+
}
|
|
267
|
+
|
|
145
268
|
if (existingCredentials) {
|
|
146
269
|
mkdirSync(dirname(credentialsPath), { recursive: true })
|
|
147
270
|
writeFileSync(credentialsPath, existingCredentials)
|
|
@@ -175,28 +298,6 @@ function restartGateway() {
|
|
|
175
298
|
}
|
|
176
299
|
}
|
|
177
300
|
|
|
178
|
-
async function fetchJson(url, init, stepLabel) {
|
|
179
|
-
let res
|
|
180
|
-
try {
|
|
181
|
-
res = await fetch(url, init)
|
|
182
|
-
} catch (error) {
|
|
183
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
184
|
-
throw new Error(`${stepLabel} failed for ${url}: ${message}`)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (!res.ok) {
|
|
188
|
-
const text = await res.text().catch(() => '')
|
|
189
|
-
throw new Error(`${stepLabel} failed (${res.status}) for ${url}: ${text || res.statusText}`)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
return await res.json()
|
|
194
|
-
} catch (error) {
|
|
195
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
196
|
-
throw new Error(`${stepLabel} returned invalid JSON for ${url}: ${message}`)
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
301
|
async function ensureMailboxIdentity({ aampHost, slug, credentialsFile }) {
|
|
201
302
|
const resolvedCreds = expandHome(credentialsFile)
|
|
202
303
|
if (existsSync(resolvedCreds)) {
|
|
@@ -204,30 +305,33 @@ async function ensureMailboxIdentity({ aampHost, slug, credentialsFile }) {
|
|
|
204
305
|
}
|
|
205
306
|
|
|
206
307
|
const base = normalizeBaseUrl(aampHost)
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
{
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
308
|
+
const registerRes = await fetch(`${base}/api/nodes/self-register`, {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
headers: { 'Content-Type': 'application/json' },
|
|
311
|
+
body: JSON.stringify({
|
|
312
|
+
slug,
|
|
313
|
+
description: 'OpenClaw AAMP agent node',
|
|
314
|
+
}),
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
if (!registerRes.ok) {
|
|
318
|
+
const text = await registerRes.text().catch(() => '')
|
|
319
|
+
throw new Error(`AAMP self-register failed (${registerRes.status}): ${text || registerRes.statusText}`)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const registerData = await registerRes.json()
|
|
220
323
|
const code = registerData?.registrationCode
|
|
221
324
|
if (!code) {
|
|
222
325
|
throw new Error('AAMP self-register succeeded but no registrationCode was returned')
|
|
223
326
|
}
|
|
224
327
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
328
|
+
const credRes = await fetch(`${base}/api/nodes/credentials?code=${encodeURIComponent(code)}`)
|
|
329
|
+
if (!credRes.ok) {
|
|
330
|
+
const text = await credRes.text().catch(() => '')
|
|
331
|
+
throw new Error(`AAMP credential exchange failed (${credRes.status}): ${text || credRes.statusText}`)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const credData = await credRes.json()
|
|
231
335
|
const identity = {
|
|
232
336
|
email: credData?.email,
|
|
233
337
|
jmapToken: credData?.jmap?.token,
|
|
@@ -269,6 +373,7 @@ async function runInit() {
|
|
|
269
373
|
let senderPolicies = previousConfig?.senderPolicies
|
|
270
374
|
let slug = previousSlug
|
|
271
375
|
let reuseExistingConfig = Boolean(previousConfig)
|
|
376
|
+
let includeCodingBaseline = false
|
|
272
377
|
|
|
273
378
|
if (input.isTTY) {
|
|
274
379
|
const rl = createInterface({ input, output })
|
|
@@ -310,6 +415,28 @@ async function runInit() {
|
|
|
310
415
|
senderPolicies = undefined
|
|
311
416
|
}
|
|
312
417
|
}
|
|
418
|
+
|
|
419
|
+
const codingPromptPlan = planToolPolicyUpdate(existing?.tools, { includeCodingBaseline: true })
|
|
420
|
+
const shouldOfferCodingBaseline =
|
|
421
|
+
codingPromptPlan.missingCodingTools.length > 0 &&
|
|
422
|
+
!Array.isArray(existing?.tools?.allow) &&
|
|
423
|
+
!(typeof existing?.tools?.profile === 'string' && existing.tools.profile.trim())
|
|
424
|
+
|
|
425
|
+
if (shouldOfferCodingBaseline) {
|
|
426
|
+
output.write(
|
|
427
|
+
[
|
|
428
|
+
'',
|
|
429
|
+
'Optional tool policy upgrade:',
|
|
430
|
+
' Default init only adds the AAMP plugin tools needed for mailbox-style task receive/reply.',
|
|
431
|
+
' If this agent also needs file/shell/web coding workflows, you can additionally add',
|
|
432
|
+
' the coding baseline tool set now.',
|
|
433
|
+
currentToolPolicySummary(codingPromptPlan),
|
|
434
|
+
'',
|
|
435
|
+
].join('\n'),
|
|
436
|
+
)
|
|
437
|
+
const toolAnswer = await rl.question('Also add coding baseline tools? [y/N]: ')
|
|
438
|
+
includeCodingBaseline = isYes(toolAnswer, false)
|
|
439
|
+
}
|
|
313
440
|
} finally {
|
|
314
441
|
rl.close()
|
|
315
442
|
}
|
|
@@ -333,11 +460,14 @@ async function runInit() {
|
|
|
333
460
|
output.write('\nInstalling OpenClaw plugin files...\n')
|
|
334
461
|
const extensionDir = installPluginFiles(previousCredentialsFile)
|
|
335
462
|
|
|
463
|
+
const toolPolicyPlan = planToolPolicyUpdate(existing?.tools, { includeCodingBaseline })
|
|
336
464
|
const next = ensurePluginConfig(existing, {
|
|
337
465
|
aampHost,
|
|
338
466
|
slug,
|
|
339
467
|
credentialsFile: DEFAULT_CREDENTIALS_FILE,
|
|
340
468
|
...(senderPolicies ? { senderPolicies } : {}),
|
|
469
|
+
}, {
|
|
470
|
+
includeCodingBaseline,
|
|
341
471
|
})
|
|
342
472
|
|
|
343
473
|
writeJsonFile(configPath, next)
|
|
@@ -361,6 +491,8 @@ async function runInit() {
|
|
|
361
491
|
` aampHost: ${aampHost}`,
|
|
362
492
|
` credentialsFile: ${DEFAULT_CREDENTIALS_FILE}`,
|
|
363
493
|
` senderPolicies: ${senderPolicies ? JSON.stringify(senderPolicies) : '(allow all)'}`,
|
|
494
|
+
` tools.allow: ${JSON.stringify(next.tools?.allow ?? [])}`,
|
|
495
|
+
` codingBaselineAdded: ${toolPolicyPlan.missingCodingTools.length > 0 && includeCodingBaseline ? 'yes' : 'no'}`,
|
|
364
496
|
identityResult.created
|
|
365
497
|
? ` mailbox: ${identityResult.email} (registered and saved to ${identityResult.credentialsPath})`
|
|
366
498
|
: ` mailbox: existing credentials reused from ${identityResult.credentialsPath}`,
|
package/dist/file-store.js
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
// src/file-store.ts
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
function defaultCredentialsPath() {
|
|
6
|
+
return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".credentials.json");
|
|
7
|
+
}
|
|
8
|
+
function loadCachedIdentity(file) {
|
|
9
|
+
const resolved = file ?? defaultCredentialsPath();
|
|
10
|
+
if (!existsSync(resolved))
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(readFileSync(resolved, "utf-8"));
|
|
14
|
+
if (!parsed.email || !parsed.jmapToken || !parsed.smtpPassword)
|
|
15
|
+
return null;
|
|
16
|
+
return parsed;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function saveCachedIdentity(identity, file) {
|
|
22
|
+
const resolved = file ?? defaultCredentialsPath();
|
|
23
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
24
|
+
writeFileSync(resolved, JSON.stringify(identity, null, 2), "utf-8");
|
|
25
|
+
}
|
|
26
|
+
function ensureDir(dir) {
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
function readBinaryFile(path) {
|
|
30
|
+
return readFileSync(path);
|
|
31
|
+
}
|
|
32
|
+
function writeBinaryFile(path, content) {
|
|
33
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
34
|
+
writeFileSync(path, content);
|
|
35
|
+
}
|
|
10
36
|
export {
|
|
11
37
|
defaultCredentialsPath,
|
|
12
38
|
ensureDir,
|
package/dist/file-store.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": [],
|
|
4
|
-
"sourcesContent": [],
|
|
5
|
-
"mappings": "",
|
|
3
|
+
"sources": ["../src/file-store.ts"],
|
|
4
|
+
"sourcesContent": ["import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { homedir } from 'node:os'\n\nexport interface Identity {\n email: string\n jmapToken: string\n smtpPassword: string\n}\n\nexport function defaultCredentialsPath(): string {\n return join(homedir(), '.openclaw', 'extensions', 'aamp-openclaw-plugin', '.credentials.json')\n}\n\nexport function loadCachedIdentity(file?: string): Identity | null {\n const resolved = file ?? defaultCredentialsPath()\n if (!existsSync(resolved)) return null\n try {\n const parsed = JSON.parse(readFileSync(resolved, 'utf-8')) as Partial<Identity>\n if (!parsed.email || !parsed.jmapToken || !parsed.smtpPassword) return null\n return parsed as Identity\n } catch {\n return null\n }\n}\n\nexport function saveCachedIdentity(identity: Identity, file?: string): void {\n const resolved = file ?? defaultCredentialsPath()\n mkdirSync(dirname(resolved), { recursive: true })\n writeFileSync(resolved, JSON.stringify(identity, null, 2), 'utf-8')\n}\n\nexport function ensureDir(dir: string): void {\n mkdirSync(dir, { recursive: true })\n}\n\nexport function readBinaryFile(path: string): Buffer {\n return readFileSync(path)\n}\n\nexport function writeBinaryFile(path: string, content: Uint8Array | Buffer): void {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, content)\n}\n"],
|
|
5
|
+
"mappings": ";AAAA,SAAS,YAAY,WAAW,cAAc,qBAAqB;AACnE,SAAS,SAAS,YAAY;AAC9B,SAAS,eAAe;AAQjB,SAAS,yBAAiC;AAC/C,SAAO,KAAK,QAAQ,GAAG,aAAa,cAAc,wBAAwB,mBAAmB;AAC/F;AAEO,SAAS,mBAAmB,MAAgC;AACjE,QAAM,WAAW,QAAQ,uBAAuB;AAChD,MAAI,CAAC,WAAW,QAAQ;AAAG,WAAO;AAClC,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,aAAa,UAAU,OAAO,CAAC;AACzD,QAAI,CAAC,OAAO,SAAS,CAAC,OAAO,aAAa,CAAC,OAAO;AAAc,aAAO;AACvE,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,mBAAmB,UAAoB,MAAqB;AAC1E,QAAM,WAAW,QAAQ,uBAAuB;AAChD,YAAU,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAChD,gBAAc,UAAU,KAAK,UAAU,UAAU,MAAM,CAAC,GAAG,OAAO;AACpE;AAEO,SAAS,UAAU,KAAmB;AAC3C,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACpC;AAEO,SAAS,eAAe,MAAsB;AACnD,SAAO,aAAa,IAAI;AAC1B;AAEO,SAAS,gBAAgB,MAAc,SAAoC;AAChF,YAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC5C,gBAAc,MAAM,OAAO;AAC7B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|