lark-remote 0.5.12 → 0.5.13
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 +13 -2
- package/README.en.md +1 -14
- package/README.md +1 -14
- package/dist/bridge/approval-coordinator.d.ts +1 -0
- package/dist/bridge/approval-coordinator.js +12 -24
- package/dist/bridge/index.d.ts +7 -0
- package/dist/bridge/index.js +77 -227
- package/dist/bridge/run-approval.d.ts +15 -0
- package/dist/bridge/run-approval.js +41 -0
- package/dist/bridge/run-usage.d.ts +46 -0
- package/dist/bridge/run-usage.js +96 -0
- package/dist/bridge/session-writeback.d.ts +18 -0
- package/dist/bridge/session-writeback.js +36 -0
- package/dist/card/bash-card-session.js +2 -5
- package/dist/card/card-session.d.ts +2 -0
- package/dist/card/card-session.js +20 -22
- package/dist/card/card-shared.d.ts +4 -4
- package/dist/card/card-shared.js +3 -9
- package/dist/card/collapsible.js +3 -2
- package/dist/card/final-answer-overflow.d.ts +7 -0
- package/dist/card/final-answer-overflow.js +46 -0
- package/dist/card/run-card-session.js +2 -7
- package/dist/card/run-renderer.d.ts +14 -0
- package/dist/card/run-renderer.js +119 -1
- package/dist/card/run-state.d.ts +12 -1
- package/dist/card/run-state.js +73 -34
- package/dist/config/codex-config.js +4 -4
- package/dist/config/index.d.ts +4 -2
- package/dist/config/index.js +6 -4
- package/dist/inbound/turn-assembler.js +6 -0
- package/dist/index.js +15 -409
- package/dist/message-handlers.d.ts +26 -0
- package/dist/message-handlers.js +275 -0
- package/dist/router/card-action-payload.d.ts +1 -1
- package/dist/router/card-action-payload.js +1 -1
- package/dist/router/commands/ls.d.ts +9 -0
- package/dist/router/commands/ls.js +243 -0
- package/dist/router/commands/order.d.ts +12 -0
- package/dist/router/commands/order.js +178 -0
- package/dist/router/commands/resume.d.ts +47 -0
- package/dist/router/commands/resume.js +246 -0
- package/dist/router/commands/ws.d.ts +15 -0
- package/dist/router/commands/ws.js +189 -0
- package/dist/router/config/claude.js +3 -3
- package/dist/router/config/codex.js +2 -6
- package/dist/router/config/common/turn-idle.d.ts +2 -0
- package/dist/router/config/common/turn-idle.js +9 -0
- package/dist/router/config/kimi.js +2 -6
- package/dist/router/config/opencode.js +2 -6
- package/dist/router/index.d.ts +14 -22
- package/dist/router/index.js +70 -1152
- package/dist/router/paging.d.ts +13 -0
- package/dist/router/paging.js +20 -0
- package/dist/router/path-validation.d.ts +27 -0
- package/dist/router/path-validation.js +45 -0
- package/dist/runner/claude/runner.d.ts +3 -4
- package/dist/runner/claude/runner.js +4 -5
- package/dist/runner/claude/session.js +1 -1
- package/dist/runner/codex/app-server/translator.d.ts +2 -0
- package/dist/runner/codex/app-server/translator.js +65 -9
- package/dist/runner/common/acp/base-translator.d.ts +3 -8
- package/dist/runner/common/acp/base-translator.js +16 -7
- package/dist/runner/common/spawning-runner.js +10 -4
- package/dist/runner/common/turn-started.d.ts +2 -0
- package/dist/runner/common/turn-started.js +10 -0
- package/dist/runner/dsh/auth.js +2 -3
- package/dist/runner/dsh/client.d.ts +1 -6
- package/dist/runner/dsh/client.js +11 -165
- package/dist/runner/dsh/daemon.d.ts +0 -2
- package/dist/runner/dsh/daemon.js +1 -6
- package/dist/runner/dsh/errors.d.ts +5 -0
- package/dist/runner/dsh/errors.js +11 -0
- package/dist/runner/dsh/exit-cancel.d.ts +0 -2
- package/dist/runner/dsh/exit-cancel.js +1 -1
- package/dist/runner/dsh/home.d.ts +2 -0
- package/dist/runner/dsh/home.js +8 -0
- package/dist/runner/dsh/inflight.d.ts +0 -2
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.d.ts +26 -0
- package/dist/runner/dsh/mux-stream.js +157 -0
- package/dist/runner/dsh/translator.d.ts +0 -10
- package/dist/runner/dsh/translator.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +1 -12
- package/dist/runner/opencode/acp/translator.js +1 -12
- package/dist/runner/pi/rpc/translator.d.ts +2 -8
- package/dist/runner/pi/rpc/translator.js +2 -7
- package/dist/runner/registry.d.ts +0 -19
- package/dist/runner/registry.js +0 -32
- package/dist/runner/types.d.ts +17 -1
- package/dist/session/claude/session-index.js +4 -2
- package/dist/session/kimi/sessions.js +2 -2
- package/dist/session/opencode/sessions.js +2 -1
- package/dist/session/pi/sessions.js +3 -2
- package/package.json +1 -1
package/dist/card/run-state.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { keepTail } from '../common/truncate.js';
|
|
2
|
+
import { asRecord } from '../common/tool-summary.js';
|
|
2
3
|
const MAX_REASONING_CHARS = 4_000;
|
|
3
4
|
const MAX_TEXT_CHARS = 12_000;
|
|
4
5
|
const MAX_TOOL_DETAIL_CHARS = 3_000;
|
|
@@ -10,7 +11,7 @@ const MAX_BLOCKS = 24;
|
|
|
10
11
|
* 禁止各自再抄一份字面量——三处漂移过一次(goal_continuing 只进白名单不进
|
|
11
12
|
* 按钮判据),新增终态时必漏一处的风险是结构性的。
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
+
const RUN_TERMINAL_STATES = [
|
|
14
15
|
'done',
|
|
15
16
|
'error',
|
|
16
17
|
'interrupted',
|
|
@@ -50,6 +51,8 @@ export function reduceRunState(state, event) {
|
|
|
50
51
|
return reduceBackgroundJobsEvent(state, event);
|
|
51
52
|
if (event.type === 'result')
|
|
52
53
|
return reduceResultEvent(state, event);
|
|
54
|
+
if (event.type === 'final_answer')
|
|
55
|
+
return reduceFinalAnswerEvent(state, event);
|
|
53
56
|
if (event.type === 'assistant')
|
|
54
57
|
return reduceAssistantEvent(state, event);
|
|
55
58
|
if (event.type === 'user')
|
|
@@ -187,7 +190,7 @@ function reduceTurnDiffEvent(state, event) {
|
|
|
187
190
|
id: event.itemId,
|
|
188
191
|
name: event.toolName ?? 'command',
|
|
189
192
|
input: inputStr,
|
|
190
|
-
parsedInput:
|
|
193
|
+
parsedInput: asRecord(inputStr),
|
|
191
194
|
...(event.toolHint !== undefined ? { toolHint: event.toolHint } : {}),
|
|
192
195
|
output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
|
|
193
196
|
status: complete ? (event.toolStatus ?? 'ok') : 'running',
|
|
@@ -320,31 +323,82 @@ function reduceResultEvent(state, event) {
|
|
|
320
323
|
// sessionId is set by system.init; undefined means init hasn't arrived yet.
|
|
321
324
|
if (state.sessionId === undefined)
|
|
322
325
|
return state;
|
|
326
|
+
const withFinalAnswer = promoteFinalAnswer(state, 'inferred');
|
|
323
327
|
const resultEvent = event;
|
|
324
328
|
const incomingSubtype = event.subtype;
|
|
325
329
|
const incomingErrorMsg = incomingSubtype === 'error' ? (resultEvent.errorMessage ?? 'Agent 返回错误结果') : undefined;
|
|
326
330
|
// 合并 subtype:error 优先,其次 interrupted(任一为 error 即 error;
|
|
327
331
|
// 否则任一为 interrupted 即 interrupted;interrupted 不得并入 success)。
|
|
328
|
-
const mergedSubtype =
|
|
332
|
+
const mergedSubtype = withFinalAnswer.resultSubtype === 'error' || incomingSubtype === 'error'
|
|
329
333
|
? 'error'
|
|
330
|
-
:
|
|
334
|
+
: withFinalAnswer.resultSubtype === 'interrupted' || incomingSubtype === 'interrupted'
|
|
331
335
|
? 'interrupted'
|
|
332
336
|
: 'success';
|
|
333
337
|
// 合并 errorMsg:首个非 undefined 胜出(existing ?? incoming)
|
|
334
|
-
const mergedErrorMsg =
|
|
338
|
+
const mergedErrorMsg = withFinalAnswer.errorMsg ?? incomingErrorMsg;
|
|
335
339
|
return {
|
|
336
|
-
...
|
|
340
|
+
...withFinalAnswer,
|
|
337
341
|
terminal: 'finalizing',
|
|
338
342
|
footer: null,
|
|
339
|
-
blocks: markThinkingInactive(
|
|
343
|
+
blocks: markThinkingInactive(withFinalAnswer.blocks),
|
|
340
344
|
resultSubtype: mergedSubtype,
|
|
341
345
|
errorMsg: mergedErrorMsg,
|
|
342
346
|
// costUsd:首个非 undefined 胜出(与 errorMsg 合并语义一致)
|
|
343
|
-
...(event.total_cost_usd !== undefined &&
|
|
347
|
+
...(event.total_cost_usd !== undefined && withFinalAnswer.costUsd === undefined
|
|
344
348
|
? { costUsd: event.total_cost_usd }
|
|
345
349
|
: {}),
|
|
346
350
|
};
|
|
347
351
|
}
|
|
352
|
+
/** Record an authoritative protocol final answer and remove its progress copy. */
|
|
353
|
+
function reduceFinalAnswerEvent(state, event) {
|
|
354
|
+
if (event.type !== 'final_answer')
|
|
355
|
+
return state;
|
|
356
|
+
if (!event.text.trim() && !state.finalAnswer)
|
|
357
|
+
return state;
|
|
358
|
+
const blocks = event.itemId
|
|
359
|
+
? state.blocks.filter((block) => block.kind !== 'text' || block.itemId !== event.itemId)
|
|
360
|
+
: state.blocks;
|
|
361
|
+
const content = event.text.trim() ? event.text : (state.finalAnswer?.content ?? '');
|
|
362
|
+
return {
|
|
363
|
+
...state,
|
|
364
|
+
blocks,
|
|
365
|
+
finalAnswer: {
|
|
366
|
+
content,
|
|
367
|
+
timestamp: state.finalAnswer?.timestamp ?? event.timestamp,
|
|
368
|
+
complete: event.complete ?? state.finalAnswer?.complete ?? false,
|
|
369
|
+
source: 'protocol',
|
|
370
|
+
},
|
|
371
|
+
footer: 'streaming',
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Agents without an explicit final-answer marker use their last non-empty
|
|
376
|
+
* assistant text block. Moving it out of `blocks` protects it from
|
|
377
|
+
* MAX_BLOCKS and lets the renderer assign it first claim on card space.
|
|
378
|
+
*/
|
|
379
|
+
function promoteFinalAnswer(state, source) {
|
|
380
|
+
if (state.finalAnswer) {
|
|
381
|
+
return state.finalAnswer.complete
|
|
382
|
+
? state
|
|
383
|
+
: { ...state, finalAnswer: { ...state.finalAnswer, complete: true } };
|
|
384
|
+
}
|
|
385
|
+
for (let i = state.blocks.length - 1; i >= 0; i--) {
|
|
386
|
+
const block = state.blocks[i];
|
|
387
|
+
if (block.kind !== 'text' || !block.content.trim())
|
|
388
|
+
continue;
|
|
389
|
+
return {
|
|
390
|
+
...state,
|
|
391
|
+
blocks: state.blocks.filter((_, index) => index !== i),
|
|
392
|
+
finalAnswer: {
|
|
393
|
+
content: block.content,
|
|
394
|
+
timestamp: block.timestamp,
|
|
395
|
+
complete: true,
|
|
396
|
+
source,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return state;
|
|
401
|
+
}
|
|
348
402
|
/**
|
|
349
403
|
* 后台任务名单 → RunState.backgroundJobs。
|
|
350
404
|
*
|
|
@@ -458,7 +512,7 @@ function reduceAssistantEvent(state, event) {
|
|
|
458
512
|
id: content.id,
|
|
459
513
|
name: content.name,
|
|
460
514
|
input: inputStr,
|
|
461
|
-
parsedInput:
|
|
515
|
+
parsedInput: asRecord(inputStr),
|
|
462
516
|
...(content.summary !== undefined ? { summary: content.summary } : {}),
|
|
463
517
|
status: 'running',
|
|
464
518
|
startedAt: event.timestamp,
|
|
@@ -618,30 +672,33 @@ function reduceFileChangeEvent(state, event) {
|
|
|
618
672
|
};
|
|
619
673
|
}
|
|
620
674
|
export function finishRun(state, terminal, meta = {}) {
|
|
675
|
+
const withFinalAnswer = promoteFinalAnswer(state, 'inferred');
|
|
621
676
|
// 允许从 running 或 finalizing finish(/stop、idle watchdog、进程退出 finally)。
|
|
622
677
|
// finalizing 是非终态(result 已收到,进程未退出),可被 finish 转终态。
|
|
623
678
|
// 如果已经是终态,仍然允许补充 meta 信息(如 usage 等,bridge finally 的
|
|
624
679
|
// else-if-sawResult 分支用此路径补充 token 统计)。
|
|
625
|
-
if (
|
|
680
|
+
if (withFinalAnswer.terminal !== 'running' && withFinalAnswer.terminal !== 'finalizing') {
|
|
626
681
|
// Already terminal: apply meta fields if provided (e.g. usage from jsonl)
|
|
627
682
|
const hasMeta = Object.keys(meta).some((k) => meta[k] !== undefined);
|
|
628
683
|
if (!hasMeta)
|
|
629
|
-
return
|
|
630
|
-
return { ...
|
|
684
|
+
return withFinalAnswer;
|
|
685
|
+
return { ...withFinalAnswer, ...sanitizeMeta(meta) };
|
|
631
686
|
}
|
|
632
687
|
// interrupted 终态的中断来源:显式传入优先;否则从顶层持久标记推导
|
|
633
688
|
// (approval_expired 已置位但调用方未传 reason 的路径,如协调器过期自动
|
|
634
689
|
// cancel。原因必须在终态确定,不依赖调用方记得传参)。
|
|
635
690
|
const derivedReason = meta.interruptedReason ??
|
|
636
|
-
(terminal === 'interrupted' &&
|
|
691
|
+
(terminal === 'interrupted' && withFinalAnswer.approvalExpired === true
|
|
692
|
+
? 'approval_timeout'
|
|
693
|
+
: undefined);
|
|
637
694
|
return {
|
|
638
|
-
...
|
|
695
|
+
...withFinalAnswer,
|
|
639
696
|
terminal,
|
|
640
697
|
footer: null,
|
|
641
698
|
// 耗时定格:终态之后再渲染(补 token 统计、点「详情」重渲染)都沿用同一
|
|
642
699
|
// 结束时刻,否则每次 patch 的数字都会变大。
|
|
643
|
-
finishedAt:
|
|
644
|
-
blocks: markThinkingInactive(
|
|
700
|
+
finishedAt: withFinalAnswer.finishedAt ?? Date.now(),
|
|
701
|
+
blocks: markThinkingInactive(withFinalAnswer.blocks),
|
|
645
702
|
...sanitizeMeta(meta),
|
|
646
703
|
...(derivedReason !== undefined ? { interruptedReason: derivedReason } : {}),
|
|
647
704
|
};
|
|
@@ -673,24 +730,6 @@ function stringifyUnknown(value) {
|
|
|
673
730
|
return String(value);
|
|
674
731
|
}
|
|
675
732
|
}
|
|
676
|
-
/**
|
|
677
|
-
* parse a (possibly truncated) string into a record, mirroring what
|
|
678
|
-
* `tool-render.ts`'s `asRecord` did on every render. Returns `null` when the
|
|
679
|
-
* string is empty, non-JSON, or parses to a non-object — so a truncation-broken
|
|
680
|
-
* JSON yields `null` exactly as the old per-render parse did. Computed once at
|
|
681
|
-
* store time and cached on `ToolEntry.parsedInput`.
|
|
682
|
-
*/
|
|
683
|
-
function tryParseRecord(input) {
|
|
684
|
-
if (!input)
|
|
685
|
-
return null;
|
|
686
|
-
try {
|
|
687
|
-
const p = JSON.parse(input);
|
|
688
|
-
return p && typeof p === 'object' ? p : null;
|
|
689
|
-
}
|
|
690
|
-
catch {
|
|
691
|
-
return null;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
733
|
function keepLatestBlocks(blocks) {
|
|
695
734
|
return blocks.length <= MAX_BLOCKS ? blocks : blocks.slice(-MAX_BLOCKS);
|
|
696
735
|
}
|
|
@@ -519,10 +519,10 @@ export function loadCodexConfig(opts = {}) {
|
|
|
519
519
|
}
|
|
520
520
|
catch {
|
|
521
521
|
// config.toml 读取/解析失败:readModelCatalogJsonPath 对此返回
|
|
522
|
-
// TOML_PARSE_FAILED_SENTINEL(isCodexCatalogMode → false
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
522
|
+
// TOML_PARSE_FAILED_SENTINEL(isCodexCatalogMode → false),因此正常路径下
|
|
523
|
+
// catalogMode 恒为 false,下面的 `if (catalogMode)` 分支只在两次读取之间
|
|
524
|
+
// config.toml 被改写(TOCTOU)时可达;保留该防御,避免把“已声明 catalog、
|
|
525
|
+
// 随后配置损坏”误降级成 bundled 模型。
|
|
526
526
|
if (catalogMode) {
|
|
527
527
|
return {
|
|
528
528
|
currentModel: '',
|
package/dist/config/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export declare const PI_THINKING_LEVELS: readonly ["off", "minimal", "low", "med
|
|
|
22
22
|
export declare const DEFAULT_STOP_GRACE_MS: 5000;
|
|
23
23
|
/** Codex app-server turn idle timeout default (minutes). */
|
|
24
24
|
export declare const DEFAULT_TURN_IDLE_TIMEOUT_MINUTES: 30;
|
|
25
|
+
/** Approval timeout default (ms), shared by schema and bridge fallback. */
|
|
26
|
+
export declare const DEFAULT_APPROVAL_TIMEOUT_MS: number;
|
|
25
27
|
/**
|
|
26
28
|
* 入站媒体默认配置(schema 默认与 connector 兜底共用,避免双源漂移)。
|
|
27
29
|
*
|
|
@@ -48,10 +50,10 @@ declare const ClaudeConfigSchema: z.ZodObject<{
|
|
|
48
50
|
permissionMode: z.ZodDefault<z.ZodEnum<{
|
|
49
51
|
default: "default";
|
|
50
52
|
plan: "plan";
|
|
51
|
-
bypassPermissions: "bypassPermissions";
|
|
52
53
|
auto: "auto";
|
|
53
54
|
manual: "manual";
|
|
54
55
|
acceptEdits: "acceptEdits";
|
|
56
|
+
bypassPermissions: "bypassPermissions";
|
|
55
57
|
dontAsk: "dontAsk";
|
|
56
58
|
}>>;
|
|
57
59
|
approvalTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
@@ -163,10 +165,10 @@ export declare const AppConfigSchema: z.ZodObject<{
|
|
|
163
165
|
permissionMode: z.ZodDefault<z.ZodEnum<{
|
|
164
166
|
default: "default";
|
|
165
167
|
plan: "plan";
|
|
166
|
-
bypassPermissions: "bypassPermissions";
|
|
167
168
|
auto: "auto";
|
|
168
169
|
manual: "manual";
|
|
169
170
|
acceptEdits: "acceptEdits";
|
|
171
|
+
bypassPermissions: "bypassPermissions";
|
|
170
172
|
dontAsk: "dontAsk";
|
|
171
173
|
}>>;
|
|
172
174
|
approvalTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
package/dist/config/index.js
CHANGED
|
@@ -50,6 +50,8 @@ const DEFAULTS = {
|
|
|
50
50
|
export const DEFAULT_STOP_GRACE_MS = DEFAULTS.STOP_GRACE_MS;
|
|
51
51
|
/** Codex app-server turn idle timeout default (minutes). */
|
|
52
52
|
export const DEFAULT_TURN_IDLE_TIMEOUT_MINUTES = DEFAULTS.TURN_IDLE_TIMEOUT_MINUTES;
|
|
53
|
+
/** Approval timeout default (ms), shared by schema and bridge fallback. */
|
|
54
|
+
export const DEFAULT_APPROVAL_TIMEOUT_MS = DEFAULTS.APPROVAL_TIMEOUT_MS;
|
|
53
55
|
/**
|
|
54
56
|
* 入站媒体默认配置(schema 默认与 connector 兜底共用,避免双源漂移)。
|
|
55
57
|
*
|
|
@@ -83,10 +85,10 @@ const ClaudeConfigSchema = z.object({
|
|
|
83
85
|
effort: z.enum(CLAUDE_EFFORTS).default('medium'),
|
|
84
86
|
/**
|
|
85
87
|
* Claude 权限模式(官方 --permission-mode 枚举 + 'default'=省略该参数)。
|
|
86
|
-
* 默认
|
|
87
|
-
*
|
|
88
|
+
* 默认 auto:低风险操作自动放行,高风险操作走审批(control_request →
|
|
89
|
+
* run 卡审批区);bypassPermissions 仍可选,但不再是默认值。
|
|
88
90
|
*/
|
|
89
|
-
permissionMode: z.enum(CLAUDE_PERMISSION_MODES).default('
|
|
91
|
+
permissionMode: z.enum(CLAUDE_PERMISSION_MODES).default('auto'),
|
|
90
92
|
/** 审批请求超时(ms),默认 5 分钟;超时自动发送 cancel 并中断 turn。
|
|
91
93
|
* 0 = 立即过期(fail-fast,避免审批永久挂起),不推荐;勿随意改短。 */
|
|
92
94
|
approvalTimeoutMs: z.number().int().min(0).default(DEFAULTS.APPROVAL_TIMEOUT_MS),
|
|
@@ -330,7 +332,7 @@ const TEMPLATE = `feishu:
|
|
|
330
332
|
claude:
|
|
331
333
|
model: ${DEFAULTS.CLAUDE_MODEL}
|
|
332
334
|
effort: medium
|
|
333
|
-
permissionMode:
|
|
335
|
+
permissionMode: auto
|
|
334
336
|
approvalTimeoutMs: ${DEFAULTS.APPROVAL_TIMEOUT_MS}
|
|
335
337
|
idleTtlMinutes: ${DEFAULTS.CLAUDE_IDLE_TTL_MINUTES}
|
|
336
338
|
stopGraceMs: ${DEFAULTS.STOP_GRACE_MS}
|
|
@@ -227,6 +227,12 @@ export class InboundTurnAssembler {
|
|
|
227
227
|
}
|
|
228
228
|
catch (err) {
|
|
229
229
|
getLogger().error('[inbound] turn commit failed:', err?.message ?? String(err));
|
|
230
|
+
try {
|
|
231
|
+
await this.deps.onReceipt?.(ctx, '⚠️ 消息处理失败,请重试;若持续失败请查看 lark-remote 日志。');
|
|
232
|
+
}
|
|
233
|
+
catch (receiptErr) {
|
|
234
|
+
getLogger().error('[inbound] failed to report turn commit failure:', receiptErr?.message ?? String(receiptErr));
|
|
235
|
+
}
|
|
230
236
|
}
|
|
231
237
|
}
|
|
232
238
|
}
|