@keyou007/agent-bot 0.1.13-alpha.0 → 0.1.13-alpha.1
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/.env.example +2 -2
- package/CHANGELOG.md +9 -0
- package/README.md +10 -4
- package/README.zh.md +10 -4
- package/dist/cli/ServerStarter.js +17 -2
- package/dist/cli/ServerStarter.js.map +1 -1
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +12 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli.js +39 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/FeishuUserOpenIdStore.js +76 -0
- package/dist/config/FeishuUserOpenIdStore.js.map +1 -0
- package/dist/config/loadConfig.js +3 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/feishu/CardRenderer.js +82 -22
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/index.js +103 -23
- package/dist/index.js.map +1 -1
- package/dist/presentation/turnGraph.js +36 -0
- package/dist/presentation/turnGraph.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +270 -47
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +16 -7
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/startup/startFeishu.js +2 -2
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/state/StateStore.js +169 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +21 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +46 -3
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +65 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/replacementSupervisor.js +27 -2
- package/dist/supervision/replacementSupervisor.js.map +1 -1
- package/dist/supervisor.js +27 -2
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +11 -4
- package/docs/technical-reference.zh.md +11 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +10 -4
- package/src/cli/ServerStarter.ts +25 -2
- package/src/cli/controlProtocol.ts +6 -1
- package/src/cli/help.ts +12 -2
- package/src/cli.ts +57 -0
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/config/FeishuUserOpenIdStore.ts +81 -0
- package/src/config/loadConfig.ts +3 -0
- package/src/feishu/CardRenderer.ts +109 -22
- package/src/index.ts +161 -21
- package/src/presentation/turnGraph.ts +48 -0
- package/src/proxy/ProxySessionController.ts +323 -55
- package/src/startup/StartupNotifier.ts +23 -7
- package/src/startup/startFeishu.ts +3 -2
- package/src/state/StateStore.ts +221 -0
- package/src/state/migrations.ts +21 -0
- package/src/supervision/SafeRestartNotifier.ts +53 -4
- package/src/supervision/SafeRestartScheduler.ts +78 -7
- package/src/supervision/replacementSupervisor.ts +27 -1
- package/src/supervisor.ts +33 -2
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { LocalControlServer } from "./cli/LocalControlServer.js";
|
|
|
5
5
|
import { controlEndpoint, type ControlRequest, type ControlResponse } from "./cli/controlProtocol.js";
|
|
6
6
|
import { readPackageVersion } from "./cli/packageVersion.js";
|
|
7
7
|
import { loadConfig } from "./config/loadConfig.js";
|
|
8
|
+
import { persistFeishuUserOpenIdIfMissing } from "./config/FeishuUserOpenIdStore.js";
|
|
9
|
+
import { defaultDotEnvPath } from "./config/paths.js";
|
|
8
10
|
import { ConsoleConnector } from "./console/ConsoleConnector.js";
|
|
9
11
|
import { ConsoleTurnPresenter } from "./console/ConsoleTurnPresenter.js";
|
|
10
12
|
import { CardRenderer } from "./feishu/CardRenderer.js";
|
|
@@ -12,6 +14,8 @@ import { ConsoleFeishuClient } from "./feishu/ConsoleFeishuClient.js";
|
|
|
12
14
|
import { FeishuConnector } from "./feishu/FeishuConnector.js";
|
|
13
15
|
import { FeishuMessageClient } from "./feishu/FeishuMessageClient.js";
|
|
14
16
|
import { FeishuTurnPresenter } from "./feishu/FeishuTurnPresenter.js";
|
|
17
|
+
import { baseChatContextKey, isThreadContextKey } from "./feishu/contextKey.js";
|
|
18
|
+
import type { MessageReplyTarget } from "./feishu/types.js";
|
|
15
19
|
import { requireServerFeishuTransport } from "./feishu/transport.js";
|
|
16
20
|
import { createLogger } from "./logging/logger.js";
|
|
17
21
|
import { OutboundRouter, type OutboundRoute } from "./presentation/OutboundRouter.js";
|
|
@@ -22,8 +26,13 @@ import { StartupNotifier } from "./startup/StartupNotifier.js";
|
|
|
22
26
|
import { SessionMetadataHydrator } from "./startup/SessionMetadataHydrator.js";
|
|
23
27
|
import { startFeishu } from "./startup/startFeishu.js";
|
|
24
28
|
import { STOP_EXIT_CODE } from "./supervision/restartPolicy.js";
|
|
25
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
RESTART_GROUP_CONTEXTS_ENV,
|
|
31
|
+
replacementSupervisorEnvironment,
|
|
32
|
+
restartGroupContextKeysFromEnvironment,
|
|
33
|
+
} from "./supervision/replacementSupervisor.js";
|
|
26
34
|
import { SafeRestartScheduler } from "./supervision/SafeRestartScheduler.js";
|
|
35
|
+
import type { RestartNotificationTarget } from "./supervision/SafeRestartScheduler.js";
|
|
27
36
|
import { SafeRestartNotifier } from "./supervision/SafeRestartNotifier.js";
|
|
28
37
|
import {
|
|
29
38
|
nodeDiagnosticReportArguments,
|
|
@@ -37,6 +46,9 @@ const agentBotVersion = readPackageVersion();
|
|
|
37
46
|
const supervised = process.env.AGENT_BOT_SUPERVISED === "1";
|
|
38
47
|
const startupReason = process.env.AGENT_BOT_RESTART_REASON?.trim()
|
|
39
48
|
|| (supervised ? "Supervisor 启动" : "直接启动");
|
|
49
|
+
const restartGroupContextKeys = restartGroupContextKeysFromEnvironment(
|
|
50
|
+
process.env[RESTART_GROUP_CONTEXTS_ENV],
|
|
51
|
+
);
|
|
40
52
|
const consoleOnly = process.env.AGENT_BOT_CONSOLE_ONLY === "1";
|
|
41
53
|
const config = loadConfig();
|
|
42
54
|
const transport = consoleOnly ? "console" : requireServerFeishuTransport(config.feishu);
|
|
@@ -92,7 +104,7 @@ let shuttingDown = false;
|
|
|
92
104
|
let restartRequested = false;
|
|
93
105
|
const safeRestart = new SafeRestartScheduler({
|
|
94
106
|
readActivity: () => store.getServerActivityState(),
|
|
95
|
-
onReady: (reason) => initiateRestart(reason),
|
|
107
|
+
onReady: (reason, notificationTargets) => initiateRestart(reason, notificationTargets),
|
|
96
108
|
onStatus: (status) => safeRestartNotifier?.update(status),
|
|
97
109
|
onStatusError: (error) => logger.warn({ error }, "Failed to publish safe restart status."),
|
|
98
110
|
});
|
|
@@ -100,6 +112,16 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
|
|
|
100
112
|
supervised,
|
|
101
113
|
restart: requestRestart,
|
|
102
114
|
cancelSafeRestart: (scheduleId) => safeRestart.cancelScheduled(scheduleId),
|
|
115
|
+
rememberFeishuUserOpenId: (userOpenId) => {
|
|
116
|
+
if (config.feishu.userOpenId?.startsWith("ou_")) return;
|
|
117
|
+
const persisted = persistFeishuUserOpenIdIfMissing(defaultDotEnvPath(), userOpenId);
|
|
118
|
+
config.feishu.userOpenId = persisted.userOpenId;
|
|
119
|
+
process.env.FEISHU_USER_OPEN_ID = persisted.userOpenId;
|
|
120
|
+
logger.info(
|
|
121
|
+
{ status: persisted.status },
|
|
122
|
+
"Stored the default Lark user Open ID from the first private chat message.",
|
|
123
|
+
);
|
|
124
|
+
},
|
|
103
125
|
});
|
|
104
126
|
|
|
105
127
|
if (feishuOutbound) feishuConnector = new FeishuConnector(config, controller, logger);
|
|
@@ -126,6 +148,7 @@ if (feishuConnector && startupNotifier) {
|
|
|
126
148
|
startupReason,
|
|
127
149
|
startControlServer,
|
|
128
150
|
() => { serverReady = true; },
|
|
151
|
+
restartGroupContextKeys,
|
|
129
152
|
);
|
|
130
153
|
} else {
|
|
131
154
|
await startControlServer();
|
|
@@ -134,40 +157,59 @@ if (feishuConnector && startupNotifier) {
|
|
|
134
157
|
}
|
|
135
158
|
consoleConnector?.start();
|
|
136
159
|
|
|
137
|
-
async function requestRestart(
|
|
160
|
+
async function requestRestart(
|
|
161
|
+
contextKey: string,
|
|
162
|
+
force: boolean,
|
|
163
|
+
replyTarget?: MessageReplyTarget,
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
const notificationTarget: RestartNotificationTarget = {
|
|
166
|
+
contextKey,
|
|
167
|
+
...(replyTarget ? { replyMessageId: replyTarget.messageId } : {}),
|
|
168
|
+
};
|
|
138
169
|
if (restartRequested) {
|
|
139
170
|
await outbound.sendText(contextKey, "Agent Bot 已在重启中,请稍候。").catch(() => undefined);
|
|
140
171
|
return;
|
|
141
172
|
}
|
|
142
173
|
if (force) {
|
|
143
|
-
await initiateRestart("用户执行 /restart --force 命令",
|
|
174
|
+
await initiateRestart("用户执行 /restart --force 命令", [notificationTarget]);
|
|
144
175
|
return;
|
|
145
176
|
}
|
|
146
|
-
const newlyScheduled = safeRestart.schedule("用户执行 /restart 命令");
|
|
177
|
+
const newlyScheduled = safeRestart.schedule("用户执行 /restart 命令", notificationTarget);
|
|
147
178
|
await outbound.sendText(
|
|
148
179
|
contextKey,
|
|
149
180
|
newlyScheduled
|
|
150
181
|
? "已安排安全重启。Agent Bot 会等待所有任务完成、最终结果投递完成,并保持 15 秒无新消息后重启。"
|
|
151
|
-
: "
|
|
182
|
+
: "安全重启已在等待中;当前会话已加入通知范围,并已更新重启原因。",
|
|
152
183
|
).catch((error: unknown) => {
|
|
153
184
|
logger.warn({ error, contextKey }, "Failed to send safe restart acknowledgement.");
|
|
154
185
|
});
|
|
155
186
|
}
|
|
156
187
|
|
|
157
|
-
async function initiateRestart(
|
|
188
|
+
async function initiateRestart(
|
|
189
|
+
reason: string,
|
|
190
|
+
notificationTargets: RestartNotificationTarget[] = [],
|
|
191
|
+
): Promise<void> {
|
|
158
192
|
if (restartRequested) {
|
|
159
|
-
|
|
193
|
+
await sendRestartTexts(notificationTargets, "Agent Bot 已在重启中,请稍候。");
|
|
160
194
|
return;
|
|
161
195
|
}
|
|
162
196
|
restartRequested = true;
|
|
163
197
|
await safeRestart.cancelCurrent();
|
|
164
198
|
await safeRestartNotifier?.flush();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
const allNotificationTargets = mergeRestartNotificationTargets(
|
|
200
|
+
notificationTargets,
|
|
201
|
+
safeRestartNotifier?.getNotificationTargets() ?? [],
|
|
202
|
+
);
|
|
203
|
+
const restartGroupContextKeys = groupContextKeysForRestartTargets(allNotificationTargets);
|
|
204
|
+
await sendRestartTexts(
|
|
205
|
+
allNotificationTargets,
|
|
206
|
+
"Agent Bot 正在重启,恢复在线后会发送启动状态通知。",
|
|
207
|
+
);
|
|
208
|
+
setTimeout(() => void shutdown(
|
|
209
|
+
supervised ? STOP_EXIT_CODE : 0,
|
|
210
|
+
reason,
|
|
211
|
+
restartGroupContextKeys,
|
|
212
|
+
), 100);
|
|
171
213
|
}
|
|
172
214
|
|
|
173
215
|
async function handleControlRequest(request: ControlRequest): Promise<ControlResponse> {
|
|
@@ -187,9 +229,10 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
187
229
|
activity: store.getServerActivityState(),
|
|
188
230
|
},
|
|
189
231
|
};
|
|
190
|
-
case "server_restart":
|
|
232
|
+
case "server_restart": {
|
|
233
|
+
const notificationTarget = inferControlRestartTarget(request.notificationSessionId);
|
|
191
234
|
if (request.mode === "safe") {
|
|
192
|
-
const newlyScheduled = safeRestart.schedule(request.reason);
|
|
235
|
+
const newlyScheduled = safeRestart.schedule(request.reason, notificationTarget);
|
|
193
236
|
return {
|
|
194
237
|
ok: true,
|
|
195
238
|
message: newlyScheduled
|
|
@@ -197,8 +240,12 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
197
240
|
: "A safe restart is already pending. Its reason has been updated.",
|
|
198
241
|
};
|
|
199
242
|
}
|
|
200
|
-
setTimeout(() => void initiateRestart(
|
|
243
|
+
setTimeout(() => void initiateRestart(
|
|
244
|
+
request.reason,
|
|
245
|
+
notificationTarget ? [notificationTarget] : [],
|
|
246
|
+
), 25);
|
|
201
247
|
return { ok: true, message: "Immediate restart requested." };
|
|
248
|
+
}
|
|
202
249
|
case "server_stop":
|
|
203
250
|
safeRestart.cancel();
|
|
204
251
|
setTimeout(() => void shutdown(STOP_EXIT_CODE), 25);
|
|
@@ -235,7 +282,11 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
235
282
|
}
|
|
236
283
|
}
|
|
237
284
|
|
|
238
|
-
async function shutdown(
|
|
285
|
+
async function shutdown(
|
|
286
|
+
exitCode: number,
|
|
287
|
+
restartReason?: string,
|
|
288
|
+
restartGroupContextKeys: string[] = [],
|
|
289
|
+
): Promise<void> {
|
|
239
290
|
if (shuttingDown) return;
|
|
240
291
|
shuttingDown = true;
|
|
241
292
|
logger.info({ exitCode }, "Shutting down Agent Bot.");
|
|
@@ -250,11 +301,11 @@ async function shutdown(exitCode: number, restartReason?: string): Promise<void>
|
|
|
250
301
|
runtimes.close();
|
|
251
302
|
await controlServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local control endpoint."));
|
|
252
303
|
store.close();
|
|
253
|
-
if (restartReason) startReplacementSupervisor(restartReason);
|
|
304
|
+
if (restartReason) startReplacementSupervisor(restartReason, restartGroupContextKeys);
|
|
254
305
|
process.exit(exitCode);
|
|
255
306
|
}
|
|
256
307
|
|
|
257
|
-
function startReplacementSupervisor(restartReason: string): void {
|
|
308
|
+
function startReplacementSupervisor(restartReason: string, restartGroupContextKeys: string[]): void {
|
|
258
309
|
const supervisorEntry = fileURLToPath(new URL("./supervisor.js", import.meta.url));
|
|
259
310
|
const reportDirectory = resolveSupervisorDiagnosticsPaths(config).crashReportDirectory;
|
|
260
311
|
prepareCrashReportDirectory(reportDirectory);
|
|
@@ -275,11 +326,100 @@ function startReplacementSupervisor(restartReason: string): void {
|
|
|
275
326
|
detached: true,
|
|
276
327
|
windowsHide: true,
|
|
277
328
|
stdio: "ignore",
|
|
278
|
-
env: replacementSupervisorEnvironment(
|
|
329
|
+
env: replacementSupervisorEnvironment(
|
|
330
|
+
restartReason,
|
|
331
|
+
environmentRefresh.environment,
|
|
332
|
+
restartGroupContextKeys,
|
|
333
|
+
),
|
|
279
334
|
});
|
|
280
335
|
supervisor.unref();
|
|
281
336
|
}
|
|
282
337
|
|
|
338
|
+
function inferControlRestartTarget(notificationSessionId?: string): RestartNotificationTarget | undefined {
|
|
339
|
+
const requestedSessionId = notificationSessionId?.trim();
|
|
340
|
+
if (requestedSessionId) {
|
|
341
|
+
const requested = store.getSession(requestedSessionId)
|
|
342
|
+
?? store.findSessionByRemoteSessionId(requestedSessionId);
|
|
343
|
+
if (!requested) throw new Error(`Cannot route restart notifications: unknown task ${requestedSessionId}.`);
|
|
344
|
+
return restartNotificationTargetForSession(requested);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const runningSessions = store.listAllSessions().filter((session) => session.status === "running");
|
|
348
|
+
const runningContexts = new Set(runningSessions.map((session) => session.contextKey));
|
|
349
|
+
if (runningContexts.size > 1) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
"Cannot determine which conversation requested the restart because multiple conversations are running. Pass --task <task>.",
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const contextKey = runningContexts.size === 1 ? runningContexts.values().next().value : undefined;
|
|
355
|
+
const session = contextKey
|
|
356
|
+
? runningSessions.find((candidate) => candidate.contextKey === contextKey)
|
|
357
|
+
: undefined;
|
|
358
|
+
return session ? restartNotificationTargetForSession(session) : undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function restartNotificationTargetForSession(
|
|
362
|
+
session: { localSessionId: string; contextKey: string },
|
|
363
|
+
): RestartNotificationTarget {
|
|
364
|
+
const replyMessageId = isThreadContextKey(session.contextKey)
|
|
365
|
+
? store.findLatestMessageIdForSession(session.localSessionId, session.contextKey)
|
|
366
|
+
?? store.findLatestMessageIdForContext(session.contextKey)
|
|
367
|
+
: undefined;
|
|
368
|
+
if (isThreadContextKey(session.contextKey) && !replyMessageId) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
"Cannot route restart notifications to the task topic because it has no message anchor. Send /restart in that topic instead.",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
contextKey: session.contextKey,
|
|
375
|
+
...(replyMessageId ? { replyMessageId } : {}),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function sendRestartText(target: RestartNotificationTarget, text: string): Promise<string | undefined> {
|
|
380
|
+
if (isThreadContextKey(target.contextKey) && !target.replyMessageId) {
|
|
381
|
+
throw new Error("Cannot send a restart notice to a topic without a reply message anchor.");
|
|
382
|
+
}
|
|
383
|
+
const replyTarget = target.replyMessageId
|
|
384
|
+
? { messageId: target.replyMessageId, replyInThread: true as const }
|
|
385
|
+
: undefined;
|
|
386
|
+
return outbound.withReplyTarget(target.contextKey, replyTarget, () =>
|
|
387
|
+
outbound.sendText(target.contextKey, text));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function sendRestartTexts(targets: RestartNotificationTarget[], text: string): Promise<void> {
|
|
391
|
+
await Promise.all(targets.map(async (target) => {
|
|
392
|
+
await sendRestartText(target, text).catch((error: unknown) => {
|
|
393
|
+
logger.warn(
|
|
394
|
+
{ error, contextKey: target.contextKey },
|
|
395
|
+
"Failed to send restart acknowledgement.",
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function groupContextKeysForRestartTargets(targets: RestartNotificationTarget[]): string[] {
|
|
402
|
+
const knownGroupContextKeys = new Set(
|
|
403
|
+
store.listChatContexts("group").map((chat) => chat.contextKey),
|
|
404
|
+
);
|
|
405
|
+
return [...new Set(targets
|
|
406
|
+
.map((target) => baseChatContextKey(target.contextKey))
|
|
407
|
+
.filter((contextKey) => knownGroupContextKeys.has(contextKey)))];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function mergeRestartNotificationTargets(
|
|
411
|
+
...targetGroups: RestartNotificationTarget[][]
|
|
412
|
+
): RestartNotificationTarget[] {
|
|
413
|
+
const targets = new Map<string, RestartNotificationTarget>();
|
|
414
|
+
for (const target of targetGroups.flat()) {
|
|
415
|
+
const existing = targets.get(target.contextKey);
|
|
416
|
+
if (!existing || (!existing.replyMessageId && target.replyMessageId)) {
|
|
417
|
+
targets.set(target.contextKey, { ...target });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return [...targets.values()];
|
|
421
|
+
}
|
|
422
|
+
|
|
283
423
|
function delay(milliseconds: number): Promise<void> {
|
|
284
424
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
285
425
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface TurnGraphNode {
|
|
2
|
+
turnId: string;
|
|
3
|
+
parentTurnId?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface TurnGraphRow {
|
|
7
|
+
sequence: number;
|
|
8
|
+
nodeLine: string;
|
|
9
|
+
connectorLine?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildTurnGraphRows(nodes: readonly TurnGraphNode[]): TurnGraphRow[] {
|
|
13
|
+
const visibleTurnIds = new Set(nodes.map((node) => node.turnId));
|
|
14
|
+
const lanes: string[] = [];
|
|
15
|
+
|
|
16
|
+
return nodes.map((node, index) => {
|
|
17
|
+
let lane = lanes.indexOf(node.turnId);
|
|
18
|
+
if (lane < 0) {
|
|
19
|
+
lane = lanes.length;
|
|
20
|
+
lanes.push(node.turnId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sequence = index + 1;
|
|
24
|
+
const nodeLine = `${lanes.map((_, laneIndex) => laneIndex === lane ? "●" : "│").join(" ")} ${sequence}`;
|
|
25
|
+
const parentTurnId = node.parentTurnId && visibleTurnIds.has(node.parentTurnId)
|
|
26
|
+
? node.parentTurnId
|
|
27
|
+
: undefined;
|
|
28
|
+
let connectorLine: string | undefined;
|
|
29
|
+
|
|
30
|
+
if (!parentTurnId) {
|
|
31
|
+
lanes.splice(lane, 1);
|
|
32
|
+
connectorLine = lanes.length > 0 ? lanes.map(() => "│").join(" ") : undefined;
|
|
33
|
+
} else {
|
|
34
|
+
const existingParentLane = lanes.indexOf(parentTurnId);
|
|
35
|
+
if (existingParentLane < 0 || existingParentLane === lane) {
|
|
36
|
+
lanes[lane] = parentTurnId;
|
|
37
|
+
connectorLine = lanes.map(() => "│").join(" ");
|
|
38
|
+
} else {
|
|
39
|
+
const connectorLanes = lanes.map(() => "│");
|
|
40
|
+
connectorLanes[lane] = lane > existingParentLane ? "╱" : "╲";
|
|
41
|
+
connectorLine = connectorLanes.join(" ");
|
|
42
|
+
lanes.splice(lane, 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { sequence, nodeLine, ...(connectorLine ? { connectorLine } : {}) };
|
|
47
|
+
});
|
|
48
|
+
}
|