lark-remote 0.1.8 → 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 +18 -0
- package/dist/bridge/approval-coordinator.d.ts +15 -0
- package/dist/bridge/approval-coordinator.js +58 -3
- package/dist/bridge/index.d.ts +17 -0
- package/dist/bridge/index.js +31 -2
- package/dist/card/approval-render.js +105 -8
- package/dist/index.js +13 -3
- 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/index.d.ts +19 -3
- package/dist/router/index.js +359 -84
- package/dist/runner/claude/runner.js +22 -0
- package/dist/runner/claude/session.d.ts +26 -0
- package/dist/runner/claude/session.js +90 -13
- package/dist/runner/common/connection-based-runner.js +10 -5
- package/dist/runner/dsh/runner.js +94 -74
- package/dist/runner/kimi/acp/runner.js +15 -0
- package/dist/runner/pi/rpc/runner.js +6 -1
- package/dist/runner/types.d.ts +31 -0
- package/dist/session/dsh/sessions.d.ts +6 -1
- package/dist/session/dsh/sessions.js +20 -5
- package/dist/workspace/index.d.ts +8 -0
- package/dist/workspace/index.js +20 -1
- package/package.json +1 -1
- package/dist/order/alias-store.d.ts +0 -38
- package/dist/order/alias-store.js +0 -106
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.9] - 2026-08-20
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
|
|
12
|
+
- ExitPlanMode 计划审批:计划全文折叠展示 + 五决策对齐 TUI(自动放行 / 附意见 / 采纳修改)
|
|
13
|
+
- 卡片别名管理重构:`alias-store` 并入 `alias-resolve`,`/order` 卡片新增「+别名」交互(绑定/修改/删除,已绑定指令显示 `$name` 标签 + ✕)
|
|
14
|
+
- DSH / connection-based runner 启动 / 配置热更 / 失败路径修复(CC-01~CC-08)
|
|
15
|
+
|
|
16
|
+
### 变更
|
|
17
|
+
|
|
18
|
+
- 别名 API 由「触发词 → 文本」改为「指令 → 别名」:`/order alias add <orderId|序号> <别名>` 绑定、`/order alias rm` 移除;别名随指令持久化在 `orders.json`,删除指令时别名一并删除;名称不能为保留子命令、不能数字开头
|
|
19
|
+
- 用户文档同步别名卡片化交互,移除 `/order alias <name> <text>` 旧用法
|
|
20
|
+
|
|
21
|
+
### 修复
|
|
22
|
+
|
|
23
|
+
- 别名卡片成功回调:携带 `card` 替换 pre-click 卡,删除文案对齐新交互
|
|
24
|
+
- kimi ACP runner 模型下发:迁移到纯 ACP 后 `session/set_config_option` 必须紧跟 `session/new|resume` 下发,否则实际跑的是默认模型(CC-07)
|
|
25
|
+
|
|
8
26
|
## [0.1.8] - 2026-08-19
|
|
9
27
|
|
|
10
28
|
### 新增
|
|
@@ -22,6 +22,8 @@ export interface TrackedApproval {
|
|
|
22
22
|
answers?: Map<number, string[]>;
|
|
23
23
|
/** AskUserQuestion 补充说明(Codex user_note):问题索引 → 文本。 */
|
|
24
24
|
notes?: Map<number, string>;
|
|
25
|
+
/** 计划审批修改意见(ExitPlanMode):planFeedback 回流,供反馈类决策注入。 */
|
|
26
|
+
feedback?: string;
|
|
25
27
|
}
|
|
26
28
|
type ApprovalResponder = (requestId: number | string, response: unknown) => Promise<void>;
|
|
27
29
|
export type { ApprovalAction } from '../runner/types.js';
|
|
@@ -115,6 +117,17 @@ export declare class ApprovalCoordinator {
|
|
|
115
117
|
requestId: number | string;
|
|
116
118
|
nonce?: string;
|
|
117
119
|
}): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* 计划审批修改意见(ExitPlanMode):输入框提交 → coordinator 记录并回流
|
|
122
|
+
* 卡片回显,随后「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
|
|
123
|
+
* 同一请求可多次修改(新 nonce 来自新渲染)。
|
|
124
|
+
*/
|
|
125
|
+
planFeedback(action: {
|
|
126
|
+
text: string;
|
|
127
|
+
}, ctx: {
|
|
128
|
+
requestId: number | string;
|
|
129
|
+
nonce?: string;
|
|
130
|
+
}): Promise<void>;
|
|
118
131
|
/**
|
|
119
132
|
* Update a pending approval's view (out-of-order item/started arrives late).
|
|
120
133
|
* Only applies while the approval is still pending — resolved/expired
|
|
@@ -132,6 +145,8 @@ export declare class ApprovalCoordinator {
|
|
|
132
145
|
private recordAnswer;
|
|
133
146
|
private getPendingQuestion;
|
|
134
147
|
private allQuestionsAnswered;
|
|
148
|
+
/** 推送审批卡视图更新(approval_view_updated 重渲染;失败仅记日志)。 */
|
|
149
|
+
private pushApprovalViewUpdate;
|
|
135
150
|
/** 推送 question 卡片视图更新(选项勾选状态变化)。 */
|
|
136
151
|
private pushQuestionViewUpdate;
|
|
137
152
|
/** 全部问题已答:构建 answers 并提交(非空校验 + 原子状态迁移)。 */
|
|
@@ -97,12 +97,33 @@ export class ApprovalCoordinator {
|
|
|
97
97
|
if (!tracked.view.availableDecisions.includes(decision) && !safetyOverride) {
|
|
98
98
|
throw new Error(`Decision "${decision}" not in available decisions: ${tracked.view.availableDecisions.join(', ')}`);
|
|
99
99
|
}
|
|
100
|
+
// 反馈类决策:注入用户已填写的修改意见(卡片输入框经 planFeedback 回流到
|
|
101
|
+
// coordinator)。decisionToApprovalAction 只带决策名,payload 在此补齐;
|
|
102
|
+
// 调用方直接传完整 payload 时以显式值为准。
|
|
103
|
+
let effectiveAction = action;
|
|
104
|
+
if (action.action === 'decline_with_feedback' || action.action === 'accept_with_feedback') {
|
|
105
|
+
const explicit = action.action === 'decline_with_feedback' ? action.message : action.plan;
|
|
106
|
+
const text = explicit.trim() || tracked.feedback?.trim() || '';
|
|
107
|
+
if (!text) {
|
|
108
|
+
throw new Error('请先填写修改意见');
|
|
109
|
+
}
|
|
110
|
+
effectiveAction =
|
|
111
|
+
action.action === 'decline_with_feedback'
|
|
112
|
+
? { action: 'decline_with_feedback', message: text }
|
|
113
|
+
: {
|
|
114
|
+
action: 'accept_with_feedback',
|
|
115
|
+
// 批准并采纳修改:新计划 = 原计划 + 用户意见(无原计划时意见即计划)。
|
|
116
|
+
plan: tracked.view.plan?.trim()
|
|
117
|
+
? `${tracked.view.plan.trim()}\n\n## 用户修改意见\n${text}`
|
|
118
|
+
: `# 用户修改意见\n\n${text}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
100
121
|
// Atomic state transition: pending → submitting(同步,无 await)
|
|
101
122
|
tracked.state = 'submitting';
|
|
102
123
|
this.clearTimer(tracked);
|
|
103
124
|
// Send response via the responder
|
|
104
125
|
try {
|
|
105
|
-
await this.responder(ctx.requestId,
|
|
126
|
+
await this.responder(ctx.requestId, effectiveAction);
|
|
106
127
|
tracked.state = 'resolved';
|
|
107
128
|
// 提交成功后立即从卡片移除审批区(agent 无关):claude 控制协议没有
|
|
108
129
|
// resolved 回发,不推就会「已点允许按钮仍在」残留 UI;codex 的 server
|
|
@@ -249,6 +270,28 @@ export class ApprovalCoordinator {
|
|
|
249
270
|
q.note = text;
|
|
250
271
|
this.pushQuestionViewUpdate(ctx.requestId, tracked);
|
|
251
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* 计划审批修改意见(ExitPlanMode):输入框提交 → coordinator 记录并回流
|
|
275
|
+
* 卡片回显,随后「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
|
|
276
|
+
* 同一请求可多次修改(新 nonce 来自新渲染)。
|
|
277
|
+
*/
|
|
278
|
+
async planFeedback(action, ctx) {
|
|
279
|
+
const tracked = this.approvals.get(ctx.requestId);
|
|
280
|
+
if (!tracked) {
|
|
281
|
+
throw new Error(`Approval request ${ctx.requestId} not found`);
|
|
282
|
+
}
|
|
283
|
+
if (tracked.state !== 'pending') {
|
|
284
|
+
throw new Error(`Approval request ${ctx.requestId} is no longer pending`);
|
|
285
|
+
}
|
|
286
|
+
const text = action.text.trim();
|
|
287
|
+
if (!text) {
|
|
288
|
+
throw new Error('请输入修改意见');
|
|
289
|
+
}
|
|
290
|
+
this.assertFreshNonce(ctx.requestId, ctx.nonce);
|
|
291
|
+
tracked.feedback = text;
|
|
292
|
+
tracked.view.planFeedback = text;
|
|
293
|
+
this.pushApprovalViewUpdate(ctx.requestId, tracked);
|
|
294
|
+
}
|
|
252
295
|
/**
|
|
253
296
|
* Update a pending approval's view (out-of-order item/started arrives late).
|
|
254
297
|
* Only applies while the approval is still pending — resolved/expired
|
|
@@ -320,8 +363,8 @@ export class ApprovalCoordinator {
|
|
|
320
363
|
}
|
|
321
364
|
return true;
|
|
322
365
|
}
|
|
323
|
-
/**
|
|
324
|
-
|
|
366
|
+
/** 推送审批卡视图更新(approval_view_updated 重渲染;失败仅记日志)。 */
|
|
367
|
+
pushApprovalViewUpdate(requestId, tracked) {
|
|
325
368
|
void this.pushToCard([
|
|
326
369
|
{
|
|
327
370
|
type: 'approval_view_updated',
|
|
@@ -333,6 +376,10 @@ export class ApprovalCoordinator {
|
|
|
333
376
|
getLogger().warn(`[approval-coordinator] question view update failed requestId=${requestId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
334
377
|
});
|
|
335
378
|
}
|
|
379
|
+
/** 推送 question 卡片视图更新(选项勾选状态变化)。 */
|
|
380
|
+
pushQuestionViewUpdate(requestId, tracked) {
|
|
381
|
+
this.pushApprovalViewUpdate(requestId, tracked);
|
|
382
|
+
}
|
|
336
383
|
/** 全部问题已答:构建 answers 并提交(非空校验 + 原子状态迁移)。 */
|
|
337
384
|
async respondQuestionAnswers(tracked, ctx) {
|
|
338
385
|
this.assertFreshNonce(ctx.requestId, ctx.nonce);
|
|
@@ -413,6 +460,10 @@ export class ApprovalCoordinator {
|
|
|
413
460
|
return 'acceptAll';
|
|
414
461
|
case 'answer':
|
|
415
462
|
return 'answer';
|
|
463
|
+
case 'decline_with_feedback':
|
|
464
|
+
return 'declineWithFeedback';
|
|
465
|
+
case 'accept_with_feedback':
|
|
466
|
+
return 'acceptWithFeedback';
|
|
416
467
|
case 'decline':
|
|
417
468
|
return 'decline';
|
|
418
469
|
case 'cancel':
|
|
@@ -431,6 +482,10 @@ export function decisionToApprovalAction(decision) {
|
|
|
431
482
|
return { action: 'accept_with_execpolicy_amendment' };
|
|
432
483
|
case 'acceptAll':
|
|
433
484
|
return { action: 'accept_all' };
|
|
485
|
+
case 'declineWithFeedback':
|
|
486
|
+
return { action: 'decline_with_feedback', message: '' };
|
|
487
|
+
case 'acceptWithFeedback':
|
|
488
|
+
return { action: 'accept_with_feedback', plan: '' };
|
|
434
489
|
case 'decline':
|
|
435
490
|
return { action: 'decline' };
|
|
436
491
|
case 'cancel':
|
package/dist/bridge/index.d.ts
CHANGED
|
@@ -121,6 +121,13 @@ export declare class Bridge {
|
|
|
121
121
|
* blocked the queue forever.
|
|
122
122
|
*/
|
|
123
123
|
private runners;
|
|
124
|
+
/**
|
|
125
|
+
* Runner 槽位在「活跃运行期间配置变更」时被标 stale(CC-06/P1):clearRunners() 对
|
|
126
|
+
* 活跃 workspace 只标 stale、不立即 evict(避免误杀长驻连接),当前 run 结束后由
|
|
127
|
+
* finalizeRun 安全 evict+dispose,下一轮 getRunner 创建新配置的 runner。
|
|
128
|
+
* key 为 `${cwd}\u0000${agentKind}`。
|
|
129
|
+
*/
|
|
130
|
+
private staleRunnerSlots;
|
|
124
131
|
/** Queue manager for per-workspace serial processing queue. */
|
|
125
132
|
/** 普通消息队列管理器(public:测试注入 queue-card 状态用)。 */
|
|
126
133
|
queueManager: QueueManager;
|
|
@@ -383,6 +390,16 @@ export declare class Bridge {
|
|
|
383
390
|
decision: string;
|
|
384
391
|
nonce: string;
|
|
385
392
|
}): Promise<void>;
|
|
393
|
+
/**
|
|
394
|
+
* 计划审批修改意见(ExitPlanMode 输入框):记录到 coordinator 并回流卡片。
|
|
395
|
+
* 「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
|
|
396
|
+
*/
|
|
397
|
+
handleApprovalPlanFeedback(opts: {
|
|
398
|
+
runId: string;
|
|
399
|
+
requestId: number | string;
|
|
400
|
+
text: string;
|
|
401
|
+
nonce: string;
|
|
402
|
+
}): Promise<void>;
|
|
386
403
|
/**
|
|
387
404
|
* Handle a permission toggle from a card button click.
|
|
388
405
|
* Routes to the ApprovalCoordinator for the given run.
|
package/dist/bridge/index.js
CHANGED
|
@@ -89,6 +89,13 @@ export class Bridge {
|
|
|
89
89
|
* blocked the queue forever.
|
|
90
90
|
*/
|
|
91
91
|
runners = new Map();
|
|
92
|
+
/**
|
|
93
|
+
* Runner 槽位在「活跃运行期间配置变更」时被标 stale(CC-06/P1):clearRunners() 对
|
|
94
|
+
* 活跃 workspace 只标 stale、不立即 evict(避免误杀长驻连接),当前 run 结束后由
|
|
95
|
+
* finalizeRun 安全 evict+dispose,下一轮 getRunner 创建新配置的 runner。
|
|
96
|
+
* key 为 `${cwd}\u0000${agentKind}`。
|
|
97
|
+
*/
|
|
98
|
+
staleRunnerSlots = new Set();
|
|
92
99
|
/** Queue manager for per-workspace serial processing queue. */
|
|
93
100
|
/** 普通消息队列管理器(public:测试注入 queue-card 状态用)。 */
|
|
94
101
|
queueManager;
|
|
@@ -259,10 +266,18 @@ export class Bridge {
|
|
|
259
266
|
// keeping it is correct; finalizeRun deletes the cache entry when the run
|
|
260
267
|
// ends, and the next run picks up the new config/agent via getRunner.
|
|
261
268
|
for (const cwd of [...this.runners.keys()]) {
|
|
262
|
-
if (this.activeRuns.has(cwd))
|
|
269
|
+
if (this.activeRuns.has(cwd)) {
|
|
270
|
+
// CC-06/P1: 活跃 workspace 不能立即 evict(会误杀长驻连接),但必须标记 stale,
|
|
271
|
+
// 否则 finalizeRun 对 workspace-lifetime runner 也不 evict → 下一轮复用旧配置实例。
|
|
272
|
+
// 这里只标待淘汰,当前 run 结束后由 finalizeRun 安全 evict。
|
|
273
|
+
for (const kind of [...this.runners.get(cwd).keys()]) {
|
|
274
|
+
this.staleRunnerSlots.add(`${cwd}\u0000${kind}`);
|
|
275
|
+
}
|
|
263
276
|
continue;
|
|
277
|
+
}
|
|
264
278
|
for (const kind of [...this.runners.get(cwd).keys()]) {
|
|
265
279
|
this.evictRunnerSlot(cwd, kind);
|
|
280
|
+
this.staleRunnerSlots.delete(`${cwd}\u0000${kind}`);
|
|
266
281
|
}
|
|
267
282
|
}
|
|
268
283
|
}
|
|
@@ -287,6 +302,7 @@ export class Bridge {
|
|
|
287
302
|
void evicted.dispose();
|
|
288
303
|
}
|
|
289
304
|
workspaceMap.delete(kind);
|
|
305
|
+
this.staleRunnerSlots.delete(`${cwd}\u0000${kind}`);
|
|
290
306
|
if (workspaceMap.size === 0) {
|
|
291
307
|
this.runners.delete(cwd);
|
|
292
308
|
}
|
|
@@ -1302,9 +1318,13 @@ export class Bridge {
|
|
|
1302
1318
|
// evicted+disposed by clearRunners (/config) and interruptCurrentRun
|
|
1303
1319
|
// (/stop). Spawn-per-message runners are evicted as before to prevent
|
|
1304
1320
|
// stale process references accumulating.
|
|
1305
|
-
|
|
1321
|
+
// CC-06/P1: 配置变更期间(clearRunners 标了 stale)的 workspace-lifetime
|
|
1322
|
+
// runner 也必须在 run 结束后 evict,否则下一轮复用旧配置实例。
|
|
1323
|
+
const slotKey = `${cwd}\u0000${activeRun.agentKind}`;
|
|
1324
|
+
if (activeRun.runner.lifetime !== 'workspace' || this.staleRunnerSlots.has(slotKey)) {
|
|
1306
1325
|
this.evictRunnerSlot(cwd, activeRun.agentKind);
|
|
1307
1326
|
}
|
|
1327
|
+
this.staleRunnerSlots.delete(slotKey);
|
|
1308
1328
|
// Compact 卡片需要 runId + sessionId;record 供终态卡上的 Compact 按钮
|
|
1309
1329
|
// 校验与取参(activeRuns 已清空,无法再查到)。异常退出(error/
|
|
1310
1330
|
// interrupted/idle_timeout)同样记录——上下文往往更大,压缩后再续。
|
|
@@ -1447,6 +1467,15 @@ export class Bridge {
|
|
|
1447
1467
|
await coordinator.submit(action, { requestId: opts.requestId, nonce: opts.nonce });
|
|
1448
1468
|
});
|
|
1449
1469
|
}
|
|
1470
|
+
/**
|
|
1471
|
+
* 计划审批修改意见(ExitPlanMode 输入框):记录到 coordinator 并回流卡片。
|
|
1472
|
+
* 「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
|
|
1473
|
+
*/
|
|
1474
|
+
async handleApprovalPlanFeedback(opts) {
|
|
1475
|
+
return this.withCoordinator(opts.runId, async (coordinator) => {
|
|
1476
|
+
await coordinator.planFeedback({ text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce });
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1450
1479
|
/**
|
|
1451
1480
|
* Handle a permission toggle from a card button click.
|
|
1452
1481
|
* Routes to the ApprovalCoordinator for the given run.
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
* Renders command/file/permissions approval areas with button mappings per §8.3.
|
|
5
5
|
* Uses CardKit 2.0 (no tag:"action" container — 200861 rule).
|
|
6
6
|
*/
|
|
7
|
+
import { collapsibleMarkdownPanel } from './collapsible.js';
|
|
8
|
+
import { truncateUtf8, truncateMarkdownTables } from './text-truncate.js';
|
|
7
9
|
/** Unique nonce for card button callbacks (SDK dedup + router validation). */
|
|
8
10
|
function mkNonce() {
|
|
9
11
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
10
12
|
}
|
|
13
|
+
/** 计划面板正文字节预算(折叠展示 + 截断保护 28KB 卡片预算)。 */
|
|
14
|
+
const PLAN_PANEL_BYTES = 6000;
|
|
11
15
|
/** reason div(3 处共用);反引号中和防 lark_md 解析错误(review P2-2)。 */
|
|
12
16
|
function reasonDiv(approval) {
|
|
13
17
|
if (!approval.reason)
|
|
@@ -71,20 +75,26 @@ function renderDecisionButtons(approval, expired, runId) {
|
|
|
71
75
|
},
|
|
72
76
|
];
|
|
73
77
|
}
|
|
78
|
+
// ExitPlanMode 计划审批用 TUI 语义文案;其他审批保持原文案。
|
|
79
|
+
const isPlanExit = approval.kind === 'tool' && approval.toolName === 'ExitPlanMode';
|
|
74
80
|
const BUTTONS = {
|
|
75
|
-
accept: { label: '✅ 允许', type: 'primary' },
|
|
81
|
+
accept: { label: isPlanExit ? '✅ 批准并执行' : '✅ 允许', type: 'primary' },
|
|
76
82
|
acceptForSession: { label: '✅ 允许本次会话', type: 'primary' },
|
|
77
83
|
acceptWithExecpolicyAmendment: { label: '🔒 允许并记住命令', type: 'primary' },
|
|
78
|
-
acceptAll: { label: '✅ 允许所有', type: 'primary' },
|
|
84
|
+
acceptAll: { label: isPlanExit ? '✅ 批准并自动放行' : '✅ 允许所有', type: 'primary' },
|
|
85
|
+
acceptWithFeedback: { label: '✍️ 批准并采纳修改', type: 'primary' },
|
|
86
|
+
declineWithFeedback: { label: '✏️ 拒绝并附意见', type: 'default' },
|
|
79
87
|
decline: { label: '❌ 拒绝', type: 'danger' },
|
|
80
88
|
};
|
|
81
89
|
// Fixed presentation order; only offer decisions the protocol listed
|
|
82
|
-
// (accept/decline are always available;
|
|
90
|
+
// (accept/decline are always available; 其余按 availableDecisions 出现)。
|
|
83
91
|
const offered = [
|
|
84
92
|
'accept',
|
|
85
93
|
'acceptForSession',
|
|
86
94
|
'acceptWithExecpolicyAmendment',
|
|
87
95
|
'acceptAll',
|
|
96
|
+
'acceptWithFeedback',
|
|
97
|
+
'declineWithFeedback',
|
|
88
98
|
'decline',
|
|
89
99
|
].filter((d) => d === 'accept' || d === 'decline' || approval.availableDecisions.includes(d));
|
|
90
100
|
// 决策按钮逐个作为 body 直接元素返回(CardKit body 纵向堆叠 = 上下排列)。
|
|
@@ -113,18 +123,21 @@ function renderDecisionButtons(approval, expired, runId) {
|
|
|
113
123
|
// =============================================================================
|
|
114
124
|
/**
|
|
115
125
|
* 通用工具权限请求(kind === 'tool'):展示工具名 + 用途说明(reason)+ 原始
|
|
116
|
-
* input(若有)。ExitPlanMode
|
|
117
|
-
* 不再落入 command 槽位显示无意义 `{}`。
|
|
126
|
+
* input(若有)。ExitPlanMode 走独立计划审批分支(计划全文 + 修改意见输入)。
|
|
118
127
|
*/
|
|
119
128
|
function renderToolApproval(approval, expired, runId) {
|
|
129
|
+
// ExitPlanMode:对齐 TUI「先看完整计划再交互」——计划全文折叠面板 +
|
|
130
|
+
// 修改意见输入 + 五决策按钮(批准并执行 / 批准并自动放行 / 拒绝并附意见 /
|
|
131
|
+
// 批准并采纳修改 / 拒绝)。
|
|
132
|
+
if (approval.toolName === 'ExitPlanMode') {
|
|
133
|
+
return renderPlanApproval(approval, expired, runId);
|
|
134
|
+
}
|
|
120
135
|
const elements = [];
|
|
121
|
-
// ExitPlanMode 是「退出计划模式」:标题用计划审批语义;其他工具用通用标题。
|
|
122
|
-
const isPlanExit = approval.toolName === 'ExitPlanMode';
|
|
123
136
|
elements.push({
|
|
124
137
|
tag: 'div',
|
|
125
138
|
text: {
|
|
126
139
|
tag: 'lark_md',
|
|
127
|
-
content:
|
|
140
|
+
content: '**🔧 工具请求**',
|
|
128
141
|
},
|
|
129
142
|
});
|
|
130
143
|
const toolLabel = approval.toolName ?? '(unknown tool)';
|
|
@@ -144,6 +157,90 @@ function renderToolApproval(approval, expired, runId) {
|
|
|
144
157
|
elements.push(...renderDecisionButtons(approval, expired, runId));
|
|
145
158
|
return elements;
|
|
146
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* ExitPlanMode 计划审批区:
|
|
162
|
+
* - 计划全文(input.plan 或计划文件读取)折叠展示,反引号中和 + 表格限数 +
|
|
163
|
+
* 字节预算截断;
|
|
164
|
+
* - 计划文件路径(有则展示,含模型写错目录的退化场景);
|
|
165
|
+
* - 修改意见输入框(approval.planFeedback → coordinator 回流回显),供
|
|
166
|
+
* 「拒绝并附意见」/「批准并采纳修改」复用;
|
|
167
|
+
* - 决策按钮由 renderDecisionButtons 按 availableDecisions 渲染。
|
|
168
|
+
*/
|
|
169
|
+
function renderPlanApproval(approval, expired, runId) {
|
|
170
|
+
const elements = [];
|
|
171
|
+
elements.push({
|
|
172
|
+
tag: 'div',
|
|
173
|
+
text: { tag: 'lark_md', content: '**📋 计划审批**' },
|
|
174
|
+
});
|
|
175
|
+
pushIf(elements, reasonDiv(approval));
|
|
176
|
+
if (approval.planFilePath) {
|
|
177
|
+
const neutralizedPath = approval.planFilePath.replace(/`/g, '·');
|
|
178
|
+
elements.push({
|
|
179
|
+
tag: 'div',
|
|
180
|
+
text: { tag: 'lark_md', content: `📄 ${neutralizedPath}` },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (approval.plan) {
|
|
184
|
+
// 反引号中和防 11311(同 reasonDiv/command 策略);表格限数防 11310;
|
|
185
|
+
// 字节预算截断防 28KB 超限。
|
|
186
|
+
const neutralized = approval.plan.replace(/`/g, '·');
|
|
187
|
+
const tableSafe = truncateMarkdownTables(neutralized);
|
|
188
|
+
const content = truncateUtf8(tableSafe, PLAN_PANEL_BYTES);
|
|
189
|
+
elements.push(collapsibleMarkdownPanel({
|
|
190
|
+
title: '📋 计划',
|
|
191
|
+
expanded: false,
|
|
192
|
+
border: 'blue',
|
|
193
|
+
content,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
elements.push({
|
|
198
|
+
tag: 'div',
|
|
199
|
+
text: {
|
|
200
|
+
tag: 'lark_md',
|
|
201
|
+
content: '📋 未获取到计划文件内容(可参考上方正文摘要)',
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (!expired) {
|
|
206
|
+
elements.push(renderPlanFeedbackInput(approval.requestId, runId));
|
|
207
|
+
if (approval.planFeedback) {
|
|
208
|
+
const neutralized = approval.planFeedback.replace(/`/g, '·');
|
|
209
|
+
elements.push({
|
|
210
|
+
tag: 'div',
|
|
211
|
+
text: { tag: 'lark_md', content: `📝 ${neutralized}` },
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
elements.push(...renderDecisionButtons(approval, expired, runId));
|
|
216
|
+
return elements;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 计划修改意见输入行:输入后点击右侧 ✓ 提交图标,input_value 经 connector
|
|
220
|
+
* raw 事件回传(CardKit input 的 input_value 会被 SDK normalizer 丢弃,
|
|
221
|
+
* 必须走 includeRawEvent 路径,同 answerNote)。
|
|
222
|
+
*/
|
|
223
|
+
function renderPlanFeedbackInput(requestId, runId) {
|
|
224
|
+
return {
|
|
225
|
+
tag: 'input',
|
|
226
|
+
name: `plan-feedback-${requestId}`,
|
|
227
|
+
placeholder: {
|
|
228
|
+
tag: 'plain_text',
|
|
229
|
+
content: '填写修改意见(可配合「拒绝并附意见」/「批准并采纳修改」)…',
|
|
230
|
+
},
|
|
231
|
+
behaviors: [
|
|
232
|
+
{
|
|
233
|
+
type: 'callback',
|
|
234
|
+
value: {
|
|
235
|
+
cmd: 'approval.planFeedback',
|
|
236
|
+
requestId,
|
|
237
|
+
runId,
|
|
238
|
+
nonce: mkNonce(),
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
147
244
|
// =============================================================================
|
|
148
245
|
// Command Approval
|
|
149
246
|
// =============================================================================
|
package/dist/index.js
CHANGED
|
@@ -241,7 +241,13 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
241
241
|
sessionReaderRegistry.register('kimi', kimiSessionReader);
|
|
242
242
|
// Register DshRunner (HTTP-only DSH Web Host agent) and DshSessionReader.
|
|
243
243
|
const dshSessionReader = new DshSessionReader({
|
|
244
|
-
|
|
244
|
+
// hostProvider 每次调用读最新 config:/config 修改 host 后 runner 重建为
|
|
245
|
+
// 新 host,reader(/resume、用量、完成卡)也跟随新 host(CC-02)。
|
|
246
|
+
hostProvider: () => {
|
|
247
|
+
const container = agentRegistry.getConfigContainer();
|
|
248
|
+
const latestConfig = container?.current ?? config;
|
|
249
|
+
return getAgentConfig(latestConfig, 'dsh')?.host;
|
|
250
|
+
},
|
|
245
251
|
});
|
|
246
252
|
agentRegistry.register('dsh', (_ws) => {
|
|
247
253
|
// Read latest config from container so /config host changes take effect
|
|
@@ -526,19 +532,24 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
526
532
|
// 直返列表外,过期/重复 nonce/非法选项等错误 toast 被静默吞掉)。
|
|
527
533
|
// 审批响应尤其不能落串行队列:run 任务占用队列头直到 turn 结束,审批响应
|
|
528
534
|
// 排在后面会形成死锁(run 不结束不执行、run 结束 coordinator 已删响应空转)。
|
|
535
|
+
// order.aliasInput / order.aliasRemove 也返回 toast,需直返避免被吞。
|
|
529
536
|
if (actionValue.cmd === 'queue.input' ||
|
|
537
|
+
actionValue.cmd === 'order.aliasInput' ||
|
|
538
|
+
actionValue.cmd === 'order.aliasRemove' ||
|
|
530
539
|
actionValue.cmd === 'config.save' ||
|
|
531
540
|
actionValue.cmd === 'approval.respond' ||
|
|
532
541
|
actionValue.cmd === 'approval.toggle' ||
|
|
533
542
|
actionValue.cmd === 'approval.answer' ||
|
|
534
543
|
actionValue.cmd === 'approval.answerSubmit' ||
|
|
535
|
-
actionValue.cmd === 'approval.answerCustom'
|
|
544
|
+
actionValue.cmd === 'approval.answerCustom' ||
|
|
545
|
+
actionValue.cmd === 'approval.answerNote') {
|
|
536
546
|
return router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
537
547
|
}
|
|
538
548
|
// order.exec → equivalent queued message (Plan A): resolve the order text
|
|
539
549
|
// at the enqueue boundary and route it through router.handle, exactly like
|
|
540
550
|
// a hand-typed message. See order-exec-dispatch.ts for the contract and
|
|
541
551
|
// why an internal key replaces the Feishu card messageId.
|
|
552
|
+
// (order.aliasEdit 是控制动作,走 enqueueImmediate 弹卡即可,不需直返。)
|
|
542
553
|
if (actionValue.cmd === 'order.exec') {
|
|
543
554
|
void dispatchOrderExecForQueue({
|
|
544
555
|
router,
|
|
@@ -650,7 +661,6 @@ async function main() {
|
|
|
650
661
|
configPath: path.join(configDir, 'config.yaml'),
|
|
651
662
|
workspacePath: path.join(configDir, 'workspace.json'),
|
|
652
663
|
ordersPath: path.join(configDir, 'orders.json'),
|
|
653
|
-
aliasesPath: path.join(configDir, 'aliases.json'),
|
|
654
664
|
restartSpawner: () => spawnReplacementBridge(path.join(configDir, 'logs')),
|
|
655
665
|
sessionReaderRegistry,
|
|
656
666
|
devMode: cliArgs.dev,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/** 参与别名展开的条目:别名名 + 展开文本(即指令文本)。 */
|
|
2
|
+
export interface AliasResolveEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
2
6
|
/**
|
|
3
7
|
* 一次别名展开。
|
|
4
8
|
*
|
|
@@ -10,4 +14,4 @@ import type { AliasEntry } from './alias-store.js';
|
|
|
10
14
|
*
|
|
11
15
|
* @returns 展开后的完整消息;无匹配时返回 undefined。
|
|
12
16
|
*/
|
|
13
|
-
export declare function resolveAlias(message: string, aliases: readonly
|
|
17
|
+
export declare function resolveAlias(message: string, aliases: readonly AliasResolveEntry[]): string | undefined;
|
package/dist/order/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
interface OrderEntry {
|
|
1
|
+
export interface OrderEntry {
|
|
2
2
|
id: string;
|
|
3
3
|
text: string;
|
|
4
4
|
createdAt: string;
|
|
5
5
|
usedAt?: string;
|
|
6
|
+
/** 可选别名名(该指令的 $name 触发词)。全局唯一,由 OrderStore 校验。 */
|
|
7
|
+
alias?: string;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
10
|
* Global persistent order store.
|
|
@@ -20,8 +22,13 @@ export declare class OrderStore {
|
|
|
20
22
|
private persist;
|
|
21
23
|
save(text: string): OrderEntry;
|
|
22
24
|
updateUsedAt(id: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* 给指令绑定别名(或解绑)。别名全局唯一,撞名抛错;名称必须通过
|
|
27
|
+
* `ALIAS_NAME_PATTERN`(字母/数字/下划线、不能数字开头、≤20 字符)。
|
|
28
|
+
* @returns 绑定后的条目。
|
|
29
|
+
*/
|
|
30
|
+
setAlias(id: string, name: string | undefined): OrderEntry | undefined;
|
|
23
31
|
get(): OrderEntry[];
|
|
24
32
|
has(id: string): boolean;
|
|
25
33
|
remove(id: string): void;
|
|
26
34
|
}
|
|
27
|
-
export {};
|
package/dist/order/index.js
CHANGED
|
@@ -4,6 +4,9 @@ import { atomicWriteJson } from '../persistence/atomic-write.js';
|
|
|
4
4
|
import { loadJsonFile } from '../persistence/load-json-file.js';
|
|
5
5
|
const MAX_TEXT_LENGTH = 200;
|
|
6
6
|
const MAX_ORDERS = 50;
|
|
7
|
+
/** 别名名称规则:`[A-Za-z_][A-Za-z0-9_]*`,≤20 字符,不能数字开头。 */
|
|
8
|
+
const ALIAS_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9
|
+
const MAX_ALIAS_NAME_LENGTH = 20;
|
|
7
10
|
/**
|
|
8
11
|
* Global persistent order store.
|
|
9
12
|
*
|
|
@@ -45,6 +48,11 @@ export class OrderStore {
|
|
|
45
48
|
};
|
|
46
49
|
if (typeof e.usedAt === 'string')
|
|
47
50
|
entry.usedAt = e.usedAt;
|
|
51
|
+
if (typeof e.alias === 'string' &&
|
|
52
|
+
ALIAS_NAME_PATTERN.test(e.alias) &&
|
|
53
|
+
e.alias.length <= MAX_ALIAS_NAME_LENGTH) {
|
|
54
|
+
entry.alias = e.alias;
|
|
55
|
+
}
|
|
48
56
|
valid.push(entry);
|
|
49
57
|
}
|
|
50
58
|
}
|
|
@@ -76,6 +84,31 @@ export class OrderStore {
|
|
|
76
84
|
this.persist();
|
|
77
85
|
}
|
|
78
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* 给指令绑定别名(或解绑)。别名全局唯一,撞名抛错;名称必须通过
|
|
89
|
+
* `ALIAS_NAME_PATTERN`(字母/数字/下划线、不能数字开头、≤20 字符)。
|
|
90
|
+
* @returns 绑定后的条目。
|
|
91
|
+
*/
|
|
92
|
+
setAlias(id, name) {
|
|
93
|
+
const entry = this.data.find((e) => e.id === id);
|
|
94
|
+
if (!entry)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (name === undefined || name === '') {
|
|
97
|
+
delete entry.alias;
|
|
98
|
+
this.persist();
|
|
99
|
+
return entry;
|
|
100
|
+
}
|
|
101
|
+
if (!ALIAS_NAME_PATTERN.test(name) || name.length > MAX_ALIAS_NAME_LENGTH) {
|
|
102
|
+
throw new Error(`别名名称只能包含字母/数字/下划线,不能数字开头,且不超过 ${MAX_ALIAS_NAME_LENGTH} 字符`);
|
|
103
|
+
}
|
|
104
|
+
const existing = this.data.find((e) => e.id !== id && e.alias === name);
|
|
105
|
+
if (existing) {
|
|
106
|
+
throw new Error(`别名 $${name} 已被其他指令占用`);
|
|
107
|
+
}
|
|
108
|
+
entry.alias = name;
|
|
109
|
+
this.persist();
|
|
110
|
+
return entry;
|
|
111
|
+
}
|
|
79
112
|
get() {
|
|
80
113
|
return [...this.data];
|
|
81
114
|
}
|
package/dist/router/index.d.ts
CHANGED
|
@@ -101,7 +101,6 @@ export declare class CommandRouter {
|
|
|
101
101
|
configPath: string;
|
|
102
102
|
private workspaceStore;
|
|
103
103
|
private orderStore;
|
|
104
|
-
private aliasStore;
|
|
105
104
|
private exitHandler;
|
|
106
105
|
private pendingExit;
|
|
107
106
|
/**
|
|
@@ -156,8 +155,6 @@ export declare class CommandRouter {
|
|
|
156
155
|
configPath: string;
|
|
157
156
|
workspacePath?: string;
|
|
158
157
|
ordersPath?: string;
|
|
159
|
-
/** 别名存储路径(<configDir>/aliases.json)。 */
|
|
160
|
-
aliasesPath?: string;
|
|
161
158
|
exitHandler?: () => void;
|
|
162
159
|
/** Spawn the detached replacement bridge process and return its pid (throws on failure). */
|
|
163
160
|
restartSpawner?: () => number;
|
|
@@ -193,6 +190,7 @@ export declare class CommandRouter {
|
|
|
193
190
|
/**
|
|
194
191
|
* 一次别名展开:bridge 收到用户消息后、命令分发前调用。
|
|
195
192
|
* 仅 `$name` 开头的消息可能被展开;未知别名 / 非 `$` 消息原样返回。
|
|
193
|
+
* 别名来自 order 的 `alias` 字段(1 条指令 = 1 个别名),展开文本即指令文本。
|
|
196
194
|
*/
|
|
197
195
|
expandAliasMessage(message: string): string;
|
|
198
196
|
/**
|
|
@@ -298,6 +296,18 @@ export declare class CommandRouter {
|
|
|
298
296
|
* Mirrors handleLsPage — updates the same card, never sends a new one.
|
|
299
297
|
*/
|
|
300
298
|
private handleOrderPage;
|
|
299
|
+
/**
|
|
300
|
+
* Handle order.aliasEdit: 展示别名编辑卡(input + ✓ 提交图标)。
|
|
301
|
+
* 有别名时预填;提交 `order.aliasInput` 处理(含校验与唯一性)。
|
|
302
|
+
*/
|
|
303
|
+
private handleOrderAliasEdit;
|
|
304
|
+
/**
|
|
305
|
+
* Handle order.aliasInput: 提交别名(绑/改/解绑),随后重渲染 /order 列表卡。
|
|
306
|
+
* 留空 = 解绑;非空按命名规则 + 全局唯一校验,失败仅 toast 不重渲染。
|
|
307
|
+
*/
|
|
308
|
+
private handleOrderAliasInput;
|
|
309
|
+
/** Handle order.aliasRemove: 删除某条指令的别名并原地重渲染列表卡。 */
|
|
310
|
+
private handleOrderAliasRemove;
|
|
301
311
|
/**
|
|
302
312
|
* Handle ws.use card action: switch to the workspace, then refresh the /ws
|
|
303
313
|
* list card in place so "recent" sort order is immediately visible.
|
|
@@ -467,5 +477,11 @@ export declare class CommandRouter {
|
|
|
467
477
|
buildConfigCard(): CommandResult;
|
|
468
478
|
/** /order 实现(public:测试直接调用,替代 as unknown as)。 */
|
|
469
479
|
cmdOrder(args: string[], _ctx: CommandContext, offset?: number): CommandResult;
|
|
480
|
+
/** /order alias add:按 orderId 或列表序号(1 基)解析目标指令并绑定别名。 */
|
|
481
|
+
private cmdOrderAliasAdd;
|
|
482
|
+
/** /order alias rm:按 orderId 或列表序号(1 基)移除指令的别名。 */
|
|
483
|
+
private cmdOrderAliasRemove;
|
|
484
|
+
/** 将 `/order alias add|rm` 的目标解析为 order:优先 orderId 精确匹配,回退列表序号(1 基)。 */
|
|
485
|
+
private resolveOrderTarget;
|
|
470
486
|
}
|
|
471
487
|
export {};
|