@keyou007/agent-bot 0.1.17-alpha.1 → 0.1.17
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 +12 -0
- package/README.md +3 -1
- package/README.zh.md +3 -1
- package/config.example.yaml +12 -0
- package/dist/config/loadConfig.js +10 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +6 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +6 -16
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +42 -13
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +33 -10
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/LocalImageMarkdown.js +16 -2
- package/dist/feishu/LocalImageMarkdown.js.map +1 -1
- package/dist/index.js +33 -5
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +1064 -0
- package/dist/local-files/LocalFileViewerServer.js.map +1 -0
- package/dist/local-files/NetworkAddressSelector.js +97 -0
- package/dist/local-files/NetworkAddressSelector.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +81 -34
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +7 -4
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +15 -2
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/docs/technical-reference.md +10 -0
- package/docs/technical-reference.zh.md +10 -0
- package/npm-shrinkwrap.json +12 -2
- package/package.json +2 -1
- package/skills/agent-bot/SKILL.md +1 -1
- package/src/config/loadConfig.ts +10 -0
- package/src/config/schema.ts +9 -0
- package/src/feishu/CardRenderer.ts +6 -22
- package/src/feishu/FeishuMarkdown.ts +51 -12
- package/src/feishu/FeishuTurnPresenter.ts +41 -11
- package/src/feishu/LocalImageMarkdown.ts +23 -2
- package/src/index.ts +33 -4
- package/src/local-files/LocalFileViewerServer.ts +1142 -0
- package/src/local-files/NetworkAddressSelector.ts +111 -0
- package/src/proxy/ProxySessionController.ts +75 -24
- package/src/startup/StartupNotifier.ts +9 -4
- package/src/supervision/SafeRestartScheduler.ts +23 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
|
|
3
|
+
export type NetworkConnectionKind = "wired" | "wifi" | "other" | "vpn";
|
|
4
|
+
|
|
5
|
+
export interface PreferredNetworkAddress {
|
|
6
|
+
address: string;
|
|
7
|
+
interfaceName: string;
|
|
8
|
+
kind: NetworkConnectionKind;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type NetworkInterfaces = ReturnType<typeof os.networkInterfaces>;
|
|
12
|
+
|
|
13
|
+
const KIND_PRIORITY: Record<NetworkConnectionKind, number> = {
|
|
14
|
+
wired: 0,
|
|
15
|
+
wifi: 1,
|
|
16
|
+
other: 2,
|
|
17
|
+
vpn: 3,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const VIRTUAL_INTERFACE = /(?:loopback|hyper[- ]?v|vEthernet|vmware|virtualbox|docker|wsl|bluetooth|npcap|container|\bbridge\b|^br[-_]|^virbr|^veth|^awdl|^llw)/iu;
|
|
21
|
+
const VPN_INTERFACE = /(?:\bvpn\b|wireguard|tailscale|zerotier|hamachi|nordlynx|openvpn|anyconnect|fortinet|globalprotect|^utun\d*$|^tun\d*$|^tap\d*$|^wg\d*$|^ppp\d*$|^ipsec\d*$)/iu;
|
|
22
|
+
const WIFI_INTERFACE = /(?:wi-?fi|wireless|wlan|airport|无线|^wl[a-z0-9_-]*)/iu;
|
|
23
|
+
const WIRED_INTERFACE = /(?:ethernet|以太网|local area connection|本地连接|\blan\b|^(?:eth|enp|ens|eno|enx)[a-z0-9_-]*)/iu;
|
|
24
|
+
|
|
25
|
+
export function selectPreferredNetworkAddress(
|
|
26
|
+
interfaces: NetworkInterfaces = os.networkInterfaces(),
|
|
27
|
+
): PreferredNetworkAddress | undefined {
|
|
28
|
+
const candidates: Array<PreferredNetworkAddress & { privateAddress: boolean }> = [];
|
|
29
|
+
|
|
30
|
+
for (const [interfaceName, addresses] of Object.entries(interfaces)) {
|
|
31
|
+
if (!addresses || isExcludedInterface(interfaceName)) continue;
|
|
32
|
+
const kind = classifyInterface(interfaceName);
|
|
33
|
+
for (const address of addresses) {
|
|
34
|
+
if (address.internal || address.family !== "IPv4" || !isUsableIpv4(address.address)) continue;
|
|
35
|
+
candidates.push({
|
|
36
|
+
address: address.address,
|
|
37
|
+
interfaceName,
|
|
38
|
+
kind,
|
|
39
|
+
privateAddress: isPrivateIpv4(address.address),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
candidates.sort((left, right) => {
|
|
45
|
+
const kindDifference = KIND_PRIORITY[left.kind] - KIND_PRIORITY[right.kind];
|
|
46
|
+
if (kindDifference !== 0) return kindDifference;
|
|
47
|
+
if (left.privateAddress !== right.privateAddress) return left.privateAddress ? -1 : 1;
|
|
48
|
+
const interfaceDifference = compareText(left.interfaceName, right.interfaceName);
|
|
49
|
+
return interfaceDifference !== 0 ? interfaceDifference : compareIpv4(left.address, right.address);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const selected = candidates[0];
|
|
53
|
+
return selected
|
|
54
|
+
? { address: selected.address, interfaceName: selected.interfaceName, kind: selected.kind }
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function classifyInterface(interfaceName: string): NetworkConnectionKind {
|
|
59
|
+
if (VPN_INTERFACE.test(interfaceName)) return "vpn";
|
|
60
|
+
if (WIFI_INTERFACE.test(interfaceName)) return "wifi";
|
|
61
|
+
if (WIRED_INTERFACE.test(interfaceName)) return "wired";
|
|
62
|
+
return "other";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isExcludedInterface(interfaceName: string): boolean {
|
|
66
|
+
return /^lo\d*$/iu.test(interfaceName) || VIRTUAL_INTERFACE.test(interfaceName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isUsableIpv4(address: string): boolean {
|
|
70
|
+
const parts = ipv4Parts(address);
|
|
71
|
+
if (!parts) return false;
|
|
72
|
+
const [first, second] = parts;
|
|
73
|
+
return first !== 0
|
|
74
|
+
&& first !== 127
|
|
75
|
+
&& first < 224
|
|
76
|
+
&& !(first === 169 && second === 254);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isPrivateIpv4(address: string): boolean {
|
|
80
|
+
const parts = ipv4Parts(address);
|
|
81
|
+
if (!parts) return false;
|
|
82
|
+
const [first, second] = parts;
|
|
83
|
+
return first === 10
|
|
84
|
+
|| (first === 172 && second >= 16 && second <= 31)
|
|
85
|
+
|| (first === 192 && second === 168)
|
|
86
|
+
|| (first === 100 && second >= 64 && second <= 127);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ipv4Parts(address: string): [number, number, number, number] | undefined {
|
|
90
|
+
const values = address.split(".").map((part) => Number(part));
|
|
91
|
+
if (values.length !== 4 || values.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return values as [number, number, number, number];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function compareText(left: string, right: string): number {
|
|
98
|
+
const normalizedLeft = left.toLocaleLowerCase("en-US");
|
|
99
|
+
const normalizedRight = right.toLocaleLowerCase("en-US");
|
|
100
|
+
return normalizedLeft < normalizedRight ? -1 : normalizedLeft > normalizedRight ? 1 : 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compareIpv4(left: string, right: string): number {
|
|
104
|
+
const leftParts = ipv4Parts(left) ?? [0, 0, 0, 0];
|
|
105
|
+
const rightParts = ipv4Parts(right) ?? [0, 0, 0, 0];
|
|
106
|
+
for (let index = 0; index < leftParts.length; index += 1) {
|
|
107
|
+
const difference = leftParts[index]! - rightParts[index]!;
|
|
108
|
+
if (difference !== 0) return difference;
|
|
109
|
+
}
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
} from "../feishu/GroupNameFormatter.js";
|
|
42
42
|
import { normalizeFeishuPostText } from "../feishu/InboundText.js";
|
|
43
43
|
import { allowsFeishuUser } from "../feishu/ownerAccess.js";
|
|
44
|
+
import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
|
|
44
45
|
import type { OutboundRouter } from "../presentation/OutboundRouter.js";
|
|
45
46
|
import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
|
|
46
47
|
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
@@ -124,6 +125,7 @@ const SESSION_PROMPT_PREVIEW_LENGTH = 50;
|
|
|
124
125
|
const DIRECTORY_PAGE_SIZE = 15;
|
|
125
126
|
const RESET_HISTORY_PAGE_SIZE = 10;
|
|
126
127
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
128
|
+
const QUEUED_PROMPT_RETRY_DELAY_MS = 5_000;
|
|
127
129
|
const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
|
|
128
130
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
129
131
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
@@ -131,17 +133,12 @@ const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
|
131
133
|
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
132
134
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
133
135
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
136
|
+
const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
|
|
134
137
|
const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
|
|
135
138
|
const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
|
|
136
139
|
const IMAGE_FILE_EXTENSIONS = new Set([
|
|
137
140
|
".avif", ".bmp", ".gif", ".heic", ".heif", ".ico", ".jpeg", ".jpg", ".png", ".svg", ".tif", ".tiff", ".webp",
|
|
138
141
|
]);
|
|
139
|
-
const BINARY_FILE_EXTENSIONS = new Set([
|
|
140
|
-
".7z", ".a", ".apk", ".avi", ".bin", ".bz2", ".class", ".dat", ".db", ".deb", ".dll", ".dmg", ".doc", ".docx",
|
|
141
|
-
".dylib", ".eot", ".exe", ".flac", ".gz", ".ico", ".ipa", ".iso", ".jar", ".lib", ".mdb", ".mkv", ".mov", ".mp3",
|
|
142
|
-
".mp4", ".msi", ".o", ".obj", ".ogg", ".otf", ".pdf", ".ppt", ".pptx", ".rar", ".rpm", ".so", ".sqlite", ".tar",
|
|
143
|
-
".ttf", ".wav", ".webm", ".woff", ".woff2", ".xls", ".xlsx", ".xz", ".zip", ".zst",
|
|
144
|
-
]);
|
|
145
142
|
|
|
146
143
|
interface HelpCommandDefinition {
|
|
147
144
|
command: string;
|
|
@@ -432,6 +429,7 @@ export class ProxySessionController {
|
|
|
432
429
|
private readonly threadInitializations = new Map<string, Promise<void>>();
|
|
433
430
|
private readonly llmRetryingSessions = new Set<string>();
|
|
434
431
|
private readonly retriedFailureTurnIds = new Set<string>();
|
|
432
|
+
private readonly queuedPromptRetryTimers = new Map<string, NodeJS.Timeout>();
|
|
435
433
|
private readonly recoveryRetryTimers = new Map<string, NodeJS.Timeout>();
|
|
436
434
|
private readonly pendingMergedForwards = new Map<string, PendingForwardAttachment>();
|
|
437
435
|
private readonly pendingResourceForwards = new Map<string, PendingForwardAttachment>();
|
|
@@ -1101,6 +1099,8 @@ export class ProxySessionController {
|
|
|
1101
1099
|
for (const pending of this.pendingResourceForwards.values()) pending.resolveAttachment(undefined);
|
|
1102
1100
|
this.pendingResourceForwards.clear();
|
|
1103
1101
|
this.relatedReactionMessageIds.clear();
|
|
1102
|
+
for (const timer of this.queuedPromptRetryTimers.values()) clearTimeout(timer);
|
|
1103
|
+
this.queuedPromptRetryTimers.clear();
|
|
1104
1104
|
for (const timer of this.recoveryRetryTimers.values()) clearTimeout(timer);
|
|
1105
1105
|
this.recoveryRetryTimers.clear();
|
|
1106
1106
|
for (const [jobId] of this.shellCommandJobMonitors) this.stopShellCommandJobMonitor(jobId);
|
|
@@ -1675,9 +1675,10 @@ export class ProxySessionController {
|
|
|
1675
1675
|
await this.assertBrowsableDirectory(directory);
|
|
1676
1676
|
const entries = (await fs.readdir(directory, { withFileTypes: true }))
|
|
1677
1677
|
.map((entry) => ({
|
|
1678
|
+
dirent: entry,
|
|
1678
1679
|
name: entry.name,
|
|
1679
1680
|
path: path.join(directory, entry.name),
|
|
1680
|
-
kind:
|
|
1681
|
+
kind: entry.isDirectory() ? "directory" as const : "file" as const,
|
|
1681
1682
|
}))
|
|
1682
1683
|
.sort((left, right) => Number(right.kind === "directory") - Number(left.kind === "directory")
|
|
1683
1684
|
|| left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" }));
|
|
@@ -1690,7 +1691,15 @@ export class ProxySessionController {
|
|
|
1690
1691
|
page,
|
|
1691
1692
|
totalPages,
|
|
1692
1693
|
totalEntries: entries.length,
|
|
1693
|
-
entries: entries
|
|
1694
|
+
entries: entries
|
|
1695
|
+
.slice(page * DIRECTORY_PAGE_SIZE, (page + 1) * DIRECTORY_PAGE_SIZE)
|
|
1696
|
+
.map((entry) => ({
|
|
1697
|
+
name: entry.name,
|
|
1698
|
+
path: entry.path,
|
|
1699
|
+
kind: entry.kind === "directory"
|
|
1700
|
+
? "directory" as const
|
|
1701
|
+
: directoryBrowserEntryKind(entry.dirent, entry.path),
|
|
1702
|
+
})),
|
|
1694
1703
|
};
|
|
1695
1704
|
}
|
|
1696
1705
|
|
|
@@ -3988,6 +3997,11 @@ export class ProxySessionController {
|
|
|
3988
3997
|
}
|
|
3989
3998
|
|
|
3990
3999
|
private scheduleNextQueuedPrompt(sessionId: string): Promise<void> {
|
|
4000
|
+
const retryTimer = this.queuedPromptRetryTimers.get(sessionId);
|
|
4001
|
+
if (retryTimer) {
|
|
4002
|
+
clearTimeout(retryTimer);
|
|
4003
|
+
this.queuedPromptRetryTimers.delete(sessionId);
|
|
4004
|
+
}
|
|
3991
4005
|
const previous = this.queuedPromptStarts.get(sessionId) ?? Promise.resolve();
|
|
3992
4006
|
const next = previous.catch(() => undefined).then(() => this.startNextQueuedPromptIfIdle(sessionId));
|
|
3993
4007
|
this.queuedPromptStarts.set(sessionId, next);
|
|
@@ -3996,6 +4010,23 @@ export class ProxySessionController {
|
|
|
3996
4010
|
});
|
|
3997
4011
|
}
|
|
3998
4012
|
|
|
4013
|
+
private scheduleQueuedPromptRetry(sessionId: string): void {
|
|
4014
|
+
if (this.queuedPromptRetryTimers.has(sessionId)) return;
|
|
4015
|
+
if (this.store.countQueuedPrompts(sessionId) === 0) return;
|
|
4016
|
+
const timer = setTimeout(() => {
|
|
4017
|
+
this.queuedPromptRetryTimers.delete(sessionId);
|
|
4018
|
+
void this.scheduleNextQueuedPrompt(sessionId).catch((error: unknown) => {
|
|
4019
|
+
this.logger.warn(
|
|
4020
|
+
{ error, errorMessage: runtimeErrorMessage(error), sessionId },
|
|
4021
|
+
"Failed to retry queued prompt start.",
|
|
4022
|
+
);
|
|
4023
|
+
this.scheduleQueuedPromptRetry(sessionId);
|
|
4024
|
+
});
|
|
4025
|
+
}, QUEUED_PROMPT_RETRY_DELAY_MS);
|
|
4026
|
+
timer.unref?.();
|
|
4027
|
+
this.queuedPromptRetryTimers.set(sessionId, timer);
|
|
4028
|
+
}
|
|
4029
|
+
|
|
3999
4030
|
private async startNextQueuedPromptIfIdle(sessionId: string): Promise<void> {
|
|
4000
4031
|
if (this.store.countQueuedPrompts(sessionId) === 0) return;
|
|
4001
4032
|
const baseRecord = this.store.getSession(sessionId);
|
|
@@ -4023,10 +4054,22 @@ export class ProxySessionController {
|
|
|
4023
4054
|
);
|
|
4024
4055
|
if (prompt.messageId) await this.bindMessageReactionToTurn(prompt.messageId, sessionId, turnId);
|
|
4025
4056
|
} catch (error) {
|
|
4026
|
-
this.logger.warn(
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4057
|
+
this.logger.warn(
|
|
4058
|
+
{
|
|
4059
|
+
error,
|
|
4060
|
+
errorMessage: runtimeErrorMessage(error),
|
|
4061
|
+
phase: prompt ? "start" : "preflight",
|
|
4062
|
+
sessionId,
|
|
4063
|
+
},
|
|
4064
|
+
"Failed to start queued prompt.",
|
|
4065
|
+
);
|
|
4066
|
+
if (!prompt) {
|
|
4067
|
+
this.scheduleQueuedPromptRetry(sessionId);
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
if (prompt.messageId) await this.finalizeStandaloneMessageReaction(prompt.messageId, "failed");
|
|
4071
|
+
await this.sendError(prompt.contextKey, error);
|
|
4072
|
+
if (this.store.countQueuedPrompts(sessionId) > 0) {
|
|
4030
4073
|
queueMicrotask(() => void this.scheduleNextQueuedPrompt(sessionId));
|
|
4031
4074
|
}
|
|
4032
4075
|
}
|
|
@@ -4247,7 +4290,7 @@ export class ProxySessionController {
|
|
|
4247
4290
|
remoteSessionId: record.remoteSessionId,
|
|
4248
4291
|
turnId,
|
|
4249
4292
|
});
|
|
4250
|
-
await this.outbound.sendText(record.contextKey,
|
|
4293
|
+
await this.outbound.sendText(record.contextKey, TURN_STOP_REQUESTED_MESSAGE);
|
|
4251
4294
|
return;
|
|
4252
4295
|
}
|
|
4253
4296
|
|
|
@@ -5347,9 +5390,10 @@ export class ProxySessionController {
|
|
|
5347
5390
|
}
|
|
5348
5391
|
const entries = directoryEntries
|
|
5349
5392
|
.map((entry) => ({
|
|
5393
|
+
dirent: entry,
|
|
5350
5394
|
name: entry.name,
|
|
5351
5395
|
path: path.join(directory, entry.name),
|
|
5352
|
-
kind:
|
|
5396
|
+
kind: entry.isDirectory() ? "directory" as const : "file" as const,
|
|
5353
5397
|
}))
|
|
5354
5398
|
.sort((left, right) => Number(right.kind === "directory") - Number(left.kind === "directory")
|
|
5355
5399
|
|| left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" }));
|
|
@@ -5377,8 +5421,9 @@ export class ProxySessionController {
|
|
|
5377
5421
|
...parentEntries,
|
|
5378
5422
|
...entries
|
|
5379
5423
|
.slice(offset, offset + pageSize)
|
|
5380
|
-
.map((entry): DirectoryBrowserCardEntry =>
|
|
5381
|
-
|
|
5424
|
+
.map((entry): DirectoryBrowserCardEntry => {
|
|
5425
|
+
if (entry.kind === "directory") {
|
|
5426
|
+
return {
|
|
5382
5427
|
name: entry.name,
|
|
5383
5428
|
kind: entry.kind,
|
|
5384
5429
|
openAction: {
|
|
@@ -5389,10 +5434,11 @@ export class ProxySessionController {
|
|
|
5389
5434
|
contextKey,
|
|
5390
5435
|
},
|
|
5391
5436
|
},
|
|
5392
|
-
}
|
|
5393
|
-
|
|
5437
|
+
};
|
|
5438
|
+
}
|
|
5439
|
+
return {
|
|
5394
5440
|
name: entry.name,
|
|
5395
|
-
kind: entry.
|
|
5441
|
+
kind: directoryBrowserEntryKind(entry.dirent, entry.path),
|
|
5396
5442
|
openAction: {
|
|
5397
5443
|
text: entry.name,
|
|
5398
5444
|
value: {
|
|
@@ -5401,7 +5447,8 @@ export class ProxySessionController {
|
|
|
5401
5447
|
contextKey,
|
|
5402
5448
|
},
|
|
5403
5449
|
},
|
|
5404
|
-
}
|
|
5450
|
+
};
|
|
5451
|
+
}),
|
|
5405
5452
|
];
|
|
5406
5453
|
const navigationActions = directoryPaginationActions(directory, page, totalPages, contextKey);
|
|
5407
5454
|
const directoryCount = entries.filter((entry) => entry.kind === "directory").length;
|
|
@@ -5903,7 +5950,7 @@ export class ProxySessionController {
|
|
|
5903
5950
|
turnId,
|
|
5904
5951
|
source: "sessions_card",
|
|
5905
5952
|
});
|
|
5906
|
-
await this.outbound.sendText(contextKey,
|
|
5953
|
+
await this.outbound.sendText(contextKey, TURN_STOP_REQUESTED_MESSAGE);
|
|
5907
5954
|
}
|
|
5908
5955
|
|
|
5909
5956
|
private async archiveSessionReference(
|
|
@@ -7114,12 +7161,16 @@ function isWindowsDriveRoot(directory: string): boolean {
|
|
|
7114
7161
|
return /^[a-z]:\\$/iu.test(path.win32.normalize(directory));
|
|
7115
7162
|
}
|
|
7116
7163
|
|
|
7117
|
-
function directoryBrowserEntryKind(entry: Dirent): DirectoryBrowserCardEntry["kind"] {
|
|
7164
|
+
function directoryBrowserEntryKind(entry: Dirent, filePath: string): DirectoryBrowserCardEntry["kind"] {
|
|
7118
7165
|
if (entry.isDirectory()) return "directory";
|
|
7166
|
+
try {
|
|
7167
|
+
if (classifyFileContent(filePath).kind === "text") return "file";
|
|
7168
|
+
} catch {
|
|
7169
|
+
return "file";
|
|
7170
|
+
}
|
|
7119
7171
|
const extension = path.extname(entry.name).toLowerCase();
|
|
7120
7172
|
if (IMAGE_FILE_EXTENSIONS.has(extension)) return "image";
|
|
7121
|
-
|
|
7122
|
-
return "file";
|
|
7173
|
+
return "binary";
|
|
7123
7174
|
}
|
|
7124
7175
|
|
|
7125
7176
|
function directoryActionPath(value: unknown): string {
|
|
@@ -48,8 +48,13 @@ export class StartupNotifier {
|
|
|
48
48
|
.filter((chat) => !isThreadContextKey(chat.contextKey))
|
|
49
49
|
.map((chat) => [chat.contextKey, chat]),
|
|
50
50
|
);
|
|
51
|
+
const knownPrivateChats = new Map(
|
|
52
|
+
this.store.listChatContexts("p2p")
|
|
53
|
+
.filter((chat) => !isThreadContextKey(chat.contextKey))
|
|
54
|
+
.map((chat) => [chat.contextKey, chat]),
|
|
55
|
+
);
|
|
51
56
|
const targetsByContext = new Map<string, StartupNotificationTarget>();
|
|
52
|
-
for (const chat of
|
|
57
|
+
for (const chat of knownPrivateChats.values()) {
|
|
53
58
|
targetsByContext.set(chat.contextKey, {
|
|
54
59
|
contextKey: chat.contextKey,
|
|
55
60
|
context: this.store.getUserContext(chat.contextKey),
|
|
@@ -71,10 +76,11 @@ export class StartupNotifier {
|
|
|
71
76
|
if (!contextKey) continue;
|
|
72
77
|
if (isThreadContextKey(contextKey)) {
|
|
73
78
|
const baseContextKey = baseChatContextKey(contextKey);
|
|
74
|
-
|
|
79
|
+
const parentChat = knownGroups.get(baseContextKey) ?? knownPrivateChats.get(baseContextKey);
|
|
80
|
+
if (!parentChat) continue;
|
|
75
81
|
const replyMessageId = target.replyMessageId?.trim();
|
|
76
82
|
if (replyMessageId) {
|
|
77
|
-
if (!explicitBaseContextKeys.has(baseContextKey)) {
|
|
83
|
+
if (parentChat.chatType === "group" && !explicitBaseContextKeys.has(baseContextKey)) {
|
|
78
84
|
targetsByContext.delete(baseContextKey);
|
|
79
85
|
}
|
|
80
86
|
targetsByContext.set(contextKey, {
|
|
@@ -83,7 +89,6 @@ export class StartupNotifier {
|
|
|
83
89
|
context: this.store.getUserContext(contextKey),
|
|
84
90
|
});
|
|
85
91
|
} else {
|
|
86
|
-
const group = knownGroups.get(baseContextKey)!;
|
|
87
92
|
targetsByContext.set(baseContextKey, {
|
|
88
93
|
contextKey: baseContextKey,
|
|
89
94
|
context: this.store.getUserContext(baseContextKey),
|
|
@@ -35,6 +35,10 @@ export interface SafeRestartSchedulerOptions {
|
|
|
35
35
|
pollIntervalMs?: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface SafeRestartScheduleOptions {
|
|
39
|
+
restartImmediatelyIfIdle?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
export class SafeRestartScheduler {
|
|
39
43
|
private timer?: NodeJS.Timeout;
|
|
40
44
|
private polling?: Promise<void>;
|
|
@@ -44,6 +48,7 @@ export class SafeRestartScheduler {
|
|
|
44
48
|
private idleSince?: number;
|
|
45
49
|
private idleInboundAt?: string;
|
|
46
50
|
private scheduleId = 0;
|
|
51
|
+
private restartImmediatelyIfIdle = false;
|
|
47
52
|
|
|
48
53
|
constructor(private readonly options: SafeRestartSchedulerOptions) {}
|
|
49
54
|
|
|
@@ -55,10 +60,17 @@ export class SafeRestartScheduler {
|
|
|
55
60
|
return this.reason;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
schedule(
|
|
63
|
+
schedule(
|
|
64
|
+
reason: string,
|
|
65
|
+
notificationTarget?: RestartNotificationTarget,
|
|
66
|
+
options: SafeRestartScheduleOptions = {},
|
|
67
|
+
): boolean {
|
|
59
68
|
const newlyScheduled = !this.reason;
|
|
60
69
|
if (newlyScheduled) {
|
|
61
70
|
this.notificationTargets = [];
|
|
71
|
+
this.restartImmediatelyIfIdle = options.restartImmediatelyIfIdle === true;
|
|
72
|
+
} else if (options.restartImmediatelyIfIdle) {
|
|
73
|
+
this.restartImmediatelyIfIdle = true;
|
|
62
74
|
}
|
|
63
75
|
this.scheduleId += 1;
|
|
64
76
|
this.addNotificationTarget(notificationTarget, reason);
|
|
@@ -104,6 +116,7 @@ export class SafeRestartScheduler {
|
|
|
104
116
|
this.notificationTargets = [];
|
|
105
117
|
this.idleSince = undefined;
|
|
106
118
|
this.idleInboundAt = undefined;
|
|
119
|
+
this.restartImmediatelyIfIdle = false;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
private async poll(): Promise<void> {
|
|
@@ -123,6 +136,10 @@ export class SafeRestartScheduler {
|
|
|
123
136
|
});
|
|
124
137
|
return;
|
|
125
138
|
}
|
|
139
|
+
if (this.restartImmediatelyIfIdle) {
|
|
140
|
+
await this.restart(reason, scheduleId, state);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
126
143
|
const quietPeriodMs = this.options.quietPeriodMs ?? 15_000;
|
|
127
144
|
const latestInboundAt = state.latestInboundAt ?? "";
|
|
128
145
|
if (this.idleSince === undefined || this.idleInboundAt !== latestInboundAt) {
|
|
@@ -178,6 +195,10 @@ export class SafeRestartScheduler {
|
|
|
178
195
|
}
|
|
179
196
|
return;
|
|
180
197
|
}
|
|
198
|
+
await this.restart(reason, scheduleId, confirmed);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async restart(reason: string, scheduleId: number, activity: ServerActivityState): Promise<void> {
|
|
181
202
|
const notificationTargets = this.notificationTargetsSnapshot();
|
|
182
203
|
this.clear();
|
|
183
204
|
await this.emitStatus({
|
|
@@ -185,7 +206,7 @@ export class SafeRestartScheduler {
|
|
|
185
206
|
reason,
|
|
186
207
|
notificationTargets,
|
|
187
208
|
phase: "restarting",
|
|
188
|
-
activity
|
|
209
|
+
activity,
|
|
189
210
|
remainingMs: 0,
|
|
190
211
|
});
|
|
191
212
|
await this.options.onReady(reason, notificationTargets);
|