@twinsearth/dsh-2xray 0.2.7 → 0.2.9
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 +20 -0
- package/README.md +6 -0
- package/lib/binary.js +23 -1
- package/lib/supervisor.js +9 -2
- package/lib/xray-config.js +3 -1
- package/package.json +4 -1
- package/test/assets.test.js +35 -0
- package/test/binary.test.js +12 -1
- package/test/routing.test.js +2 -0
- package/tools/build-core-packages.mjs +114 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.9 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
### 内核分发
|
|
6
|
+
|
|
7
|
+
- 新增按平台拆分的可选内核包(`@twinsearth/dsh-2xray-core-<os>-<arch>`),主包通过 `optionalDependencies` 引用,npm 只装当前平台。
|
|
8
|
+
- `resolveBinary()` 新增 `core-package` 来源;未安装时继续回退到插件 `bin/`、数据目录、v2rayN 发现与 PATH。
|
|
9
|
+
- 新增 `tools/build-core-packages.mjs`(官方 zip → npm 包)与 `.github/workflows/core-packages.yml`(下载、`.dgst` 校验、发布;无 `NPM_TOKEN` 时只上传 artifact)。
|
|
10
|
+
|
|
11
|
+
### 验证
|
|
12
|
+
|
|
13
|
+
- 真机 xray 26.9.30 对 global / cn-direct / whitelist 三种配置执行 `run -test` 全部通过(含 geodata 与 IPIfNonMatch)。
|
|
14
|
+
|
|
15
|
+
## 0.2.8 — 2026-10-29
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
|
|
19
|
+
- `cn-direct` 模式设置 `routing.domainStrategy: IPIfNonMatch`:未被 `geosite:cn` 命中的域名会解析为 IP 再匹配 `geoip:cn`,国内站点不再漏判。
|
|
20
|
+
- `XRAY_LOCATION_ASSET` 显式传入 `run` 与 `run -test` 的 spawn 环境:服务启动器会清理环境,之前 geodata 路径可能丢失,导致 `geoip:cn`/`geosite:cn` 静默失效。
|
|
21
|
+
- 启动与 `xray_validate_config` 前都先复制 `geoip.dat`/`geosite.dat` 到数据目录;缺失时日志明确告警。
|
|
22
|
+
|
|
3
23
|
## 0.2.7 — 2026-10-29
|
|
4
24
|
|
|
5
25
|
### 新增
|
package/README.md
CHANGED
|
@@ -328,6 +328,12 @@ v0.2.0 提供两个入口,互不替代:
|
|
|
328
328
|
| 状态一直 `degraded` + `route-install-failed` | 查日志里的模块解析诊断;必要时确认 DSH 安装目录结构 |
|
|
329
329
|
| `xray_switch_node` 报 `unknown service ... RoutingService` | xray 版本早于 2024-09;升级二进制,或关闭 `control.enabled` 只用 `activeNode` |
|
|
330
330
|
|
|
331
|
+
### 平台内核包
|
|
332
|
+
|
|
333
|
+
`@twinsearth/dsh-2xray` 通过 `optionalDependencies` 引用按平台拆分的核心包(例如 `@twinsearth/dsh-2xray-core-win32-x64`),npm 只安装当前平台那一个。本地没有可用内核包时,仍按 `插件 bin/ → 数据目录 → v2rayN 发现 → PATH` 回退,或用 `tools/install-xray-core.mjs` 安装。
|
|
334
|
+
|
|
335
|
+
维护者用 `tools/build-core-packages.mjs` 从官方 zip 生成平台包;`.github/workflows/core-packages.yml` 会在具备 `NPM_TOKEN` secret 时自动下载、校验并发布各平台包。
|
|
336
|
+
|
|
331
337
|
## 路由模式与黑白名单
|
|
332
338
|
|
|
333
339
|
- `global`(默认):全部流量走 2Xray。
|
package/lib/binary.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, readdirSync } from 'node:fs'
|
|
2
|
-
import {
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { delimiter, dirname, join } from 'node:path'
|
|
3
4
|
import { fileURLToPath } from 'node:url'
|
|
4
5
|
|
|
5
6
|
/** Package root, so a bundle that ships its own `bin/xray(.exe)` is found first. */
|
|
@@ -29,6 +30,23 @@ function executableName() {
|
|
|
29
30
|
return process.platform === 'win32' ? 'xray.exe' : 'xray'
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Platform cores are published as separate optional packages so the main
|
|
35
|
+
* package stays small. npm installs only the matching os/cpu one.
|
|
36
|
+
*/
|
|
37
|
+
export function optionalCorePaths({ platform = process.platform, arch = process.arch, resolvePackage } = {}) {
|
|
38
|
+
const name = `@twinsearth/dsh-2xray-core-${platform}-${arch}`
|
|
39
|
+
const resolver = resolvePackage ?? ((specifier) => createRequire(import.meta.url).resolve(specifier))
|
|
40
|
+
try {
|
|
41
|
+
const manifest = resolver(`${name}/package.json`)
|
|
42
|
+
const base = dirname(manifest)
|
|
43
|
+
const executable = platform === 'win32' ? 'xray.exe' : 'xray'
|
|
44
|
+
return [...new Set([join(base, 'bin', executable), join(base, 'bin', 'xray')])]
|
|
45
|
+
} catch {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
function isExecutable(file) {
|
|
33
51
|
try {
|
|
34
52
|
accessSync(file, constants.X_OK)
|
|
@@ -56,12 +74,16 @@ export async function resolveBinary({
|
|
|
56
74
|
packageRoot = PACKAGE_ROOT,
|
|
57
75
|
discoveryRoots,
|
|
58
76
|
discoveryOptions = {},
|
|
77
|
+
resolvePackage,
|
|
59
78
|
}) {
|
|
60
79
|
const candidates = []
|
|
61
80
|
const explicit = config.binary.path?.trim()
|
|
62
81
|
if (explicit) candidates.push({ path: explicit, source: 'config' })
|
|
63
82
|
const fromEnv = process.env.DSH_XRAY_BIN?.trim()
|
|
64
83
|
if (fromEnv) candidates.push({ path: fromEnv, source: 'env' })
|
|
84
|
+
for (const path of optionalCorePaths({ resolvePackage })) {
|
|
85
|
+
candidates.push({ path, source: 'core-package' })
|
|
86
|
+
}
|
|
65
87
|
candidates.push({ path: join(packageRoot, 'bin', executableName()), source: 'bundled' })
|
|
66
88
|
candidates.push({ path: join(packageRoot, 'bin', 'xray'), source: 'bundled' })
|
|
67
89
|
candidates.push({ path: join(dataDir, 'bin', executableName()), source: 'managed' })
|
package/lib/supervisor.js
CHANGED
|
@@ -279,7 +279,9 @@ export class Supervisor {
|
|
|
279
279
|
argv: [binary.path, 'run', '-c', configPath],
|
|
280
280
|
cwd: this.dataDir,
|
|
281
281
|
graceMs: Math.max(500, lifecycle.stopGraceMs),
|
|
282
|
-
env
|
|
282
|
+
// geodata is copied next to the managed config; without this env the
|
|
283
|
+
// scrubbed service environment makes geoip:cn/geosite:cn fail silently.
|
|
284
|
+
env: this.dataDir ? { XRAY_LOCATION_ASSET: this.dataDir } : {},
|
|
283
285
|
})
|
|
284
286
|
} catch (error) {
|
|
285
287
|
this.#setState('failed', 'spawn-failed', String(error))
|
|
@@ -455,7 +457,7 @@ export class Supervisor {
|
|
|
455
457
|
argv: [binaryPath, 'run', '-test', '-c', configPath],
|
|
456
458
|
cwd: this.dataDir ?? process.cwd(),
|
|
457
459
|
graceMs: 2000,
|
|
458
|
-
env: {},
|
|
460
|
+
env: this.dataDir ? { XRAY_LOCATION_ASSET: this.dataDir } : {},
|
|
459
461
|
})
|
|
460
462
|
} catch (error) {
|
|
461
463
|
return { ok: false, detail: String(error) }
|
|
@@ -503,6 +505,11 @@ export class Supervisor {
|
|
|
503
505
|
}
|
|
504
506
|
}
|
|
505
507
|
|
|
508
|
+
const routing = this.config.routing ?? {}
|
|
509
|
+
if (routing.mode === 'cn-direct' || routing.geositeCn || routing.blockAds) {
|
|
510
|
+
ensureXrayAssets({ binaryPath: binary.path, dataDir: this.dataDir, logger: this.#logger })
|
|
511
|
+
}
|
|
512
|
+
|
|
506
513
|
const built = buildXrayConfig(this.config)
|
|
507
514
|
const text = JSON.stringify(built, null, 2)
|
|
508
515
|
const file = join(this.dataDir, 'conf', `validate-${Date.now()}-${process.pid}.json`)
|
package/lib/xray-config.js
CHANGED
|
@@ -231,7 +231,9 @@ export function buildXrayConfig(config) {
|
|
|
231
231
|
inbounds,
|
|
232
232
|
outbounds,
|
|
233
233
|
routing: {
|
|
234
|
-
|
|
234
|
+
// cn-direct relies on resolving unmatched domains to match geoip:cn;
|
|
235
|
+
// global/whitelist stay AsIs so no extra DNS lookups are introduced.
|
|
236
|
+
domainStrategy: mode === 'cn-direct' ? 'IPIfNonMatch' : 'AsIs',
|
|
235
237
|
rules,
|
|
236
238
|
...(balancers.length > 0 ? { balancers } : {}),
|
|
237
239
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/dsh-2xray",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "DSH-2Xray: supervises xray-core and routes DSH process-wide HTTP(S) traffic through its local inbound, with optional multi-node gRPC control plane",
|
|
6
6
|
"type": "module",
|
|
@@ -48,5 +48,8 @@
|
|
|
48
48
|
],
|
|
49
49
|
"platform": "web"
|
|
50
50
|
}
|
|
51
|
+
},
|
|
52
|
+
"optionalDependencies": {
|
|
53
|
+
"@twinsearth/dsh-2xray-core-win32-x64": "0.2.9"
|
|
51
54
|
}
|
|
52
55
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { ensureXrayAssets } from '../lib/assets.js'
|
|
7
|
+
|
|
8
|
+
test('geodata next to a v2rayN binary is copied into the managed data dir', () => {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-xray-assets-'))
|
|
10
|
+
try {
|
|
11
|
+
const bin = join(root, 'v2rayN-windows-64', 'bin')
|
|
12
|
+
const xrayDir = join(bin, 'xray')
|
|
13
|
+
mkdirSync(xrayDir, { recursive: true })
|
|
14
|
+
writeFileSync(join(bin, 'geoip.dat'), 'geoip')
|
|
15
|
+
writeFileSync(join(bin, 'geosite.dat'), 'geosite')
|
|
16
|
+
const dataDir = join(root, 'data')
|
|
17
|
+
const result = ensureXrayAssets({ binaryPath: join(xrayDir, 'xray.exe'), dataDir })
|
|
18
|
+
assert.deepEqual(result.copied.sort(), ['geoip.dat', 'geosite.dat'])
|
|
19
|
+
assert.equal(existsSync(join(dataDir, 'geoip.dat')), true)
|
|
20
|
+
assert.equal(existsSync(join(dataDir, 'geosite.dat')), true)
|
|
21
|
+
} finally {
|
|
22
|
+
rmSync(root, { recursive: true, force: true })
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('missing geodata is reported rather than silently ignored', () => {
|
|
27
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-xray-assets-'))
|
|
28
|
+
try {
|
|
29
|
+
const result = ensureXrayAssets({ binaryPath: join(root, 'xray.exe'), dataDir: join(root, 'data') })
|
|
30
|
+
assert.deepEqual(result.copied, [])
|
|
31
|
+
assert.deepEqual(result.missing.sort(), ['geoip.dat', 'geosite.dat'])
|
|
32
|
+
} finally {
|
|
33
|
+
rmSync(root, { recursive: true, force: true })
|
|
34
|
+
}
|
|
35
|
+
})
|
package/test/binary.test.js
CHANGED
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
|
|
|
3
3
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
|
-
import { BinaryError, resolveBinary } from '../lib/binary.js'
|
|
6
|
+
import { BinaryError, optionalCorePaths, resolveBinary } from '../lib/binary.js'
|
|
7
7
|
import { normalizeConfig } from '../lib/config.js'
|
|
8
8
|
|
|
9
9
|
const packageRoot = join('C:', 'plugins', 'dsh-2xray')
|
|
@@ -129,3 +129,14 @@ test('discovers a v2rayN layout when nothing else resolves', async () => {
|
|
|
129
129
|
rmSync(dir, { recursive: true, force: true })
|
|
130
130
|
}
|
|
131
131
|
})
|
|
132
|
+
|
|
133
|
+
test('optional platform core packages resolve on their own platform', () => {
|
|
134
|
+
const manifest = join('C:', 'profile', 'node_modules', '@twinsearth', 'dsh-2xray-core-darwin-arm64', 'package.json')
|
|
135
|
+
const paths = optionalCorePaths({
|
|
136
|
+
platform: 'darwin',
|
|
137
|
+
arch: 'arm64',
|
|
138
|
+
resolvePackage: () => manifest,
|
|
139
|
+
})
|
|
140
|
+
assert.deepEqual(paths, [join('C:', 'profile', 'node_modules', '@twinsearth', 'dsh-2xray-core-darwin-arm64', 'bin', 'xray')])
|
|
141
|
+
assert.deepEqual(optionalCorePaths({ platform: 'darwin', arch: 'arm64', resolvePackage: () => { throw new Error('missing') } }), [])
|
|
142
|
+
})
|
package/test/routing.test.js
CHANGED
|
@@ -26,6 +26,7 @@ test('global mode keeps the proxy catch-all and private direct rule', () => {
|
|
|
26
26
|
const { config, errors } = base()
|
|
27
27
|
assert.deepEqual(errors, [])
|
|
28
28
|
const rules = rulesOf(config)
|
|
29
|
+
assert.equal(buildXrayConfig(config).routing.domainStrategy, 'AsIs')
|
|
29
30
|
assert.ok(rules.some((rule) => Array.isArray(rule.ip) && rule.ip.includes('10.0.0.0/8')))
|
|
30
31
|
const catchAll = rules[rules.length - 1]
|
|
31
32
|
assert.equal(catchAll.network, 'tcp,udp')
|
|
@@ -35,6 +36,7 @@ test('global mode keeps the proxy catch-all and private direct rule', () => {
|
|
|
35
36
|
test('cn-direct adds geosite:cn and geoip:cn direct rules', () => {
|
|
36
37
|
const { config } = base({ routing: { mode: 'cn-direct' } })
|
|
37
38
|
const rules = rulesOf(config)
|
|
39
|
+
assert.equal(buildXrayConfig(config).routing.domainStrategy, 'IPIfNonMatch')
|
|
38
40
|
assert.ok(rules.some((rule) => rule.domain?.includes('geosite:cn')))
|
|
39
41
|
assert.ok(rules.some((rule) => rule.ip?.includes('geoip:cn')))
|
|
40
42
|
assert.equal(rules[rules.length - 1].outboundTag, 'proxy')
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Turn official Xray-core zips into per-platform npm packages.
|
|
4
|
+
*
|
|
5
|
+
* npm installs only the package whose `os`/`cpu` matches, so the main package
|
|
6
|
+
* can depend on all of them without downloading five cores. CI can run this on
|
|
7
|
+
* each runner (where GitHub release downloads work); locally it also accepts a
|
|
8
|
+
* directory of already-downloaded zips.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node tools/build-core-packages.mjs --zips ./vendor/zips --out ./dist-core --version 0.2.9
|
|
12
|
+
*
|
|
13
|
+
* Expected zip names (official release assets):
|
|
14
|
+
* Xray-windows-64.zip, Xray-windows-arm64-v8a.zip
|
|
15
|
+
* Xray-macos-64.zip, Xray-macos-arm64-v8a.zip
|
|
16
|
+
* Xray-linux-64.zip, Xray-linux-arm64-v8a.zip
|
|
17
|
+
*/
|
|
18
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { tmpdir } from 'node:os'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
import { spawnSync } from 'node:child_process'
|
|
22
|
+
|
|
23
|
+
const TARGETS = [
|
|
24
|
+
{ zip: 'Xray-windows-64.zip', pkg: 'win32-x64', os: 'win32', cpu: 'x64', exe: 'xray.exe' },
|
|
25
|
+
{ zip: 'Xray-windows-arm64-v8a.zip', pkg: 'win32-arm64', os: 'win32', cpu: 'arm64', exe: 'xray.exe' },
|
|
26
|
+
{ zip: 'Xray-macos-64.zip', pkg: 'darwin-x64', os: 'darwin', cpu: 'x64', exe: 'xray' },
|
|
27
|
+
{ zip: 'Xray-macos-arm64-v8a.zip', pkg: 'darwin-arm64', os: 'darwin', cpu: 'arm64', exe: 'xray' },
|
|
28
|
+
{ zip: 'Xray-linux-64.zip', pkg: 'linux-x64', os: 'linux', cpu: 'x64', exe: 'xray' },
|
|
29
|
+
{ zip: 'Xray-linux-arm64-v8a.zip', pkg: 'linux-arm64', os: 'linux', cpu: 'arm64', exe: 'xray' },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
function args() {
|
|
33
|
+
const parsed = {}
|
|
34
|
+
for (let index = 2; index < process.argv.length; index += 2) {
|
|
35
|
+
const key = process.argv[index].replace(/^--/, '')
|
|
36
|
+
parsed[key] = process.argv[index + 1]
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
zips: parsed.zips ?? './vendor/zips',
|
|
40
|
+
out: parsed.out ?? './dist-core',
|
|
41
|
+
version: parsed.version ?? JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function extract(zip, destination) {
|
|
46
|
+
const attempts =
|
|
47
|
+
process.platform === 'win32'
|
|
48
|
+
? [['tar', ['-xf', zip, '-C', destination]], ['powershell', ['-NoProfile', '-Command', `Expand-Archive -LiteralPath '${zip}' -DestinationPath '${destination}' -Force`]]]
|
|
49
|
+
: [['unzip', ['-o', zip, '-d', destination]], ['tar', ['-xf', zip, '-C', destination]]]
|
|
50
|
+
for (const [command, argv] of attempts) {
|
|
51
|
+
if (spawnSync(command, argv, { stdio: 'ignore' }).status === 0) return
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`could not extract ${zip}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findBinary(dir, name) {
|
|
57
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
58
|
+
const full = join(dir, entry.name)
|
|
59
|
+
if (entry.isFile() && entry.name === name) return full
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
const nested = findBinary(full, name)
|
|
62
|
+
if (nested) return nested
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const options = args()
|
|
69
|
+
mkdirSync(options.out, { recursive: true })
|
|
70
|
+
const built = []
|
|
71
|
+
for (const target of TARGETS) {
|
|
72
|
+
const zip = join(options.zips, target.zip)
|
|
73
|
+
if (!existsSync(zip)) {
|
|
74
|
+
console.log(`skip ${target.pkg}: ${target.zip} not present`)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
const workspace = mkdtempSync(join(tmpdir(), `dsh-core-${target.pkg}-`))
|
|
78
|
+
try {
|
|
79
|
+
extract(zip, workspace)
|
|
80
|
+
const binary = findBinary(workspace, target.exe)
|
|
81
|
+
if (!binary) throw new Error(`${target.exe} not found in ${target.zip}`)
|
|
82
|
+
const packageName = `@twinsearth/dsh-2xray-core-${target.pkg}`
|
|
83
|
+
const dir = join(options.out, `dsh-2xray-core-${target.pkg}`)
|
|
84
|
+
mkdirSync(join(dir, 'bin'), { recursive: true })
|
|
85
|
+
copyFileSync(binary, join(dir, 'bin', target.exe))
|
|
86
|
+
writeFileSync(
|
|
87
|
+
join(dir, 'package.json'),
|
|
88
|
+
JSON.stringify(
|
|
89
|
+
{
|
|
90
|
+
name: packageName,
|
|
91
|
+
version: options.version,
|
|
92
|
+
description: `Xray-core binary for ${target.pkg} (DSH-2Xray optional dependency)`,
|
|
93
|
+
license: 'MPL-2.0',
|
|
94
|
+
os: [target.os],
|
|
95
|
+
cpu: [target.cpu],
|
|
96
|
+
files: ['bin'],
|
|
97
|
+
},
|
|
98
|
+
null,
|
|
99
|
+
2,
|
|
100
|
+
) + '\n',
|
|
101
|
+
)
|
|
102
|
+
built.push({ name: packageName, dir, size: statSync(join(dir, 'bin', target.exe)).size })
|
|
103
|
+
} finally {
|
|
104
|
+
rmSync(workspace, { recursive: true, force: true })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const entry of built) {
|
|
109
|
+
console.log(`built ${entry.name} (${Math.round(entry.size / 1048576)} MB) -> ${entry.dir}`)
|
|
110
|
+
}
|
|
111
|
+
if (built.length === 0) {
|
|
112
|
+
console.log(`no zips found in ${options.zips}`)
|
|
113
|
+
process.exitCode = 1
|
|
114
|
+
}
|