lark-remote 0.1.6 → 0.1.8
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 +51 -1
- package/README.en.md +9 -1
- package/README.md +14 -2
- package/dist/bridge/approval-coordinator.d.ts +71 -34
- package/dist/bridge/approval-coordinator.js +225 -41
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +97 -13
- package/dist/bridge/index.js +227 -87
- package/dist/bridge/queue-manager.d.ts +2 -1
- package/dist/bridge/queue-manager.js +1 -0
- package/dist/card/approval-render.js +294 -36
- package/dist/card/bash-card-session.d.ts +2 -24
- package/dist/card/bash-card-session.js +6 -73
- package/dist/card/bash-renderer.js +2 -3
- package/dist/card/card-budget.js +29 -75
- package/dist/card/card-session.d.ts +48 -0
- package/dist/card/card-session.js +86 -0
- package/dist/card/card-shared.d.ts +7 -16
- package/dist/card/card-shared.js +7 -30
- package/dist/card/run-card-session.d.ts +2 -55
- package/dist/card/run-card-session.js +6 -104
- package/dist/card/run-renderer.d.ts +7 -0
- package/dist/card/run-renderer.js +116 -238
- package/dist/card/run-state.d.ts +13 -0
- package/dist/card/run-state.js +25 -16
- package/dist/card/text-truncate.d.ts +13 -12
- package/dist/card/text-truncate.js +13 -63
- package/dist/common/fs.d.ts +8 -0
- package/dist/common/fs.js +17 -0
- package/dist/common/guards.d.ts +0 -20
- package/dist/common/guards.js +0 -43
- package/dist/common/sleep.d.ts +2 -0
- package/dist/common/sleep.js +5 -0
- package/dist/common/truncate.d.ts +39 -0
- package/dist/common/truncate.js +92 -0
- package/dist/config/codex-config.d.ts +1 -1
- package/dist/config/codex-config.js +1 -1
- package/dist/config/index.d.ts +112 -24
- package/dist/config/index.js +139 -37
- package/dist/connector/index.d.ts +69 -2
- package/dist/connector/index.js +107 -1
- package/dist/index.js +150 -118
- package/dist/instance-lock.js +4 -12
- package/dist/order/alias-resolve.d.ts +13 -0
- package/dist/order/alias-resolve.js +22 -0
- package/dist/order/alias-store.d.ts +38 -0
- package/dist/order/alias-store.js +106 -0
- package/dist/persistence/atomic-write.d.ts +8 -2
- package/dist/persistence/atomic-write.js +23 -7
- package/dist/restart.js +2 -1
- package/dist/router/card-action-payload.d.ts +30 -0
- package/dist/router/card-action-payload.js +39 -0
- package/dist/router/card-helpers.d.ts +63 -0
- package/dist/router/card-helpers.js +146 -0
- package/dist/router/config/claude.js +30 -1
- package/dist/router/config/codex.js +29 -50
- package/dist/router/config/common/model-patch.d.ts +22 -0
- package/dist/router/config/common/model-patch.js +26 -0
- package/dist/router/config/common/render.js +5 -4
- package/dist/router/config/dsh.d.ts +51 -0
- package/dist/router/config/dsh.js +179 -0
- package/dist/router/config/index.d.ts +12 -0
- package/dist/router/config/index.js +28 -1
- package/dist/router/config/kimi.d.ts +4 -1
- package/dist/router/config/kimi.js +27 -0
- package/dist/router/config/opencode.js +18 -5
- package/dist/router/config/pi.js +4 -5
- package/dist/router/config/types.d.ts +17 -2
- package/dist/router/index.d.ts +76 -22
- package/dist/router/index.js +479 -548
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +16 -2
- package/dist/runner/agent-choices-common.d.ts +16 -0
- package/dist/runner/agent-choices-common.js +34 -0
- package/dist/runner/claude/runner.d.ts +47 -10
- package/dist/runner/claude/runner.js +117 -46
- package/dist/runner/claude/session.d.ts +166 -0
- package/dist/runner/claude/session.js +768 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +66 -39
- package/dist/runner/codex/app-server/protocol-types.js +2 -13
- package/dist/runner/codex/app-server/runner.d.ts +52 -59
- package/dist/runner/codex/app-server/runner.js +216 -338
- package/dist/runner/codex/app-server/translator.d.ts +8 -21
- package/dist/runner/codex/app-server/translator.js +45 -18
- package/dist/runner/codex/index.d.ts +0 -2
- package/dist/runner/codex/index.js +0 -2
- package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
- package/dist/runner/common/acp/protocol-helpers.js +21 -0
- package/dist/runner/common/acp/protocol-types.d.ts +271 -0
- package/dist/runner/common/acp/protocol-types.js +77 -0
- package/dist/runner/common/connection-based-runner.d.ts +111 -0
- package/dist/runner/common/connection-based-runner.js +261 -0
- package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
- package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
- package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
- package/dist/runner/common/jsonrpc/types.d.ts +32 -0
- package/dist/runner/common/jsonrpc/types.js +9 -0
- package/dist/runner/common/process-stopper.d.ts +2 -1
- package/dist/runner/common/process-stopper.js +1 -0
- package/dist/runner/common/runner-utils.d.ts +6 -13
- package/dist/runner/common/runner-utils.js +6 -47
- package/dist/runner/common/spawning-runner.d.ts +24 -70
- package/dist/runner/common/spawning-runner.js +98 -152
- 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 +191 -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 +5 -4
- package/dist/runner/index.js +5 -4
- package/dist/runner/kimi/acp/runner.d.ts +106 -0
- package/dist/runner/kimi/acp/runner.js +516 -0
- package/dist/runner/kimi/acp/translator.d.ts +138 -0
- package/dist/runner/kimi/acp/translator.js +441 -0
- package/dist/runner/kimi/index.d.ts +1 -1
- package/dist/runner/kimi/index.js +1 -1
- package/dist/runner/opencode/acp/index.d.ts +1 -0
- package/dist/runner/opencode/acp/index.js +2 -0
- package/dist/runner/opencode/acp/runner.d.ts +121 -0
- package/dist/runner/opencode/acp/runner.js +429 -0
- package/dist/runner/opencode/acp/translator.d.ts +119 -0
- package/dist/runner/opencode/acp/translator.js +345 -0
- package/dist/runner/opencode/index.d.ts +1 -1
- package/dist/runner/opencode/index.js +1 -1
- package/dist/runner/pi/index.d.ts +1 -1
- package/dist/runner/pi/index.js +1 -1
- package/dist/runner/pi/rpc/client.d.ts +52 -0
- package/dist/runner/pi/rpc/client.js +114 -0
- package/dist/runner/pi/rpc/index.d.ts +4 -0
- package/dist/runner/pi/rpc/index.js +4 -0
- package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
- package/dist/runner/pi/rpc/protocol-types.js +21 -0
- package/dist/runner/pi/rpc/runner.d.ts +74 -0
- package/dist/runner/pi/rpc/runner.js +190 -0
- package/dist/runner/pi/rpc/translator.d.ts +55 -0
- package/dist/runner/pi/rpc/translator.js +258 -0
- 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/registry.d.ts +7 -8
- package/dist/runner/registry.js +3 -4
- package/dist/runner/resolve-agent-choices.js +18 -59
- package/dist/runner/sync-agent-choices.js +16 -67
- package/dist/runner/types.d.ts +97 -14
- package/dist/session/claude/session-index.js +2 -3
- package/dist/session/claude/session-reader.js +3 -6
- package/dist/session/claude/sessions.js +43 -28
- package/dist/session/codex/rollout-reader.d.ts +2 -2
- package/dist/session/codex/rollout-reader.js +14 -17
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +2 -2
- package/dist/session/common/content-blocks.d.ts +7 -0
- package/dist/session/common/content-blocks.js +13 -9
- package/dist/session/common/jsonl.d.ts +8 -25
- package/dist/session/common/jsonl.js +126 -261
- package/dist/session/common/pagination.d.ts +28 -0
- package/dist/session/common/pagination.js +33 -0
- package/dist/session/dsh/index.d.ts +1 -0
- package/dist/session/dsh/index.js +2 -0
- package/dist/session/dsh/sessions.d.ts +39 -0
- package/dist/session/dsh/sessions.js +237 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/kimi/sessions.d.ts +34 -1
- package/dist/session/kimi/sessions.js +208 -98
- package/dist/session/opencode/sessions.d.ts +17 -3
- package/dist/session/opencode/sessions.js +29 -29
- package/dist/session/pi/sessions.d.ts +5 -1
- package/dist/session/pi/sessions.js +12 -16
- package/dist/session/session-store.d.ts +11 -1
- package/dist/session/session-store.js +75 -178
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/install.d.ts +20 -2
- package/dist/update/install.js +51 -7
- package/dist/workspace/index.d.ts +4 -0
- package/dist/workspace/index.js +8 -1
- package/package.json +2 -1
- package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
- package/dist/runner/codex/argv.d.ts +0 -33
- package/dist/runner/codex/argv.js +0 -40
- package/dist/runner/codex/factory.d.ts +0 -29
- package/dist/runner/codex/factory.js +0 -39
- package/dist/runner/codex/jsonl.d.ts +0 -37
- package/dist/runner/codex/jsonl.js +0 -221
- package/dist/runner/codex/runner.d.ts +0 -66
- package/dist/runner/codex/runner.js +0 -129
- package/dist/runner/common/exec-translator.d.ts +0 -73
- package/dist/runner/common/exec-translator.js +0 -105
- package/dist/runner/kimi/jsonl.d.ts +0 -28
- package/dist/runner/kimi/jsonl.js +0 -143
- package/dist/runner/kimi/runner.d.ts +0 -49
- package/dist/runner/kimi/runner.js +0 -105
- package/dist/runner/opencode/argv.d.ts +0 -32
- package/dist/runner/opencode/argv.js +0 -35
- package/dist/runner/opencode/jsonl.d.ts +0 -68
- package/dist/runner/opencode/jsonl.js +0 -295
- package/dist/runner/opencode/runner.d.ts +0 -61
- package/dist/runner/opencode/runner.js +0 -93
- package/dist/runner/pi/jsonl.d.ts +0 -128
- package/dist/runner/pi/jsonl.js +0 -249
- package/dist/runner/pi/runner.d.ts +0 -36
- package/dist/runner/pi/runner.js +0 -76
- package/dist/session/common/catch-up.d.ts +0 -12
- package/dist/session/common/catch-up.js +0 -31
package/dist/connector/index.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { createLarkChannel, } from '@larksuite/channel';
|
|
2
2
|
import { getLogger } from '../logger/index.js';
|
|
3
3
|
import { MAX_FILE_UPLOAD_SIZE } from './file-limits.js';
|
|
4
|
+
import { DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB } from '../config/index.js';
|
|
5
|
+
import { sleep } from '../common/sleep.js';
|
|
4
6
|
import axios from 'axios';
|
|
5
7
|
import fs from 'node:fs';
|
|
8
|
+
import { silentlyUnlink } from '../common/fs.js';
|
|
6
9
|
import https from 'node:https';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
7
13
|
import FormData from 'form-data';
|
|
8
14
|
/**
|
|
9
15
|
* Dedicated https.Agent with keepAlive disabled for sendFile's three axios
|
|
@@ -36,6 +42,20 @@ const noKeepAliveAgent = new https.Agent({ keepAlive: false });
|
|
|
36
42
|
* 详见 design.md §9.8。原地更新逻辑新增按钮时参考该节去重风险分类。
|
|
37
43
|
*/
|
|
38
44
|
export const DEDUP_TTL_MS = 300;
|
|
45
|
+
/** 单资源下载超时(毫秒):SDK 无内置超时,挂起会导致提示丢失 + 残留临时文件。 */
|
|
46
|
+
const RESOURCE_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
|
|
47
|
+
function withTimeout(promise, ms, label) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
50
|
+
promise.then((value) => {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
resolve(value);
|
|
53
|
+
}, (err) => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
reject(err);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
39
59
|
/**
|
|
40
60
|
* 判断 send 失败是否值得重试一次(§9.5 限流重试口径,§P1-3 修正)。
|
|
41
61
|
*
|
|
@@ -75,9 +95,11 @@ function tryGetPatchService(channel) {
|
|
|
75
95
|
return c?.im?.v1?.message;
|
|
76
96
|
}
|
|
77
97
|
export class FeishuConnector {
|
|
98
|
+
/** 飞书 channel 实例(public:测试直接访问/注入 mock,替代 as unknown as)。 */
|
|
78
99
|
channel;
|
|
79
100
|
onMessage;
|
|
80
101
|
onCardAction;
|
|
102
|
+
onInboundMediaDetected;
|
|
81
103
|
isConnected = false;
|
|
82
104
|
appId;
|
|
83
105
|
appSecret;
|
|
@@ -125,6 +147,15 @@ export class FeishuConnector {
|
|
|
125
147
|
this.channel.on('message', (msg) => {
|
|
126
148
|
if (msg.chatType !== 'p2p')
|
|
127
149
|
return;
|
|
150
|
+
// 入站图片/文件:不转发文本内容(图片消息的 content 只是占位文案),
|
|
151
|
+
// 而是下载资源后交给 bridge 落盘(runner 零改动)。
|
|
152
|
+
if ((msg.rawContentType === 'image' || msg.rawContentType === 'file') &&
|
|
153
|
+
msg.resources.length > 0) {
|
|
154
|
+
// 只上报"媒体到达",不在此下载:owner/配置闸门在 index.ts 里先执行,
|
|
155
|
+
// 通过后才调用 downloadInboundMedia(避免未认证大文件下载打爆内存)。
|
|
156
|
+
void this.handleInboundMediaDetected(msg);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
128
159
|
this.onMessage?.({
|
|
129
160
|
userId: msg.senderId,
|
|
130
161
|
messageId: msg.messageId,
|
|
@@ -158,6 +189,81 @@ export class FeishuConnector {
|
|
|
158
189
|
setCardActionHandler(handler) {
|
|
159
190
|
this.onCardAction = handler;
|
|
160
191
|
}
|
|
192
|
+
setInboundMediaDetectedHandler(handler) {
|
|
193
|
+
this.onInboundMediaDetected = handler;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 上报媒体消息到达(轻量,不含下载内容)。上层认证/配置闸门通过后
|
|
197
|
+
* 再调 downloadInboundMedia。
|
|
198
|
+
*/
|
|
199
|
+
async handleInboundMediaDetected(msg) {
|
|
200
|
+
if (!this.onInboundMediaDetected) {
|
|
201
|
+
getLogger().warn('[feishu] inbound media received but no detected handler registered');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
await this.onInboundMediaDetected({
|
|
206
|
+
userId: msg.senderId,
|
|
207
|
+
chatId: msg.chatId,
|
|
208
|
+
messageId: msg.messageId,
|
|
209
|
+
replyToMessageId: msg.replyToMessageId,
|
|
210
|
+
resources: msg.resources.map((r) => ({
|
|
211
|
+
type: r.type === 'image' ? 'image' : 'file',
|
|
212
|
+
fileKey: r.fileKey,
|
|
213
|
+
fileName: r.fileName,
|
|
214
|
+
})),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
getLogger().error('[feishu] inbound media detected handler failed:', err.message);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 下载消息里的全部资源(流式写临时文件,避免大文件全量进堆内存)。
|
|
223
|
+
* 逐资源 try/catch:单个失败不影响其他资源;超限项不进入 media(不落盘)。
|
|
224
|
+
* 下载失败/超限都随 failures 交给 bridge 提示用户(对齐"发送文件失败"文案风格)。
|
|
225
|
+
* 调用方必须已通过 owner/配置闸门(index.ts);maxFileSizeMb 由调用方从
|
|
226
|
+
* 当前配置传入(避免 connector 持有过期的 config 快照)。
|
|
227
|
+
*/
|
|
228
|
+
async downloadInboundMedia(msg, opts) {
|
|
229
|
+
const maxBytes = (opts?.maxFileSizeMb ?? DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB) * 1024 * 1024;
|
|
230
|
+
const timeoutMs = opts?.downloadTimeoutMs ?? RESOURCE_DOWNLOAD_TIMEOUT_MS;
|
|
231
|
+
const media = [];
|
|
232
|
+
const failures = [];
|
|
233
|
+
for (const res of msg.resources) {
|
|
234
|
+
const tmpPath = path.join(os.tmpdir(), `lark-remote-inbound-${randomUUID()}`);
|
|
235
|
+
try {
|
|
236
|
+
const { contentType, bytesWritten } = await withTimeout(this.channel.downloadResourceToFile(msg.messageId, res.fileKey, res.type, tmpPath), timeoutMs, `downloadResource fileKey=${res.fileKey}`);
|
|
237
|
+
if (bytesWritten > maxBytes) {
|
|
238
|
+
silentlyUnlink(tmpPath);
|
|
239
|
+
failures.push({
|
|
240
|
+
fileName: res.fileName,
|
|
241
|
+
reason: `超过 ${maxBytes / (1024 * 1024)}MB 大小限制`,
|
|
242
|
+
});
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
media.push({
|
|
246
|
+
type: res.type,
|
|
247
|
+
fileName: res.fileName,
|
|
248
|
+
mimeType: contentType,
|
|
249
|
+
tempPath: tmpPath,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
silentlyUnlink(tmpPath);
|
|
254
|
+
getLogger().warn(`[feishu] downloadResource failed fileKey=${res.fileKey} type=${res.type}:`, err.message);
|
|
255
|
+
failures.push({ fileName: res.fileName, reason: `下载失败: ${err.message}` });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
userId: msg.userId,
|
|
260
|
+
chatId: msg.chatId,
|
|
261
|
+
messageId: msg.messageId,
|
|
262
|
+
replyToMessageId: msg.replyToMessageId,
|
|
263
|
+
media,
|
|
264
|
+
failures,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
161
267
|
async connect() {
|
|
162
268
|
try {
|
|
163
269
|
await this.channel.connect();
|
|
@@ -200,7 +306,7 @@ export class FeishuConnector {
|
|
|
200
306
|
catch (err) {
|
|
201
307
|
if (shouldRetrySendError(err)) {
|
|
202
308
|
getLogger().warn('[feishu] rate limited, retrying in 200ms...');
|
|
203
|
-
await
|
|
309
|
+
await sleep(200);
|
|
204
310
|
try {
|
|
205
311
|
const result = await this.channel.send(chatId, input, { replyTo: opts?.replyTo });
|
|
206
312
|
return result.messageId;
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { loadConfig, getAgentConfig
|
|
1
|
+
import { loadConfig, getAgentConfig } from './config/index.js';
|
|
2
2
|
import { resolveAgentChoices, } from './runner/index.js';
|
|
3
3
|
import { ensureConfig } from './config/wizard.js';
|
|
4
4
|
import { parseCliArgs, resolveConfigDir, setConfigDir, printHelp, printVersion, } from './config/dir.js';
|
|
5
5
|
import { FeishuConnector } from './connector/index.js';
|
|
6
|
-
import { ClaudeRunner,
|
|
6
|
+
import { ClaudeRunner, CodexAppServerRunner, OpencodeAcpRunner, PiRpcRunner, KimiAcpRunner, DshRunner, } from './runner/index.js';
|
|
7
7
|
import { AgentRegistry } from './runner/registry.js';
|
|
8
8
|
import { probeAllAgents } from './runner/probe.js';
|
|
9
9
|
import { SessionReaderRegistry, SessionStore } from './session/index.js';
|
|
10
|
-
import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, } from './session/index.js';
|
|
11
|
-
import { CommandRouter, isImmediateAction
|
|
10
|
+
import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, DshSessionReader, } from './session/index.js';
|
|
11
|
+
import { CommandRouter, isImmediateAction } from './router/index.js';
|
|
12
12
|
import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
|
|
13
|
+
import { buildCardActionFullValue } from './router/card-action-payload.js';
|
|
13
14
|
import { Bridge } from './bridge/index.js';
|
|
14
15
|
import { initLogger, getLogger } from './logger/index.js';
|
|
15
16
|
import { StartupContactStore, sendStartupHello } from './startup-contact.js';
|
|
@@ -19,11 +20,11 @@ import { spawnReplacementBridge, waitForPreviousInstance } from './restart.js';
|
|
|
19
20
|
import { checkLatestVersion, isNewer, runInstallLatest, formatUpdateHint } from './update/index.js';
|
|
20
21
|
import { classifyRejection } from './error-classification.js';
|
|
21
22
|
import { WorkspaceStore } from './workspace/index.js';
|
|
22
|
-
import {
|
|
23
|
-
import { sessionEventPanel } from './router/card-helpers.js';
|
|
23
|
+
import { buildSessionHistoryCard } from './router/card-helpers.js';
|
|
24
24
|
import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
|
|
25
25
|
import path from 'node:path';
|
|
26
26
|
import fs from 'node:fs';
|
|
27
|
+
import { silentlyUnlink } from './common/fs.js';
|
|
27
28
|
/** Send auto-resume card for a restored session */
|
|
28
29
|
async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false) {
|
|
29
30
|
if (!contact.userId)
|
|
@@ -31,68 +32,32 @@ async function sendAutoResumeCard(connector, contact, cwd, session, state, compa
|
|
|
31
32
|
const recipient = contact.chatId ?? contact.userId;
|
|
32
33
|
if (!recipient)
|
|
33
34
|
return;
|
|
34
|
-
// Build header with displayTitle (aiTitle or last user message) and recap
|
|
35
|
-
let header = `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`;
|
|
36
|
-
const sections = [];
|
|
37
|
-
if (state.displayTitle) {
|
|
38
|
-
const label = state.aiTitle ? 'AI 标题' : '最近输入';
|
|
39
|
-
sections.push(`🏷️ **${label}**\n${state.displayTitle}`);
|
|
40
|
-
}
|
|
41
|
-
if (state.recap) {
|
|
42
|
-
const recapPreview = state.recap.length > 200 ? state.recap.slice(0, 197) + '...' : state.recap;
|
|
43
|
-
sections.push(`📝 **Recap**\n${recapPreview}`);
|
|
44
|
-
}
|
|
45
|
-
if (sections.length > 0) {
|
|
46
|
-
header += '\n\n' + sections.join('\n\n──\n\n');
|
|
47
|
-
}
|
|
48
|
-
const elements = [markdownDiv(header), { tag: 'hr' }];
|
|
49
35
|
// Limit history events to prevent card size exceeded error (11310)
|
|
50
36
|
const MAX_HISTORY_EVENTS = 5;
|
|
51
37
|
const eventsToShow = state.events.slice(-MAX_HISTORY_EVENTS);
|
|
52
38
|
const hiddenCount = state.events.length - MAX_HISTORY_EVENTS;
|
|
53
|
-
//
|
|
54
|
-
if (hiddenCount > 0) {
|
|
55
|
-
elements.push(markdownDiv(`📜 还有 ${hiddenCount} 个更早的事件未显示`));
|
|
56
|
-
}
|
|
57
|
-
// Fold session history into collapsible panels: the last 2 events stay expanded
|
|
39
|
+
// Action buttons: Compact (runCompact-capable runner) + new session.
|
|
58
40
|
const agentKind = state.agentKind ?? 'claude';
|
|
59
|
-
eventsToShow.forEach((ev, i) => {
|
|
60
|
-
elements.push(sessionEventPanel(ev, i, eventsToShow.length, 2, agentKind));
|
|
61
|
-
});
|
|
62
|
-
// Add usage/stats at the end
|
|
63
|
-
if (state.usage) {
|
|
64
|
-
const usageStr = formatUsageStats(state.usage);
|
|
65
|
-
elements.push(markdownDiv(usageStr));
|
|
66
|
-
}
|
|
67
|
-
// Add action buttons - use column_set+column (div+elements not supported in 2.0)
|
|
68
41
|
const actionButtons = [];
|
|
69
|
-
// Compact 按钮:仅 codex app-server 模式(runner 有 runCompact)且未在跑。
|
|
70
|
-
// 启动自动恢复时不会存在该会话的 activeRun,直接渲染。
|
|
71
42
|
if (compactSupported) {
|
|
72
43
|
actionButtons.push(resumeCompactButton(session.sessionId, state.agentKind ?? 'codex'));
|
|
73
44
|
}
|
|
74
45
|
actionButtons.push(newSessionButton());
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
46
|
+
const card = buildSessionHistoryCard({
|
|
47
|
+
sessionId: session.sessionId,
|
|
48
|
+
cwd,
|
|
49
|
+
displayTitle: state.displayTitle,
|
|
50
|
+
aiTitle: state.aiTitle,
|
|
51
|
+
recap: state.recap,
|
|
52
|
+
events: eventsToShow,
|
|
53
|
+
usage: state.usage,
|
|
54
|
+
}, {
|
|
55
|
+
agentKind,
|
|
56
|
+
headerText: `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`,
|
|
57
|
+
title: `🔁 自动恢复会话 · ${agentDisplayName(agentKind)}`,
|
|
58
|
+
hiddenCount,
|
|
59
|
+
actions: actionButtons,
|
|
82
60
|
});
|
|
83
|
-
const card = {
|
|
84
|
-
schema: '2.0',
|
|
85
|
-
config: {
|
|
86
|
-
wide_screen_mode: true,
|
|
87
|
-
},
|
|
88
|
-
header: {
|
|
89
|
-
title: {
|
|
90
|
-
tag: 'plain_text',
|
|
91
|
-
content: `🔁 自动恢复会话 · ${agentDisplayName(agentKind)}`,
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
body: { elements },
|
|
95
|
-
};
|
|
96
61
|
await connector.sendWithRetry(recipient, { card });
|
|
97
62
|
}
|
|
98
63
|
/** Initialize CLI args, config, and logger. Returns { config, configDir, logger, instanceLock }. */
|
|
@@ -165,37 +130,36 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
165
130
|
settings: cliArgs.settings,
|
|
166
131
|
pidDir: configDir,
|
|
167
132
|
workspace: ws,
|
|
133
|
+
permissionMode: claudeConfig.permissionMode,
|
|
134
|
+
idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
|
|
168
135
|
});
|
|
169
136
|
});
|
|
170
|
-
// Register Codex runner.
|
|
171
|
-
// so runtime config changes (
|
|
172
|
-
// reasoningEffort, etc.) take effect after bridge.setConfig()
|
|
173
|
-
//
|
|
174
|
-
// approval + sandbox); otherwise CodexExecRunner (codex exec --json).
|
|
137
|
+
// Register Codex runner (app-server only). The registry factory reads latest
|
|
138
|
+
// config from container.current so runtime config changes (model, sandbox,
|
|
139
|
+
// approvalPolicy, reasoningEffort, etc.) take effect after bridge.setConfig()
|
|
140
|
+
// + clearRunners().
|
|
175
141
|
const codexSessionReader = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
|
|
176
|
-
agentRegistry.register('codex', (
|
|
142
|
+
agentRegistry.register('codex', (_ws) => {
|
|
177
143
|
const container = agentRegistry.getConfigContainer();
|
|
178
144
|
const latestConfig = container?.current ?? config;
|
|
179
145
|
const codexConfig = getAgentConfig(latestConfig, 'codex');
|
|
180
|
-
return
|
|
146
|
+
return new CodexAppServerRunner({
|
|
147
|
+
kind: 'codex',
|
|
181
148
|
model: codexConfig?.model,
|
|
182
149
|
modelProvider: codexConfig?.modelProvider,
|
|
183
150
|
reasoningEffort: codexConfig?.reasoningEffort,
|
|
184
|
-
stopGraceMs: codexConfig?.stopGraceMs ?? latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
|
|
185
|
-
pidDir: configDir,
|
|
186
|
-
workspace: ws,
|
|
187
151
|
sessionReader: codexSessionReader,
|
|
188
|
-
serviceMode: codexConfig?.serviceMode,
|
|
189
152
|
sandbox: codexConfig?.sandbox,
|
|
190
153
|
approvalPolicy: codexConfig?.approvalPolicy,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
154
|
+
binary: codexConfig?.appServer?.binary,
|
|
155
|
+
requestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
|
|
156
|
+
idleTtlMs: codexConfig?.appServer?.idleTtlMs,
|
|
157
|
+
turnTimeoutMs: codexConfig?.appServer?.turnIdleTimeoutMinutes != null
|
|
158
|
+
? codexConfig.appServer.turnIdleTimeoutMinutes * 60_000
|
|
159
|
+
: undefined,
|
|
195
160
|
});
|
|
196
161
|
});
|
|
197
|
-
// Register
|
|
198
|
-
// Using `opencode run --format json --auto` instead of HTTP+SSE server mode
|
|
162
|
+
// Register OpencodeAcpRunner (pure ACP mode, `opencode acp` JSON-RPC over stdio)
|
|
199
163
|
// Register session reader (same instance as used by runner)
|
|
200
164
|
const sessionReaderRegistry = new SessionReaderRegistry();
|
|
201
165
|
sessionReaderRegistry.register('claude', new ClaudeSessionReader());
|
|
@@ -203,7 +167,7 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
203
167
|
// Register OpencodeSessionReader (CLI version)
|
|
204
168
|
// Using `opencode session list` and `opencode export` instead of HTTP API
|
|
205
169
|
const opencodeSessionReader = new OpencodeSessionReader();
|
|
206
|
-
agentRegistry.register('opencode', (
|
|
170
|
+
agentRegistry.register('opencode', (_ws) => {
|
|
207
171
|
// Get latest config from container (set below for pi)
|
|
208
172
|
const container = agentRegistry.getConfigContainer();
|
|
209
173
|
const latestConfig = container?.current ?? config;
|
|
@@ -220,17 +184,15 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
220
184
|
model = `${provider}/${ocConfig.modelID}`;
|
|
221
185
|
}
|
|
222
186
|
}
|
|
223
|
-
return new
|
|
187
|
+
return new OpencodeAcpRunner({
|
|
188
|
+
kind: 'opencode',
|
|
224
189
|
model,
|
|
225
|
-
// P1-15: read stopGraceMs from latestConfig (not startup closure)
|
|
226
|
-
stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
|
|
227
|
-
pidDir: configDir,
|
|
228
|
-
workspace: ws,
|
|
229
190
|
sessionReader: opencodeSessionReader,
|
|
191
|
+
mode: ocConfig?.mode ?? 'build',
|
|
230
192
|
});
|
|
231
193
|
});
|
|
232
194
|
sessionReaderRegistry.register('opencode', opencodeSessionReader);
|
|
233
|
-
// Register
|
|
195
|
+
// Register PiRpcRunner and PiSessionReader
|
|
234
196
|
// 2026-07-12: 修复 config.save 后 pi provider 不生效的问题
|
|
235
197
|
// 关键:factory 闭包必须能访问到最新的 config,而不是启动时的快照
|
|
236
198
|
// 用可变容器存储 config 引用,registry 持有容器引用,factory 从容器读取最新值
|
|
@@ -242,7 +204,7 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
242
204
|
const container = agentRegistry.getConfigContainer();
|
|
243
205
|
const latestConfig = container?.current ?? config;
|
|
244
206
|
const piConf = getAgentConfig(latestConfig, 'pi');
|
|
245
|
-
return new
|
|
207
|
+
return new PiRpcRunner({
|
|
246
208
|
provider: piConf?.provider ?? 'Volcano',
|
|
247
209
|
model: piConf?.model ?? 'glm-5.2',
|
|
248
210
|
thinking: piConf?.thinking ?? 'medium',
|
|
@@ -250,36 +212,60 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
250
212
|
.split(',')
|
|
251
213
|
.map((t) => t.trim())
|
|
252
214
|
.filter(Boolean),
|
|
253
|
-
pidDir: configDir,
|
|
254
215
|
workspace: ws,
|
|
255
216
|
sessionReader: piSessionReader,
|
|
256
217
|
});
|
|
257
218
|
});
|
|
258
219
|
sessionReaderRegistry.register('pi', piSessionReader);
|
|
259
|
-
// Register
|
|
220
|
+
// Register KimiAcpRunner (pure ACP mode) and KimiSessionReader
|
|
260
221
|
const kimiSessionReader = new KimiSessionReader();
|
|
261
|
-
agentRegistry.register('kimi', (
|
|
222
|
+
agentRegistry.register('kimi', (_ws) => {
|
|
262
223
|
// 每次 factory 调用时从 registry 获取最新 config
|
|
263
224
|
const container = agentRegistry.getConfigContainer();
|
|
264
225
|
const latestConfig = container?.current ?? config;
|
|
265
226
|
const kimiConf = getAgentConfig(latestConfig, 'kimi');
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// P1-15: read stopGraceMs from latestConfig (not startup closure)
|
|
270
|
-
stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
|
|
271
|
-
pidDir: configDir,
|
|
272
|
-
workspace: ws,
|
|
227
|
+
const acpConf = kimiConf?.acp;
|
|
228
|
+
return new KimiAcpRunner({
|
|
229
|
+
kind: 'kimi',
|
|
273
230
|
sessionReader: kimiSessionReader,
|
|
231
|
+
binary: acpConf?.binary ?? 'kimi',
|
|
232
|
+
requestTimeoutMs: acpConf?.requestTimeoutMs,
|
|
233
|
+
idleTtlMs: acpConf?.idleTtlMs,
|
|
234
|
+
turnIdleTimeoutMs: acpConf?.turnIdleTimeoutMinutes != null
|
|
235
|
+
? acpConf.turnIdleTimeoutMinutes * 60_000
|
|
236
|
+
: undefined,
|
|
237
|
+
model: kimiConf?.model ?? 'kimi-code/k3',
|
|
238
|
+
permissionMode: kimiConf?.permissionMode ?? 'manual',
|
|
274
239
|
});
|
|
275
240
|
});
|
|
276
241
|
sessionReaderRegistry.register('kimi', kimiSessionReader);
|
|
242
|
+
// Register DshRunner (HTTP-only DSH Web Host agent) and DshSessionReader.
|
|
243
|
+
const dshSessionReader = new DshSessionReader({
|
|
244
|
+
host: getAgentConfig(config, 'dsh')?.host,
|
|
245
|
+
});
|
|
246
|
+
agentRegistry.register('dsh', (_ws) => {
|
|
247
|
+
// Read latest config from container so /config host changes take effect
|
|
248
|
+
// after bridge.setConfig() + clearRunners().
|
|
249
|
+
const container = agentRegistry.getConfigContainer();
|
|
250
|
+
const latestConfig = container?.current ?? config;
|
|
251
|
+
const dshConf = getAgentConfig(latestConfig, 'dsh');
|
|
252
|
+
return new DshRunner({
|
|
253
|
+
kind: 'dsh',
|
|
254
|
+
sessionReader: dshSessionReader,
|
|
255
|
+
host: dshConf?.host,
|
|
256
|
+
agentPreset: dshConf?.agentPreset,
|
|
257
|
+
model: dshConf?.model,
|
|
258
|
+
reasoningEffort: dshConf?.reasoningEffort,
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
sessionReaderRegistry.register('dsh', dshSessionReader);
|
|
277
262
|
// 2026-07-17: 注册所有 agent 的显示名,实现单点真相
|
|
278
263
|
agentRegistry.registerDisplayName('claude', 'Claude');
|
|
279
264
|
agentRegistry.registerDisplayName('codex', 'Codex');
|
|
280
265
|
agentRegistry.registerDisplayName('opencode', 'Opencode');
|
|
281
266
|
agentRegistry.registerDisplayName('pi', 'Pi');
|
|
282
267
|
agentRegistry.registerDisplayName('kimi', 'Kimi');
|
|
268
|
+
agentRegistry.registerDisplayName('dsh', 'DSH');
|
|
283
269
|
// 设置全局 registry,供 agentDisplayName 等全局函数使用
|
|
284
270
|
AgentRegistry.setGlobalInstance(agentRegistry);
|
|
285
271
|
// Probe agent CLI availability (fire-and-forget, populates cache for /config card).
|
|
@@ -301,6 +287,48 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
301
287
|
}
|
|
302
288
|
/** Setup message and card action handlers. */
|
|
303
289
|
function setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger, config) {
|
|
290
|
+
// 入站媒体(图片/文件):先认证后下载(P1 review 修复)。
|
|
291
|
+
// connector 只上报"媒体到达",这里先过 owner + enabled 闸门,通过后才
|
|
292
|
+
// 下载——未认证/关闭配置时不会发生任何网络下载或内存/磁盘占用。
|
|
293
|
+
connector.setInboundMediaDetectedHandler((msg) => {
|
|
294
|
+
if (!binder.isOwner(msg.userId)) {
|
|
295
|
+
logger.warn(`[media] rejected inbound media from non-owner ${msg.userId}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// 读 router.config(活引用):/config 保存后 setConfigValues 返回新对象,
|
|
299
|
+
// 启动时捕获的 config 参数会过期(P2 review 修复)。
|
|
300
|
+
if (!router.config.inboundMedia.enabled) {
|
|
301
|
+
// 不静默(P3 review):关闭时给 owner 明确反馈,避免发图后无任何反应。
|
|
302
|
+
// 不下载、不落盘,只回一条说明。
|
|
303
|
+
void bridge
|
|
304
|
+
.sendResult({
|
|
305
|
+
text: '⚠️ 入站媒体保存已关闭(inboundMedia.enabled: false),未保存文件。' +
|
|
306
|
+
'如需自动保存图片/文件,请开启后重试',
|
|
307
|
+
}, {
|
|
308
|
+
userId: msg.userId,
|
|
309
|
+
chatId: msg.chatId,
|
|
310
|
+
messageId: msg.messageId,
|
|
311
|
+
})
|
|
312
|
+
.catch((err) => logger.error('[media] disabled feedback send failed:', err));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
void (async () => {
|
|
316
|
+
const payload = await connector.downloadInboundMedia(msg, {
|
|
317
|
+
maxFileSizeMb: router.config.inboundMedia.maxFileSizeMb,
|
|
318
|
+
});
|
|
319
|
+
try {
|
|
320
|
+
await bridge.onInboundMedia(payload);
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
// handle() 的正常路径会清理临时文件;这里兜底 handle 进入 mkdir
|
|
324
|
+
// try 之前意外抛错的情况,避免 os.tmpdir 残留。
|
|
325
|
+
for (const item of payload.media) {
|
|
326
|
+
silentlyUnlink(item.tempPath);
|
|
327
|
+
}
|
|
328
|
+
throw err;
|
|
329
|
+
}
|
|
330
|
+
})().catch((err) => logger.error('[media] inbound media download/save failed:', err));
|
|
331
|
+
});
|
|
304
332
|
connector.setMessageHandler((msg) => {
|
|
305
333
|
// 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时要求 PIN 认领
|
|
306
334
|
const decision = binder.classify(msg.userId, msg.content, msg.chatId);
|
|
@@ -358,9 +386,16 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
358
386
|
}
|
|
359
387
|
// decision.kind === 'owner':正常处理(不再每条覆盖 startup-contact)
|
|
360
388
|
logger.info(`message from ${msg.userId}: ${msg.content.slice(0, 100)}`);
|
|
389
|
+
// 文本到达先冲刷待合批的媒体保存提示(先图后文字时文字到达
|
|
390
|
+
// 先冲刷批次,避免合批提示被后续消息淹没/丢图)。
|
|
391
|
+
bridge.flushMediaNotifications(msg.userId, msg.chatId);
|
|
361
392
|
// Add Typing reaction to indicate "still alive"
|
|
362
393
|
void connector.addReaction(msg.messageId, 'Typing');
|
|
363
|
-
|
|
394
|
+
// 别名展开:命令分发前对消息做一次 $name 展开(不递归)。
|
|
395
|
+
// `!` / `/` 开头的消息不会进入展开路径($PATH、$HOME 等 shell 变量不受影响),
|
|
396
|
+
// 未知 $xxx 原样透传;展开结果若以 `/` 开头会自然落入命令路径。
|
|
397
|
+
const content = router.expandAliasMessage(msg.content);
|
|
398
|
+
const trimmedLower = content.trim().toLowerCase();
|
|
364
399
|
if (trimmedLower === '/stop' || trimmedLower === '/t') {
|
|
365
400
|
void (async () => {
|
|
366
401
|
const stopped = await bridge.interruptCurrentRun({
|
|
@@ -377,9 +412,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
377
412
|
})().catch((err) => logger.error('[control] /stop failed:', err));
|
|
378
413
|
return;
|
|
379
414
|
}
|
|
380
|
-
if (
|
|
415
|
+
if (content.trim().startsWith('/')) {
|
|
381
416
|
void router
|
|
382
|
-
.handle(
|
|
417
|
+
.handle(content, {
|
|
383
418
|
userId: msg.userId,
|
|
384
419
|
chatId: msg.chatId,
|
|
385
420
|
messageId: msg.messageId,
|
|
@@ -392,9 +427,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
392
427
|
// in the same workspace (design.md §9.6: the queue exists to prevent
|
|
393
428
|
// concurrent claude runs, not to serialize bash). router.handle dispatches
|
|
394
429
|
// `!` to bridge.executeBash, which tracks the run independently.
|
|
395
|
-
if (
|
|
430
|
+
if (content.trim().startsWith('!')) {
|
|
396
431
|
void router
|
|
397
|
-
.handle(
|
|
432
|
+
.handle(content, {
|
|
398
433
|
userId: msg.userId,
|
|
399
434
|
chatId: msg.chatId,
|
|
400
435
|
messageId: msg.messageId,
|
|
@@ -418,7 +453,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
418
453
|
// 捕获点 Bridge.currentBinding。
|
|
419
454
|
const binding = bridge.currentBinding(msg.userId);
|
|
420
455
|
bridge.enqueue(workspace, async () => {
|
|
421
|
-
await router.handle(
|
|
456
|
+
await router.handle(content, {
|
|
422
457
|
userId: msg.userId,
|
|
423
458
|
chatId: msg.chatId,
|
|
424
459
|
messageId: msg.messageId,
|
|
@@ -433,7 +468,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
433
468
|
userId: msg.userId,
|
|
434
469
|
chatId: msg.chatId,
|
|
435
470
|
messageId: msg.messageId,
|
|
436
|
-
messagePreview:
|
|
471
|
+
messagePreview: content.slice(0, 3000),
|
|
437
472
|
// D3/Step4: binding 随 taskMeta 存进 QueuedTask,供替换闭包
|
|
438
473
|
// (queue.edit/queue.immediate)复用,不重新快照。
|
|
439
474
|
binding,
|
|
@@ -480,27 +515,24 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
480
515
|
}
|
|
481
516
|
const isImmediate = isImmediateAction(actionValue.cmd);
|
|
482
517
|
const workspace = sessionStore.getCwd(userId) ?? '';
|
|
483
|
-
// Build full value object — spread all fields from actionValue,
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
const fullValue =
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
inputValue: action.raw?.action?.input_value ??
|
|
493
|
-
action.action.input_value,
|
|
494
|
-
};
|
|
495
|
-
// queue.input / config.save / approval.respond / approval.toggle 返回
|
|
496
|
-
// CardActionResponse toast 给点击用户即时反馈。必须直接返回值 --
|
|
497
|
-
// enqueueImmediate / enqueueConfigAction 是 fire-and-forget,会吞掉返回值。
|
|
518
|
+
// Build full value object — spread all fields from actionValue, then merge
|
|
519
|
+
// component out-of-band fields (option/formValue/inputValue/options) where
|
|
520
|
+
// present. See card-action-payload.ts for the component-type contract.
|
|
521
|
+
const fullValue = buildCardActionFullValue(actionValue, action);
|
|
522
|
+
// queue.input / config.save / approval.respond / approval.toggle /
|
|
523
|
+
// approval.answer 系列返回 CardActionResponse toast 给点击用户即时反馈。
|
|
524
|
+
// 必须直接返回值 -- enqueueImmediate / enqueueConfigAction 是
|
|
525
|
+
// fire-and-forget,会吞掉返回值(2026-08-17 review:answer 家族曾漏在
|
|
526
|
+
// 直返列表外,过期/重复 nonce/非法选项等错误 toast 被静默吞掉)。
|
|
498
527
|
// 审批响应尤其不能落串行队列:run 任务占用队列头直到 turn 结束,审批响应
|
|
499
528
|
// 排在后面会形成死锁(run 不结束不执行、run 结束 coordinator 已删响应空转)。
|
|
500
529
|
if (actionValue.cmd === 'queue.input' ||
|
|
501
530
|
actionValue.cmd === 'config.save' ||
|
|
502
531
|
actionValue.cmd === 'approval.respond' ||
|
|
503
|
-
actionValue.cmd === 'approval.toggle'
|
|
532
|
+
actionValue.cmd === 'approval.toggle' ||
|
|
533
|
+
actionValue.cmd === 'approval.answer' ||
|
|
534
|
+
actionValue.cmd === 'approval.answerSubmit' ||
|
|
535
|
+
actionValue.cmd === 'approval.answerCustom') {
|
|
504
536
|
return router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
505
537
|
}
|
|
506
538
|
// order.exec → equivalent queued message (Plan A): resolve the order text
|
|
@@ -618,6 +650,7 @@ async function main() {
|
|
|
618
650
|
configPath: path.join(configDir, 'config.yaml'),
|
|
619
651
|
workspacePath: path.join(configDir, 'workspace.json'),
|
|
620
652
|
ordersPath: path.join(configDir, 'orders.json'),
|
|
653
|
+
aliasesPath: path.join(configDir, 'aliases.json'),
|
|
621
654
|
restartSpawner: () => spawnReplacementBridge(path.join(configDir, 'logs')),
|
|
622
655
|
sessionReaderRegistry,
|
|
623
656
|
devMode: cliArgs.dev,
|
|
@@ -683,9 +716,8 @@ async function main() {
|
|
|
683
716
|
displayTitle: content.displayTitle,
|
|
684
717
|
agentKind: config.defaultAgent,
|
|
685
718
|
},
|
|
686
|
-
// Compact
|
|
687
|
-
config.defaultAgent
|
|
688
|
-
getAgentConfig(config, 'codex')?.serviceMode === 'app-server');
|
|
719
|
+
// Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
|
|
720
|
+
bridge.hasRunCompact(restoredCwd, config.defaultAgent));
|
|
689
721
|
}
|
|
690
722
|
catch (err) {
|
|
691
723
|
getLogger().warn('[startup] sendAutoResumeCard failed:', err);
|
package/dist/instance-lock.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { silentlyUnlink } from './common/fs.js';
|
|
4
5
|
export class InstanceAlreadyRunningError extends Error {
|
|
5
6
|
pid;
|
|
6
7
|
lockPath;
|
|
@@ -68,10 +69,7 @@ export class InstanceLock {
|
|
|
68
69
|
throw new InstanceAlreadyRunningError(pid, this.lockPath);
|
|
69
70
|
}
|
|
70
71
|
// Stale lock: remove and retry once
|
|
71
|
-
|
|
72
|
-
fs.unlinkSync(this.lockPath);
|
|
73
|
-
}
|
|
74
|
-
catch { }
|
|
72
|
+
silentlyUnlink(this.lockPath);
|
|
75
73
|
try {
|
|
76
74
|
const fd = fs.openSync(this.lockPath, 'wx');
|
|
77
75
|
fs.writeSync(fd, this.lockContents(process.pid));
|
|
@@ -99,10 +97,7 @@ export class InstanceLock {
|
|
|
99
97
|
release() {
|
|
100
98
|
const { pid } = this.readPidAndComm();
|
|
101
99
|
if (pid === process.pid) {
|
|
102
|
-
|
|
103
|
-
fs.unlinkSync(this.lockPath);
|
|
104
|
-
}
|
|
105
|
-
catch { }
|
|
100
|
+
silentlyUnlink(this.lockPath);
|
|
106
101
|
}
|
|
107
102
|
}
|
|
108
103
|
registerExitHandlers() {
|
|
@@ -142,10 +137,7 @@ export class InstanceLock {
|
|
|
142
137
|
if (Number.isInteger(pid) && pid > 0 && comm)
|
|
143
138
|
return { pid, comm };
|
|
144
139
|
// Malformed or missing comm — treat as corrupt, unlink and return undefined
|
|
145
|
-
|
|
146
|
-
fs.unlinkSync(this.lockPath);
|
|
147
|
-
}
|
|
148
|
-
catch { }
|
|
140
|
+
silentlyUnlink(this.lockPath);
|
|
149
141
|
return { pid: undefined, comm: undefined };
|
|
150
142
|
}
|
|
151
143
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AliasEntry } from './alias-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* 一次别名展开。
|
|
4
|
+
*
|
|
5
|
+
* - 仅匹配消息开头的 `$name`(`^$name`),`$name` 后接空格或行尾,全词精确匹配;
|
|
6
|
+
* - 名称必须 `[A-Za-z_][A-Za-z0-9_]*`(数字开头的 `$500` 永不匹配);
|
|
7
|
+
* - 未知别名返回 undefined(原样透传给 agent,不报错);
|
|
8
|
+
* - 单次不递归:展开结果不再二次展开,防死循环;
|
|
9
|
+
* - `!` / `/` 开头消息天然不匹配(正则要求 `$` 开头),bash 的 `$PATH` 不受影响。
|
|
10
|
+
*
|
|
11
|
+
* @returns 展开后的完整消息;无匹配时返回 undefined。
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveAlias(message: string, aliases: readonly AliasEntry[]): string | undefined;
|