@moguiyu/dsh-tavily 0.2.8 → 0.3.1
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/README.md +21 -21
- package/lib/client.js +29 -118
- package/package.json +3 -12
- package/src/backend.js +376 -0
- package/src/client.js +29 -118
- package/src/index.js +30 -157
- package/src/lib.js +65 -0
- package/src/tools.js +632 -0
package/README.md
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
# @moguiyu/dsh-tavily
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`tavily_map`, `tavily_crawl`), the
|
|
5
|
-
|
|
3
|
+
Everything in one package: the advanced Tavily model tools (`tavily_search`, `tavily_extract`,
|
|
4
|
+
`tavily_map`, `tavily_crawl`), the key/usage HTTP routes, and the browser card on the **Plugins**
|
|
5
|
+
page.
|
|
6
6
|
|
|
7
7
|
- **`tavily_search`** — full Tavily surface (`max_results`, `search_depth`, `topic`, `days`,
|
|
8
|
-
`include_answer`, `include_raw_content`, `include_domains`, `exclude_domains`).
|
|
9
|
-
off by default.
|
|
8
|
+
`include_answer`, `include_raw_content`, `include_domains`, `exclude_domains`).
|
|
10
9
|
- **`tavily_extract` / `tavily_map` / `tavily_crawl`** — read a known URL's content, map a
|
|
11
10
|
site's links, or crawl a site and return its pages. Same key-rotation/failover as search.
|
|
12
|
-
- **
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
generations are detected at runtime).
|
|
18
|
-
The switch writes the namespace and restarts the row; the choice is mirrored to
|
|
19
|
-
`~/.dsh/tavily-tool.json`.
|
|
11
|
+
- **Card** — key list, usage gauge, and the strategy selector. It registers into the Plugins
|
|
12
|
+
page's `plugins.bundle.config` slot, keyed `@moguiyu/dsh-tavily`, and needs DSH
|
|
13
|
+
**0.1.6-alpha.2 or newer** — the release that retired `settings.plugin.item` and moved plugin
|
|
14
|
+
configuration off the Settings page. On older lines the tools and routes work; there is simply
|
|
15
|
+
no card.
|
|
20
16
|
- **`web_search` is never replaced** — no `ctx.web` provider is registered and
|
|
21
|
-
`web.searchProvider` is never rewritten; the built-in
|
|
22
|
-
|
|
17
|
+
`web.searchProvider` is never rewritten; the built-in `web_search` keeps its native provider
|
|
18
|
+
and schema.
|
|
23
19
|
|
|
24
|
-
Host half: plain ESM (`src/index.js`
|
|
25
|
-
at `lib/client.js`, generated from
|
|
20
|
+
Host half: plain ESM (`src/index.js`, `src/tools.js`, `src/backend.js`, `src/lib.js`). Client
|
|
21
|
+
half: prebuilt `window.__ModuleLoader__` bundle at `lib/client.js`, generated from
|
|
22
|
+
`src/client.js` via `pnpm build`.
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
Earlier releases split this across `@moguiyu/dsh-tool-tavily-search` and
|
|
25
|
+
`@moguiyu/dsh-tavily-backend`, which this package imported as regular dependencies. From 0.3.0
|
|
26
|
+
both are folded in here: one package means no dependency range can drift behind the contract,
|
|
27
|
+
which is exactly how the old tool-level switch broke. Both old packages are deprecated.
|
|
28
|
+
|
|
29
|
+
There is no tool-level on/off switch. Composing the plugin registers the tools, and the Plugins
|
|
30
|
+
page's own bundle toggle is the only on/off.
|
|
31
31
|
|
|
32
32
|
See the [workspace README](../../README.md) for install and configuration.
|
package/lib/client.js
CHANGED
|
@@ -2,7 +2,6 @@ window.__ModuleLoader__.load({
|
|
|
2
2
|
id: "@moguiyu/dsh-tavily",
|
|
3
3
|
factory: function factory(require) {
|
|
4
4
|
const react = require('react')
|
|
5
|
-
const _primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
6
5
|
|
|
7
6
|
const inject = ['slots']
|
|
8
7
|
|
|
@@ -52,18 +51,6 @@ window.__ModuleLoader__.load({
|
|
|
52
51
|
}, react.createElement(SvgIcon, { name: icon, size: 13 }))
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
function Switch({ checked, disabled, label, onChange }) {
|
|
56
|
-
return react.createElement('button', {
|
|
57
|
-
type: 'button',
|
|
58
|
-
role: 'switch',
|
|
59
|
-
'aria-checked': checked === true,
|
|
60
|
-
'aria-label': label,
|
|
61
|
-
disabled: disabled || checked === null,
|
|
62
|
-
onClick: onChange,
|
|
63
|
-
style: { width: 36, height: 20, borderRadius: 10, border: '1px solid var(--dsw-alias-border-l2)', background: checked === true ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-bg-layer-1)', cursor: 'pointer', padding: 0, position: 'relative', flex: 'none', opacity: disabled ? 0.5 : 1 }
|
|
64
|
-
}, react.createElement('span', { style: { position: 'absolute', top: 2, left: checked === true ? 18 : 2, width: 14, height: 14, borderRadius: 7, background: '#fff', transition: 'left .12s ease' } }))
|
|
65
|
-
}
|
|
66
|
-
|
|
67
54
|
function UsageCircle({ percent, size, label, onClick }) {
|
|
68
55
|
const radius = 15
|
|
69
56
|
const circumference = 2 * Math.PI * radius
|
|
@@ -79,8 +66,6 @@ window.__ModuleLoader__.load({
|
|
|
79
66
|
}
|
|
80
67
|
|
|
81
68
|
function TavilySettingsCard() {
|
|
82
|
-
const [enabled, setEnabled] = react.useState(null)
|
|
83
|
-
const [expanded, setExpanded] = react.useState(false)
|
|
84
69
|
const [server, setServer] = react.useState(null)
|
|
85
70
|
const [loadError, setLoadError] = react.useState(null)
|
|
86
71
|
const [strategy, setStrategy] = react.useState('rotate')
|
|
@@ -96,13 +81,6 @@ window.__ModuleLoader__.load({
|
|
|
96
81
|
const [usageError, setUsageError] = react.useState(null)
|
|
97
82
|
|
|
98
83
|
const refresh = react.useCallback(async () => {
|
|
99
|
-
try {
|
|
100
|
-
const response = await fetch('/api/tavily-tool', { cache: 'no-store' })
|
|
101
|
-
const data = await response.json()
|
|
102
|
-
if (data.ok) { setEnabled(data.enabled); setLoadError(null) } else { setLoadError(data.error || 'Failed to load settings') }
|
|
103
|
-
} catch (error) {
|
|
104
|
-
setLoadError(String(error && error.message ? error.message : error))
|
|
105
|
-
}
|
|
106
84
|
try {
|
|
107
85
|
const response = await fetch('/api/tavily-manager', { cache: 'no-store' })
|
|
108
86
|
const data = await response.json()
|
|
@@ -122,27 +100,6 @@ window.__ModuleLoader__.load({
|
|
|
122
100
|
|
|
123
101
|
react.useEffect(() => { refresh() }, [refresh])
|
|
124
102
|
|
|
125
|
-
const toggleEnabled = async () => {
|
|
126
|
-
const next = enabled !== true
|
|
127
|
-
setBusy(true)
|
|
128
|
-
setNotice(null)
|
|
129
|
-
try {
|
|
130
|
-
const response = await fetch('/api/tavily-tool', {
|
|
131
|
-
method: 'POST',
|
|
132
|
-
headers: { 'content-type': 'application/json' },
|
|
133
|
-
body: JSON.stringify({ enabled: next })
|
|
134
|
-
})
|
|
135
|
-
const data = await response.json()
|
|
136
|
-
if (!data.ok) { setNotice({ error: data.error || 'Switch failed' }); return }
|
|
137
|
-
setEnabled(data.enabled)
|
|
138
|
-
setNotice({ ok: data.enabled ? 'Advanced tavily_search tool enabled.' : 'Advanced tavily_search tool disabled — built-in web_search is unchanged.' })
|
|
139
|
-
} catch (error) {
|
|
140
|
-
setNotice({ error: String(error && error.message ? error.message : error) })
|
|
141
|
-
} finally {
|
|
142
|
-
setBusy(false)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
103
|
const saveStrategy = async (next) => {
|
|
147
104
|
setBusy(true)
|
|
148
105
|
setNotice(null)
|
|
@@ -283,49 +240,19 @@ window.__ModuleLoader__.load({
|
|
|
283
240
|
const headStyle = { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid var(--dsw-alias-border-l2)', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' }
|
|
284
241
|
const cellStyle = { padding: '8px', verticalAlign: 'top' }
|
|
285
242
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return react.createElement('div', { className: expanded ? 'dts-card dts-card-open' : 'dts-card', style: { minWidth: 0 } },
|
|
293
|
-
react.createElement('style', null, '.dts-card{border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s}.dts-card:hover{border-color:var(--dsw-alias-label-dimmed)}.dts-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.dts-icon-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.dts-icon-btn:disabled{opacity:.4;cursor:default}.dts-icon-btn-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}'),
|
|
294
|
-
react.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, padding: '12px 14px', minHeight: 52, boxSizing: 'border-box' } },
|
|
295
|
-
react.createElement('button', {
|
|
296
|
-
type: 'button',
|
|
297
|
-
'aria-expanded': expanded,
|
|
298
|
-
onClick: () => setExpanded((current) => !current),
|
|
299
|
-
style: { flex: 1, minWidth: 0, textAlign: 'left', background: 'none', border: 'none', padding: 0, cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 2, font: 'inherit', color: 'inherit' }
|
|
300
|
-
},
|
|
301
|
-
react.createElement('strong', { style: { fontSize: 14, fontWeight: 600, lineHeight: '20px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, 'Tavily Search'),
|
|
302
|
-
react.createElement('span', { style: { fontSize: 12, lineHeight: '16px', color: 'var(--dsw-alias-label-tertiary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, 'Advanced tavily_search tool · key rotation · usage')
|
|
303
|
-
),
|
|
304
|
-
react.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6, flex: 'none' } },
|
|
305
|
-
react.createElement(Switch, { checked: enabled, disabled: busy, label: 'Advanced tavily_search tool enabled', onChange: toggleEnabled }),
|
|
306
|
-
react.createElement('button', {
|
|
307
|
-
type: 'button',
|
|
308
|
-
className: 'dts-icon-btn',
|
|
309
|
-
title: expanded ? 'Collapse' : 'Expand',
|
|
310
|
-
'aria-label': expanded ? 'Collapse' : 'Expand',
|
|
311
|
-
onClick: () => setExpanded((current) => !current),
|
|
312
|
-
style: { width: 26, height: 26, borderRadius: 6, border: 'none', background: 'transparent', color: 'var(--dsw-alias-label-tertiary)', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0 }
|
|
313
|
-
},
|
|
314
|
-
react.createElement('span', { style: { display: 'inline-flex', transform: expanded ? 'rotate(180deg)' : undefined, transition: 'transform .14s var(--ds-ease-in-out)' } },
|
|
315
|
-
react.createElement(_primitives.IconChevronDownOutline14, { size: 12, 'aria-hidden': 'true' })
|
|
316
|
-
)
|
|
317
|
-
)
|
|
318
|
-
)
|
|
319
|
-
),
|
|
320
|
-
expanded && react.createElement('div', { style: { borderTop: '1px solid var(--dsw-alias-border-l2)', padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 16 } },
|
|
321
|
-
isOff
|
|
322
|
-
? react.createElement('p', { style: { margin: 0, fontSize: 13, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
323
|
-
'The advanced tavily_search tool is off. The built-in web_search keeps its native provider and is never replaced.'
|
|
324
|
-
)
|
|
325
|
-
: react.createElement(react.Fragment, null,
|
|
243
|
+
// The Plugins page draws the title, the icon, and the crumb — the
|
|
244
|
+
// `plugins.bundle.config` contract puts the form alone in the entry — so
|
|
245
|
+
// this renders no card chrome, no second heading, and no collapse.
|
|
246
|
+
return react.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 16, minWidth: 0 } },
|
|
247
|
+
react.createElement('style', null, '.dts-icon-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.dts-icon-btn:disabled{opacity:.4;cursor:default}.dts-icon-btn-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}'),
|
|
248
|
+
react.createElement(react.Fragment, null,
|
|
326
249
|
react.createElement('p', { style: { margin: 0, fontSize: 13, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
327
250
|
'All keys are listed below; the green dot marks the first (primary) key. The tavily_search tool uses all keys according to the strategy. Built-in web_search is unaffected.'
|
|
328
251
|
),
|
|
252
|
+
server !== null && server.writable !== undefined && server.writable.keys === false &&
|
|
253
|
+
react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
254
|
+
'Read-only: these keys come from the environment that launched dsh, so this card cannot change them. Unset TAVILY_API_KEYS / TAVILY_API_KEY in that shell and restart dsh to manage keys here.'
|
|
255
|
+
),
|
|
329
256
|
loadError !== null && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(loadError)),
|
|
330
257
|
react.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } },
|
|
331
258
|
react.createElement('thead', null,
|
|
@@ -381,11 +308,15 @@ window.__ModuleLoader__.load({
|
|
|
381
308
|
!isRemoved && react.createElement(IconButton, { icon: 'pencil', title: 'Edit', onClick: () => startReplace(masked), disabled: busy }),
|
|
382
309
|
react.createElement(IconButton, {
|
|
383
310
|
icon: isRemoved ? 'restore' : (confirm[masked] === true ? 'check' : 'trash'),
|
|
384
|
-
|
|
311
|
+
// A key the launching environment supplies is read-only, so the
|
|
312
|
+
// server would refuse. Say so on the control instead.
|
|
313
|
+
title: key.removable === false
|
|
314
|
+
? 'Read-only: this key comes from the environment that launched dsh. Unset it there and restart dsh to change it here.'
|
|
315
|
+
: (isRemoved ? 'Restore' : (confirm[masked] === true ? 'Click again to confirm' : 'Delete')),
|
|
385
316
|
danger: !isRemoved,
|
|
386
317
|
className: isRemoved ? 'dts-icon-btn' : 'dts-icon-btn dts-icon-btn-danger',
|
|
387
318
|
onClick: () => isRemoved ? restore(masked) : markRemoved(masked),
|
|
388
|
-
disabled: busy
|
|
319
|
+
disabled: busy || (key.removable === false && !isRemoved)
|
|
389
320
|
})
|
|
390
321
|
)
|
|
391
322
|
)
|
|
@@ -434,35 +365,27 @@ window.__ModuleLoader__.load({
|
|
|
434
365
|
usageError !== null && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usageError)),
|
|
435
366
|
usage !== null && usage.ok === false && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usage.error))
|
|
436
367
|
)
|
|
437
|
-
)
|
|
438
368
|
)
|
|
439
369
|
}
|
|
440
370
|
|
|
441
|
-
// The
|
|
442
|
-
// `
|
|
371
|
+
// The entry the Plugins page renders. The contract asks for two views:
|
|
372
|
+
// `summary` for the one-liner under the title, and `page` for the form with
|
|
373
|
+
// its own save control. The page supplies the title, icon, and crumb, so the
|
|
374
|
+
// summary is a plain string and the page view is the form alone — matching
|
|
375
|
+
// how the shipped cards (WebSearchCard and friends) are written.
|
|
443
376
|
function TavilyCard(props) {
|
|
444
|
-
if (props && props.view === 'summary')
|
|
377
|
+
if (props && props.view === 'summary') {
|
|
378
|
+
return 'Manage Tavily API keys and the key-usage strategy. The built-in web_search is never replaced.'
|
|
379
|
+
}
|
|
445
380
|
return react.createElement(TavilySettingsCard, null)
|
|
446
381
|
}
|
|
447
382
|
|
|
448
|
-
// One-liner for the summary view. Deliberately static and cheap: the page
|
|
449
|
-
// draws the title, the icon, and the crumb itself, and never asks this entry
|
|
450
|
-
// for anything but `page`.
|
|
451
|
-
function TavilySummary() {
|
|
452
|
-
return react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
453
|
-
'Rotating Tavily API keys for the advanced search tools. The built-in web_search keeps its native provider and is never replaced.'
|
|
454
|
-
)
|
|
455
|
-
}
|
|
456
|
-
|
|
457
383
|
function apply(ctx) {
|
|
458
|
-
// 0.1.6-alpha.2 moved plugin configuration
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
// line, and neither throws on the other. Keep both -- dropping the retired
|
|
464
|
-
// name silently removes the card from rc.7 through 0.1.6-alpha.1, and
|
|
465
|
-
// dropping the new one silently removes it from 0.1.6-alpha.2 onward.
|
|
384
|
+
// ONE slot. 0.1.6-alpha.2 moved plugin configuration to the sidebar
|
|
385
|
+
// Plugins page and retired `settings.plugin.item`. This package no longer
|
|
386
|
+
// registers a settings namespace, so the retired keyed slot has no key it
|
|
387
|
+
// could claim. On rc.7 through 0.1.6-alpha.1 the tools and routes work but
|
|
388
|
+
// this card has no host — see `docs/agents/plugin-design.md`.
|
|
466
389
|
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
467
390
|
name: 'plugins.bundle.config',
|
|
468
391
|
// Keyed by the bundle's package name. The Plugins page renders this on
|
|
@@ -470,18 +393,6 @@ window.__ModuleLoader__.load({
|
|
|
470
393
|
key: '@moguiyu/dsh-tavily',
|
|
471
394
|
id: '@moguiyu/dsh-tavily'
|
|
472
395
|
}, TavilyCard))
|
|
473
|
-
|
|
474
|
-
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
|
475
|
-
name: 'settings.plugin.item',
|
|
476
|
-
// rc.7 through 0.1.6-alpha.1: keyed by the settings namespace the Host
|
|
477
|
-
// serves (`tavily-search`); the Plugins configuration tab dispatches it
|
|
478
|
-
// when the namespace is present. The id/order/label props keep rc.6
|
|
479
|
-
// list-slot deployments rendering the card as well.
|
|
480
|
-
key: 'tavily-search',
|
|
481
|
-
id: 'tavily-search',
|
|
482
|
-
order: 30,
|
|
483
|
-
label: 'Tavily Search'
|
|
484
|
-
}, TavilyCard))
|
|
485
396
|
}
|
|
486
397
|
|
|
487
398
|
return { apply, inject }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moguiyu/dsh-tavily",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Advanced Tavily model tools for DeepSeek Harness (tavily_search, tavily_extract, tavily_map, tavily_crawl) with a key and usage management card on the Plugins page. The built-in web_search is never replaced.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -43,8 +43,6 @@
|
|
|
43
43
|
"client": {
|
|
44
44
|
"platform": "web",
|
|
45
45
|
"inject": [
|
|
46
|
-
"@deepseek-ai/dsh-client-ui-settings",
|
|
47
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
48
46
|
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
49
47
|
]
|
|
50
48
|
}
|
|
@@ -68,16 +66,9 @@
|
|
|
68
66
|
"peerDependencies": {
|
|
69
67
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
70
68
|
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.3-alpha.1 || ^0.1.5-0 || ^0.1.6-0",
|
|
71
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.3-alpha.1 || ^0.1.5-0 || ^0.1.6-0"
|
|
72
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.3-alpha.1 || ^0.1.5-0 || ^0.1.6-0"
|
|
73
|
-
},
|
|
74
|
-
"dependencies": {
|
|
75
|
-
"@deepseek-ai/schemastery": "^3.18.2",
|
|
76
|
-
"@moguiyu/dsh-tavily-backend": "^0.2.1",
|
|
77
|
-
"@moguiyu/dsh-tool-tavily-search": "^0.2.1"
|
|
69
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.3-alpha.1 || ^0.1.5-0 || ^0.1.6-0"
|
|
78
70
|
},
|
|
79
71
|
"devDependencies": {
|
|
80
|
-
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
|
|
81
72
|
"@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.2"
|
|
82
73
|
}
|
|
83
74
|
}
|