lark-remote 0.4.4 → 0.5.0
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 +28 -0
- package/dist/binder.d.ts +11 -10
- package/dist/binder.js +17 -16
- package/dist/bridge/approval-coordinator.d.ts +1 -1
- package/dist/bridge/approval-coordinator.js +3 -3
- package/dist/bridge/inbound-media.d.ts +1 -27
- package/dist/bridge/inbound-media.js +23 -21
- package/dist/bridge/index.d.ts +57 -57
- package/dist/bridge/index.js +136 -129
- package/dist/bridge/queue-manager.d.ts +45 -38
- package/dist/bridge/queue-manager.js +136 -136
- package/dist/card/approval-render.d.ts +1 -1
- package/dist/card/approval-render.js +6 -6
- package/dist/card/bash-card-session.d.ts +3 -3
- package/dist/card/bash-card-session.js +3 -3
- package/dist/card/bash-renderer.js +5 -5
- package/dist/card/card-session.d.ts +4 -4
- package/dist/card/card-session.js +6 -6
- package/dist/card/card-shared.d.ts +1 -1
- package/dist/card/card-shared.js +2 -2
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.d.ts +1 -1
- package/dist/card/run-renderer.js +22 -22
- package/dist/card/run-state.d.ts +2 -2
- package/dist/card/run-state.js +3 -3
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +6 -6
- package/dist/cli.js +5 -6
- package/dist/clone.d.ts +6 -9
- package/dist/clone.js +12 -8
- package/dist/common/clamp.d.ts +2 -0
- package/dist/common/clamp.js +5 -0
- package/dist/common/fs.d.ts +4 -3
- package/dist/common/fs.js +5 -4
- package/dist/common/truncate.d.ts +1 -1
- package/dist/common/truncate.js +2 -2
- package/dist/common/ttl-cache.d.ts +2 -2
- package/dist/common/ttl-cache.js +2 -2
- package/dist/config/codex-config.d.ts +9 -9
- package/dist/config/codex-config.js +18 -18
- package/dist/config/dir.d.ts +4 -5
- package/dist/config/dir.js +12 -10
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +6 -6
- package/dist/config/opencode-config.js +3 -3
- package/dist/config/wizard.js +2 -2
- package/dist/connector/index.d.ts +5 -7
- package/dist/connector/index.js +29 -16
- package/dist/inbound/event.d.ts +1 -1
- package/dist/inbound/event.js +1 -1
- package/dist/inbound/placeholder.d.ts +2 -2
- package/dist/inbound/placeholder.js +2 -2
- package/dist/inbound/receipt.d.ts +1 -3
- package/dist/inbound/receipt.js +2 -2
- package/dist/inbound/turn-assembler.d.ts +1 -3
- package/dist/inbound/turn-assembler.js +2 -2
- package/dist/index.js +38 -30
- package/dist/instance-lock.js +8 -7
- package/dist/logger/index.d.ts +1 -1
- package/dist/logger/index.js +4 -4
- package/dist/persistence/atomic-write.d.ts +1 -1
- package/dist/persistence/atomic-write.js +9 -5
- package/dist/platform/agent-stopper.d.ts +2 -2
- package/dist/platform/identity.d.ts +7 -1
- package/dist/platform/identity.js +2 -2
- package/dist/platform/path.d.ts +6 -0
- package/dist/platform/path.js +14 -8
- package/dist/platform/select.d.ts +1 -1
- package/dist/platform/select.js +1 -1
- package/dist/platform/shell.d.ts +2 -3
- package/dist/platform/spawn.d.ts +2 -2
- package/dist/platform/spawn.js +2 -2
- package/dist/platform/terminator-posix.js +6 -6
- package/dist/platform/terminator-win32.js +3 -3
- package/dist/platform/terminator.d.ts +3 -3
- package/dist/platform/terminator.js +2 -2
- package/dist/platform/types.d.ts +1 -1
- package/dist/restart.d.ts +1 -1
- package/dist/restart.js +5 -3
- package/dist/router/card-helpers.d.ts +1 -3
- package/dist/router/card-helpers.js +2 -2
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +3 -3
- package/dist/router/config/common/model-patch.d.ts +1 -2
- package/dist/router/config/common/model-patch.js +1 -2
- package/dist/router/config/opencode.js +2 -2
- package/dist/router/config/pi.js +1 -1
- package/dist/router/index.d.ts +30 -30
- package/dist/router/index.js +103 -84
- package/dist/router/order-exec-dispatch.d.ts +3 -10
- package/dist/router/order-exec-dispatch.js +5 -5
- package/dist/router/{utils.d.ts → usage-format.d.ts} +2 -4
- package/dist/router/{utils.js → usage-format.js} +3 -8
- package/dist/runner/bash/runner.d.ts +2 -2
- package/dist/runner/bash/runner.js +6 -6
- package/dist/runner/claude/runner.d.ts +1 -1
- package/dist/runner/claude/runner.js +2 -3
- package/dist/runner/claude/session.d.ts +4 -6
- package/dist/runner/claude/session.js +21 -23
- package/dist/runner/codex/app-server/runner.js +3 -3
- package/dist/runner/codex/app-server/translator.d.ts +2 -2
- package/dist/runner/codex/app-server/translator.js +5 -5
- package/dist/runner/common/acp/{protocol-helpers.d.ts → acp-approval.d.ts} +4 -10
- package/dist/runner/common/acp/{protocol-helpers.js → acp-approval.js} +7 -6
- package/dist/runner/common/acp/base-acp-runner.d.ts +1 -1
- package/dist/runner/common/acp/base-acp-runner.js +2 -2
- package/dist/runner/common/acp/base-translator.d.ts +3 -3
- package/dist/runner/common/acp/base-translator.js +4 -4
- package/dist/runner/common/acp/protocol-types.js +1 -1
- package/dist/runner/common/connection-based-runner.d.ts +5 -5
- package/dist/runner/common/connection-based-runner.js +5 -5
- package/dist/runner/common/jsonl-stream.d.ts +2 -2
- package/dist/runner/common/jsonl-stream.js +12 -12
- package/dist/runner/common/jsonrpc/client.d.ts +1 -1
- package/dist/runner/common/jsonrpc/client.js +1 -1
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +13 -13
- package/dist/runner/common/jsonrpc/connection-manager.js +37 -37
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +2 -2
- package/dist/runner/common/{usage.d.ts → result-usage.d.ts} +1 -1
- package/dist/runner/common/{usage.js → result-usage.js} +2 -2
- package/dist/runner/common/spawning-runner.d.ts +23 -10
- package/dist/runner/common/spawning-runner.js +48 -22
- package/dist/runner/common/{runner-utils.d.ts → synthetic-events.d.ts} +4 -4
- package/dist/runner/common/{runner-utils.js → synthetic-events.js} +5 -5
- package/dist/runner/dsh/client.d.ts +1 -1
- package/dist/runner/dsh/client.js +1 -1
- package/dist/runner/dsh/runner.js +3 -3
- package/dist/runner/kimi/acp/runner.d.ts +4 -4
- package/dist/runner/kimi/acp/runner.js +10 -10
- package/dist/runner/kimi/acp/translator.d.ts +2 -2
- package/dist/runner/kimi/acp/translator.js +5 -5
- package/dist/runner/opencode/acp/runner.d.ts +4 -4
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/opencode/acp/translator.js +2 -2
- package/dist/runner/pi/rpc/runner.d.ts +1 -1
- package/dist/runner/pi/rpc/runner.js +2 -2
- package/dist/runner/pi/rpc/translator.d.ts +1 -2
- package/dist/runner/pi/rpc/translator.js +4 -6
- package/dist/runner/registry.d.ts +3 -3
- package/dist/runner/registry.js +3 -3
- package/dist/runner/types.d.ts +1 -1
- package/dist/session/claude/session-index.d.ts +13 -1
- package/dist/session/claude/session-index.js +21 -6
- package/dist/session/claude/session-reader.d.ts +1 -1
- package/dist/session/claude/session-reader.js +1 -1
- package/dist/session/claude/sessions.js +10 -10
- package/dist/session/codex/rollout-reader.d.ts +4 -4
- package/dist/session/codex/rollout-reader.js +14 -14
- package/dist/session/common/two-pass.js +2 -2
- package/dist/session/common/usage-accumulator.d.ts +2 -2
- package/dist/session/common/usage-accumulator.js +2 -2
- package/dist/session/dsh/sessions.d.ts +1 -1
- package/dist/session/dsh/sessions.js +3 -3
- package/dist/session/kimi/sessions.d.ts +2 -6
- package/dist/session/kimi/sessions.js +13 -19
- package/dist/session/opencode/sessions.d.ts +3 -3
- package/dist/session/opencode/sessions.js +15 -13
- package/dist/session/pi/sessions.js +13 -12
- package/dist/session/registry.d.ts +1 -1
- package/dist/session/registry.js +1 -1
- package/dist/session/session-store.d.ts +3 -4
- package/dist/session/session-store.js +3 -5
- package/dist/update/index.d.ts +1 -1
- package/dist/update/index.js +1 -1
- package/dist/update/version-check.d.ts +5 -3
- package/dist/update/version-check.js +10 -8
- package/dist/workspace/index.d.ts +4 -4
- package/dist/workspace/index.js +5 -5
- package/package.json +1 -1
|
@@ -73,7 +73,7 @@ export interface InboundMediaPayload {
|
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
75
|
* 媒体消息到达的轻量描述(未下载):供上层先做 owner/配置闸门,
|
|
76
|
-
* 通过后再调 downloadInboundMedia ——
|
|
76
|
+
* 通过后再调 downloadInboundMedia —— 下载发生在认证之后(修复)。
|
|
77
77
|
*/
|
|
78
78
|
/** SDK 给出的可下载资源种类(`resources[].type`)。 */
|
|
79
79
|
export type InboundResourceKind = 'image' | 'file' | 'audio' | 'video' | 'sticker';
|
|
@@ -107,8 +107,6 @@ type CardActionHandler = (action: CardActionEvent) => void | CardActionResponse
|
|
|
107
107
|
* 卡片 patch 的重试上限。含义:1 次原始尝试 + 最多 PATCH_MAX_RETRIES 次重试。
|
|
108
108
|
*/
|
|
109
109
|
export declare const PATCH_MAX_RETRIES = 3;
|
|
110
|
-
/** 重试退避基数(ms);第 n 次重试前等待 n × 基数,给对端恢复的时间。 */
|
|
111
|
-
export declare const PATCH_RETRY_BASE_DELAY_MS = 150;
|
|
112
110
|
export declare class FeishuConnector {
|
|
113
111
|
/** 飞书 channel 实例(public:测试直接访问/注入 mock,替代 as unknown as)。 */
|
|
114
112
|
channel: LarkChannel;
|
|
@@ -182,14 +180,14 @@ export declare class FeishuConnector {
|
|
|
182
180
|
*/
|
|
183
181
|
private checkUploadable;
|
|
184
182
|
/**
|
|
185
|
-
* sendFile/sendImage 共用骨架:token →
|
|
183
|
+
* sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁)
|
|
186
184
|
* → 发送媒体消息。上传端点 / form 字段 / 响应 key / msg_type 的差异由调用方
|
|
187
185
|
* 以 uploadUrl + buildForm + keyField + msgType 表达;timeout、noKeepAliveAgent
|
|
188
186
|
* (Bun keep-alive 修复)与错误包装在此单源。label 是对外错误前缀与日志
|
|
189
187
|
* 标签('sendFile' | 'sendImage')。
|
|
190
188
|
*/
|
|
191
189
|
private uploadAndSendMedia;
|
|
192
|
-
/** 单次「上传 +
|
|
190
|
+
/** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁。 */
|
|
193
191
|
private uploadWithToken;
|
|
194
192
|
/**
|
|
195
193
|
* Upload a file to Feishu and send it to the specified chat.
|
|
@@ -203,13 +201,13 @@ export declare class FeishuConnector {
|
|
|
203
201
|
*/
|
|
204
202
|
sendImage(chatId: string, filePath: string): Promise<string>;
|
|
205
203
|
/**
|
|
206
|
-
* Cached tenant_access_token for sendFile
|
|
204
|
+
* Cached tenant_access_token for sendFile. The token is valid ~2h;
|
|
207
205
|
* fetching it on every file send wastes a round-trip and an unguarded fetch
|
|
208
206
|
* could return data.code != 0 with an undefined token. Cache with expiry and
|
|
209
207
|
* validate the response code.
|
|
210
208
|
*
|
|
211
209
|
* 缓存的两条失效路径:到期(下面 expire)与被服务端判 token 无效
|
|
212
|
-
* (invalidateTenantToken
|
|
210
|
+
* (invalidateTenantToken)。
|
|
213
211
|
*/
|
|
214
212
|
private cachedToken;
|
|
215
213
|
private cachedTokenExpireAt;
|
package/dist/connector/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { sleep } from '../common/sleep.js';
|
|
|
7
7
|
import { isTransientTransportError } from '../error-classification.js';
|
|
8
8
|
import axios from 'axios';
|
|
9
9
|
import fs from 'node:fs';
|
|
10
|
-
import {
|
|
10
|
+
import { bestEffortUnlink } from '../common/fs.js';
|
|
11
11
|
import { displayName } from '../platform/path.js';
|
|
12
12
|
import https from 'node:https';
|
|
13
13
|
import os from 'node:os';
|
|
@@ -111,7 +111,7 @@ function withTimeout(promise, ms, label, onLateSettle) {
|
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
114
|
-
* 判断 send 失败是否值得重试一次(§9.5
|
|
114
|
+
* 判断 send 失败是否值得重试一次(§9.5 限流重试口径)。
|
|
115
115
|
*
|
|
116
116
|
* @larksuite/channel@0.7.1 的 classifyError 把飞书业务码 99991400/99991401
|
|
117
117
|
* (频率控制)归类为 `code='permission_denied'`(SDK 源码实证),并保留原始
|
|
@@ -145,7 +145,7 @@ function shouldRetrySendError(err) {
|
|
|
145
145
|
*/
|
|
146
146
|
export const PATCH_MAX_RETRIES = 3;
|
|
147
147
|
/** 重试退避基数(ms);第 n 次重试前等待 n × 基数,给对端恢复的时间。 */
|
|
148
|
-
|
|
148
|
+
const PATCH_RETRY_BASE_DELAY_MS = 150;
|
|
149
149
|
/** 记日志用的错误摘要(不打印 axios 巨型循环对象)。 */
|
|
150
150
|
function formatErrorForLog(err) {
|
|
151
151
|
if (err instanceof Error) {
|
|
@@ -271,6 +271,19 @@ export class FeishuConnector {
|
|
|
271
271
|
ttl: DEDUP_TTL_MS,
|
|
272
272
|
maxEntries: 1000,
|
|
273
273
|
},
|
|
274
|
+
// 关掉 SDK 的入站去抖合批(`ChatPipeline` + `mergeBatch`),delayMs=0 让每条
|
|
275
|
+
// 消息单独成批、保留自己的 messageId(`enabled` 仍为 true,per-chat 串行与
|
|
276
|
+
// 卡片动作同通道不受影响)。
|
|
277
|
+
//
|
|
278
|
+
// 背景:SDK 默认 600ms 内把同一会话的多条消息合并成一条,`mergeBatch` 只保留
|
|
279
|
+
// **最后一条**的 `messageId` 却把多条消息的 `resources` 求并集。飞书客户端
|
|
280
|
+
// "一次选多图/多文件"会背靠背秒发成多条独立消息,被合并后下载侧拿同一个
|
|
281
|
+
// `messageId` 下所有 `fileKey`,除最后一条外全部报错 234003(HTTP 400):
|
|
282
|
+
// 表现为"多图只有 1 张保存成功,其余下载失败"。合批本应由本项目自己的
|
|
283
|
+
// 700ms 装配器(`InboundTurnAssembler`)负责,SDK 这层是叠加的第二次合批。
|
|
284
|
+
batch: {
|
|
285
|
+
text: { delayMs: 0 },
|
|
286
|
+
},
|
|
274
287
|
},
|
|
275
288
|
});
|
|
276
289
|
// 卡片 patch 的统一出口(观测 + 重试):
|
|
@@ -305,7 +318,7 @@ export class FeishuConnector {
|
|
|
305
318
|
// SDK 只为 image/file/audio/video/media/sticker 五类产出非空 resources,
|
|
306
319
|
// 枚举 msg_type 必然漏(2026-09-15 事故:mp4 被当文本转发给 agent)。
|
|
307
320
|
// 一条消息可以**同时**产出资源事件与文本事件(post 既有文字又有图),
|
|
308
|
-
//
|
|
321
|
+
// 由的装配器合并成同一个 turn。
|
|
309
322
|
const hasResources = msg.resources.length > 0;
|
|
310
323
|
if (hasResources) {
|
|
311
324
|
// 只上报「媒体到达」,不在此下载:owner/配置闸门在 index.ts 里先执行,
|
|
@@ -403,9 +416,9 @@ export class FeishuConnector {
|
|
|
403
416
|
for (const res of msg.resources) {
|
|
404
417
|
const tmpPath = path.join(os.tmpdir(), `lark-remote-inbound-${randomUUID()}`);
|
|
405
418
|
try {
|
|
406
|
-
const { contentType, bytesWritten } = await withTimeout(this.channel.downloadResourceToFile(msg.messageId, res.fileKey, res.type, tmpPath), timeoutMs, `downloadResource fileKey=${res.fileKey}`, () =>
|
|
419
|
+
const { contentType, bytesWritten } = await withTimeout(this.channel.downloadResourceToFile(msg.messageId, res.fileKey, res.type, tmpPath), timeoutMs, `downloadResource fileKey=${res.fileKey}`, () => bestEffortUnlink(tmpPath));
|
|
407
420
|
if (bytesWritten > maxBytes) {
|
|
408
|
-
|
|
421
|
+
bestEffortUnlink(tmpPath);
|
|
409
422
|
const limitMb = maxBytes / (1024 * 1024);
|
|
410
423
|
failures.push({
|
|
411
424
|
kind: res.kind,
|
|
@@ -429,7 +442,7 @@ export class FeishuConnector {
|
|
|
429
442
|
catch (err) {
|
|
430
443
|
// 超时不在这里删文件:上面已把清理挂到底层传输的 settle 上。
|
|
431
444
|
if (!(err instanceof ResourceTimeoutError))
|
|
432
|
-
|
|
445
|
+
bestEffortUnlink(tmpPath);
|
|
433
446
|
getLogger().warn(`[feishu] downloadResource failed fileKey=${res.fileKey} type=${res.type}:`, err.message);
|
|
434
447
|
failures.push({
|
|
435
448
|
kind: res.kind,
|
|
@@ -489,7 +502,7 @@ export class FeishuConnector {
|
|
|
489
502
|
return result.messageId;
|
|
490
503
|
}
|
|
491
504
|
catch (retryErr) {
|
|
492
|
-
//
|
|
505
|
+
// throw the RETRY error, not the outer `err`. Previously
|
|
493
506
|
// `throw err` here re-threw the first (rate-limit) failure, hiding
|
|
494
507
|
// the real reason the retry attempt failed (e.g. auth invalid).
|
|
495
508
|
getLogger().warn('[feishu] retry after rate limit still failed');
|
|
@@ -595,7 +608,7 @@ export class FeishuConnector {
|
|
|
595
608
|
}
|
|
596
609
|
}
|
|
597
610
|
/**
|
|
598
|
-
* sendFile/sendImage 共用骨架:token →
|
|
611
|
+
* sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁)
|
|
599
612
|
* → 发送媒体消息。上传端点 / form 字段 / 响应 key / msg_type 的差异由调用方
|
|
600
613
|
* 以 uploadUrl + buildForm + keyField + msgType 表达;timeout、noKeepAliveAgent
|
|
601
614
|
* (Bun keep-alive 修复)与错误包装在此单源。label 是对外错误前缀与日志
|
|
@@ -627,13 +640,13 @@ export class FeishuConnector {
|
|
|
627
640
|
throw new Error(`${label} failed: ${errorInfo}`, { cause: err });
|
|
628
641
|
}
|
|
629
642
|
}
|
|
630
|
-
/** 单次「上传 +
|
|
643
|
+
/** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁。 */
|
|
631
644
|
async uploadWithToken(accessToken, chatId, filePath, uploadUrl, buildForm, keyField, msgType) {
|
|
632
645
|
let fileStream = null;
|
|
633
646
|
try {
|
|
634
647
|
fileStream = fs.createReadStream(filePath);
|
|
635
648
|
const form = buildForm(fileStream);
|
|
636
|
-
//
|
|
649
|
+
// timeout 120s(大文件慢链路);httpsAgent: noKeepAliveAgent 防止
|
|
637
650
|
// Bun 复用已被服务端 RST 的 keep-alive socket(ECONNRESET after ~30s)。
|
|
638
651
|
const uploadResp = await axios.post(uploadUrl, form, {
|
|
639
652
|
headers: {
|
|
@@ -647,7 +660,7 @@ export class FeishuConnector {
|
|
|
647
660
|
throw new FeishuBusinessError(`Upload failed: ${uploadResp.data.msg}`, uploadResp.data.code);
|
|
648
661
|
}
|
|
649
662
|
const mediaKey = uploadResp.data.data[keyField];
|
|
650
|
-
//
|
|
663
|
+
// timeout 30s;noKeepAlive 同上。
|
|
651
664
|
const sendResp = await axios.post('https://open.feishu.cn/open-apis/im/v1/messages', {
|
|
652
665
|
receive_id: chatId,
|
|
653
666
|
msg_type: msgType,
|
|
@@ -667,7 +680,7 @@ export class FeishuConnector {
|
|
|
667
680
|
return sendResp.data.data.message_id;
|
|
668
681
|
}
|
|
669
682
|
catch (err) {
|
|
670
|
-
//
|
|
683
|
+
// destroy the read stream on failure so the fd is released.
|
|
671
684
|
if (fileStream) {
|
|
672
685
|
try {
|
|
673
686
|
fileStream.destroy();
|
|
@@ -708,13 +721,13 @@ export class FeishuConnector {
|
|
|
708
721
|
}, 'image_key', 'image');
|
|
709
722
|
}
|
|
710
723
|
/**
|
|
711
|
-
* Cached tenant_access_token for sendFile
|
|
724
|
+
* Cached tenant_access_token for sendFile. The token is valid ~2h;
|
|
712
725
|
* fetching it on every file send wastes a round-trip and an unguarded fetch
|
|
713
726
|
* could return data.code != 0 with an undefined token. Cache with expiry and
|
|
714
727
|
* validate the response code.
|
|
715
728
|
*
|
|
716
729
|
* 缓存的两条失效路径:到期(下面 expire)与被服务端判 token 无效
|
|
717
|
-
* (invalidateTenantToken
|
|
730
|
+
* (invalidateTenantToken)。
|
|
718
731
|
*/
|
|
719
732
|
cachedToken = null;
|
|
720
733
|
cachedTokenExpireAt = 0;
|
|
@@ -736,7 +749,7 @@ export class FeishuConnector {
|
|
|
736
749
|
this.cachedTokenExpireAt = 0;
|
|
737
750
|
}
|
|
738
751
|
async fetchTenantAccessToken() {
|
|
739
|
-
//
|
|
752
|
+
// timeout 30s on the token request.
|
|
740
753
|
// httpsAgent: noKeepAlive — same Bun keep-alive fix as upload above.
|
|
741
754
|
const tokenResp = await axios.post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
|
|
742
755
|
app_id: this.appId,
|
package/dist/inbound/event.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 装配器事件(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
|
|
5
5
|
* 下载是异步的(`index.ts` 先过 owner/enabled 闸门再下载),若等下载完成才投递事件,
|
|
6
6
|
* 「先图后文」在慢下载下会先提交一个只含文本的 turn(正是要修的症状)。因此媒体事件
|
|
7
7
|
* 携带一个 **outcome promise**:装配器立刻知道「本 turn 还有 in-flight 下载」,
|
package/dist/inbound/event.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 装配器事件(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
|
|
5
5
|
* 下载是异步的(`index.ts` 先过 owner/enabled 闸门再下载),若等下载完成才投递事件,
|
|
6
6
|
* 「先图后文」在慢下载下会先提交一个只含文本的 turn(正是要修的症状)。因此媒体事件
|
|
7
7
|
* 携带一个 **outcome promise**:装配器立刻知道「本 turn 还有 in-flight 下载」,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 入站消息结构占位符识别与剥离(纯函数,无 IO,便于表驱动测试)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 背景(事故 2026-09-15):`@larksuite/channel` 把非文本消息渲染成结构占位符,
|
|
5
5
|
* 富文本(`post`)里的图片是 `` —— 首字符恰好是 `!`,于是被
|
|
6
6
|
* `src/router/index.ts` 的裸前缀判定当成 bash 命令执行了整段消息。
|
|
7
7
|
*
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 取舍(有意为之):剥离对所有消息生效,所以用户在**纯文本**消息里手打
|
|
13
13
|
* `` / `<file key="…"/>` 这类字面量也会被当成占位符剥掉。代价是极少数
|
|
14
14
|
* 「讨论本工具自身语法」的消息会丢片段;换来的是任何入口都不可能把占位符当命令
|
|
15
|
-
*
|
|
15
|
+
* (事故正是 `` 的首字符 `!` 命中 bash 判定)。
|
|
16
16
|
*/
|
|
17
17
|
export type PlaceholderKind = 'image' | 'file' | 'video' | 'audio' | 'sticker' | 'share' | 'location' | 'folder' | 'vote' | 'todo' | 'meeting' | 'calendar' | 'hongbao' | 'forwarded' | 'unsupported' | 'unknown';
|
|
18
18
|
export interface StripResult {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 入站消息结构占位符识别与剥离(纯函数,无 IO,便于表驱动测试)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 背景(事故 2026-09-15):`@larksuite/channel` 把非文本消息渲染成结构占位符,
|
|
5
5
|
* 富文本(`post`)里的图片是 `` —— 首字符恰好是 `!`,于是被
|
|
6
6
|
* `src/router/index.ts` 的裸前缀判定当成 bash 命令执行了整段消息。
|
|
7
7
|
*
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 取舍(有意为之):剥离对所有消息生效,所以用户在**纯文本**消息里手打
|
|
13
13
|
* `` / `<file key="…"/>` 这类字面量也会被当成占位符剥掉。代价是极少数
|
|
14
14
|
* 「讨论本工具自身语法」的消息会丢片段;换来的是任何入口都不可能把占位符当命令
|
|
15
|
-
*
|
|
15
|
+
* (事故正是 `` 的首字符 `!` 命中 bash 判定)。
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
18
|
* 可下载资源类占位符:这些占位符对应的资源会由媒体通道下载落盘,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 入站回执文案(`inbound-unified-input-design.md` §5.7)。
|
|
3
3
|
*
|
|
4
4
|
* 两类回执合并成一条:
|
|
5
5
|
* 1. 无文本纯附件(决策 2):不自动起 turn,只回已保存路径 + 引导语;
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { PlaceholderKind } from './placeholder.js';
|
|
12
12
|
import type { RejectedItem } from './turn.js';
|
|
13
|
-
/** 单条回执最多列出的文件数;超出折叠为 "… 等 N 个文件"(沿用旧提示口径)。 */
|
|
14
|
-
export declare const MAX_PATHS_IN_RECEIPT = 10;
|
|
15
13
|
/** 「不支持」回执句式(§5.7 表:不支持类型统一「暂不支持 X」)。 */
|
|
16
14
|
export declare function unsupportedReason(kind: PlaceholderKind): string;
|
|
17
15
|
export interface ReceiptInput {
|
package/dist/inbound/receipt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 入站回执文案(`inbound-unified-input-design.md` §5.7)。
|
|
3
3
|
*
|
|
4
4
|
* 两类回执合并成一条:
|
|
5
5
|
* 1. 无文本纯附件(决策 2):不自动起 turn,只回已保存路径 + 引导语;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
/** 单条回执最多列出的文件数;超出折叠为 "… 等 N 个文件"(沿用旧提示口径)。 */
|
|
13
|
-
|
|
13
|
+
const MAX_PATHS_IN_RECEIPT = 10;
|
|
14
14
|
/** 占位符种类 → 面向人的名称。 */
|
|
15
15
|
const PLACEHOLDER_LABELS = {
|
|
16
16
|
image: '图片',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* InboundTurnAssembler:统一静默期窗口 + 合并 + 下载等待 + commit。
|
|
3
3
|
*
|
|
4
|
-
* 设计依据:`docs/zh/architecture/inbound-unified-input-design.md` §4
|
|
4
|
+
* 设计依据:`docs/zh/architecture/inbound-unified-input-design.md` §4、§5.2。
|
|
5
5
|
*
|
|
6
6
|
* 规则:
|
|
7
7
|
* - 任何事件到达 → **重置** 700ms 定时器(按 `userId:chatId` 分桶),保证图/文任意顺序等价;
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { InboundEvent } from './event.js';
|
|
15
15
|
import { type CommitReason, type InboundTurn } from './turn.js';
|
|
16
|
-
/** 统一静默期窗口(ms)——决策 1:不做「纯文本零延迟」快路径。 */
|
|
17
|
-
export declare const DEFAULT_TURN_WINDOW_MS = 700;
|
|
18
16
|
export interface AssemblerContext {
|
|
19
17
|
userId: string;
|
|
20
18
|
chatId: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* InboundTurnAssembler:统一静默期窗口 + 合并 + 下载等待 + commit。
|
|
3
3
|
*
|
|
4
|
-
* 设计依据:`docs/zh/architecture/inbound-unified-input-design.md` §4
|
|
4
|
+
* 设计依据:`docs/zh/architecture/inbound-unified-input-design.md` §4、§5.2。
|
|
5
5
|
*
|
|
6
6
|
* 规则:
|
|
7
7
|
* - 任何事件到达 → **重置** 700ms 定时器(按 `userId:chatId` 分桶),保证图/文任意顺序等价;
|
|
@@ -17,7 +17,7 @@ import { buildPrompt } from './prompt.js';
|
|
|
17
17
|
import { buildReceipt, unsupportedReason } from './receipt.js';
|
|
18
18
|
import { bucketKey, } from './turn.js';
|
|
19
19
|
/** 统一静默期窗口(ms)——决策 1:不做「纯文本零延迟」快路径。 */
|
|
20
|
-
|
|
20
|
+
const DEFAULT_TURN_WINDOW_MS = 700;
|
|
21
21
|
export class InboundTurnAssembler {
|
|
22
22
|
deps;
|
|
23
23
|
buckets = new Map();
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { probeAllAgents } from './runner/probe.js';
|
|
|
9
9
|
import { warmCodexCatalogCache } from './config/codex-config.js';
|
|
10
10
|
import { SessionReaderRegistry, SessionStore } from './session/index.js';
|
|
11
11
|
import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, DshSessionReader, } from './session/index.js';
|
|
12
|
-
import { CommandRouter, isImmediateAction, DIRECT_RETURN_CMDS, } from './router/index.js';
|
|
12
|
+
import { CommandRouter, isImmediateAction, DIRECT_RETURN_CMDS, SESSION_MUTATING_ACTION_CMDS, } from './router/index.js';
|
|
13
13
|
import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
|
|
14
14
|
import { buildCardActionFullValue } from './router/card-action-payload.js';
|
|
15
15
|
import { actionFeedbackText } from './router/card-action-feedback.js';
|
|
@@ -22,7 +22,7 @@ import { InstanceAlreadyRunningError, InstanceLock } from './instance-lock.js';
|
|
|
22
22
|
import { spawnReplacementBridge, waitForPreviousInstance } from './restart.js';
|
|
23
23
|
import { currentPlatform } from './platform/select.js';
|
|
24
24
|
import { startSleepBlocker } from './platform/sleep-blocker.js';
|
|
25
|
-
import { checkLatestVersion,
|
|
25
|
+
import { checkLatestVersion, compareVersions, runInstallLatest, formatUpdateHint, } from './update/index.js';
|
|
26
26
|
import { classifyRejection } from './error-classification.js';
|
|
27
27
|
import { WorkspaceStore } from './workspace/index.js';
|
|
28
28
|
import { InboundTurnAssembler } from './inbound/turn-assembler.js';
|
|
@@ -31,7 +31,7 @@ import { buildSessionHistoryCard } from './router/card-helpers.js';
|
|
|
31
31
|
import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
|
|
32
32
|
import path from 'node:path';
|
|
33
33
|
import fs from 'node:fs';
|
|
34
|
-
import {
|
|
34
|
+
import { bestEffortUnlink } from './common/fs.js';
|
|
35
35
|
/**
|
|
36
36
|
* /stop 命令别名单源:普通消息的停止分支与 clone 活跃期拦截的排除条件
|
|
37
37
|
* 共用同一判定,防止将来加别名时两处漂移。
|
|
@@ -133,10 +133,10 @@ async function setupInstanceLockAndHandlers(instanceLock, logger, configDir) {
|
|
|
133
133
|
/** Initialize per-workspace runner factory + agent registry. */
|
|
134
134
|
function initializeRunner(config, configDir, cliArgs) {
|
|
135
135
|
const agentRegistry = new AgentRegistry();
|
|
136
|
-
//
|
|
136
|
+
// 单点化:runner factory 取最新 config 的唯一入口(语义保持,
|
|
137
137
|
// container 未接线时回退启动快照)。
|
|
138
138
|
const latest = () => agentRegistry.getConfigContainer()?.current ?? config;
|
|
139
|
-
//
|
|
139
|
+
// Claude factory reads from configContainer (not closure) so runtime
|
|
140
140
|
// config changes (model, effort, stopGraceMs) take effect after
|
|
141
141
|
// bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
|
|
142
142
|
agentRegistry.register('claude', (ws) => {
|
|
@@ -147,7 +147,7 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
147
147
|
stopGraceMs: claudeConfig.stopGraceMs,
|
|
148
148
|
settings: cliArgs.settings,
|
|
149
149
|
pidDir: configDir,
|
|
150
|
-
|
|
150
|
+
cwd: ws,
|
|
151
151
|
permissionMode: claudeConfig.permissionMode,
|
|
152
152
|
idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
|
|
153
153
|
});
|
|
@@ -228,7 +228,7 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
228
228
|
.split(',')
|
|
229
229
|
.map((t) => t.trim())
|
|
230
230
|
.filter(Boolean),
|
|
231
|
-
|
|
231
|
+
cwd: ws,
|
|
232
232
|
sessionReader: piSessionReader,
|
|
233
233
|
});
|
|
234
234
|
});
|
|
@@ -256,7 +256,7 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
256
256
|
// Register DshRunner (HTTP-only DSH Web Host agent) and DshSessionReader.
|
|
257
257
|
const dshSessionReader = new DshSessionReader({
|
|
258
258
|
// hostProvider 每次调用读最新 config:/config 修改 host 后 runner 重建为
|
|
259
|
-
// 新 host,reader(/resume、用量、完成卡)也跟随新 host
|
|
259
|
+
// 新 host,reader(/resume、用量、完成卡)也跟随新 host。
|
|
260
260
|
hostProvider: () => getAgentConfig(latest(), 'dsh')?.host,
|
|
261
261
|
});
|
|
262
262
|
agentRegistry.register('dsh', (_ws) => {
|
|
@@ -308,7 +308,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
308
308
|
// 附件路径统一注入 prompt;无文本纯附件只回执(决策 2)。
|
|
309
309
|
const assembler = new InboundTurnAssembler({
|
|
310
310
|
onCommit: (turn, prompt) => {
|
|
311
|
-
//
|
|
311
|
+
// 入队时刻快照 cwd + agent/session 绑定,语义与旧路径一致
|
|
312
312
|
// (排队期间 /cd、/config 不再导致语义漂移)。
|
|
313
313
|
const messageId = turn.messageIds[turn.messageIds.length - 1] ?? '';
|
|
314
314
|
// turnMessageIds:终态收尾要覆盖本轮每一条消息(逐条挂的 Typing 得逐条撤)
|
|
@@ -318,17 +318,17 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
318
318
|
messageId,
|
|
319
319
|
turnMessageIds: turn.messageIds,
|
|
320
320
|
};
|
|
321
|
-
let
|
|
322
|
-
if (!
|
|
321
|
+
let cwd = sessionStore.getCwd(turn.userId) ?? '';
|
|
322
|
+
if (!cwd && workspaceStore) {
|
|
323
323
|
const workspaces = workspaceStore.list();
|
|
324
324
|
if (workspaces.length > 0)
|
|
325
|
-
|
|
325
|
+
cwd = workspaces[0].path;
|
|
326
326
|
}
|
|
327
327
|
const binding = bridge.currentBinding(turn.userId);
|
|
328
|
-
bridge.enqueue(
|
|
328
|
+
bridge.enqueue(cwd, async () => {
|
|
329
329
|
// allowCommandPrefix: false —— prompt 已剥离占位符,绝不再做命令分发。
|
|
330
330
|
await router.handle(prompt, ctx, {
|
|
331
|
-
cwdOverride:
|
|
331
|
+
cwdOverride: cwd,
|
|
332
332
|
binding,
|
|
333
333
|
allowCommandPrefix: false,
|
|
334
334
|
});
|
|
@@ -347,7 +347,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
347
347
|
},
|
|
348
348
|
});
|
|
349
349
|
router.setInboundFlusher(() => assembler.flushAll('flush'));
|
|
350
|
-
//
|
|
350
|
+
// 入站媒体(图片/文件/视频/语音/表情):先认证后下载(修复)。
|
|
351
351
|
// connector 只上报"媒体到达",这里先过 owner + enabled 闸门,通过后才
|
|
352
352
|
// 下载——未认证/关闭配置时不会发生任何网络下载或内存/磁盘占用。
|
|
353
353
|
// 下载是异步的:立刻把在途 promise 交给装配器(`kind: 'media'`),
|
|
@@ -358,9 +358,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
358
358
|
return;
|
|
359
359
|
}
|
|
360
360
|
// 读 router.config(活引用):/config 保存后 setConfigValues 返回新对象,
|
|
361
|
-
// 启动时捕获的 config
|
|
361
|
+
// 启动时捕获的 config 参数会过期(修复)。
|
|
362
362
|
if (!router.config.inboundMedia.enabled) {
|
|
363
|
-
//
|
|
363
|
+
// 不静默:关闭时给 owner 明确反馈,避免发图后无任何反应。
|
|
364
364
|
// 不下载、不落盘,只进 rejected(随 turn 回执一起发出)。
|
|
365
365
|
assembler.ingest({
|
|
366
366
|
kind: 'rejected',
|
|
@@ -386,7 +386,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
386
386
|
// save() 的正常路径会清理临时文件;这里兜底 save 进入 mkdir
|
|
387
387
|
// try 之前意外抛错的情况,避免 os.tmpdir 残留。
|
|
388
388
|
for (const item of payload.media) {
|
|
389
|
-
|
|
389
|
+
bestEffortUnlink(item.tempPath);
|
|
390
390
|
}
|
|
391
391
|
throw err;
|
|
392
392
|
}
|
|
@@ -402,8 +402,8 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
402
402
|
});
|
|
403
403
|
});
|
|
404
404
|
connector.setMessageHandler((msg) => {
|
|
405
|
-
// 绑定/授权闸门:仅 owner 放行;非 owner
|
|
406
|
-
const decision = binder.
|
|
405
|
+
// 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时首条消息认领
|
|
406
|
+
const decision = binder.authorize(msg.userId, msg.chatId);
|
|
407
407
|
if (decision.kind === 'rejected')
|
|
408
408
|
return;
|
|
409
409
|
if (decision.kind === 'bind_success') {
|
|
@@ -562,11 +562,19 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
562
562
|
return;
|
|
563
563
|
}
|
|
564
564
|
const isImmediate = isImmediateAction(actionValue.cmd);
|
|
565
|
-
const
|
|
565
|
+
const cwd = sessionStore.getCwd(userId) ?? '';
|
|
566
566
|
// Build full value object — spread all fields from actionValue, then merge
|
|
567
567
|
// component out-of-band fields (option/formValue/inputValue/options) where
|
|
568
568
|
// present. See card-action-payload.ts for the component-type contract.
|
|
569
569
|
const fullValue = buildCardActionFullValue(actionValue, action);
|
|
570
|
+
// 定序(2026-10-07):会话 / cwd / agent 变更类卡片动作(new-session、
|
|
571
|
+
// resume.use、ls.switch、ws.use、config.save)执行前先冲刷入站装配窗口,
|
|
572
|
+
// 保证窗口内先到的文本消息在本动作之前 commit(绑定旧 session/cwd/agent)。
|
|
573
|
+
// 否则 700ms 静默窗内的消息会漂移到动作之后的新状态:例如输入「重新构建」
|
|
574
|
+
// 后立即点「新会话」,消息会落到新会话。名单以 router 单源为准。
|
|
575
|
+
if (SESSION_MUTATING_ACTION_CMDS.has(actionValue.cmd)) {
|
|
576
|
+
await assembler.flush(userId, chatId, 'flush');
|
|
577
|
+
}
|
|
570
578
|
// queue.input / config.save / approval.respond / approval.toggle /
|
|
571
579
|
// approval.answer 系列返回 CardActionResponse toast 给点击用户即时反馈。
|
|
572
580
|
// 必须直接返回值 -- enqueueImmediate / enqueueConfigAction 是
|
|
@@ -577,7 +585,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
577
585
|
// order.aliasInput / order.aliasRemove / order.textInput 也返回 toast+card,
|
|
578
586
|
// 需直返避免被吞(order.textInput 曾漏在列表外,编辑卡停留在编辑界面)。
|
|
579
587
|
// approval.planFeedback 同 answer 系列:附意见的 toast 需直返 + 不落串行队列。
|
|
580
|
-
//
|
|
588
|
+
// 直返名单以 router 的 DIRECT_RETURN_CMDS 为单一来源(历史注释:
|
|
581
589
|
// order.textInput 曾漏在列表外、answer 家族曾漏在直返列表外——两份拷贝漂移)。
|
|
582
590
|
if (DIRECT_RETURN_CMDS.has(actionValue.cmd)) {
|
|
583
591
|
return router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
@@ -591,13 +599,13 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
591
599
|
void dispatchOrderExecForQueue({
|
|
592
600
|
router,
|
|
593
601
|
bridge,
|
|
594
|
-
|
|
602
|
+
cwd,
|
|
595
603
|
orderId: actionValue.orderId,
|
|
596
604
|
ctx: { userId, chatId, messageId },
|
|
597
605
|
}).catch((err) => logger.error('[control] order.exec dispatch failed:', err));
|
|
598
606
|
return;
|
|
599
607
|
}
|
|
600
|
-
//
|
|
608
|
+
// 非直返命令到这里已经没有回调响应可回(enqueue* 是 fire-and-forget),
|
|
601
609
|
// handler 返回的 toast 必须显式转成持久文本,否则失败毫无反馈。
|
|
602
610
|
const forwardActionFeedback = (res) => {
|
|
603
611
|
const text = actionFeedbackText(res);
|
|
@@ -608,13 +616,13 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
608
616
|
.catch((err) => logger.error('[control] card action feedback failed:', err));
|
|
609
617
|
};
|
|
610
618
|
if (isImmediate) {
|
|
611
|
-
bridge.enqueueImmediate(
|
|
619
|
+
bridge.enqueueImmediate(cwd, async () => {
|
|
612
620
|
const res = await router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
613
621
|
forwardActionFeedback(res);
|
|
614
622
|
});
|
|
615
623
|
}
|
|
616
624
|
else {
|
|
617
|
-
bridge.enqueue(
|
|
625
|
+
bridge.enqueue(cwd, async () => {
|
|
618
626
|
const res = await router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
619
627
|
forwardActionFeedback(res);
|
|
620
628
|
}, {
|
|
@@ -624,7 +632,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
624
632
|
messageId,
|
|
625
633
|
messagePreview: `card action: ${actionValue.cmd}`,
|
|
626
634
|
// Compact 是单向操作,排队卡不允许编辑(编辑预览无意义)。
|
|
627
|
-
editable: actionValue.cmd !== '
|
|
635
|
+
editable: actionValue.cmd !== 'compact' && actionValue.cmd !== 'resume.compact',
|
|
628
636
|
},
|
|
629
637
|
});
|
|
630
638
|
}
|
|
@@ -651,7 +659,7 @@ async function main() {
|
|
|
651
659
|
initLogger({ dir: path.join(updateConfigDir, 'logs') });
|
|
652
660
|
try {
|
|
653
661
|
const { current, latest } = await checkLatestVersion();
|
|
654
|
-
if (
|
|
662
|
+
if (compareVersions(current, latest) !== 1) {
|
|
655
663
|
console.log(`Already up to date: ${current}`);
|
|
656
664
|
process.exit(0);
|
|
657
665
|
}
|
|
@@ -699,9 +707,9 @@ async function main() {
|
|
|
699
707
|
logger.info('[binder] awaiting first binding (any first private message binds)');
|
|
700
708
|
}
|
|
701
709
|
else {
|
|
702
|
-
logger.info(`[binder] bound to owner
|
|
710
|
+
logger.info(`[binder] bound to owner userId=${binder.boundUserId()}`);
|
|
703
711
|
}
|
|
704
|
-
const sessionStore = new SessionStore(path.join(configDir, 'last-session.json')
|
|
712
|
+
const sessionStore = new SessionStore(path.join(configDir, 'last-session.json'));
|
|
705
713
|
const workspaceStore = new WorkspaceStore(path.join(configDir, 'workspace.json'));
|
|
706
714
|
const bridge = new Bridge({
|
|
707
715
|
connector,
|
package/dist/instance-lock.js
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import fs from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
-
import {
|
|
20
|
+
import { bestEffortUnlink } from './common/fs.js';
|
|
21
|
+
import { isWin32 } from './platform/select.js';
|
|
21
22
|
import { binaryName, verifyPidIdentityVerdict } from './platform/identity.js';
|
|
22
23
|
export class InstanceAlreadyRunningError extends Error {
|
|
23
24
|
pid;
|
|
@@ -29,7 +30,7 @@ export class InstanceAlreadyRunningError extends Error {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
|
-
* Probe whether a pid is alive.
|
|
33
|
+
* Probe whether a pid is alive.
|
|
33
34
|
* - process.kill(pid, 0) returning normally → alive.
|
|
34
35
|
* - ESRCH (no such process) → dead.
|
|
35
36
|
* - EPERM (operation not permitted) → the process EXISTS but is owned by
|
|
@@ -86,7 +87,7 @@ export class InstanceLock {
|
|
|
86
87
|
throw new InstanceAlreadyRunningError(pid, this.lockPath);
|
|
87
88
|
}
|
|
88
89
|
// Stale lock: remove and retry once
|
|
89
|
-
|
|
90
|
+
bestEffortUnlink(this.lockPath);
|
|
90
91
|
try {
|
|
91
92
|
const fd = fs.openSync(this.lockPath, 'wx');
|
|
92
93
|
fs.writeSync(fd, this.lockContents(process.pid));
|
|
@@ -114,7 +115,7 @@ export class InstanceLock {
|
|
|
114
115
|
release() {
|
|
115
116
|
const { pid } = this.readPidAndName();
|
|
116
117
|
if (pid === process.pid) {
|
|
117
|
-
|
|
118
|
+
bestEffortUnlink(this.lockPath);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
registerExitHandlers() {
|
|
@@ -127,14 +128,14 @@ export class InstanceLock {
|
|
|
127
128
|
this.release();
|
|
128
129
|
process.exit(143);
|
|
129
130
|
});
|
|
130
|
-
if (
|
|
131
|
+
if (!isWin32()) {
|
|
131
132
|
process.on('SIGHUP', () => {
|
|
132
133
|
this.release();
|
|
133
134
|
process.exit(129);
|
|
134
135
|
});
|
|
135
136
|
}
|
|
136
137
|
else {
|
|
137
|
-
// win32 无 SIGHUP;CTRL_BREAK
|
|
138
|
+
// win32 无 SIGHUP;CTRL_BREAK 退出前也要释放锁(对照 cli.ts 信号转发)
|
|
138
139
|
process.on('SIGBREAK', () => {
|
|
139
140
|
this.release();
|
|
140
141
|
process.exit(149);
|
|
@@ -160,7 +161,7 @@ export class InstanceLock {
|
|
|
160
161
|
if (Number.isInteger(pid) && pid > 0 && name)
|
|
161
162
|
return { pid, name };
|
|
162
163
|
// Malformed or missing name — treat as corrupt, unlink and return undefined
|
|
163
|
-
|
|
164
|
+
bestEffortUnlink(this.lockPath);
|
|
164
165
|
return { pid: undefined, name: undefined };
|
|
165
166
|
}
|
|
166
167
|
/**
|
package/dist/logger/index.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare class Logger {
|
|
|
35
35
|
getCurrentLogFile(): string;
|
|
36
36
|
private ensureFile;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* remove daily subdirectories older than the retention window.
|
|
39
39
|
* Best-effort — any error is swallowed (logging must never throw).
|
|
40
40
|
*/
|
|
41
41
|
private pruneOldDirs;
|