lark-remote 0.1.7 → 0.1.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 +46 -0
- package/dist/bridge/approval-coordinator.d.ts +30 -1
- package/dist/bridge/approval-coordinator.js +93 -5
- package/dist/bridge/index.d.ts +36 -6
- package/dist/bridge/index.js +56 -10
- package/dist/card/approval-render.js +287 -48
- package/dist/card/card-shared.d.ts +6 -4
- package/dist/card/card-shared.js +6 -4
- package/dist/card/run-renderer.d.ts +7 -0
- package/dist/card/run-renderer.js +22 -11
- package/dist/config/index.d.ts +24 -1
- package/dist/config/index.js +34 -3
- package/dist/index.js +40 -16
- package/dist/order/alias-resolve.d.ts +6 -2
- package/dist/order/index.d.ts +9 -2
- package/dist/order/index.js +33 -0
- package/dist/router/card-action-payload.d.ts +30 -0
- package/dist/router/card-action-payload.js +39 -0
- package/dist/router/config/claude.js +8 -3
- package/dist/router/config/dsh.d.ts +51 -0
- package/dist/router/config/dsh.js +179 -0
- package/dist/router/config/index.d.ts +1 -1
- package/dist/router/config/index.js +4 -1
- package/dist/router/config/types.d.ts +6 -0
- package/dist/router/index.d.ts +39 -4
- package/dist/router/index.js +471 -90
- package/dist/router/utils.js +12 -2
- package/dist/runner/agent-choices-common.js +3 -0
- package/dist/runner/claude/runner.js +22 -0
- package/dist/runner/claude/session.d.ts +26 -0
- package/dist/runner/claude/session.js +105 -9
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -0
- package/dist/runner/codex/app-server/protocol-types.js +1 -1
- package/dist/runner/codex/app-server/runner.d.ts +10 -0
- package/dist/runner/codex/app-server/runner.js +56 -1
- package/dist/runner/codex/app-server/translator.d.ts +7 -0
- package/dist/runner/codex/app-server/translator.js +45 -0
- package/dist/runner/common/acp/protocol-types.d.ts +33 -0
- package/dist/runner/common/acp/protocol-types.js +2 -0
- package/dist/runner/common/connection-based-runner.js +10 -5
- package/dist/runner/dsh/client.d.ts +95 -0
- package/dist/runner/dsh/client.js +418 -0
- package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
- package/dist/runner/dsh/fake-dsh-server.js +156 -0
- package/dist/runner/dsh/index.d.ts +3 -0
- package/dist/runner/dsh/index.js +4 -0
- package/dist/runner/dsh/runner.d.ts +57 -0
- package/dist/runner/dsh/runner.js +211 -0
- package/dist/runner/dsh/translator.d.ts +28 -0
- package/dist/runner/dsh/translator.js +98 -0
- package/dist/runner/dsh/types.d.ts +155 -0
- package/dist/runner/dsh/types.js +14 -0
- package/dist/runner/index.d.ts +1 -0
- package/dist/runner/index.js +1 -0
- package/dist/runner/kimi/acp/runner.js +84 -1
- package/dist/runner/kimi/acp/translator.d.ts +14 -0
- package/dist/runner/kimi/acp/translator.js +108 -3
- package/dist/runner/pi/rpc/runner.d.ts +1 -7
- package/dist/runner/pi/rpc/runner.js +8 -12
- package/dist/runner/probe.js +6 -1
- package/dist/runner/question-common.d.ts +26 -0
- package/dist/runner/question-common.js +42 -0
- package/dist/runner/types.d.ts +94 -10
- package/dist/session/claude/sessions.js +37 -15
- package/dist/session/dsh/index.d.ts +1 -0
- package/dist/session/dsh/index.js +2 -0
- package/dist/session/dsh/sessions.d.ts +44 -0
- package/dist/session/dsh/sessions.js +252 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/workspace/index.d.ts +12 -0
- package/dist/workspace/index.js +28 -2
- package/package.json +2 -1
- package/dist/order/alias-store.d.ts +0 -38
- package/dist/order/alias-store.js +0 -106
package/dist/router/utils.js
CHANGED
|
@@ -80,6 +80,16 @@ export function formatUsageStats(usage, options) {
|
|
|
80
80
|
const cumTotalSuffix = usage?.cumulativeTotalTokens !== undefined
|
|
81
81
|
? ` · 累计 ${formatTokenK(usage.cumulativeTotalTokens)}`
|
|
82
82
|
: '';
|
|
83
|
+
// 一致性护栏:本 run ≤ 累计 是硬不变量(累计 = 所有 run 之和,必 ≥ 本 run)。
|
|
84
|
+
// 违反说明来源 scope 混用(如 DeepSeek live result usage 是累积/超大 scope,
|
|
85
|
+
// 大于 jsonl 去重聚合的 session 累计)。只标记不修正——数字保持原样便于排查,
|
|
86
|
+
// 由数据源修复。两侧都有值时校验(避免旧数据/缺失字段误报)。
|
|
87
|
+
// 两个分支(real input/output / 10% 估计)共用,保证"本 run > 累计"在任意
|
|
88
|
+
// 路径都显式暴露。
|
|
89
|
+
const withCumGuard = (totalTokens) => cumTotalSuffix +
|
|
90
|
+
(usage?.cumulativeTotalTokens !== undefined && totalTokens > usage.cumulativeTotalTokens
|
|
91
|
+
? ' · ⚠️ 累计异常'
|
|
92
|
+
: '');
|
|
83
93
|
// 计算累计 cache 命中率
|
|
84
94
|
const cumInput = usage?.cumulativeInputTokens;
|
|
85
95
|
const cumCacheRead = usage?.cumulativeCacheReadTokens;
|
|
@@ -110,7 +120,7 @@ export function formatUsageStats(usage, options) {
|
|
|
110
120
|
lines.push(`Cache create - ${formatTokenK(cacheCreation)}${cumCacheCreateSuffix}`);
|
|
111
121
|
}
|
|
112
122
|
lines.push(`Cached token - ${formatTokenK(cacheRead)} (${cachePercent}%)${cumCacheReadSuffix}`);
|
|
113
|
-
lines.push(`Total token - ${formatTokenK(totalTokens)}${
|
|
123
|
+
lines.push(`Total token - ${formatTokenK(totalTokens)}${withCumGuard(totalTokens)}`);
|
|
114
124
|
}
|
|
115
125
|
else {
|
|
116
126
|
// Estimate path: when real input/output are absent,
|
|
@@ -127,7 +137,7 @@ export function formatUsageStats(usage, options) {
|
|
|
127
137
|
lines.push(`Cache create - ${formatTokenK(cacheCreation)}${cumCacheCreateSuffix}`);
|
|
128
138
|
}
|
|
129
139
|
lines.push(`Cached token - ${formatTokenK(cacheRead)} (${cachePercent}%)${cumCacheReadSuffix}`);
|
|
130
|
-
lines.push(`Total token - ${formatTokenK(totalTokens)}${
|
|
140
|
+
lines.push(`Total token - ${formatTokenK(totalTokens)}${withCumGuard(totalTokens)}`);
|
|
131
141
|
}
|
|
132
142
|
}
|
|
133
143
|
return lines.join('\n');
|
|
@@ -23,6 +23,9 @@ export const AGENT_CHOICE_FIELDS = {
|
|
|
23
23
|
{ configKey: 'model', choicesKey: 'model' },
|
|
24
24
|
{ configKey: 'thinkingEffort', choicesKey: 'thinkingEffort' },
|
|
25
25
|
],
|
|
26
|
+
// DSH has no model/provider choice fields (host endpoint is connection
|
|
27
|
+
// config, not a per-run choice) — empty to satisfy the Record type.
|
|
28
|
+
dsh: [],
|
|
26
29
|
};
|
|
27
30
|
/** 返回指定 agent 的字段映射;claude 或未知 agent 返回 undefined。 */
|
|
28
31
|
export function choiceFieldsFor(agent) {
|
|
@@ -102,6 +102,28 @@ export class ClaudeRunner {
|
|
|
102
102
|
this.session.setAutoApprove(true);
|
|
103
103
|
await this.session.respondPermission(requestId, { behavior: 'allow' });
|
|
104
104
|
return;
|
|
105
|
+
case 'accept_with_feedback': {
|
|
106
|
+
// 计划审批「批准并采纳修改」:allow + updatedInput.plan(改写过的新计划),
|
|
107
|
+
// 触发 Claude 侧 planWasEdited → tool_result 回显 edited plan 并写回文件。
|
|
108
|
+
const plan = response.plan;
|
|
109
|
+
if (!plan || !plan.trim()) {
|
|
110
|
+
throw new Error('approval accept_with_feedback missing plan payload');
|
|
111
|
+
}
|
|
112
|
+
await this.session.respondPermission(requestId, {
|
|
113
|
+
behavior: 'allow',
|
|
114
|
+
updatedInput: { plan },
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
case 'decline_with_feedback': {
|
|
119
|
+
// 计划审批「拒绝并附意见」:deny + message,Claude 留在 plan 模式修订。
|
|
120
|
+
const message = response.message?.trim();
|
|
121
|
+
if (!message) {
|
|
122
|
+
throw new Error('approval decline_with_feedback missing message payload');
|
|
123
|
+
}
|
|
124
|
+
await this.session.respondPermission(requestId, { behavior: 'deny', message });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
105
127
|
case 'decline':
|
|
106
128
|
await this.session.respondPermission(requestId, { behavior: 'deny' });
|
|
107
129
|
return;
|
|
@@ -46,6 +46,11 @@ export interface ClaudeSessionOptions {
|
|
|
46
46
|
export interface PermissionResult {
|
|
47
47
|
behavior: 'allow' | 'deny';
|
|
48
48
|
message?: string;
|
|
49
|
+
/**
|
|
50
|
+
* allow 时显式覆盖 updatedInput(默认回写 pendingToolInputs 原始 input)。
|
|
51
|
+
* 计划审批「批准并采纳修改意见」用它携带改写的 plan。
|
|
52
|
+
*/
|
|
53
|
+
updatedInput?: unknown;
|
|
49
54
|
}
|
|
50
55
|
/**
|
|
51
56
|
* ClaudeSession extends SpawningRunner 复用其 spawn/pid 文件/killOrphan/
|
|
@@ -74,6 +79,12 @@ export declare class ClaudeSession extends SpawningRunner {
|
|
|
74
79
|
private waitResolve;
|
|
75
80
|
/** 待审批的 control_request 原始 input(回写 updatedInput 需要)。 */
|
|
76
81
|
private pendingToolInputs;
|
|
82
|
+
/**
|
|
83
|
+
* 本会话内跟踪到的计划文件路径:Write/Edit 工具把文件写到 `plans` 目录
|
|
84
|
+
* 时记录(ExitPlanMode 的 plan 内容不总是经 input 注入,文件是可靠兜底)。
|
|
85
|
+
* 会话进程生命周期内有效(长驻进程 = 一个会话)。
|
|
86
|
+
*/
|
|
87
|
+
private planFilePath;
|
|
77
88
|
/** 允许所有:后续 control_request 自动 allow(本会话进程生命周期内有效)。 */
|
|
78
89
|
private autoApprove;
|
|
79
90
|
/** 串行化 stdin 写入(用户消息与审批响应交错到达)。 */
|
|
@@ -160,6 +171,21 @@ export declare class ClaudeSession extends SpawningRunner {
|
|
|
160
171
|
*/
|
|
161
172
|
private writeStdin;
|
|
162
173
|
private translateEvent;
|
|
174
|
+
/**
|
|
175
|
+
* 跟踪会话内 Write/Edit 工具写到 `plans` 目录的文件路径:CLI 对
|
|
176
|
+
* ExitPlanMode 的 plan 注入不稳定(input.plan 时有时无),但模型被要求
|
|
177
|
+
* 先把计划写入计划文件,路径可经工具调用可靠捕获(含模型写错目录的
|
|
178
|
+
* 退化场景——渲染层可展示实际落盘位置)。
|
|
179
|
+
*/
|
|
180
|
+
private trackPlanFilePath;
|
|
181
|
+
/**
|
|
182
|
+
* ExitPlanMode 计划解析,优先级:
|
|
183
|
+
* 1. control_request input.plan(CLI normalizeToolInput 从磁盘注入,非稳定);
|
|
184
|
+
* 2. input.planFilePath 指向的文件;
|
|
185
|
+
* 3. 会话内 Write/Edit 跟踪到的计划文件。
|
|
186
|
+
* 全部落空时返回空对象,渲染层展示「未获取到计划文件内容」兜底。
|
|
187
|
+
*/
|
|
188
|
+
private resolvePlanContent;
|
|
163
189
|
private translateControlRequest;
|
|
164
190
|
/** 统一方案:所有 runner 自己生成 timestamp(缺失时补)。 */
|
|
165
191
|
private withTimestamp;
|
|
@@ -27,13 +27,17 @@
|
|
|
27
27
|
* 进程在 turn 之间保持存活(stdin 不关闭),/stop / /new / /cd / 看门狗超时
|
|
28
28
|
* 时经 ProcessStopper 组杀,下条消息按 SessionStore 的 sessionId --resume。
|
|
29
29
|
*/
|
|
30
|
+
import { readFile } from 'node:fs/promises';
|
|
30
31
|
import { silentlyUnlink } from '../../common/fs.js';
|
|
31
32
|
import { getLogger } from '../../logger/index.js';
|
|
32
33
|
import { SpawningRunner } from '../common/spawning-runner.js';
|
|
33
34
|
import { pipeAllStdio } from '../common/runner-utils.js';
|
|
34
35
|
import { authErrorEvent, syntheticInitEvent } from '../common/runner-utils.js';
|
|
36
|
+
import { makeQuestionApprovalEvent } from '../question-common.js';
|
|
35
37
|
/** 拒绝权限时的默认提示(进入 claude 上下文,中文用户可读)。 */
|
|
36
38
|
const DENY_MESSAGE = '用户拒绝了此工具调用,请停止并等待用户指令。';
|
|
39
|
+
/** ExitPlanMode 计划文件读取上限(超出截断,防超大计划拖垮事件翻译)。 */
|
|
40
|
+
const MAX_PLAN_CHARS = 60_000;
|
|
37
41
|
/** 会话级空闲回收默认 TTL(对齐 codex appServer.idleTtlMs 默认 30 分钟)。 */
|
|
38
42
|
const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
39
43
|
/** result 事件里表示「turn 中途压缩」的 subtype(不是 turn 结束)。 */
|
|
@@ -65,6 +69,12 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
65
69
|
waitResolve = null;
|
|
66
70
|
/** 待审批的 control_request 原始 input(回写 updatedInput 需要)。 */
|
|
67
71
|
pendingToolInputs = new Map();
|
|
72
|
+
/**
|
|
73
|
+
* 本会话内跟踪到的计划文件路径:Write/Edit 工具把文件写到 `plans` 目录
|
|
74
|
+
* 时记录(ExitPlanMode 的 plan 内容不总是经 input 注入,文件是可靠兜底)。
|
|
75
|
+
* 会话进程生命周期内有效(长驻进程 = 一个会话)。
|
|
76
|
+
*/
|
|
77
|
+
planFilePath = '';
|
|
68
78
|
/** 允许所有:后续 control_request 自动 allow(本会话进程生命周期内有效)。 */
|
|
69
79
|
autoApprove = false;
|
|
70
80
|
/** 串行化 stdin 写入(用户消息与审批响应交错到达)。 */
|
|
@@ -181,7 +191,9 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
181
191
|
if (result.behavior === 'allow') {
|
|
182
192
|
await this.writeControlResponse(requestId, {
|
|
183
193
|
behavior: 'allow',
|
|
184
|
-
updatedInput
|
|
194
|
+
// 显式 updatedInput 优先(计划审批「批准并采纳修改」携带改写 plan);
|
|
195
|
+
// 缺省回写原始 input,保证其他工具审批行为不变。
|
|
196
|
+
updatedInput: result.updatedInput !== undefined ? result.updatedInput : (input ?? {}),
|
|
185
197
|
});
|
|
186
198
|
}
|
|
187
199
|
else {
|
|
@@ -343,7 +355,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
343
355
|
for await (const raw of stream) {
|
|
344
356
|
if (generation !== this.processGeneration)
|
|
345
357
|
return; // 旧进程残留事件丢弃
|
|
346
|
-
const events = this.translateEvent(raw);
|
|
358
|
+
const events = await this.translateEvent(raw);
|
|
347
359
|
if (events.length > 0) {
|
|
348
360
|
this.eventQueue.push(...events);
|
|
349
361
|
this.wakeWaiters();
|
|
@@ -529,7 +541,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
529
541
|
// =========================================================================
|
|
530
542
|
// Internal: event translation
|
|
531
543
|
// =========================================================================
|
|
532
|
-
translateEvent(raw) {
|
|
544
|
+
async translateEvent(raw) {
|
|
533
545
|
const event = raw;
|
|
534
546
|
const type = event.type;
|
|
535
547
|
// review P3-3:--resume 会先重放上一轮历史事件(旧 result 之外还可能有
|
|
@@ -537,6 +549,10 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
537
549
|
// 内容混进当前卡片(result 分支的 sawInit 守卫升级为全类型守卫)。
|
|
538
550
|
if (type !== 'system' && !this.sawInit)
|
|
539
551
|
return [];
|
|
552
|
+
// 跟踪会话内计划文件写入(ExitPlanMode 审批卡的 plan 文件兜底)。
|
|
553
|
+
if (type === 'assistant') {
|
|
554
|
+
this.trackPlanFilePath(event);
|
|
555
|
+
}
|
|
540
556
|
if (type === 'control_request') {
|
|
541
557
|
return this.translateControlRequest(event);
|
|
542
558
|
}
|
|
@@ -587,7 +603,63 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
587
603
|
// 下游 reducer 忽略,保留诊断价值。
|
|
588
604
|
return [this.withTimestamp(event)];
|
|
589
605
|
}
|
|
590
|
-
|
|
606
|
+
/**
|
|
607
|
+
* 跟踪会话内 Write/Edit 工具写到 `plans` 目录的文件路径:CLI 对
|
|
608
|
+
* ExitPlanMode 的 plan 注入不稳定(input.plan 时有时无),但模型被要求
|
|
609
|
+
* 先把计划写入计划文件,路径可经工具调用可靠捕获(含模型写错目录的
|
|
610
|
+
* 退化场景——渲染层可展示实际落盘位置)。
|
|
611
|
+
*/
|
|
612
|
+
trackPlanFilePath(raw) {
|
|
613
|
+
const content = raw.message?.content;
|
|
614
|
+
if (!Array.isArray(content))
|
|
615
|
+
return;
|
|
616
|
+
for (const block of content) {
|
|
617
|
+
const b = block;
|
|
618
|
+
if (b?.type !== 'tool_use')
|
|
619
|
+
continue;
|
|
620
|
+
if (b.name !== 'Write' && b.name !== 'Edit')
|
|
621
|
+
continue;
|
|
622
|
+
const fp = b.input?.file_path;
|
|
623
|
+
if (typeof fp === 'string' && fp.includes('/plans/')) {
|
|
624
|
+
this.planFilePath = fp;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* ExitPlanMode 计划解析,优先级:
|
|
630
|
+
* 1. control_request input.plan(CLI normalizeToolInput 从磁盘注入,非稳定);
|
|
631
|
+
* 2. input.planFilePath 指向的文件;
|
|
632
|
+
* 3. 会话内 Write/Edit 跟踪到的计划文件。
|
|
633
|
+
* 全部落空时返回空对象,渲染层展示「未获取到计划文件内容」兜底。
|
|
634
|
+
*/
|
|
635
|
+
async resolvePlanContent(input, trackedPath) {
|
|
636
|
+
const direct = typeof input.plan === 'string' && input.plan.trim() ? input.plan : undefined;
|
|
637
|
+
if (direct !== undefined) {
|
|
638
|
+
// 计划已随 input 注入时仍携带文件路径(input.planFilePath 或会话内跟踪),
|
|
639
|
+
// 供卡片展示「计划落盘位置」。
|
|
640
|
+
const filePath = typeof input.planFilePath === 'string' && input.planFilePath
|
|
641
|
+
? input.planFilePath
|
|
642
|
+
: trackedPath;
|
|
643
|
+
return filePath ? { plan: direct, planFilePath: filePath } : { plan: direct };
|
|
644
|
+
}
|
|
645
|
+
const candidate = typeof input.planFilePath === 'string' && input.planFilePath
|
|
646
|
+
? input.planFilePath
|
|
647
|
+
: trackedPath;
|
|
648
|
+
if (!candidate)
|
|
649
|
+
return {};
|
|
650
|
+
try {
|
|
651
|
+
const content = await readFile(candidate, 'utf8');
|
|
652
|
+
const trimmed = content.trim();
|
|
653
|
+
if (trimmed) {
|
|
654
|
+
return { plan: trimmed.slice(0, MAX_PLAN_CHARS), planFilePath: candidate };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
// 计划文件缺失/不可读:fall through,渲染层兜底。
|
|
659
|
+
}
|
|
660
|
+
return {};
|
|
661
|
+
}
|
|
662
|
+
async translateControlRequest(raw) {
|
|
591
663
|
const requestId = raw.request_id;
|
|
592
664
|
if (requestId === null || requestId === undefined) {
|
|
593
665
|
getLogger().warn(`[${this.logTag}] control_request missing request_id, dropping`);
|
|
@@ -639,17 +711,36 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
639
711
|
this.pendingToolInputs.delete(requestId);
|
|
640
712
|
return [];
|
|
641
713
|
}
|
|
714
|
+
return [makeQuestionApprovalEvent(requestId, questions, this.sessionId)];
|
|
715
|
+
}
|
|
716
|
+
// ExitPlanMode:计划已写入计划文件(或经 input.plan 注入)。没有 command
|
|
717
|
+
// 语义,塞进 command 槽位会显示无意义 `{}` → 用独立 tool kind,把计划
|
|
718
|
+
// 全文 + 文件路径带给渲染层,决策对齐 TUI 四种交互。
|
|
719
|
+
if (toolName === 'ExitPlanMode') {
|
|
720
|
+
const resolved = await this.resolvePlanContent(input, this.planFilePath);
|
|
642
721
|
const view = {
|
|
643
722
|
requestId,
|
|
644
|
-
kind: '
|
|
645
|
-
|
|
646
|
-
|
|
723
|
+
kind: 'tool',
|
|
724
|
+
toolName,
|
|
725
|
+
reason: typeof request.description === 'string' ? request.description : undefined,
|
|
726
|
+
...(resolved.plan !== undefined ? { plan: resolved.plan } : {}),
|
|
727
|
+
...(resolved.planFilePath !== undefined ? { planFilePath: resolved.planFilePath } : {}),
|
|
728
|
+
// 对齐 TUI:批准并执行 / 批准并自动放行(acceptAll)/ 拒绝并附意见 /
|
|
729
|
+
// 批准并采纳修改 / 拒绝。
|
|
730
|
+
availableDecisions: [
|
|
731
|
+
'accept',
|
|
732
|
+
'acceptAll',
|
|
733
|
+
'declineWithFeedback',
|
|
734
|
+
'acceptWithFeedback',
|
|
735
|
+
'decline',
|
|
736
|
+
],
|
|
647
737
|
};
|
|
738
|
+
getLogger().info(`[${this.logTag}] permission request requestId=${requestId} tool=${toolName}`);
|
|
648
739
|
return [
|
|
649
740
|
{
|
|
650
741
|
type: 'approval_requested',
|
|
651
742
|
requestId,
|
|
652
|
-
kind: '
|
|
743
|
+
kind: 'tool',
|
|
653
744
|
threadId: this.sessionId,
|
|
654
745
|
turnId: '',
|
|
655
746
|
itemId: '',
|
|
@@ -658,7 +749,12 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
658
749
|
},
|
|
659
750
|
];
|
|
660
751
|
}
|
|
661
|
-
|
|
752
|
+
// 非命令工具的兜底防御:input 无 command 字段时 JSON 序列化,但空对象
|
|
753
|
+
// 序列化成 `{}` 对用户无意义——内容为空时回退展示工具名(review P3-5)。
|
|
754
|
+
const rawCommand = typeof input.command === 'string' ? input.command : JSON.stringify(input);
|
|
755
|
+
const command = rawCommand && rawCommand !== '{}' && rawCommand !== 'null'
|
|
756
|
+
? rawCommand
|
|
757
|
+
: toolName || '(unknown tool)';
|
|
662
758
|
const view = {
|
|
663
759
|
requestId,
|
|
664
760
|
kind: 'command',
|
|
@@ -83,6 +83,8 @@ export interface ThreadResumeParams {
|
|
|
83
83
|
modelProvider?: string | null;
|
|
84
84
|
approvalPolicy?: AskForApproval;
|
|
85
85
|
sandbox?: SandboxMode;
|
|
86
|
+
/** Configuration overrides for the resumed thread (codex v2 wire field). */
|
|
87
|
+
config?: Record<string, unknown> | null;
|
|
86
88
|
}
|
|
87
89
|
export interface ThreadResumeResponse {
|
|
88
90
|
thread: Thread;
|
|
@@ -432,6 +434,7 @@ export declare const ServerRequestMethod: {
|
|
|
432
434
|
readonly COMMAND_EXECUTION_APPROVAL: "item/commandExecution/requestApproval";
|
|
433
435
|
readonly FILE_CHANGE_APPROVAL: "item/fileChange/requestApproval";
|
|
434
436
|
readonly PERMISSIONS_APPROVAL: "item/permissions/requestApproval";
|
|
437
|
+
readonly REQUEST_USER_INPUT: "item/tool/requestUserInput";
|
|
435
438
|
};
|
|
436
439
|
export interface NetworkApprovalContext {
|
|
437
440
|
host: string;
|
|
@@ -515,5 +518,37 @@ export interface PermissionsRequestApprovalResponse {
|
|
|
515
518
|
permissions: GrantedPermissionProfile;
|
|
516
519
|
scope?: 'turn' | 'session';
|
|
517
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Codex AskUserQuestion(request_user_input)协议形状。
|
|
523
|
+
* 来源:codex-rs/codex-rs/protocol/src/request_user_input.rs +
|
|
524
|
+
* app-server-protocol v2/item.rs ToolRequestUserInput*(EXPERIMENTAL)。
|
|
525
|
+
* isOther=true → 渲染层提供自定义答案输入;isSecret 仅影响展示(按普通输入
|
|
526
|
+
* 处理);options 为 null/空 → 自由文本题。
|
|
527
|
+
*/
|
|
528
|
+
export interface ToolRequestUserInputOption {
|
|
529
|
+
label: string;
|
|
530
|
+
description: string;
|
|
531
|
+
}
|
|
532
|
+
export interface ToolRequestUserInputQuestion {
|
|
533
|
+
id: string;
|
|
534
|
+
header: string;
|
|
535
|
+
question: string;
|
|
536
|
+
isOther: boolean;
|
|
537
|
+
isSecret: boolean;
|
|
538
|
+
options: ToolRequestUserInputOption[] | null;
|
|
539
|
+
}
|
|
540
|
+
export interface ToolRequestUserInputParams {
|
|
541
|
+
threadId: string;
|
|
542
|
+
turnId: string;
|
|
543
|
+
itemId: string;
|
|
544
|
+
questions: ToolRequestUserInputQuestion[];
|
|
545
|
+
autoResolutionMs?: number | null;
|
|
546
|
+
}
|
|
547
|
+
export interface ToolRequestUserInputAnswer {
|
|
548
|
+
answers: string[];
|
|
549
|
+
}
|
|
550
|
+
export interface ToolRequestUserInputResponse {
|
|
551
|
+
answers: Record<string, ToolRequestUserInputAnswer>;
|
|
552
|
+
}
|
|
518
553
|
/** Unsupported / ignored server request methods. */
|
|
519
554
|
export declare const UNSUPPORTED_SERVER_REQUEST_METHODS: Set<string>;
|
|
@@ -41,10 +41,10 @@ export const ServerRequestMethod = {
|
|
|
41
41
|
COMMAND_EXECUTION_APPROVAL: 'item/commandExecution/requestApproval',
|
|
42
42
|
FILE_CHANGE_APPROVAL: 'item/fileChange/requestApproval',
|
|
43
43
|
PERMISSIONS_APPROVAL: 'item/permissions/requestApproval',
|
|
44
|
+
REQUEST_USER_INPUT: 'item/tool/requestUserInput',
|
|
44
45
|
};
|
|
45
46
|
/** Unsupported / ignored server request methods. */
|
|
46
47
|
export const UNSUPPORTED_SERVER_REQUEST_METHODS = new Set([
|
|
47
|
-
'item/tool/requestUserInput',
|
|
48
48
|
'mcpServer/elicitation/request',
|
|
49
49
|
'item/tool/call',
|
|
50
50
|
'account/chatgptAuthTokens/refresh',
|
|
@@ -43,6 +43,16 @@ export interface CodexAppServerRunnerOptions {
|
|
|
43
43
|
* object used by `thread/settings/update` (`sandboxPolicy`).
|
|
44
44
|
*/
|
|
45
45
|
export declare function sandboxModeToSandboxPolicy(mode: SandboxMode): SandboxPolicy;
|
|
46
|
+
/**
|
|
47
|
+
* Codex 官方 feature override(thread/start·resume 的 `config` 字段):
|
|
48
|
+
* 让 `request_user_input` 在 Default 协作模式下也可用,无需改 codex
|
|
49
|
+
* config.toml。协议键与 openai/codex 的 feature registry 一致
|
|
50
|
+
* (key `default_mode_request_user_input`),app-server 官方测试
|
|
51
|
+
* (turn_start.rs)验证同一路径。
|
|
52
|
+
*/
|
|
53
|
+
export declare const THREAD_CONFIG_DEFAULT_MODE_REQUEST_USER_INPUT: {
|
|
54
|
+
readonly 'features.default_mode_request_user_input': true;
|
|
55
|
+
};
|
|
46
56
|
export declare class CodexAppServerRunner extends ConnectionBasedRunner<JsonRpcClient<InitializeResult>, TranslatorEvent> {
|
|
47
57
|
private connectionManager;
|
|
48
58
|
private currentTranslator;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { ConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
|
|
14
14
|
import { CodexAppServerTranslator } from './translator.js';
|
|
15
15
|
import { RpcErrorCode } from '../../common/acp/protocol-types.js';
|
|
16
|
+
import { mapAnswersByIndex } from '../../question-common.js';
|
|
16
17
|
import { getLogger } from '../../../logger/index.js';
|
|
17
18
|
import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
|
|
18
19
|
/**
|
|
@@ -34,6 +35,39 @@ function buildApprovalResponse(action, view) {
|
|
|
34
35
|
}
|
|
35
36
|
return { decision: action };
|
|
36
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* 构造 request_user_input 响应。answer → 按问题 id 回编;decline/cancel →
|
|
40
|
+
* 空 answers。answers 值与问题 options 无强校验(Codex 接受任意字符串,
|
|
41
|
+
* 包括自由文本题的自定义答案)。
|
|
42
|
+
*/
|
|
43
|
+
function buildRequestUserInputResponse(action, questions, response) {
|
|
44
|
+
if (action !== 'answer') {
|
|
45
|
+
return { answers: {} };
|
|
46
|
+
}
|
|
47
|
+
const answers = response.answers;
|
|
48
|
+
if (!answers) {
|
|
49
|
+
return { answers: {} };
|
|
50
|
+
}
|
|
51
|
+
const notes = response.notes;
|
|
52
|
+
const byIndex = mapAnswersByIndex(questions ?? [], answers);
|
|
53
|
+
const result = {};
|
|
54
|
+
(questions ?? []).forEach((q, i) => {
|
|
55
|
+
const value = byIndex[i];
|
|
56
|
+
if (!q.id || value === undefined)
|
|
57
|
+
return;
|
|
58
|
+
const values = Array.isArray(value) ? value : [value];
|
|
59
|
+
const cleaned = values.filter((v) => typeof v === 'string' && v.length > 0);
|
|
60
|
+
// Codex user_note:补充说明编码为 "user_note: <text>" 条目(2026-08-18
|
|
61
|
+
// live 验证 deepseek-v4-flash 明确理解并遵守该格式)。
|
|
62
|
+
const note = notes?.[q.question]?.trim();
|
|
63
|
+
if (note)
|
|
64
|
+
cleaned.push(`user_note: ${note}`);
|
|
65
|
+
if (cleaned.length === 0)
|
|
66
|
+
return;
|
|
67
|
+
result[q.id] = { answers: cleaned };
|
|
68
|
+
});
|
|
69
|
+
return { answers: result };
|
|
70
|
+
}
|
|
37
71
|
/**
|
|
38
72
|
* Map the client-side SandboxMode enum to the response-side SandboxPolicy
|
|
39
73
|
* object used by `thread/settings/update` (`sandboxPolicy`).
|
|
@@ -54,6 +88,16 @@ export function sandboxModeToSandboxPolicy(mode) {
|
|
|
54
88
|
return { type: 'dangerFullAccess' };
|
|
55
89
|
}
|
|
56
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Codex 官方 feature override(thread/start·resume 的 `config` 字段):
|
|
93
|
+
* 让 `request_user_input` 在 Default 协作模式下也可用,无需改 codex
|
|
94
|
+
* config.toml。协议键与 openai/codex 的 feature registry 一致
|
|
95
|
+
* (key `default_mode_request_user_input`),app-server 官方测试
|
|
96
|
+
* (turn_start.rs)验证同一路径。
|
|
97
|
+
*/
|
|
98
|
+
export const THREAD_CONFIG_DEFAULT_MODE_REQUEST_USER_INPUT = {
|
|
99
|
+
'features.default_mode_request_user_input': true,
|
|
100
|
+
};
|
|
57
101
|
export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
58
102
|
connectionManager;
|
|
59
103
|
currentTranslator = null;
|
|
@@ -177,7 +221,15 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
177
221
|
if (!client || !pending)
|
|
178
222
|
return;
|
|
179
223
|
const action = response?.action ?? 'decline';
|
|
180
|
-
if (pending.kind === '
|
|
224
|
+
if (pending.kind === 'question') {
|
|
225
|
+
// AskUserQuestion 回编:文本 key 答案按问题顺序展开后映射回问题 id
|
|
226
|
+
// ({answers:{id:{answers:[值]}}});跳过/拒绝/取消 → 空 answers
|
|
227
|
+
// (Codex 视为 unanswered,turn 继续,对齐 cc-connect deny 语义)。
|
|
228
|
+
const questions = pending.view.questions ?? [];
|
|
229
|
+
const answerPayload = buildRequestUserInputResponse(action, questions, response);
|
|
230
|
+
client.respond(requestId, answerPayload);
|
|
231
|
+
}
|
|
232
|
+
else if (pending.kind === 'permissions') {
|
|
181
233
|
// 权限审批的响应没有 decision 字段(真实协议只回 granted profile +
|
|
182
234
|
// scope):decline/cancel 必须返回空授权(拒绝全部),否则会把用户已
|
|
183
235
|
// 勾选的条目当作授予返回(2026-08-12 review 发现:勾选后点「拒绝」实
|
|
@@ -258,6 +310,9 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
258
310
|
...(this.modelProvider ? { modelProvider: this.modelProvider } : {}),
|
|
259
311
|
...(this.approvalPolicyConfig ? { approvalPolicy: this.approvalPolicyConfig } : {}),
|
|
260
312
|
...(this.sandboxConfig ? { sandbox: this.sandboxConfig } : {}),
|
|
313
|
+
// Default 协作模式下默认启用 request_user_input(Ask User Question),
|
|
314
|
+
// 协议级 override,不依赖 codex config.toml。
|
|
315
|
+
config: THREAD_CONFIG_DEFAULT_MODE_REQUEST_USER_INPUT,
|
|
261
316
|
};
|
|
262
317
|
return params;
|
|
263
318
|
}
|
|
@@ -43,6 +43,13 @@ export declare class CodexAppServerTranslator {
|
|
|
43
43
|
* Handle a server request (incoming JSON-RPC request from server, e.g. approval).
|
|
44
44
|
*/
|
|
45
45
|
handleServerRequest(id: number | string, method: string, params: unknown): TranslatorEvent[];
|
|
46
|
+
/**
|
|
47
|
+
* Codex AskUserQuestion:item/tool/requestUserInput → question 审批事件。
|
|
48
|
+
* 问题 id 保留在 UserQuestion.id(回编时文本 → id 映射),options 为
|
|
49
|
+
* null/空 → 自由文本题(空数组驱动卡片渲染输入框);autoResolutionMs
|
|
50
|
+
* 透传为 per-request 超时(coordinator 优先使用)。
|
|
51
|
+
*/
|
|
52
|
+
private handleRequestUserInput;
|
|
46
53
|
/**
|
|
47
54
|
* Synthesize a ResultEvent from the current turn state.
|
|
48
55
|
* Called when the turn completes.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* completes. Method names and parameter shapes follow the real Codex
|
|
8
8
|
* app-server v2 protocol (see protocol-types.ts).
|
|
9
9
|
*/
|
|
10
|
+
import { makeQuestionApprovalEvent } from '../../question-common.js';
|
|
10
11
|
import { NotificationMethod, ServerRequestMethod, UNSUPPORTED_SERVER_REQUEST_METHODS, } from './protocol-types.js';
|
|
11
12
|
// =============================================================================
|
|
12
13
|
// Translator
|
|
@@ -86,10 +87,33 @@ export class CodexAppServerTranslator {
|
|
|
86
87
|
return this.handleFileApproval(id, params);
|
|
87
88
|
case ServerRequestMethod.PERMISSIONS_APPROVAL:
|
|
88
89
|
return this.handlePermissionsApproval(id, params);
|
|
90
|
+
case ServerRequestMethod.REQUEST_USER_INPUT:
|
|
91
|
+
return this.handleRequestUserInput(id, params);
|
|
89
92
|
default:
|
|
90
93
|
return [];
|
|
91
94
|
}
|
|
92
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Codex AskUserQuestion:item/tool/requestUserInput → question 审批事件。
|
|
98
|
+
* 问题 id 保留在 UserQuestion.id(回编时文本 → id 映射),options 为
|
|
99
|
+
* null/空 → 自由文本题(空数组驱动卡片渲染输入框);autoResolutionMs
|
|
100
|
+
* 透传为 per-request 超时(coordinator 优先使用)。
|
|
101
|
+
*/
|
|
102
|
+
handleRequestUserInput(requestId, params) {
|
|
103
|
+
const questions = params.questions
|
|
104
|
+
.map((q) => requestUserInputQuestionToUserQuestion(q))
|
|
105
|
+
.filter((q) => q !== null);
|
|
106
|
+
if (questions.length === 0) {
|
|
107
|
+
// 无有效问题:不渲染空提问卡,返回 [] 由 runner 以 method-not-found
|
|
108
|
+
// 拒绝(服务端视作请求失败,不会悬挂)。
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return [
|
|
112
|
+
makeQuestionApprovalEvent(requestId, questions, params.threadId, {
|
|
113
|
+
timeoutMs: params.autoResolutionMs ?? undefined,
|
|
114
|
+
}),
|
|
115
|
+
];
|
|
116
|
+
}
|
|
93
117
|
/**
|
|
94
118
|
* Synthesize a ResultEvent from the current turn state.
|
|
95
119
|
* Called when the turn completes.
|
|
@@ -713,4 +737,25 @@ function normalizeCommandDecisions(list) {
|
|
|
713
737
|
...(Object.keys(payloads).length > 0 ? { payloads } : {}),
|
|
714
738
|
};
|
|
715
739
|
}
|
|
740
|
+
/** Codex wire question → 统一 UserQuestion(options null/空 → 自由文本题)。 */
|
|
741
|
+
function requestUserInputQuestionToUserQuestion(q) {
|
|
742
|
+
const question = q.question?.trim();
|
|
743
|
+
if (!question)
|
|
744
|
+
return null;
|
|
745
|
+
const options = (q.options ?? []).map((opt) => ({
|
|
746
|
+
label: opt.label,
|
|
747
|
+
...(opt.description ? { description: opt.description } : {}),
|
|
748
|
+
}));
|
|
749
|
+
return {
|
|
750
|
+
id: q.id,
|
|
751
|
+
header: q.header || undefined,
|
|
752
|
+
question,
|
|
753
|
+
isOther: q.isOther,
|
|
754
|
+
isSecret: q.isSecret,
|
|
755
|
+
options,
|
|
756
|
+
// Codex user_note:选项题渲染「补充说明(可选)」输入(live 验证模型理解
|
|
757
|
+
// "user_note:" 条目);自由文本题(options 空)不需要。
|
|
758
|
+
...(options.length > 0 ? { allowNote: true } : {}),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
716
761
|
//# sourceMappingURL=translator.js.map
|
|
@@ -207,6 +207,8 @@ export interface SessionUpdateNotification {
|
|
|
207
207
|
}
|
|
208
208
|
export declare const ServerRequestMethod: {
|
|
209
209
|
readonly REQUEST_PERMISSION: "session/request_permission";
|
|
210
|
+
/** Kimi AskUserQuestion elicitation form(客户端广告 elicitation.form 时启用)。 */
|
|
211
|
+
readonly ELICITATION_CREATE: "elicitation/create";
|
|
210
212
|
};
|
|
211
213
|
export interface PermissionOption {
|
|
212
214
|
/** Opaque option identifier — echo back in response outcome.optionId. */
|
|
@@ -236,3 +238,34 @@ export interface RequestPermissionResponse {
|
|
|
236
238
|
optionId?: string;
|
|
237
239
|
};
|
|
238
240
|
}
|
|
241
|
+
export interface ElicitationEnumOption {
|
|
242
|
+
const: string;
|
|
243
|
+
title?: string;
|
|
244
|
+
description?: string;
|
|
245
|
+
}
|
|
246
|
+
export interface ElicitationPropertySchema {
|
|
247
|
+
type: 'string' | 'array';
|
|
248
|
+
title?: string;
|
|
249
|
+
description?: string;
|
|
250
|
+
minItems?: number;
|
|
251
|
+
oneOf?: ElicitationEnumOption[];
|
|
252
|
+
items?: {
|
|
253
|
+
anyOf?: ElicitationEnumOption[];
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
export interface ElicitationCreateParams {
|
|
257
|
+
sessionId: string;
|
|
258
|
+
toolCallId?: string;
|
|
259
|
+
mode: 'form';
|
|
260
|
+
message?: string;
|
|
261
|
+
requestedSchema: {
|
|
262
|
+
type: 'object';
|
|
263
|
+
required?: string[];
|
|
264
|
+
properties: Record<string, ElicitationPropertySchema>;
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
export interface ElicitationCreateResponse {
|
|
268
|
+
action: 'accept' | 'decline' | 'cancel';
|
|
269
|
+
/** accept 时的答案,key 为 q0..qn(按问题顺序)。 */
|
|
270
|
+
content?: Record<string, string | string[]>;
|
|
271
|
+
}
|
|
@@ -71,5 +71,7 @@ export const SessionEventType = {
|
|
|
71
71
|
// =============================================================================
|
|
72
72
|
export const ServerRequestMethod = {
|
|
73
73
|
REQUEST_PERMISSION: 'session/request_permission',
|
|
74
|
+
/** Kimi AskUserQuestion elicitation form(客户端广告 elicitation.form 时启用)。 */
|
|
75
|
+
ELICITATION_CREATE: 'elicitation/create',
|
|
74
76
|
};
|
|
75
77
|
//# sourceMappingURL=protocol-types.js.map
|
|
@@ -104,18 +104,23 @@ export class ConnectionBasedRunner {
|
|
|
104
104
|
* unblock; otherwise cancel now via cancelCurrentTurn().
|
|
105
105
|
*/
|
|
106
106
|
async stop(_opts) {
|
|
107
|
-
if (!this._isRunning
|
|
107
|
+
if (!this._isRunning)
|
|
108
108
|
return;
|
|
109
|
+
// CC-01: 启动阶段(client/session 未建立)也必须记录停止意图,否则 /stop 被吞、
|
|
110
|
+
// turn 照常执行。executeTurn 在 setup 完成后检查 stopRequested 补发 cancel。
|
|
111
|
+
this.stopRequested = true;
|
|
112
|
+
this.forceFinish = true;
|
|
113
|
+
this.wakeWaiters();
|
|
114
|
+
if (!this.currentClient || !this.currentSessionId()) {
|
|
115
|
+
getLogger().warn(`[${this.logTag}] stop during setup (client/session not ready); will cancel after setup`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
109
118
|
if (this.shouldDeferStop()) {
|
|
110
119
|
// turn/start 或 prompt 尚未响应:先标记,等 run() 完成后再补发 cancel。
|
|
111
|
-
this.stopRequested = true;
|
|
112
|
-
this.forceFinish = true;
|
|
113
|
-
this.wakeWaiters();
|
|
114
120
|
return;
|
|
115
121
|
}
|
|
116
122
|
// turn/start 或 prompt 已 resolved:立即 cancel;同时置 stopRequested 抑制
|
|
117
123
|
// 尚未结算的 prompt 结果翻译(opencode 语义),对已结算方无害。
|
|
118
|
-
this.stopRequested = true;
|
|
119
124
|
try {
|
|
120
125
|
await this.cancelCurrentTurn();
|
|
121
126
|
}
|