chatccc 0.2.280 → 0.2.282
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/README.md +4 -0
- package/dist/src/cardkit.js +25 -1
- package/dist/src/feishu-connection.js +144 -0
- package/dist/src/index.js +52 -17
- package/dist/src/orchestrator.js +127 -60
- package/dist/src/session.js +4 -1
- package/dist/src/shared.js +34 -17
- package/dist/src/startup-lifecycle.js +43 -1
- package/dist/src/terminal-error.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -446,6 +446,10 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
446
446
|
`/update`、`/update safe` 与短别名 `/updatesf` 会把飞书消息或按钮事件 ID 原子写入 `~/.chatccc/state/update-command-guard.json`。同一 ID 跨重启重投时会静默忽略;用户主动发送的新更新指令因事件 ID 不同,仍可执行。该保护不改变普通消息与重启指令的处理方式。
|
|
447
447
|
|
|
448
448
|
`/restart safe`(短别名 `/restartsf`)与 `/update safe`(短别名 `/updatesf`)会先建立全局准入门禁:指令到达前已经运行或进入单会话缓存队列的工作会继续完成,之后到达的新普通任务会被提示在维护完成后重发。维护任务持久化到 `~/.chatccc/state/safe-maintenance.json`,进程意外退出后可继续排空;依赖安装、会话收尾、自动恢复和 Agent Teams 执行也计入等待条件。内存缓存随重启自然重建,磁盘会话、看板、图片等持久数据不会被清理。
|
|
449
|
+
|
|
450
|
+
ChatCCC 的内部重启和更新使用跨平台父子进程握手:替代进程完成启动预检后通知父进程退出,再等待旧监听端口实际释放并接管 PID;替代进程未就绪或握手超时时,父进程会保留并继续服务。
|
|
451
|
+
|
|
452
|
+
飞书接收长连接启用 15 秒握手超时和 30 秒心跳应答超时;首次启动最多等待 45 秒真实连接确认后才提示就绪。运行期间连接持续异常 90 秒时,会关闭并重新建立接收连接,保留会话、消息去重和正在执行的任务。没有用户消息不会触发重连。连接状态与恢复记录写入运行日志和 `startup-trace.log`(`FEISHU-CONNECTION` / `feishu-connection`)。卡片更新遇到网络失败或序号冲突时不会当成送达成功,错误通知保留文本兜底;TLS 断线会明确显示为网络连接失败,已中断的 Agent 任务不会因此自动重放。
|
|
449
453
|
|
|
450
454
|
> **模型切换**:`/model` 查看当前会话 Agent 的可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。可选模型来自当前 Agent 的配置:Claude 使用 `claude.model` / `claude.subagentModel`;Cursor、Codex、CCC Agent 和 DSH 使用各自的 `model` / `alternativeModel`。
|
|
451
455
|
|
package/dist/src/cardkit.js
CHANGED
|
@@ -103,7 +103,31 @@ export async function streamCardKitElement(token, cardId, elementId, content, se
|
|
|
103
103
|
// success log is intentionally sparse — uncomment to debug streaming throughput
|
|
104
104
|
// console.log(`[${ts()}] [CARDIKT] streamElement OK cardId=${cardId} seq=${sequence}`);
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
// Keep bounded per-card sequence history; never reuse an attempted sequence
|
|
107
|
+
// because a failed fetch can mean the response (not the update) was lost.
|
|
108
|
+
const cardUpdateQueues = new Map();
|
|
109
|
+
export function updateCardKitCard(token, cardId, cardJson, sequence) {
|
|
110
|
+
let state = cardUpdateQueues.get(cardId);
|
|
111
|
+
if (!state) {
|
|
112
|
+
for (const [key, candidate] of cardUpdateQueues) {
|
|
113
|
+
if (cardUpdateQueues.size < 512)
|
|
114
|
+
break;
|
|
115
|
+
if (candidate.pending === 0)
|
|
116
|
+
cardUpdateQueues.delete(key);
|
|
117
|
+
}
|
|
118
|
+
state = { sequence: 0, tail: Promise.resolve(), pending: 0 };
|
|
119
|
+
cardUpdateQueues.set(cardId, state);
|
|
120
|
+
}
|
|
121
|
+
const queue = state;
|
|
122
|
+
queue.pending++;
|
|
123
|
+
const result = queue.tail.then(async () => {
|
|
124
|
+
queue.sequence = Math.max(sequence, queue.sequence + 1);
|
|
125
|
+
await performCardKitUpdate(token, cardId, cardJson, queue.sequence);
|
|
126
|
+
});
|
|
127
|
+
queue.tail = result.catch(() => { }).finally(() => { queue.pending--; });
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
async function performCardKitUpdate(token, cardId, cardJson, sequence) {
|
|
107
131
|
const { resp, respText } = await fetchCardKit(`${BASE_URL}/cardkit/v1/cards/${cardId}`, {
|
|
108
132
|
method: "PUT",
|
|
109
133
|
headers: {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { sanitizeTerminalErrorDetail } from "./terminal-error.js";
|
|
2
|
+
/** Owns only the receiving transport. It never resets sessions or replays tasks. */
|
|
3
|
+
export function createFeishuConnectionSupervisor(options) {
|
|
4
|
+
const stalledAfterMs = options.stalledAfterMs ?? 90_000;
|
|
5
|
+
let stopped = false;
|
|
6
|
+
let client;
|
|
7
|
+
let timer;
|
|
8
|
+
let startupTimer;
|
|
9
|
+
let startup;
|
|
10
|
+
let resolveStartup;
|
|
11
|
+
let rejectStartup;
|
|
12
|
+
let unhealthySince;
|
|
13
|
+
let lastState = "idle";
|
|
14
|
+
let connected = false;
|
|
15
|
+
let launching = false;
|
|
16
|
+
let lastAttempt = 0;
|
|
17
|
+
const log = (event, detail) => {
|
|
18
|
+
try {
|
|
19
|
+
options.log(event, detail);
|
|
20
|
+
}
|
|
21
|
+
catch { /* diagnostics cannot break recovery */ }
|
|
22
|
+
};
|
|
23
|
+
const reportError = (error) => {
|
|
24
|
+
if (stopped)
|
|
25
|
+
return;
|
|
26
|
+
connected = false;
|
|
27
|
+
unhealthySince ??= Date.now();
|
|
28
|
+
log("connection error", { reason: sanitizeTerminalErrorDetail(error instanceof Error ? error.message : String(error)) });
|
|
29
|
+
};
|
|
30
|
+
const becameConnected = () => {
|
|
31
|
+
if (stopped) {
|
|
32
|
+
// A handshake already in flight can finish after close(); never resurrect
|
|
33
|
+
// a transport belonging to a stopped setup attempt or shutting-down app.
|
|
34
|
+
try {
|
|
35
|
+
client?.close({ force: true });
|
|
36
|
+
}
|
|
37
|
+
catch { /* best effort */ }
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (connected)
|
|
41
|
+
return;
|
|
42
|
+
connected = true;
|
|
43
|
+
unhealthySince = undefined;
|
|
44
|
+
lastState = "connected";
|
|
45
|
+
log("connected");
|
|
46
|
+
if (startupTimer)
|
|
47
|
+
clearTimeout(startupTimer);
|
|
48
|
+
resolveStartup?.();
|
|
49
|
+
resolveStartup = undefined;
|
|
50
|
+
rejectStartup = undefined;
|
|
51
|
+
void Promise.resolve().then(() => stopped ? undefined : options.onConnected()).catch((error) => {
|
|
52
|
+
log("binding recovery failed", { reason: sanitizeTerminalErrorDetail(String(error)) });
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const stop = () => {
|
|
56
|
+
if (stopped)
|
|
57
|
+
return;
|
|
58
|
+
stopped = true;
|
|
59
|
+
if (timer)
|
|
60
|
+
clearInterval(timer);
|
|
61
|
+
if (startupTimer)
|
|
62
|
+
clearTimeout(startupTimer);
|
|
63
|
+
try {
|
|
64
|
+
client?.close({ force: true });
|
|
65
|
+
}
|
|
66
|
+
catch { /* best effort shutdown */ }
|
|
67
|
+
rejectStartup?.(new Error("飞书长连接在就绪前已停止"));
|
|
68
|
+
resolveStartup = undefined;
|
|
69
|
+
rejectStartup = undefined;
|
|
70
|
+
log("stopped");
|
|
71
|
+
};
|
|
72
|
+
const launch = () => {
|
|
73
|
+
if (stopped || launching || !client)
|
|
74
|
+
return;
|
|
75
|
+
launching = true;
|
|
76
|
+
lastAttempt = Date.now();
|
|
77
|
+
unhealthySince = lastAttempt;
|
|
78
|
+
void Promise.resolve().then(() => stopped ? undefined : client.start())
|
|
79
|
+
.catch(reportError).finally(() => { launching = false; });
|
|
80
|
+
};
|
|
81
|
+
const check = () => {
|
|
82
|
+
if (stopped || !client)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
const status = client.getConnectionStatus();
|
|
86
|
+
if (status.state !== lastState) {
|
|
87
|
+
lastState = status.state;
|
|
88
|
+
log("state", { ...status });
|
|
89
|
+
}
|
|
90
|
+
if (status.state === "connected") {
|
|
91
|
+
becameConnected();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
connected = false;
|
|
95
|
+
unhealthySince ??= Date.now();
|
|
96
|
+
// SDK gets a bounded chance to recover. Silence from users is irrelevant.
|
|
97
|
+
if (launching || Date.now() - unhealthySince < stalledAfterMs
|
|
98
|
+
|| Date.now() - lastAttempt < stalledAfterMs)
|
|
99
|
+
return;
|
|
100
|
+
log("restarting receiving connection", { ...status, disconnectedForMs: Date.now() - unhealthySince });
|
|
101
|
+
client.close({ force: true });
|
|
102
|
+
launch();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
reportError(error);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const start = () => {
|
|
109
|
+
if (startup)
|
|
110
|
+
return startup;
|
|
111
|
+
if (stopped)
|
|
112
|
+
return Promise.reject(new Error("飞书长连接已停止"));
|
|
113
|
+
startup = new Promise((resolve, reject) => { resolveStartup = resolve; rejectStartup = reject; });
|
|
114
|
+
startupTimer = setTimeout(() => {
|
|
115
|
+
rejectStartup?.(new Error("飞书长连接握手超时,尚未收到连接成功确认"));
|
|
116
|
+
rejectStartup = undefined;
|
|
117
|
+
stop();
|
|
118
|
+
}, options.startupTimeoutMs ?? 45_000);
|
|
119
|
+
try {
|
|
120
|
+
client = options.createClient({
|
|
121
|
+
onReady: becameConnected,
|
|
122
|
+
onReconnected: becameConnected,
|
|
123
|
+
onReconnecting() {
|
|
124
|
+
if (stopped)
|
|
125
|
+
return;
|
|
126
|
+
connected = false;
|
|
127
|
+
unhealthySince ??= Date.now();
|
|
128
|
+
log("reconnecting");
|
|
129
|
+
},
|
|
130
|
+
onError: reportError,
|
|
131
|
+
});
|
|
132
|
+
timer = setInterval(check, options.checkIntervalMs ?? 10_000);
|
|
133
|
+
timer.unref();
|
|
134
|
+
launch();
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
rejectStartup?.(error instanceof Error ? error : new Error(String(error)));
|
|
138
|
+
rejectStartup = undefined;
|
|
139
|
+
stop();
|
|
140
|
+
}
|
|
141
|
+
return startup;
|
|
142
|
+
};
|
|
143
|
+
return { start, stop };
|
|
144
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { createServer } from "node:http";
|
|
25
25
|
import { WSClient, EventDispatcher, Domain } from "@larksuiteoapi/node-sdk";
|
|
26
|
+
import { createFeishuConnectionSupervisor } from "./feishu-connection.js";
|
|
26
27
|
import WebSocket from "ws";
|
|
27
28
|
import { appendStartupTrace, attachRelayWebSocket, ensureSingleInstance, freeRelayListenPort, installCrashLogging, installEpipeGuard, waitForPortFree } from "./shared.js";
|
|
28
29
|
import { createUiRouter, setExtraApiHandler, setReloadConfigHook, startSetupMode } from "./web-ui.js";
|
|
29
30
|
import { configureAgentTeamMainAgent } from "./agent-team/main-agent-bootstrap.js";
|
|
30
|
-
import { buildWebUiUrl, createServiceLifecycleGuard, INTERNAL_RESTART_ENV_VAR, openWebUiInDefaultBrowser, shouldAutoOpenWebUi, } from "./startup-lifecycle.js";
|
|
31
|
+
import { buildWebUiUrl, createServiceLifecycleGuard, announceInternalRestartReady, INTERNAL_RESTART_ENV_VAR, openWebUiInDefaultBrowser, shouldAutoOpenWebUi, } from "./startup-lifecycle.js";
|
|
31
32
|
import { buildPlatformStartupPlan } from "./platform-startup.js";
|
|
32
33
|
import { makeTraceId, logTrace } from "./trace.js";
|
|
33
34
|
import { CHATCCC_PORT, config, APP_ID, APP_SECRET, FEISHU_ENABLED, FEISHU_PLATFORM_TYPE, ILINK_ENABLED, ILINK_REUSE_TOKEN_ON_START, BASE_URL, LOCAL_RELAY_URL, PID_FILE, PROJECT_ROOT, USE_LOCAL, USE_SIMULATE, appendChatLog, fileLog, reportEnvironmentVariableReadout, maskAppId, resolveDefaultAgentTool, toolDisplayName, ts, } from "./config.js";
|
|
@@ -256,6 +257,7 @@ async function startBotService(opts) {
|
|
|
256
257
|
throw err;
|
|
257
258
|
}
|
|
258
259
|
}
|
|
260
|
+
let feishuConnection;
|
|
259
261
|
async function startBotServiceCore() {
|
|
260
262
|
const modeTag = USE_LOCAL ? " (local relay mode)" : "";
|
|
261
263
|
console.log(`${"=".repeat(60)}`);
|
|
@@ -420,20 +422,33 @@ async function startBotServiceCore() {
|
|
|
420
422
|
// - 已处理消息的去重 set 必须保留,避免 SDK 重推老消息时 prompt 跑两遍
|
|
421
423
|
// 历史 bug:此处曾误调 resetState() 导致重连即让所有后台任务变孤儿,
|
|
422
424
|
// 同一 session 还可能双开 prompt(详见 session.ts::resetState 注释)。
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
425
|
+
feishuConnection?.stop();
|
|
426
|
+
feishuConnection = createFeishuConnectionSupervisor({
|
|
427
|
+
createClient(callbacks) {
|
|
428
|
+
const client = new WSClient({
|
|
429
|
+
appId: APP_ID,
|
|
430
|
+
appSecret: APP_SECRET,
|
|
431
|
+
domain: FEISHU_PLATFORM_TYPE === "lark" ? Domain.Lark : Domain.Feishu,
|
|
432
|
+
autoReconnect: true,
|
|
433
|
+
handshakeTimeoutMs: 15_000,
|
|
434
|
+
wsConfig: { pingTimeout: 30 },
|
|
435
|
+
...callbacks,
|
|
436
|
+
});
|
|
437
|
+
return {
|
|
438
|
+
start: () => client.start({ eventDispatcher }),
|
|
439
|
+
close: (options) => client.close(options),
|
|
440
|
+
getConnectionStatus: () => client.getConnectionStatus(),
|
|
441
|
+
};
|
|
429
442
|
},
|
|
430
|
-
|
|
431
|
-
|
|
443
|
+
onConnected: rebuildBindingsFromRegistry,
|
|
444
|
+
log(event, detail) {
|
|
445
|
+
appendStartupTrace(`feishu-connection: ${event}`, detail);
|
|
446
|
+
console.log(`[${ts()}] [FEISHU-CONNECTION] ${event}${detail ? ` ${JSON.stringify(detail)}` : ""}`);
|
|
432
447
|
},
|
|
433
448
|
});
|
|
434
449
|
console.log(`\n[启动 6/7] 飞书长连接:正在通过 SDK 建立 WebSocket …`);
|
|
435
450
|
try {
|
|
436
|
-
await
|
|
451
|
+
await feishuConnection.start();
|
|
437
452
|
}
|
|
438
453
|
catch (err) {
|
|
439
454
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -609,6 +624,23 @@ async function main() {
|
|
|
609
624
|
printServiceDidNotStart("config.json 的 port 字段配置无效(须为 1–65535 的整数)");
|
|
610
625
|
process.exit(1);
|
|
611
626
|
}
|
|
627
|
+
const isInternalRestart = process.env[INTERNAL_RESTART_ENV_VAR] === "1";
|
|
628
|
+
if (isInternalRestart) {
|
|
629
|
+
const announced = await announceInternalRestartReady();
|
|
630
|
+
appendStartupTrace("restart-handoff: replacement preflight ready", {
|
|
631
|
+
announced,
|
|
632
|
+
port: CHATCCC_PORT,
|
|
633
|
+
});
|
|
634
|
+
const portReleased = await waitForPortFree(CHATCCC_PORT, 30_000);
|
|
635
|
+
appendStartupTrace("restart-handoff: parent port wait finished", {
|
|
636
|
+
port: CHATCCC_PORT,
|
|
637
|
+
portReleased,
|
|
638
|
+
});
|
|
639
|
+
if (!portReleased) {
|
|
640
|
+
printServiceDidNotStart(`内部重启交接超时:端口 ${CHATCCC_PORT} 在 30 秒内未释放`);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
612
644
|
console.log(`\n[启动 1/7] 单实例:按 PID 文件清理旧 ChatCCC 进程`);
|
|
613
645
|
console.log(` PID 文件: ${PID_FILE}`);
|
|
614
646
|
appendStartupTrace("main: before ensureSingleInstance", { PID_FILE, CHATCCC_PORT });
|
|
@@ -680,14 +712,15 @@ async function main() {
|
|
|
680
712
|
// 启动 HTTP server(同时挂 UI router,供 dashboard / setup / agent image/file 使用)
|
|
681
713
|
appendStartupTrace("main: before freeRelayListenPort", { CHATCCC_PORT });
|
|
682
714
|
const killed = freeRelayListenPort(CHATCCC_PORT);
|
|
683
|
-
const isInternalRestart = process.env[INTERNAL_RESTART_ENV_VAR] === "1";
|
|
684
715
|
appendStartupTrace("main: after freeRelayListenPort", { CHATCCC_PORT, killed, isInternalRestart });
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
716
|
+
// 普通启动清理过旧实例后,也必须确认端口真正释放再继续监听。
|
|
717
|
+
if (killed > 0) {
|
|
718
|
+
const portReleased = await waitForPortFree(CHATCCC_PORT, 5_000);
|
|
719
|
+
appendStartupTrace("main: post-kill port wait finished", { CHATCCC_PORT, portReleased });
|
|
720
|
+
if (!portReleased) {
|
|
721
|
+
printServiceDidNotStart(`旧进程退出后端口 ${CHATCCC_PORT} 在 5 秒内仍未释放`);
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
691
724
|
}
|
|
692
725
|
const httpServer = createServer(createUiRouter());
|
|
693
726
|
await listenWithRetry(httpServer, CHATCCC_PORT, "127.0.0.1").catch((err) => {
|
|
@@ -772,6 +805,7 @@ function installShutdownHandlers(httpServer, serviceLifecycle) {
|
|
|
772
805
|
process.on("SIGINT", () => {
|
|
773
806
|
console.log("\nShutting down...");
|
|
774
807
|
serviceLifecycle.beginShutdown("SIGINT");
|
|
808
|
+
feishuConnection?.stop();
|
|
775
809
|
wechatSignal.stopped = true;
|
|
776
810
|
stopChromeDevtoolsGuard();
|
|
777
811
|
httpServer.close();
|
|
@@ -779,6 +813,7 @@ function installShutdownHandlers(httpServer, serviceLifecycle) {
|
|
|
779
813
|
});
|
|
780
814
|
process.on("SIGTERM", () => {
|
|
781
815
|
serviceLifecycle.beginShutdown("SIGTERM");
|
|
816
|
+
feishuConnection?.stop();
|
|
782
817
|
wechatSignal.stopped = true;
|
|
783
818
|
stopChromeDevtoolsGuard();
|
|
784
819
|
httpServer.close();
|
package/dist/src/orchestrator.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { execSync, spawn } from "node:child_process";
|
|
8
8
|
import { readdir, stat } from "node:fs/promises";
|
|
9
|
-
import { appendFileSync, closeSync,
|
|
9
|
+
import { appendFileSync, closeSync, mkdirSync, openSync } from "node:fs";
|
|
10
10
|
import { join, resolve, dirname } from "node:path";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { config as deepCccConfig } from "../deepccc-agent/src/config.js";
|
|
@@ -25,7 +25,7 @@ import { applySharedPrefix } from "./shared-prefix.js";
|
|
|
25
25
|
import { normalizeSessionDisplayTitle, sessionChatName, sessionDisplayTitleFromPrompt, } from "./session-name.js";
|
|
26
26
|
import { reloadRuntimeConfig } from "./runtime-reload.js";
|
|
27
27
|
import { acquireUpdateCommandGuard } from "./update-command-guard.js";
|
|
28
|
-
import { createInternalRestartEnv, INTERNAL_RESTART_ENV_VAR } from "./startup-lifecycle.js";
|
|
28
|
+
import { createInternalRestartEnv, INTERNAL_RESTART_ENV_VAR, INTERNAL_RESTART_READY_MESSAGE, } from "./startup-lifecycle.js";
|
|
29
29
|
import { resolveChatCccRuntimeSpawnSpec } from "./runtime-entry.js";
|
|
30
30
|
import { engineManager } from "./engines/engine-specs.js";
|
|
31
31
|
import { beginSafeMaintenanceTrackedWork, isSafeMaintenanceAdmissionClosed, safeMaintenanceCoordinator, } from "./safe-maintenance.js";
|
|
@@ -678,41 +678,18 @@ function syncUpdateAndRestart(options = {}) {
|
|
|
678
678
|
appendStartupTrace("update: safe update aborted before restart", {});
|
|
679
679
|
return undefined;
|
|
680
680
|
}
|
|
681
|
-
// 2.
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
appendStartupTrace("update: spawn begin", { npmPrefix: npmPrefix || "(empty)", binPath });
|
|
687
|
-
// 3. spawn new chatccc:优先 node + 全局包入口绝对路径(不依赖 PATH/shell),
|
|
688
|
-
// 避免继承环境 PATH 异常时秒退;失败时回退到 binPath(走 shell)。
|
|
681
|
+
// 2. Spawn the updated runtime directly through Node. Reusing the restart
|
|
682
|
+
// launcher guarantees an IPC handoff channel and avoids shell/PATH variance.
|
|
683
|
+
const spawnSpec = buildRestartSpawnSpec(PROJECT_ROOT);
|
|
684
|
+
updLog(`runtime path: ${spawnSpec.command} ${spawnSpec.args.join(" ")}`);
|
|
685
|
+
appendStartupTrace("update: spawn begin", { runtimeEntry: spawnSpec.args[0] });
|
|
689
686
|
try {
|
|
690
|
-
|
|
691
|
-
if (npmPrefix) {
|
|
692
|
-
const entry = join(npmPrefix, "node_modules", "chatccc", "bin", "chatccc.mjs");
|
|
693
|
-
if (existsSync(entry)) {
|
|
694
|
-
spawnSpec = { command: process.execPath, args: [entry] };
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
const child = spawnSpec
|
|
698
|
-
? spawn(spawnSpec.command, spawnSpec.args, {
|
|
699
|
-
detached: true,
|
|
700
|
-
stdio: "ignore",
|
|
701
|
-
shell: false,
|
|
702
|
-
env: createInternalRestartEnv(),
|
|
703
|
-
})
|
|
704
|
-
: spawn(binPath, [], {
|
|
705
|
-
detached: true,
|
|
706
|
-
stdio: "ignore",
|
|
707
|
-
shell: true,
|
|
708
|
-
env: createInternalRestartEnv(),
|
|
709
|
-
});
|
|
687
|
+
const child = spawnRestartChild({ projectRoot: PROJECT_ROOT });
|
|
710
688
|
child.unref();
|
|
711
|
-
|
|
712
|
-
updLog(`spawn new chatccc OK, childPid=${child.pid}, bin=${spawnedAs}`);
|
|
689
|
+
updLog(`spawn new chatccc OK, childPid=${child.pid}, bin=${spawnSpec.command} ${spawnSpec.args.join(" ")}`);
|
|
713
690
|
appendStartupTrace("update: spawn OK", {
|
|
714
691
|
childPid: child.pid,
|
|
715
|
-
binPath: spawnSpec
|
|
692
|
+
binPath: spawnSpec.args[0],
|
|
716
693
|
});
|
|
717
694
|
return child;
|
|
718
695
|
}
|
|
@@ -726,8 +703,8 @@ function syncUpdateAndRestart(options = {}) {
|
|
|
726
703
|
// ---------------------------------------------------------------------------
|
|
727
704
|
// /restart — 自重启子进程(不经过 npx/npm,避免 PATH 注入秒退;防空窗兜底)
|
|
728
705
|
// ---------------------------------------------------------------------------
|
|
729
|
-
/**
|
|
730
|
-
export const RESTART_CHILD_READY_MS =
|
|
706
|
+
/** 父进程等待替代进程通过 IPC 完成启动预检的最长时间(毫秒)。 */
|
|
707
|
+
export const RESTART_CHILD_READY_MS = 15_000;
|
|
731
708
|
/**
|
|
732
709
|
* 构建自重启的 spawn 参数:发布包直接运行编译后的 JavaScript;只有尚未
|
|
733
710
|
* build 的开发工作区才使用本地 tsx CLI。两种情况都不经过 npx/npm。
|
|
@@ -735,12 +712,64 @@ export const RESTART_CHILD_READY_MS = 3000;
|
|
|
735
712
|
export function buildRestartSpawnSpec(projectRoot = PROJECT_ROOT) {
|
|
736
713
|
return resolveChatCccRuntimeSpawnSpec(projectRoot);
|
|
737
714
|
}
|
|
715
|
+
const restartHandoffMonitors = new WeakMap();
|
|
716
|
+
function getOrCreateRestartHandoffMonitor(child) {
|
|
717
|
+
const existing = restartHandoffMonitors.get(child);
|
|
718
|
+
if (existing)
|
|
719
|
+
return existing;
|
|
720
|
+
if (typeof child.on !== "function") {
|
|
721
|
+
const unavailable = { promise: Promise.resolve({ kind: "no_ipc" }), cancel() { } };
|
|
722
|
+
restartHandoffMonitors.set(child, unavailable);
|
|
723
|
+
return unavailable;
|
|
724
|
+
}
|
|
725
|
+
const addListener = child.on.bind(child);
|
|
726
|
+
const removeListener = typeof child.off === "function"
|
|
727
|
+
? child.off.bind(child)
|
|
728
|
+
: undefined;
|
|
729
|
+
let resolveOutcome = () => { };
|
|
730
|
+
let settled = false;
|
|
731
|
+
const cleanup = () => {
|
|
732
|
+
removeListener?.("message", onMessage);
|
|
733
|
+
removeListener?.("exit", onExit);
|
|
734
|
+
removeListener?.("error", onError);
|
|
735
|
+
};
|
|
736
|
+
const settle = (outcome) => {
|
|
737
|
+
if (settled)
|
|
738
|
+
return;
|
|
739
|
+
settled = true;
|
|
740
|
+
cleanup();
|
|
741
|
+
resolveOutcome(outcome);
|
|
742
|
+
};
|
|
743
|
+
const onMessage = (message) => {
|
|
744
|
+
const ready = message;
|
|
745
|
+
if (!ready || ready.type !== INTERNAL_RESTART_READY_MESSAGE)
|
|
746
|
+
return;
|
|
747
|
+
if (typeof ready.pid === "number" && child.pid !== undefined && ready.pid !== child.pid)
|
|
748
|
+
return;
|
|
749
|
+
if (ready.parentPid !== process.pid)
|
|
750
|
+
return;
|
|
751
|
+
settle({ kind: "ready" });
|
|
752
|
+
};
|
|
753
|
+
const onExit = () => settle({ kind: "exit" });
|
|
754
|
+
const onError = (error) => settle({ kind: "error", error });
|
|
755
|
+
const promise = new Promise((resolve) => {
|
|
756
|
+
resolveOutcome = resolve;
|
|
757
|
+
});
|
|
758
|
+
const monitor = { promise, cancel: cleanup };
|
|
759
|
+
restartHandoffMonitors.set(child, monitor);
|
|
760
|
+
addListener("message", onMessage);
|
|
761
|
+
addListener("exit", onExit);
|
|
762
|
+
addListener("error", onError);
|
|
763
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
764
|
+
settle({ kind: "exit" });
|
|
765
|
+
return monitor;
|
|
766
|
+
}
|
|
738
767
|
/**
|
|
739
768
|
* spawn 自重启子进程。
|
|
740
769
|
*
|
|
741
770
|
* stdout/stderr 按启动方式分流:
|
|
742
771
|
* - **终端(TTY)场景**(用户从 cmd/PowerShell/node.exe 窗口启动):stdio 用
|
|
743
|
-
*
|
|
772
|
+
* 前三个 stdio 直接继承终端句柄,第四个通道保留给 IPC 握手;restart 后窗口日志不中断。
|
|
744
773
|
* 终端句柄的生命周期不随父进程退出而关闭,因此不存在 EPIPE 风险。
|
|
745
774
|
* - **非终端场景**(守护进程/黑匣子等管道或文件启动):stderr 重定向到磁盘日志
|
|
746
775
|
* 文件(restart-*.log),子进程继承文件句柄,父进程退出不影响写入。
|
|
@@ -756,7 +785,7 @@ export function spawnRestartChild(deps = {}) {
|
|
|
756
785
|
const isTty = deps.isTty ?? (() => process.stdout.isTTY === true || process.stderr.isTTY === true);
|
|
757
786
|
const { command, args } = buildRestartSpawnSpec(projectRoot);
|
|
758
787
|
let stderrFd;
|
|
759
|
-
const stdio = ["ignore", "ignore", "pipe"];
|
|
788
|
+
const stdio = ["ignore", "ignore", "pipe", "ipc"];
|
|
760
789
|
const tty = isTty();
|
|
761
790
|
if (tty) {
|
|
762
791
|
// 终端场景:全部 inherit(含 stdin)。注意 stdin 不能是 "ignore":
|
|
@@ -793,8 +822,11 @@ export function spawnRestartChild(deps = {}) {
|
|
|
793
822
|
detached: true,
|
|
794
823
|
stdio,
|
|
795
824
|
shell: false,
|
|
796
|
-
env: createInternalRestartEnv(),
|
|
825
|
+
env: createInternalRestartEnv(process.env, process.pid),
|
|
797
826
|
});
|
|
827
|
+
// Attach before returning so an extremely fast replacement cannot emit its
|
|
828
|
+
// IPC readiness message between spawnRestartChild() and the caller's wait.
|
|
829
|
+
getOrCreateRestartHandoffMonitor(child);
|
|
798
830
|
// 子进程已继承 stderr 文件句柄;父进程关闭自己的副本,避免"子进程早退、
|
|
799
831
|
// 父进程留下继续服务"时 fd 泄漏。
|
|
800
832
|
if (stderrFd !== undefined) {
|
|
@@ -817,24 +849,61 @@ export function spawnRestartChild(deps = {}) {
|
|
|
817
849
|
}
|
|
818
850
|
/**
|
|
819
851
|
* 决定父进程是否应退出(防空窗兜底):
|
|
820
|
-
* -
|
|
821
|
-
* -
|
|
852
|
+
* - 替代进程通过 IPC 明确完成预检 → 返回 true,父进程退出并交出端口;
|
|
853
|
+
* - 替代进程退出、报错或握手超时 → 返回 false,父进程继续服务。
|
|
822
854
|
*/
|
|
823
|
-
export async function decideRestartParentExit(child, timeoutMs,
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
855
|
+
export async function decideRestartParentExit(child, timeoutMs, _pollMs = 500, trace = appendStartupTrace) {
|
|
856
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
857
|
+
trace("restart: child died during window, keeping parent", {
|
|
858
|
+
childPid: child.pid,
|
|
859
|
+
exitCode: child.exitCode,
|
|
860
|
+
signalCode: child.signalCode,
|
|
861
|
+
});
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
const monitor = getOrCreateRestartHandoffMonitor(child);
|
|
865
|
+
let timeout;
|
|
866
|
+
const timeoutOutcome = new Promise((resolve) => {
|
|
867
|
+
timeout = setTimeout(() => resolve({ kind: "no_ipc" }), timeoutMs);
|
|
868
|
+
timeout.unref?.();
|
|
869
|
+
});
|
|
870
|
+
const outcome = await Promise.race([monitor.promise, timeoutOutcome]);
|
|
871
|
+
if (timeout)
|
|
872
|
+
clearTimeout(timeout);
|
|
873
|
+
monitor.cancel();
|
|
874
|
+
if (outcome.kind === "ready") {
|
|
875
|
+
if (child.connected && typeof child.disconnect === "function") {
|
|
876
|
+
try {
|
|
877
|
+
child.disconnect();
|
|
878
|
+
}
|
|
879
|
+
catch { /* child may disconnect first */ }
|
|
880
|
+
}
|
|
881
|
+
trace("restart: child handoff ready, parent exiting", { childPid: child.pid });
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
if (outcome.kind === "exit") {
|
|
885
|
+
trace("restart: child died during window, keeping parent", {
|
|
886
|
+
childPid: child.pid,
|
|
887
|
+
exitCode: child.exitCode,
|
|
888
|
+
signalCode: child.signalCode,
|
|
889
|
+
});
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
if (outcome.kind === "error") {
|
|
893
|
+
trace("restart: child handoff error, keeping parent", {
|
|
894
|
+
childPid: child.pid,
|
|
895
|
+
error: outcome.error.message,
|
|
896
|
+
});
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
if (typeof child.kill === "function") {
|
|
900
|
+
try {
|
|
901
|
+
child.kill();
|
|
833
902
|
}
|
|
834
|
-
|
|
903
|
+
catch { /* best effort */ }
|
|
835
904
|
}
|
|
836
|
-
trace("restart: child
|
|
837
|
-
return
|
|
905
|
+
trace("restart: child handoff timeout, keeping parent", { childPid: child.pid });
|
|
906
|
+
return false;
|
|
838
907
|
}
|
|
839
908
|
const safeMaintenancePlatforms = new Map();
|
|
840
909
|
export function configureSafeMaintenanceRuntime(platforms) {
|
|
@@ -1051,8 +1120,8 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
1051
1120
|
appendStartupTrace("restart: spawn begin", { fromPid: process.pid });
|
|
1052
1121
|
const child = spawnRestartChild();
|
|
1053
1122
|
child.unref();
|
|
1054
|
-
//
|
|
1055
|
-
//
|
|
1123
|
+
// 只有替代进程通过 IPC 明确完成预检才退出父进程;超时或早退时父进程
|
|
1124
|
+
// 继续服务,并保留替代进程 stderr 日志供排查。
|
|
1056
1125
|
void decideRestartParentExit(child, RESTART_CHILD_READY_MS).then((shouldExit) => {
|
|
1057
1126
|
if (!shouldExit)
|
|
1058
1127
|
return;
|
|
@@ -1100,10 +1169,9 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
1100
1169
|
await platform.sendText(chatId, "正在更新并重启,请稍候...").catch(() => { });
|
|
1101
1170
|
logTrace(tid, "DONE", { outcome: "update" });
|
|
1102
1171
|
appendStartupTrace("update: sync update begin", { fromPid: process.pid });
|
|
1103
|
-
const child = syncUpdateAndRestart();
|
|
1172
|
+
const child = syncUpdateAndRestart({ spawnOnUpdateFailure: false });
|
|
1104
1173
|
if (child) {
|
|
1105
|
-
//
|
|
1106
|
-
// 服务(防空窗)。
|
|
1174
|
+
// 只有替代进程通过 IPC 明确完成预检才退出父进程;否则父进程继续服务。
|
|
1107
1175
|
void decideRestartParentExit(child, RESTART_CHILD_READY_MS).then((shouldExit) => {
|
|
1108
1176
|
if (!shouldExit)
|
|
1109
1177
|
return;
|
|
@@ -1112,8 +1180,7 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
1112
1180
|
});
|
|
1113
1181
|
}
|
|
1114
1182
|
else {
|
|
1115
|
-
|
|
1116
|
-
setTimeout(() => process.exit(0), 2000);
|
|
1183
|
+
appendStartupTrace("update: replacement unavailable, parent stays alive", {});
|
|
1117
1184
|
}
|
|
1118
1185
|
return;
|
|
1119
1186
|
}
|
package/dist/src/session.js
CHANGED
|
@@ -1785,7 +1785,6 @@ export function startUnifiedDisplayLoop() {
|
|
|
1785
1785
|
console.error(`[${ts()}] [DISPLAY] terminal cardUpdate failed: ${err.message}`);
|
|
1786
1786
|
if (isCardKitSequenceConflict(err)) {
|
|
1787
1787
|
display.sequence = nextSeq;
|
|
1788
|
-
terminalCardUpdateAccepted = true;
|
|
1789
1788
|
}
|
|
1790
1789
|
});
|
|
1791
1790
|
if (terminalCardUpdateAccepted) {
|
|
@@ -1943,6 +1942,8 @@ export function startUnifiedDisplayLoop() {
|
|
|
1943
1942
|
catch (err) {
|
|
1944
1943
|
const errMsg = err.message;
|
|
1945
1944
|
console.error(`[${ts()}] CardKit update error: chatId=${chatId} ${errMsg}`);
|
|
1945
|
+
display.lastSentContent = "";
|
|
1946
|
+
display.lastSentHeaderTitle = "";
|
|
1946
1947
|
if (errMsg.includes("300317")) {
|
|
1947
1948
|
display.sequence = mySeq;
|
|
1948
1949
|
}
|
|
@@ -1977,6 +1978,8 @@ export function startUnifiedDisplayLoop() {
|
|
|
1977
1978
|
catch (err) {
|
|
1978
1979
|
const errMsg = err.message;
|
|
1979
1980
|
console.error(`[${ts()}] CardKit update error: chatId=${chatId} ${errMsg}`);
|
|
1981
|
+
display.lastSentContent = "";
|
|
1982
|
+
display.lastSentHeaderTitle = "";
|
|
1980
1983
|
if (errMsg.includes("300317")) {
|
|
1981
1984
|
display.sequence = mySeq;
|
|
1982
1985
|
}
|
package/dist/src/shared.js
CHANGED
|
@@ -164,25 +164,42 @@ export function freeRelayListenPort(port) {
|
|
|
164
164
|
return killed;
|
|
165
165
|
}
|
|
166
166
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
167
|
+
* 跨平台轮询等待端口释放。既用于 Windows taskkill 后的延迟释放,也用于
|
|
168
|
+
* Linux/macOS 内部重启时等待父进程交出监听端口。
|
|
169
169
|
*/
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const pids = collectListeningPidsOnPortWindows(port, out);
|
|
178
|
-
if (pids.length === 0)
|
|
170
|
+
async function canBindLoopbackPort(port) {
|
|
171
|
+
const probe = createServer();
|
|
172
|
+
probe.unref();
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
let settled = false;
|
|
175
|
+
const finish = (available) => {
|
|
176
|
+
if (settled)
|
|
179
177
|
return;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
settled = true;
|
|
179
|
+
probe.removeAllListeners();
|
|
180
|
+
if (probe.listening) {
|
|
181
|
+
probe.close(() => resolve(available));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
resolve(available);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
probe.once("error", () => finish(false));
|
|
188
|
+
probe.once("listening", () => finish(true));
|
|
189
|
+
probe.listen(port, "127.0.0.1");
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/** Wait until the loopback port can actually be bound on every supported OS. */
|
|
193
|
+
export async function waitForPortFree(port, timeoutMs = 3000, pollMs = 200) {
|
|
194
|
+
const deadline = Date.now() + timeoutMs;
|
|
195
|
+
do {
|
|
196
|
+
if (await canBindLoopbackPort(port))
|
|
197
|
+
return true;
|
|
198
|
+
if (Date.now() >= deadline)
|
|
199
|
+
break;
|
|
200
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
201
|
+
} while (Date.now() <= deadline);
|
|
202
|
+
return false;
|
|
186
203
|
}
|
|
187
204
|
// ---------------------------------------------------------------------------
|
|
188
205
|
// 单实例保证:PID 文件互斥(端口占用在 freeRelayListenPort + 监听前处理)
|
|
@@ -147,12 +147,54 @@ export function createServiceLifecycleGuard(options = {}) {
|
|
|
147
147
|
* 会自然穿过 cmd/bash/npx 这几层启动器,因此也适用于 Windows 与 Linux。
|
|
148
148
|
*/
|
|
149
149
|
export const INTERNAL_RESTART_ENV_VAR = "CHATCCC_INTERNAL_RESTART";
|
|
150
|
-
export
|
|
150
|
+
export const INTERNAL_RESTART_PARENT_PID_ENV_VAR = "CHATCCC_RESTART_PARENT_PID";
|
|
151
|
+
export const INTERNAL_RESTART_READY_MESSAGE = "chatccc:restart-handoff-ready";
|
|
152
|
+
export function createInternalRestartEnv(inherited = process.env, parentPid = process.pid) {
|
|
151
153
|
return {
|
|
152
154
|
...inherited,
|
|
153
155
|
[INTERNAL_RESTART_ENV_VAR]: "1",
|
|
156
|
+
[INTERNAL_RESTART_PARENT_PID_ENV_VAR]: String(parentPid),
|
|
154
157
|
};
|
|
155
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Tell the current parent that the replacement runtime loaded successfully and
|
|
161
|
+
* is ready to wait for the listening port. Older parents do not provide IPC;
|
|
162
|
+
* returning false preserves the port-wait fallback used during an upgrade from
|
|
163
|
+
* a pre-handoff ChatCCC version.
|
|
164
|
+
*/
|
|
165
|
+
export async function announceInternalRestartReady(options = {}) {
|
|
166
|
+
const env = options.env ?? process.env;
|
|
167
|
+
if (env[INTERNAL_RESTART_ENV_VAR] !== "1")
|
|
168
|
+
return false;
|
|
169
|
+
const send = options.send ?? (typeof process.send === "function"
|
|
170
|
+
? process.send.bind(process)
|
|
171
|
+
: undefined);
|
|
172
|
+
if (!send)
|
|
173
|
+
return false;
|
|
174
|
+
const pid = options.pid ?? process.pid;
|
|
175
|
+
const parentPid = Number(env[INTERNAL_RESTART_PARENT_PID_ENV_VAR]);
|
|
176
|
+
if (!Number.isInteger(parentPid) || parentPid <= 0)
|
|
177
|
+
return false;
|
|
178
|
+
const timeoutMs = options.timeoutMs ?? 2_000;
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
let settled = false;
|
|
181
|
+
const finish = (ok) => {
|
|
182
|
+
if (settled)
|
|
183
|
+
return;
|
|
184
|
+
settled = true;
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
resolve(ok);
|
|
187
|
+
};
|
|
188
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
189
|
+
timer.unref?.();
|
|
190
|
+
try {
|
|
191
|
+
send({ type: INTERNAL_RESTART_READY_MESSAGE, pid, parentPid }, (error) => finish(!error));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
finish(false);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
156
198
|
export function shouldAutoOpenWebUi(options = {}) {
|
|
157
199
|
const env = options.env ?? process.env;
|
|
158
200
|
return options.openOnStart !== false && env[INTERNAL_RESTART_ENV_VAR] !== "1";
|
|
@@ -64,7 +64,7 @@ export function classifyTerminalError(error, occurredAt = Date.now()) {
|
|
|
64
64
|
occurredAt,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
if (/econnrefused|econnreset|enotfound|eai_again|socket hang up|network error|cannot connect/.test(lower)) {
|
|
67
|
+
if (/econnrefused|econnreset|enotfound|eai_again|socket hang up|network error|cannot connect|tls handshake|before secure tls connection|stream disconnected before completion|websocket closed/.test(lower)) {
|
|
68
68
|
return {
|
|
69
69
|
kind: "network",
|
|
70
70
|
title: "无法连接模型服务",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.282",
|
|
4
4
|
"description": "Feishu bot bridge for Claude Code",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@ai-sdk/anthropic": "^3.0.105",
|
|
55
55
|
"@ai-sdk/openai-compatible": "^2.0.47",
|
|
56
|
-
"@larksuiteoapi/node-sdk": "^1.
|
|
56
|
+
"@larksuiteoapi/node-sdk": "^1.66.1",
|
|
57
57
|
"@openilink/openilink-sdk-node": "^0.6.0",
|
|
58
58
|
"@vscode/ripgrep": "^1.18.0",
|
|
59
59
|
"ai": "^6.0.184",
|