dsh-patch-edit-plus 0.1.2 → 0.2.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 +3 -4
- package/README.zh.md +3 -4
- package/dsh.plugin.json +2 -2
- package/lib/config.js +10 -2
- package/lib/index.js +18 -36
- package/lib/types/config.d.ts +38 -3
- package/lib/types/index.d.ts +15 -12
- package/package.json +13 -9
package/README.md
CHANGED
|
@@ -6,11 +6,10 @@ Patch-style file editing for [DeepSeek Harness (DSH)](https://github.com/deepsee
|
|
|
6
6
|
|
|
7
7
|
| DSH version | Status | Notes |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| `0.1.
|
|
10
|
-
| `0.1.
|
|
11
|
-
| `0.1.5-rc.2` | ✅ statically verified | `npm run typecheck:0.1.5` typechecks `src/` against the real `0.1.5-rc.2` peer packages. No `readByteRange` usage, no 0.1.5-only PTC identifiers. |
|
|
9
|
+
| `0.1.7-rc.1+` | ✅ supported (this line, v0.2.0+) | Declarative settings: `allowCodexPatch` is marked `.volatile()` — the settings form renders automatically and `loader/volatile-update` re-registers the tool without a remount. No registration call. |
|
|
10
|
+
| `0.1.2-rc.1` ~ `0.1.5-rc.2` | ↗ maintenance line | Served by the pre-0.1.7 branch / `dsh-0.1.5` dist-tag. 0.1.7 removed the imperative settings APIs the dual-API fallback relied on, so one codebase cannot serve both. |
|
|
12
11
|
|
|
13
|
-
Settings
|
|
12
|
+
Settings are declarative on this line: fields marked `.volatile()` in `Config` become the settings form; the plugin subscribes to `loader/volatile-update` instead of receiving registration hooks.
|
|
14
13
|
|
|
15
14
|
## Why patch at all? (tool routing guide)
|
|
16
15
|
|
package/README.zh.md
CHANGED
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
| DSH 版本 | 状态 | 说明 |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| `0.1.
|
|
10
|
-
| `0.1.
|
|
11
|
-
| `0.1.5-rc.2` | ✅ 静态核验 | `npm run typecheck:0.1.5` 用真实 `0.1.5-rc.2` peer 包对 `src/` 做类型核验;不调用 `readByteRange`,不含 0.1.5-only PTC 字符串。 |
|
|
9
|
+
| `0.1.7-rc.1+` | ✅ 支持(本线,v0.2.0+) | 声明式设置:`allowCodexPatch` 标记 `.volatile()`——设置表单自动生成,`loader/volatile-update` 驱动工具原位重注册(不 remount),无任何注册调用。 |
|
|
10
|
+
| `0.1.2-rc.1` ~ `0.1.5-rc.2` | ↗ 维护线 | 由 0.1.7 前的分支 / `dsh-0.1.5` dist-tag 服务。0.1.7 删除了双 API 回退所依赖的命令式注册 API,一份代码无法同时兼容两代。 |
|
|
12
11
|
|
|
13
|
-
settings
|
|
12
|
+
本线的 settings 是声明式的:`Config` 中标记 `.volatile()` 的字段即设置表单;插件订阅 `loader/volatile-update`,不再接收注册 hooks。
|
|
14
13
|
|
|
15
14
|
## 工具路由指引(为什么该用谁)
|
|
16
15
|
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-patch-edit-plus",
|
|
3
3
|
"name": "dsh-patch-edit-plus",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"description": {
|
|
6
6
|
"en": "Patch-style file editing: one apply_patch tool for git/unified diff (default) and Codex apply_patch syntax (opt-in), with all-or-nothing application and sandbox-aware delete/move.",
|
|
7
7
|
"zh": "补丁风格文件编辑:单个 apply_patch 工具,接受 git/unified diff(默认)与 Codex apply_patch 语法(可选开启),全量验证后原子应用,删除/移动走沙箱感知 shell。"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"engines": {
|
|
16
|
-
"dsh": ">=0.1.
|
|
16
|
+
"dsh": ">=0.1.7-rc.1 <0.1.8-0"
|
|
17
17
|
},
|
|
18
18
|
"components": {
|
|
19
19
|
"host": "lib/index.js"
|
package/lib/config.js
CHANGED
|
@@ -12,7 +12,7 @@ export const Config = z.object({
|
|
|
12
12
|
conflictPolicy: z.union(['rename', 'skip', 'fail']).default('rename'),
|
|
13
13
|
renameSuffix: z.string().default('_1'),
|
|
14
14
|
allowUnifiedDiff: z.boolean().default(true),
|
|
15
|
-
allowCodexPatch: z.boolean().default(false),
|
|
15
|
+
allowCodexPatch: z.boolean().default(false).volatile(),
|
|
16
16
|
deleteBackend: z.union(['shell', 'none']).default('shell'),
|
|
17
17
|
shellDialect: z.union(['auto', 'posix', 'pwsh']).default('auto'),
|
|
18
18
|
deleteCommand: z.string(),
|
|
@@ -23,6 +23,14 @@ export const Config = z.object({
|
|
|
23
23
|
maxPatchBytes: z.number().step(1).min(1024).default(512 * 1024),
|
|
24
24
|
maxDiffBytes: z.number().step(1).min(256).default(16 * 1024),
|
|
25
25
|
});
|
|
26
|
+
/** Read a `.volatile()` field: a live ref on 0.1.7+, a plain boolean otherwise. */
|
|
27
|
+
export function readVolatileBoolean(value, fallback) {
|
|
28
|
+
if (typeof value === 'boolean')
|
|
29
|
+
return value;
|
|
30
|
+
if (value && typeof value.get === 'function')
|
|
31
|
+
return Boolean(value.get());
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
26
34
|
/** Fill user config with defaults; `undefined`/null entries fall back too. */
|
|
27
35
|
export function resolveConfig(config) {
|
|
28
36
|
const c = config ?? {};
|
|
@@ -31,7 +39,7 @@ export function resolveConfig(config) {
|
|
|
31
39
|
conflictPolicy: c.conflictPolicy ?? 'rename',
|
|
32
40
|
renameSuffix: typeof c.renameSuffix === 'string' ? c.renameSuffix : '_1',
|
|
33
41
|
allowUnifiedDiff: c.allowUnifiedDiff ?? true,
|
|
34
|
-
allowCodexPatch: c.allowCodexPatch
|
|
42
|
+
allowCodexPatch: readVolatileBoolean(c.allowCodexPatch, false),
|
|
35
43
|
deleteBackend: c.deleteBackend ?? 'shell',
|
|
36
44
|
shellDialect: c.shellDialect ?? 'auto',
|
|
37
45
|
deleteCommand: typeof c.deleteCommand === 'string' && c.deleteCommand !== '' ? c.deleteCommand : undefined,
|
package/lib/index.js
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
* One `apply_patch` tool accepting git/unified diff (default) and Codex
|
|
5
5
|
* `apply_patch` syntax (opt-in). All-or-nothing application through the
|
|
6
6
|
* official fs write-intent dance; delete/move through the sandbox-aware
|
|
7
|
-
* shell.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* shell. Targets DSH 0.1.7+: settings are declarative — the fields marked
|
|
8
|
+
* `.volatile()` in `Config` render the settings form automatically (no
|
|
9
|
+
* registration call), and `loader/volatile-update` drives re-registration
|
|
10
|
+
* without a plugin remount. This line drops the pre-0.1.7 hosts; the
|
|
11
|
+
* 0.1.2-rc.1 ~ 0.1.5-rc.2 line stays on its maintenance branch.
|
|
11
12
|
*
|
|
12
13
|
* @module dsh-patch-edit-plus
|
|
13
14
|
*/
|
|
14
|
-
import {
|
|
15
|
+
import { resolveConfig } from './config.js';
|
|
15
16
|
import { registerApplyPatchTool } from './register.js';
|
|
16
17
|
/** Cordis plugin name used by Loader diagnostics. */
|
|
17
18
|
export const name = 'dsh-patch-edit-plus';
|
|
@@ -31,26 +32,21 @@ export const name = 'dsh-patch-edit-plus';
|
|
|
31
32
|
* in every profile that has no shell capability.
|
|
32
33
|
*/
|
|
33
34
|
export const inject = ['tools', 'fs'];
|
|
35
|
+
export { Config } from './config.js';
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* Register the tool from the current config snapshot.
|
|
38
|
+
*
|
|
39
|
+
* 登记级 key。工具「描述」把可用语法固化进去了(tool.ts 拼 `Accepts ${styles}`),
|
|
40
|
+
* 所以判定必须覆盖整份 resolved config,而不是只看 allowCodexPatch。
|
|
39
41
|
*/
|
|
40
|
-
export const SETTINGS_NAMESPACE = 'patch-edit-plus';
|
|
41
|
-
export { Config };
|
|
42
|
-
/** Register the tool and the settings namespace. Every registration is scoped to this plugin. */
|
|
43
42
|
export function apply(ctx, config) {
|
|
43
|
+
// 组合条目(0.1.7 起 volatile 字段在其中是 live 引用):rejudge 每次整体
|
|
44
|
+
// 重解析一次,一次调用内是同一份快照;跨调用的最新值由事件驱动重取。
|
|
44
45
|
const compositionEntry = config ?? {};
|
|
45
|
-
// 权威来源:设置层挂载时是解析后的 scope,否则是组合条目。
|
|
46
|
-
// installSection 保证在每次匹配的 onChange 之前先调用 setSource(attach 与 detach 各一次)。
|
|
47
|
-
let readSource = () => compositionEntry;
|
|
48
46
|
let disposer = null;
|
|
49
|
-
// 登记级 key。工具「描述」把可用语法固化进去了(tool.ts 拼 `Accepts ${styles}`),
|
|
50
|
-
// 所以判定必须覆盖整份 resolved config,而不是只看 allowCodexPatch。
|
|
51
47
|
let registeredKey = null;
|
|
52
48
|
const rejudge = () => {
|
|
53
|
-
const next = resolveConfig(
|
|
49
|
+
const next = resolveConfig(compositionEntry);
|
|
54
50
|
const key = JSON.stringify(next);
|
|
55
51
|
if (key === registeredKey)
|
|
56
52
|
return;
|
|
@@ -66,23 +62,9 @@ export function apply(ctx, config) {
|
|
|
66
62
|
registeredKey = null;
|
|
67
63
|
};
|
|
68
64
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
settings.installSection(ctx, SETTINGS_NAMESPACE, Config, compositionEntry, {
|
|
74
|
-
setSource: (current) => { readSource = current; },
|
|
75
|
-
onChange: () => { rejudge(); },
|
|
76
|
-
});
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (typeof settings?.register === 'function') {
|
|
80
|
-
const scope = settings.register(SETTINGS_NAMESPACE, Config, { base: compositionEntry });
|
|
81
|
-
if (scope !== undefined) {
|
|
82
|
-
readSource = () => scope.get();
|
|
83
|
-
scope.watch(() => { rejudge(); });
|
|
84
|
-
rejudge();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
65
|
+
// 0.1.7+: 只有 volatile 字段变更才走这里(不 remount);普通字段变更会整体
|
|
66
|
+
// remount 插件,新一次 `apply` 自然读到全部新值。等值变更宿主不通知。
|
|
67
|
+
ctx.on('loader/volatile-update', () => {
|
|
68
|
+
rejudge();
|
|
87
69
|
});
|
|
88
70
|
}
|
package/lib/types/config.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module dsh-patch-edit-plus/config
|
|
7
7
|
*/
|
|
8
8
|
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
import type { Volatile } from '@deepseek-ai/cosmokit';
|
|
9
10
|
/** What to do when the configured tool name is already taken at registration. */
|
|
10
11
|
export type ConflictPolicy = 'rename' | 'skip' | 'fail';
|
|
11
12
|
/** Backend used for Delete/Move operations (`ctx.fs` has no delete/move). */
|
|
@@ -22,8 +23,10 @@ export interface Config {
|
|
|
22
23
|
renameSuffix?: string;
|
|
23
24
|
/** Accept git/unified diff patches. Defaults to true. */
|
|
24
25
|
allowUnifiedDiff?: boolean;
|
|
25
|
-
/** Accept Codex `apply_patch` syntax. Defaults to false.
|
|
26
|
-
|
|
26
|
+
/** Accept Codex `apply_patch` syntax. Defaults to false.
|
|
27
|
+
* 0.1.7 `.volatile()`: the loader hands `apply` a live `Volatile` ref for this
|
|
28
|
+
* field — always read it through `readVolatileBoolean`, never cache the ref. */
|
|
29
|
+
allowCodexPatch?: boolean | Volatile<boolean>;
|
|
27
30
|
/** Delete/Move backend. Defaults to `shell`. `none` rejects Delete/Move with a structured error. */
|
|
28
31
|
deleteBackend?: DeleteBackend;
|
|
29
32
|
/** Shell dialect for the constant delete/move templates. Defaults to `auto` (pwsh on win32). */
|
|
@@ -44,7 +47,37 @@ export interface Config {
|
|
|
44
47
|
maxDiffBytes?: number;
|
|
45
48
|
}
|
|
46
49
|
/** Schemastery schema for Loader defaults and generated configuration docs. */
|
|
47
|
-
export declare const Config: z<
|
|
50
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
51
|
+
toolName: z<string, string, "defined">;
|
|
52
|
+
conflictPolicy: z<"rename" | "skip" | "fail", "rename" | "skip" | "fail", "defined">;
|
|
53
|
+
renameSuffix: z<string, string, "defined">;
|
|
54
|
+
allowUnifiedDiff: z<boolean, boolean, "defined">;
|
|
55
|
+
allowCodexPatch: z<boolean, boolean, "volatile-defined">;
|
|
56
|
+
deleteBackend: z<"shell" | "none", "shell" | "none", "defined">;
|
|
57
|
+
shellDialect: z<"auto" | "posix" | "pwsh", "auto" | "posix" | "pwsh", "defined">;
|
|
58
|
+
deleteCommand: z<string, string, "plain">;
|
|
59
|
+
moveCommand: z<string, string, "plain">;
|
|
60
|
+
dryRunByDefault: z<boolean, boolean, "defined">;
|
|
61
|
+
followSymlinks: z<boolean, boolean, "defined">;
|
|
62
|
+
maxFiles: z<number, number, "defined">;
|
|
63
|
+
maxPatchBytes: z<number, number, "defined">;
|
|
64
|
+
maxDiffBytes: z<number, number, "defined">;
|
|
65
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
66
|
+
toolName: z<string, string, "defined">;
|
|
67
|
+
conflictPolicy: z<"rename" | "skip" | "fail", "rename" | "skip" | "fail", "defined">;
|
|
68
|
+
renameSuffix: z<string, string, "defined">;
|
|
69
|
+
allowUnifiedDiff: z<boolean, boolean, "defined">;
|
|
70
|
+
allowCodexPatch: z<boolean, boolean, "volatile-defined">;
|
|
71
|
+
deleteBackend: z<"shell" | "none", "shell" | "none", "defined">;
|
|
72
|
+
shellDialect: z<"auto" | "posix" | "pwsh", "auto" | "posix" | "pwsh", "defined">;
|
|
73
|
+
deleteCommand: z<string, string, "plain">;
|
|
74
|
+
moveCommand: z<string, string, "plain">;
|
|
75
|
+
dryRunByDefault: z<boolean, boolean, "defined">;
|
|
76
|
+
followSymlinks: z<boolean, boolean, "defined">;
|
|
77
|
+
maxFiles: z<number, number, "defined">;
|
|
78
|
+
maxPatchBytes: z<number, number, "defined">;
|
|
79
|
+
maxDiffBytes: z<number, number, "defined">;
|
|
80
|
+
}>>, "plain">;
|
|
48
81
|
/** Fully-resolved configuration used by the tool body. */
|
|
49
82
|
export interface ResolvedConfig {
|
|
50
83
|
toolName: string;
|
|
@@ -62,5 +95,7 @@ export interface ResolvedConfig {
|
|
|
62
95
|
maxPatchBytes: number;
|
|
63
96
|
maxDiffBytes: number;
|
|
64
97
|
}
|
|
98
|
+
/** Read a `.volatile()` field: a live ref on 0.1.7+, a plain boolean otherwise. */
|
|
99
|
+
export declare function readVolatileBoolean(value: boolean | Volatile<boolean> | undefined, fallback: boolean): boolean;
|
|
65
100
|
/** Fill user config with defaults; `undefined`/null entries fall back too. */
|
|
66
101
|
export declare function resolveConfig(config: Config | undefined): ResolvedConfig;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,16 +4,21 @@
|
|
|
4
4
|
* One `apply_patch` tool accepting git/unified diff (default) and Codex
|
|
5
5
|
* `apply_patch` syntax (opt-in). All-or-nothing application through the
|
|
6
6
|
* official fs write-intent dance; delete/move through the sandbox-aware
|
|
7
|
-
* shell.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* shell. Targets DSH 0.1.7+: settings are declarative — the fields marked
|
|
8
|
+
* `.volatile()` in `Config` render the settings form automatically (no
|
|
9
|
+
* registration call), and `loader/volatile-update` drives re-registration
|
|
10
|
+
* without a plugin remount. This line drops the pre-0.1.7 hosts; the
|
|
11
|
+
* 0.1.2-rc.1 ~ 0.1.5-rc.2 line stays on its maintenance branch.
|
|
11
12
|
*
|
|
12
13
|
* @module dsh-patch-edit-plus
|
|
13
14
|
*/
|
|
14
15
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
-
import { Config } from './config.js';
|
|
16
16
|
import type { Config as PluginConfig } from './config.js';
|
|
17
|
+
declare module '@deepseek-ai/cordis' {
|
|
18
|
+
interface Events {
|
|
19
|
+
'loader/volatile-update': (paths: string[]) => void;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
17
22
|
/** Cordis plugin name used by Loader diagnostics. */
|
|
18
23
|
export declare const name = "dsh-patch-edit-plus";
|
|
19
24
|
/**
|
|
@@ -32,13 +37,11 @@ export declare const name = "dsh-patch-edit-plus";
|
|
|
32
37
|
* in every profile that has no shell capability.
|
|
33
38
|
*/
|
|
34
39
|
export declare const inject: string[];
|
|
40
|
+
export { Config } from './config.js';
|
|
35
41
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
42
|
+
* Register the tool from the current config snapshot.
|
|
43
|
+
*
|
|
44
|
+
* 登记级 key。工具「描述」把可用语法固化进去了(tool.ts 拼 `Accepts ${styles}`),
|
|
45
|
+
* 所以判定必须覆盖整份 resolved config,而不是只看 allowCodexPatch。
|
|
40
46
|
*/
|
|
41
|
-
export declare const SETTINGS_NAMESPACE = "patch-edit-plus";
|
|
42
|
-
export { Config };
|
|
43
|
-
/** Register the tool and the settings namespace. Every registration is scoped to this plugin. */
|
|
44
47
|
export declare function apply(ctx: Context, config?: PluginConfig): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-patch-edit-plus",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Patch-style file editing for DeepSeek Harness: one apply_patch tool accepting git/unified diff (default) and Codex apply_patch syntax (opt-in), with all-or-nothing application, read-before-write intent dance, sandbox-aware delete/move,
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Patch-style file editing for DeepSeek Harness: one apply_patch tool accepting git/unified diff (default) and Codex apply_patch syntax (opt-in), with all-or-nothing application, read-before-write intent dance, sandbox-aware delete/move, with a DSH 0.1.7+ line (declarative volatile settings).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=20",
|
|
50
|
-
"dsh": ">=0.1.
|
|
50
|
+
"dsh": ">=0.1.7-rc.1 <0.1.8-0"
|
|
51
51
|
},
|
|
52
52
|
"license": "MIT",
|
|
53
53
|
"keywords": [
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
],
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
65
|
-
"@deepseek-ai/dsh-fs": ">=0.1.
|
|
66
|
-
"@deepseek-ai/dsh-sandbox": ">=0.1.
|
|
67
|
-
"@deepseek-ai/dsh-shell": ">=0.1.
|
|
68
|
-
"@deepseek-ai/dsh-tools": ">=0.1.
|
|
65
|
+
"@deepseek-ai/dsh-fs": ">=0.1.7-rc.1 <0.1.8-0",
|
|
66
|
+
"@deepseek-ai/dsh-sandbox": ">=0.1.7-rc.1 <0.1.8-0",
|
|
67
|
+
"@deepseek-ai/dsh-shell": ">=0.1.7-rc.1 <0.1.8-0",
|
|
68
|
+
"@deepseek-ai/dsh-tools": ">=0.1.7-rc.1 <0.1.8-0"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"@deepseek-ai/cordis": {
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
88
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
@@ -98,6 +98,10 @@
|
|
|
98
98
|
"eslint": "^9.0.0",
|
|
99
99
|
"typescript": "^5.6.0",
|
|
100
100
|
"typescript-eslint": "^8.0.0",
|
|
101
|
-
"vitest": "^3.0.0"
|
|
101
|
+
"vitest": "^3.0.0",
|
|
102
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
103
|
+
},
|
|
104
|
+
"publishConfig": {
|
|
105
|
+
"tag": "dsh-0.1.7"
|
|
102
106
|
}
|
|
103
107
|
}
|