@michengai/dsh-codex-suite 0.1.12 → 0.1.14
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 +24 -22
- package/README.zh-CN.md +24 -24
- package/bin.mjs +10 -0
- package/installer.mjs +169 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DSH Codex Suite
|
|
2
2
|
|
|
3
|
-
`@michengai/dsh-codex-suite` is
|
|
3
|
+
`@michengai/dsh-codex-suite` is a one-click installer that adds Codex UI, expert management, skill management, archive management, IM Assistant, and scheduled tasks to one profile as six **direct dependencies**.
|
|
4
4
|
|
|
5
5
|
## Plugin combo
|
|
6
6
|
|
|
@@ -13,47 +13,49 @@
|
|
|
13
13
|
| IM Assistant | `@michengai/dsh-im-connect` |
|
|
14
14
|
| Scheduled tasks | `@michengai/dsh-automation` |
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Each Suite release pins one exact, verified member set. Making the members direct dependencies lets **Settings → About** detect, install, and update them independently. `dshmarket` remains optional.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## One-click installation
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`dsh plugin add` forwards to `pnpm add` in the profile directory. Without a version and official registry, a local mirror or minimum-release-age policy can leave you on an older build.
|
|
23
|
-
|
|
24
|
-
Run this from any PowerShell directory:
|
|
20
|
+
Node.js 22+ is required, and the current DSH `dsh` command must be on PATH. Run from any directory:
|
|
25
21
|
|
|
26
22
|
```powershell
|
|
27
23
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
28
24
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
29
|
-
|
|
30
|
-
dsh --profile web --dump-config
|
|
25
|
+
npx --yes @michengai/dsh-codex-suite@latest --profile web
|
|
31
26
|
```
|
|
32
27
|
|
|
33
|
-
|
|
28
|
+
The installer:
|
|
29
|
+
|
|
30
|
+
1. installs all six exact member versions from the official npm registry in one `dsh plugin add`;
|
|
31
|
+
2. records every member as a direct profile dependency and bundle;
|
|
32
|
+
3. promotes all members before removing a legacy aggregate Suite dependency;
|
|
33
|
+
4. validates the result with `dsh --profile web --dump-config`.
|
|
34
|
+
|
|
35
|
+
Restart DSH Web and hard-refresh the browser afterwards. Set `DSH_BIN` when `dsh` is not on PATH.
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
## New custom Web profile
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
Keep the same `DSH_HOME` and choose another profile name:
|
|
40
|
+
|
|
41
|
+
```powershell
|
|
42
|
+
npx --yes @michengai/dsh-codex-suite@latest --profile codex
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
DSH supplies `@deepseek-ai/dsh-base` when it creates a custom profile. The installer places DSH's built-in `@deepseek-ai/dsh-web-app` before the six member bundles without creating a separate Home or reinstalling the official Web package from npm.
|
|
38
46
|
|
|
39
47
|
## Install Codex UI only
|
|
40
48
|
|
|
41
49
|
```powershell
|
|
42
|
-
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
43
|
-
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
44
50
|
dsh plugin --profile web add @michengai/dsh-codex-ui@latest --registry=https://registry.npmjs.org/
|
|
45
51
|
```
|
|
46
52
|
|
|
47
53
|
## Local verification
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
Preview the installation plan from a repository checkout:
|
|
50
56
|
|
|
51
57
|
```powershell
|
|
52
|
-
|
|
53
|
-
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
54
|
-
dsh plugin --profile web add .\packages\dsh-codex-suite
|
|
58
|
+
node .\packages\dsh-codex-suite\bin.mjs --profile codex --dry-run
|
|
55
59
|
```
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
DSH resolves the suite patch from the profile root. If the profile uses a strict pnpm layout, set `nodeLinker: hoisted` in the profile `pnpm-workspace.yaml` and reinstall. Otherwise the packages may install but fail to resolve at startup.
|
|
61
|
+
Remove `--dry-run` to apply it. The checkout path resolves the local `workspace:*` entry to the root package version.
|
package/README.zh-CN.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DSH Codex Suite
|
|
2
2
|
|
|
3
|
-
`@michengai/dsh-codex-suite`
|
|
3
|
+
`@michengai/dsh-codex-suite` 是一键安装器:把 Codex UI、专家、技能、归档、IM 助理和定时任务作为目标 profile 的六个**直接依赖**安装。
|
|
4
4
|
|
|
5
5
|
## 插件组合
|
|
6
6
|
|
|
@@ -13,49 +13,49 @@
|
|
|
13
13
|
| IM 助理 | `@michengai/dsh-im-connect` |
|
|
14
14
|
| 定时任务 | `@michengai/dsh-automation` |
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
每个 Suite 版本锁定一组经过验证的成员精确版本。成员成为直接依赖后,「设置 → 关于」可以分别检测、安装和升级它们。`dshmarket` 不在套件内,需要时单独安装。
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## 一键安装
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`dsh plugin add` 会转发到 profile 目录里的 `pnpm add`。不写版本、不指定官方源时,本机镜像和最短发布间隔可能让你停在旧版。
|
|
23
|
-
|
|
24
|
-
在任意 PowerShell 目录执行:
|
|
20
|
+
需要 Node.js 22+,并确保当前 DSH 的 `dsh` 命令在 PATH 中。在任意目录执行:
|
|
25
21
|
|
|
26
22
|
```powershell
|
|
27
23
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
28
24
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
29
|
-
|
|
30
|
-
dsh --profile web --dump-config
|
|
25
|
+
npx --yes @michengai/dsh-codex-suite@latest --profile web
|
|
31
26
|
```
|
|
32
27
|
|
|
33
|
-
|
|
28
|
+
安装器会:
|
|
34
29
|
|
|
35
|
-
|
|
30
|
+
1. 用官方 npm 源和一次 `dsh plugin add` 安装六个精确版本成员;
|
|
31
|
+
2. 将成员写成 profile 的直接 dependencies 与 bundles;
|
|
32
|
+
3. 若存在旧版聚合 Suite,先提升全部成员,再移除旧聚合依赖;
|
|
33
|
+
4. 运行 `dsh --profile web --dump-config` 验证最终配置。
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
完成后重启 DSH Web 并在浏览器硬刷新。若 `dsh` 不在 PATH,可通过 `DSH_BIN` 指定其可执行文件。
|
|
38
36
|
|
|
39
|
-
##
|
|
37
|
+
## 新建自定义 Web profile
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
保持同一个 `DSH_HOME`,只需换一个 profile 名:
|
|
40
|
+
|
|
41
|
+
```powershell
|
|
42
|
+
npx --yes @michengai/dsh-codex-suite@latest --profile codex
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
DSH 创建自定义 profile 时会提供 `@deepseek-ai/dsh-base`。安装器会再把 DSH 自带的 `@deepseek-ai/dsh-web-app` 放到六个成员 bundle 之前,不会创建独立 Home,也不会通过 npm 重装官方 Web 包。
|
|
46
|
+
|
|
47
|
+
## 只安装 Codex UI
|
|
42
48
|
|
|
43
49
|
```powershell
|
|
44
|
-
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
45
|
-
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
46
50
|
dsh plugin --profile web add @michengai/dsh-codex-ui@latest --registry=https://registry.npmjs.org/
|
|
47
51
|
```
|
|
48
52
|
|
|
49
53
|
## 本地验证
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
仓库检出状态下可先查看安装计划:
|
|
52
56
|
|
|
53
57
|
```powershell
|
|
54
|
-
|
|
55
|
-
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
56
|
-
dsh plugin --profile web add .\packages\dsh-codex-suite
|
|
58
|
+
node .\packages\dsh-codex-suite\bin.mjs --profile codex --dry-run
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
DSH 从 profile 顶层解析本包 patch 中的子插件。若 profile 使用 pnpm 严格隔离布局,请在 profile 的 `pnpm-workspace.yaml` 设置 `nodeLinker: hoisted` 后重新安装;否则子包可能安装成功但启动时无法解析。
|
|
61
|
+
正式执行时去掉 `--dry-run`。安装器会读取工作区根包版本替换本地的 `workspace:*`。
|
package/bin.mjs
ADDED
package/installer.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
|
|
6
|
+
export const SUITE_PACKAGE = '@michengai/dsh-codex-suite'
|
|
7
|
+
export const BASE_BUNDLE = '@deepseek-ai/dsh-base'
|
|
8
|
+
export const WEB_BUNDLE = '@deepseek-ai/dsh-web-app'
|
|
9
|
+
|
|
10
|
+
const suiteManifest = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'))
|
|
11
|
+
|
|
12
|
+
/** Keep the same layer order as the legacy aggregate patch. */
|
|
13
|
+
export const MEMBER_PACKAGES = [
|
|
14
|
+
'@michengai/dsh-archive-manager',
|
|
15
|
+
'@michengai/dsh-codex-ui',
|
|
16
|
+
'@michengai/dsh-skills-manager',
|
|
17
|
+
'@michengai/dsh-agency-agents',
|
|
18
|
+
'@michengai/dsh-im-connect',
|
|
19
|
+
'@michengai/dsh-automation',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export function memberSpecs(manifest = suiteManifest) {
|
|
23
|
+
return MEMBER_PACKAGES.map((packageName) => {
|
|
24
|
+
let version = manifest.dependencies?.[packageName]
|
|
25
|
+
if (version?.startsWith('workspace:') === true && packageName === '@michengai/dsh-codex-ui') {
|
|
26
|
+
version = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version
|
|
27
|
+
}
|
|
28
|
+
if (typeof version !== 'string' || version === '' || version.startsWith('workspace:')) {
|
|
29
|
+
throw new Error(`Suite manifest does not contain a published version for ${packageName}.`)
|
|
30
|
+
}
|
|
31
|
+
return `${packageName}@${version}`
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseArgs(argv) {
|
|
36
|
+
const options = {
|
|
37
|
+
profile: 'web',
|
|
38
|
+
registry: 'https://registry.npmjs.org/',
|
|
39
|
+
dryRun: false,
|
|
40
|
+
help: false,
|
|
41
|
+
}
|
|
42
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
43
|
+
const argument = argv[index]
|
|
44
|
+
if (argument === '--help' || argument === '-h') options.help = true
|
|
45
|
+
else if (argument === '--dry-run') options.dryRun = true
|
|
46
|
+
else if (argument === '--profile') options.profile = argv[++index]
|
|
47
|
+
else if (argument.startsWith('--profile=')) options.profile = argument.slice('--profile='.length)
|
|
48
|
+
else if (argument === '--registry') options.registry = argv[++index]
|
|
49
|
+
else if (argument.startsWith('--registry=')) options.registry = argument.slice('--registry='.length)
|
|
50
|
+
else throw new Error(`Unknown argument: ${argument}`)
|
|
51
|
+
}
|
|
52
|
+
if (typeof options.profile !== 'string' || !/^[0-9A-Za-z][0-9A-Za-z._-]*$/.test(options.profile) || options.profile === 'node_modules') {
|
|
53
|
+
throw new Error(`Invalid DSH profile name: ${JSON.stringify(options.profile)}`)
|
|
54
|
+
}
|
|
55
|
+
let registry
|
|
56
|
+
try {
|
|
57
|
+
registry = new URL(options.registry)
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error(`Invalid npm registry URL: ${JSON.stringify(options.registry)}`)
|
|
60
|
+
}
|
|
61
|
+
if (registry.protocol !== 'https:' && registry.protocol !== 'http:') throw new Error('npm registry must use HTTP or HTTPS.')
|
|
62
|
+
if (registry.username !== '' || registry.password !== '' || registry.search !== '' || registry.hash !== '' || !/^[0-9A-Za-z._~/-]*$/.test(registry.pathname)) {
|
|
63
|
+
throw new Error('npm registry URL contains unsupported characters.')
|
|
64
|
+
}
|
|
65
|
+
options.registry = registry.href
|
|
66
|
+
return options
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function profileDirectory(profile, env = process.env, home = homedir()) {
|
|
70
|
+
const dshHome = env.DSH_HOME === undefined || env.DSH_HOME === '' ? resolve(home, '.dsh') : resolve(env.DSH_HOME)
|
|
71
|
+
return resolve(dshHome, 'profiles', profile)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function normalizeBundles(bundles = []) {
|
|
75
|
+
const managed = new Set([BASE_BUNDLE, WEB_BUNDLE, SUITE_PACKAGE, ...MEMBER_PACKAGES])
|
|
76
|
+
const unrelated = bundles.filter((name, index) => typeof name === 'string' && !managed.has(name) && bundles.indexOf(name) === index)
|
|
77
|
+
return [BASE_BUNDLE, WEB_BUNDLE, ...unrelated, ...MEMBER_PACKAGES]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function allowRequiredBuilds(source) {
|
|
81
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n'
|
|
82
|
+
const lines = source.split(/\r?\n/)
|
|
83
|
+
const start = lines.findIndex(line => line === 'allowBuilds:')
|
|
84
|
+
if (start === -1) return `${source}${source === '' || source.endsWith('\n') ? '' : eol}allowBuilds:${eol} protobufjs: false${eol} koffi: false${eol}`
|
|
85
|
+
let end = start + 1
|
|
86
|
+
while (end < lines.length && (lines[end] === '' || /^\s/.test(lines[end]))) end += 1
|
|
87
|
+
const body = lines.slice(start + 1, end).filter(line => !/^\s{2}(?:protobufjs|koffi):/.test(line))
|
|
88
|
+
lines.splice(start + 1, end - start - 1, ' protobufjs: false', ' koffi: false', ...body)
|
|
89
|
+
return lines.join(eol)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function directInstallArgs(profile, registry, manifest = suiteManifest) {
|
|
93
|
+
return ['plugin', '--profile', profile, 'add', ...memberSpecs(manifest), '--save-exact', `--registry=${registry}`]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function helpText() {
|
|
97
|
+
return `Usage: dsh-codex-suite [options]\n\nInstall every Codex Suite member as a direct dependency of one DSH profile.\n\nOptions:\n --profile <name> target profile (default: web)\n --registry <url> npm registry (default: https://registry.npmjs.org/)\n --dry-run print commands without changing the profile\n -h, --help show this help\n`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function quote(argument) {
|
|
101
|
+
return /[\s"']/u.test(argument) ? JSON.stringify(argument) : argument
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function runDsh(args, { dryRun = false, command = process.env.DSH_BIN || 'dsh' } = {}) {
|
|
105
|
+
process.stdout.write(`> dsh ${args.map(quote).join(' ')}\n`)
|
|
106
|
+
if (dryRun) return
|
|
107
|
+
const result = process.platform === 'win32'
|
|
108
|
+
? spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', `call ${/\s/.test(command) ? `"${command.replaceAll('"', '""')}"` : command} ${args.join(' ')}`], { encoding: 'utf8', stdio: 'inherit', windowsHide: true })
|
|
109
|
+
: spawnSync(command, args, { encoding: 'utf8', stdio: 'inherit' })
|
|
110
|
+
if (result.error !== undefined) throw new Error(`Unable to run dsh: ${result.error.message}`)
|
|
111
|
+
if (result.status !== 0) throw new Error(`dsh exited with code ${result.status ?? 1}.`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readProfileManifest(path) {
|
|
115
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function suiteDeclared(manifest) {
|
|
119
|
+
return manifest.dependencies?.[SUITE_PACKAGE] !== undefined
|
|
120
|
+
|| manifest.devDependencies?.[SUITE_PACKAGE] !== undefined
|
|
121
|
+
|| manifest.dsh?.profile?.bundles?.includes(SUITE_PACKAGE) === true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function legacyDirectPackages(manifest) {
|
|
125
|
+
const declared = { ...manifest.devDependencies, ...manifest.dependencies }
|
|
126
|
+
return [
|
|
127
|
+
...(suiteDeclared(manifest) ? [SUITE_PACKAGE] : []),
|
|
128
|
+
...(declared[WEB_BUNDLE] !== undefined ? [WEB_BUNDLE] : []),
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function writeNormalizedManifest(path) {
|
|
133
|
+
const manifest = readProfileManifest(path)
|
|
134
|
+
manifest.dsh ??= {}
|
|
135
|
+
manifest.dsh.profile ??= {}
|
|
136
|
+
manifest.dsh.profile.bundles = normalizeBundles(manifest.dsh.profile.bundles)
|
|
137
|
+
writeFileSync(path, `${JSON.stringify(manifest, undefined, 2)}\n`, 'utf8')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function installSuite(argv = process.argv.slice(2)) {
|
|
141
|
+
const options = parseArgs(argv)
|
|
142
|
+
if (options.help) {
|
|
143
|
+
process.stdout.write(helpText())
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
const profilePath = profileDirectory(options.profile)
|
|
147
|
+
const manifestPath = resolve(profilePath, 'package.json')
|
|
148
|
+
const workspacePath = resolve(profilePath, 'pnpm-workspace.yaml')
|
|
149
|
+
if (!existsSync(manifestPath)) runDsh(['plugin', '--profile', options.profile, 'install', '--lockfile-only', '--ignore-scripts'], options)
|
|
150
|
+
if (options.dryRun) {
|
|
151
|
+
process.stdout.write(`> ensure protobufjs build scripts stay explicitly disabled in ${workspacePath}\n`)
|
|
152
|
+
} else {
|
|
153
|
+
const workspace = existsSync(workspacePath) ? readFileSync(workspacePath, 'utf8') : ''
|
|
154
|
+
const nextWorkspace = allowRequiredBuilds(workspace)
|
|
155
|
+
if (nextWorkspace !== workspace) writeFileSync(workspacePath, nextWorkspace, 'utf8')
|
|
156
|
+
}
|
|
157
|
+
runDsh(directInstallArgs(options.profile, options.registry), options)
|
|
158
|
+
if (options.dryRun) {
|
|
159
|
+
process.stdout.write(`> ensure ${WEB_BUNDLE} precedes the six member bundles in ${manifestPath}\n`)
|
|
160
|
+
process.stdout.write(`> remove legacy ${SUITE_PACKAGE} and redundant direct ${WEB_BUNDLE} dependencies when present\n`)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
const installedManifest = readProfileManifest(manifestPath)
|
|
164
|
+
const legacyPackages = legacyDirectPackages(installedManifest)
|
|
165
|
+
if (legacyPackages.length > 0) runDsh(['plugin', '--profile', options.profile, 'remove', ...legacyPackages], options)
|
|
166
|
+
writeNormalizedManifest(manifestPath)
|
|
167
|
+
runDsh(['--profile', options.profile, '--dump-config'], options)
|
|
168
|
+
process.stdout.write(`\nCodex Suite members are direct plugins in profile ${options.profile}. Restart DSH and hard-refresh the browser.\n`)
|
|
169
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@michengai/dsh-codex-suite",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DSH Codex UI
|
|
3
|
+
"version": "0.1.14",
|
|
4
|
+
"description": "DSH Codex UI 一键安装器:将侧栏、专家、技能、归档、IM 助理和定时任务装为直接插件",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
7
10
|
"files": [
|
|
11
|
+
"bin.mjs",
|
|
12
|
+
"installer.mjs",
|
|
8
13
|
"cordis.patch.yml",
|
|
9
14
|
"README.md",
|
|
10
15
|
"README.zh-CN.md"
|
|
11
16
|
],
|
|
17
|
+
"bin": {
|
|
18
|
+
"dsh-codex-suite": "./bin.mjs"
|
|
19
|
+
},
|
|
12
20
|
"dsh": {
|
|
13
21
|
"bundle": {
|
|
14
22
|
"patch": "./cordis.patch.yml"
|
|
@@ -24,6 +32,6 @@
|
|
|
24
32
|
"@michengai/dsh-automation": "0.1.14",
|
|
25
33
|
"@michengai/dsh-im-connect": "0.1.23",
|
|
26
34
|
"@michengai/dsh-skills-manager": "0.1.24",
|
|
27
|
-
"@michengai/dsh-codex-ui": "0.2.
|
|
35
|
+
"@michengai/dsh-codex-ui": "0.2.82"
|
|
28
36
|
}
|
|
29
37
|
}
|