@keyou007/agent-bot 0.1.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/.env.example +8 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +675 -0
- package/README.md +187 -0
- package/README.zh.md +187 -0
- package/SECURITY.md +13 -0
- package/config.example.yaml +56 -0
- package/dist/acp/AcpJsonRpcConnection.js +155 -0
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -0
- package/dist/acp/AcpProcessManager.js +65 -0
- package/dist/acp/AcpProcessManager.js.map +1 -0
- package/dist/acp/AcpSessionManager.js +164 -0
- package/dist/acp/AcpSessionManager.js.map +1 -0
- package/dist/acp/acpTypes.js +10 -0
- package/dist/acp/acpTypes.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +349 -0
- package/dist/cli/FeishuAppConfiguration.js.map +1 -0
- package/dist/cli/FeishuAppRegistration.js +160 -0
- package/dist/cli/FeishuAppRegistration.js.map +1 -0
- package/dist/cli/Initializer.js +112 -0
- package/dist/cli/Initializer.js.map +1 -0
- package/dist/cli/LocalControlClient.js +47 -0
- package/dist/cli/LocalControlClient.js.map +1 -0
- package/dist/cli/LocalControlServer.js +66 -0
- package/dist/cli/LocalControlServer.js.map +1 -0
- package/dist/cli/SkillRegistry.js +126 -0
- package/dist/cli/SkillRegistry.js.map +1 -0
- package/dist/cli/controlProtocol.js +10 -0
- package/dist/cli/controlProtocol.js.map +1 -0
- package/dist/cli/taskChatRoute.js +23 -0
- package/dist/cli/taskChatRoute.js.map +1 -0
- package/dist/cli.js +616 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex/AppServerConnection.js +131 -0
- package/dist/codex/AppServerConnection.js.map +1 -0
- package/dist/codex/CodexEventMapper.js +296 -0
- package/dist/codex/CodexEventMapper.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +124 -0
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -0
- package/dist/codex/CodexProcessManager.js +64 -0
- package/dist/codex/CodexProcessManager.js.map +1 -0
- package/dist/codex/CodexRuntime.js +825 -0
- package/dist/codex/CodexRuntime.js.map +1 -0
- package/dist/codex/ProjectlessWorkspace.js +74 -0
- package/dist/codex/ProjectlessWorkspace.js.map +1 -0
- package/dist/codex/protocol.js +10 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/commands/CommandRouter.js +176 -0
- package/dist/commands/CommandRouter.js.map +1 -0
- package/dist/commands/commandTypes.js +2 -0
- package/dist/commands/commandTypes.js.map +1 -0
- package/dist/config/loadConfig.js +107 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/config/paths.js +32 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.js +38 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/console/ConsoleConnector.js +39 -0
- package/dist/console/ConsoleConnector.js.map +1 -0
- package/dist/console/ConsoleTurnPresenter.js +60 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -0
- package/dist/feishu/CardRenderer.js +972 -0
- package/dist/feishu/CardRenderer.js.map +1 -0
- package/dist/feishu/CardUpdateScheduler.js +148 -0
- package/dist/feishu/CardUpdateScheduler.js.map +1 -0
- package/dist/feishu/ConsoleFeishuClient.js +18 -0
- package/dist/feishu/ConsoleFeishuClient.js.map +1 -0
- package/dist/feishu/FeishuConnector.js +257 -0
- package/dist/feishu/FeishuConnector.js.map +1 -0
- package/dist/feishu/FeishuMarkdown.js +37 -0
- package/dist/feishu/FeishuMarkdown.js.map +1 -0
- package/dist/feishu/FeishuMessageClient.js +459 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -0
- package/dist/feishu/FeishuTurnPresenter.js +425 -0
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -0
- package/dist/feishu/GroupAvatarGenerator.js +86 -0
- package/dist/feishu/GroupAvatarGenerator.js.map +1 -0
- package/dist/feishu/LocalCardImage.js +13 -0
- package/dist/feishu/LocalCardImage.js.map +1 -0
- package/dist/feishu/LocalImageMarkdown.js +89 -0
- package/dist/feishu/LocalImageMarkdown.js.map +1 -0
- package/dist/feishu/contextKey.js +12 -0
- package/dist/feishu/contextKey.js.map +1 -0
- package/dist/feishu/transport.js +14 -0
- package/dist/feishu/transport.js.map +1 -0
- package/dist/feishu/types.js +2 -0
- package/dist/feishu/types.js.map +1 -0
- package/dist/index.js +213 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.js +22 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/presentation/OutboundRouter.js +127 -0
- package/dist/presentation/OutboundRouter.js.map +1 -0
- package/dist/presentation/TurnStateReducer.js +307 -0
- package/dist/presentation/TurnStateReducer.js.map +1 -0
- package/dist/presentation/splitMarkdown.js +341 -0
- package/dist/presentation/splitMarkdown.js.map +1 -0
- package/dist/presentation/turnViewTypes.js +2 -0
- package/dist/presentation/turnViewTypes.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +2819 -0
- package/dist/proxy/ProxySessionController.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +271 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -0
- package/dist/runtime/AgentRuntimeRegistry.js +18 -0
- package/dist/runtime/AgentRuntimeRegistry.js.map +1 -0
- package/dist/runtime/agentEnvironment.js +11 -0
- package/dist/runtime/agentEnvironment.js.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/startup/SessionMetadataHydrator.js +46 -0
- package/dist/startup/SessionMetadataHydrator.js.map +1 -0
- package/dist/startup/StartupNotifier.js +78 -0
- package/dist/startup/StartupNotifier.js.map +1 -0
- package/dist/startup/startFeishu.js +6 -0
- package/dist/startup/startFeishu.js.map +1 -0
- package/dist/state/StateStore.js +893 -0
- package/dist/state/StateStore.js.map +1 -0
- package/dist/state/migrations.js +129 -0
- package/dist/state/migrations.js.map +1 -0
- package/dist/supervision/SafeRestartNotifier.js +116 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -0
- package/dist/supervision/SafeRestartScheduler.js +123 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -0
- package/dist/supervision/replacementSupervisor.js +8 -0
- package/dist/supervision/replacementSupervisor.js.map +1 -0
- package/dist/supervision/restartPolicy.js +16 -0
- package/dist/supervision/restartPolicy.js.map +1 -0
- package/dist/supervisor.js +94 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/utils/executeShellCommand.js +76 -0
- package/dist/utils/executeShellCommand.js.map +1 -0
- package/dist/utils/id.js +5 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/markdown.js +29 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/spawnCommand.js +21 -0
- package/dist/utils/spawnCommand.js.map +1 -0
- package/dist/utils/taskTitle.js +10 -0
- package/dist/utils/taskTitle.js.map +1 -0
- package/docs/technical-reference.md +294 -0
- package/docs/technical-reference.zh.md +294 -0
- package/npm-shrinkwrap.json +4525 -0
- package/package.json +87 -0
- package/skills/agent-bot/SKILL.md +141 -0
- package/skills/agent-bot/agents/openai.yaml +4 -0
- package/src/acp/AcpJsonRpcConnection.ts +212 -0
- package/src/acp/AcpProcessManager.ts +87 -0
- package/src/acp/AcpSessionManager.ts +227 -0
- package/src/acp/acpTypes.ts +99 -0
- package/src/cli/FeishuAppConfiguration.ts +473 -0
- package/src/cli/FeishuAppRegistration.ts +207 -0
- package/src/cli/Initializer.ts +159 -0
- package/src/cli/LocalControlClient.ts +48 -0
- package/src/cli/LocalControlServer.ts +67 -0
- package/src/cli/SkillRegistry.ts +151 -0
- package/src/cli/controlProtocol.ts +34 -0
- package/src/cli/taskChatRoute.ts +31 -0
- package/src/cli.ts +630 -0
- package/src/codex/AppServerConnection.ts +159 -0
- package/src/codex/CodexEventMapper.ts +351 -0
- package/src/codex/CodexLocalActivityDetector.ts +148 -0
- package/src/codex/CodexProcessManager.ts +63 -0
- package/src/codex/CodexRuntime.ts +1025 -0
- package/src/codex/ProjectlessWorkspace.ts +90 -0
- package/src/codex/protocol.ts +37 -0
- package/src/commands/CommandRouter.ts +179 -0
- package/src/commands/commandTypes.ts +28 -0
- package/src/config/loadConfig.ts +115 -0
- package/src/config/paths.ts +37 -0
- package/src/config/schema.ts +46 -0
- package/src/console/ConsoleConnector.ts +40 -0
- package/src/console/ConsoleTurnPresenter.ts +64 -0
- package/src/feishu/CardRenderer.ts +1157 -0
- package/src/feishu/CardUpdateScheduler.ts +165 -0
- package/src/feishu/ConsoleFeishuClient.ts +22 -0
- package/src/feishu/FeishuConnector.ts +277 -0
- package/src/feishu/FeishuMarkdown.ts +47 -0
- package/src/feishu/FeishuMessageClient.ts +649 -0
- package/src/feishu/FeishuTurnPresenter.ts +539 -0
- package/src/feishu/GroupAvatarGenerator.ts +102 -0
- package/src/feishu/LocalCardImage.ts +13 -0
- package/src/feishu/LocalImageMarkdown.ts +92 -0
- package/src/feishu/contextKey.ts +14 -0
- package/src/feishu/transport.ts +24 -0
- package/src/feishu/types.ts +83 -0
- package/src/index.ts +228 -0
- package/src/logging/logger.ts +28 -0
- package/src/presentation/OutboundRouter.ts +205 -0
- package/src/presentation/TurnStateReducer.ts +341 -0
- package/src/presentation/splitMarkdown.ts +358 -0
- package/src/presentation/turnViewTypes.ts +54 -0
- package/src/proxy/ProxySessionController.ts +3311 -0
- package/src/runtime/AcpRuntimeAdapter.ts +324 -0
- package/src/runtime/AgentRuntimeRegistry.ts +20 -0
- package/src/runtime/agentEnvironment.ts +14 -0
- package/src/runtime/types.ts +215 -0
- package/src/startup/SessionMetadataHydrator.ts +46 -0
- package/src/startup/StartupNotifier.ts +92 -0
- package/src/startup/startFeishu.ts +19 -0
- package/src/state/StateStore.ts +1249 -0
- package/src/state/migrations.ts +128 -0
- package/src/supervision/SafeRestartNotifier.ts +122 -0
- package/src/supervision/SafeRestartScheduler.ts +149 -0
- package/src/supervision/replacementSupervisor.ts +10 -0
- package/src/supervision/restartPolicy.ts +19 -0
- package/src/supervisor.ts +106 -0
- package/src/utils/executeShellCommand.ts +97 -0
- package/src/utils/id.ts +5 -0
- package/src/utils/markdown.ts +25 -0
- package/src/utils/spawnCommand.ts +35 -0
- package/src/utils/taskTitle.ts +9 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export const migrations = [
|
|
2
|
+
`
|
|
3
|
+
CREATE TABLE IF NOT EXISTS user_contexts (
|
|
4
|
+
context_key TEXT PRIMARY KEY,
|
|
5
|
+
default_agent TEXT NOT NULL,
|
|
6
|
+
current_session_id TEXT,
|
|
7
|
+
previous_session_id TEXT,
|
|
8
|
+
bound_project_cwd TEXT,
|
|
9
|
+
created_at TEXT NOT NULL,
|
|
10
|
+
updated_at TEXT NOT NULL
|
|
11
|
+
);
|
|
12
|
+
`,
|
|
13
|
+
`
|
|
14
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
15
|
+
local_session_id TEXT PRIMARY KEY,
|
|
16
|
+
context_key TEXT NOT NULL,
|
|
17
|
+
agent_name TEXT NOT NULL,
|
|
18
|
+
cwd TEXT NOT NULL,
|
|
19
|
+
acp_session_id TEXT,
|
|
20
|
+
status TEXT NOT NULL,
|
|
21
|
+
created_at TEXT NOT NULL,
|
|
22
|
+
updated_at TEXT NOT NULL
|
|
23
|
+
);
|
|
24
|
+
`,
|
|
25
|
+
`
|
|
26
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
27
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
context_key TEXT NOT NULL,
|
|
29
|
+
event_type TEXT NOT NULL,
|
|
30
|
+
payload_json TEXT NOT NULL,
|
|
31
|
+
created_at TEXT NOT NULL
|
|
32
|
+
);
|
|
33
|
+
`,
|
|
34
|
+
`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS turn_snapshots (
|
|
36
|
+
turn_id TEXT PRIMARY KEY,
|
|
37
|
+
local_session_id TEXT NOT NULL,
|
|
38
|
+
context_key TEXT,
|
|
39
|
+
snapshot_json TEXT NOT NULL,
|
|
40
|
+
updated_at TEXT NOT NULL
|
|
41
|
+
);
|
|
42
|
+
`,
|
|
43
|
+
`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS turn_deliveries (
|
|
45
|
+
turn_id TEXT PRIMARY KEY,
|
|
46
|
+
progress_message_id TEXT,
|
|
47
|
+
final_message_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
48
|
+
final_delivered_at TEXT,
|
|
49
|
+
last_card_hash TEXT,
|
|
50
|
+
updated_at TEXT NOT NULL
|
|
51
|
+
);
|
|
52
|
+
`,
|
|
53
|
+
`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS inbound_event_receipts (
|
|
55
|
+
event_id TEXT PRIMARY KEY,
|
|
56
|
+
event_kind TEXT NOT NULL,
|
|
57
|
+
created_at TEXT NOT NULL
|
|
58
|
+
);
|
|
59
|
+
`,
|
|
60
|
+
`
|
|
61
|
+
CREATE TABLE IF NOT EXISTS message_reactions (
|
|
62
|
+
message_id TEXT PRIMARY KEY,
|
|
63
|
+
context_key TEXT NOT NULL,
|
|
64
|
+
reaction_id TEXT NOT NULL,
|
|
65
|
+
emoji_type TEXT NOT NULL,
|
|
66
|
+
local_session_id TEXT,
|
|
67
|
+
turn_id TEXT,
|
|
68
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
69
|
+
created_at TEXT NOT NULL,
|
|
70
|
+
updated_at TEXT NOT NULL
|
|
71
|
+
);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_message_reactions_turn_status
|
|
73
|
+
ON message_reactions(turn_id, status);
|
|
74
|
+
`,
|
|
75
|
+
`
|
|
76
|
+
CREATE TABLE IF NOT EXISTS message_turn_bindings (
|
|
77
|
+
message_id TEXT PRIMARY KEY,
|
|
78
|
+
local_session_id TEXT NOT NULL,
|
|
79
|
+
turn_id TEXT NOT NULL,
|
|
80
|
+
created_at TEXT NOT NULL,
|
|
81
|
+
updated_at TEXT NOT NULL
|
|
82
|
+
);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_message_turn_bindings_turn
|
|
84
|
+
ON message_turn_bindings(turn_id);
|
|
85
|
+
`,
|
|
86
|
+
`
|
|
87
|
+
CREATE TABLE IF NOT EXISTS fork_title_sequences (
|
|
88
|
+
base_title TEXT PRIMARY KEY,
|
|
89
|
+
last_sequence INTEGER NOT NULL
|
|
90
|
+
);
|
|
91
|
+
`,
|
|
92
|
+
`
|
|
93
|
+
CREATE TABLE IF NOT EXISTS context_sessions (
|
|
94
|
+
context_key TEXT NOT NULL,
|
|
95
|
+
local_session_id TEXT NOT NULL,
|
|
96
|
+
created_at TEXT NOT NULL,
|
|
97
|
+
updated_at TEXT NOT NULL,
|
|
98
|
+
PRIMARY KEY (context_key, local_session_id)
|
|
99
|
+
);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_context_sessions_local_session
|
|
101
|
+
ON context_sessions(local_session_id);
|
|
102
|
+
`,
|
|
103
|
+
`
|
|
104
|
+
CREATE TABLE IF NOT EXISTS chat_contexts (
|
|
105
|
+
context_key TEXT PRIMARY KEY,
|
|
106
|
+
chat_type TEXT NOT NULL,
|
|
107
|
+
last_activity_at TEXT,
|
|
108
|
+
created_at TEXT NOT NULL,
|
|
109
|
+
updated_at TEXT NOT NULL
|
|
110
|
+
);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_chat_contexts_type
|
|
112
|
+
ON chat_contexts(chat_type, updated_at);
|
|
113
|
+
`,
|
|
114
|
+
`
|
|
115
|
+
CREATE TABLE IF NOT EXISTS queued_prompts (
|
|
116
|
+
prompt_id TEXT PRIMARY KEY,
|
|
117
|
+
local_session_id TEXT NOT NULL,
|
|
118
|
+
context_key TEXT NOT NULL,
|
|
119
|
+
prompt_text TEXT NOT NULL,
|
|
120
|
+
local_image_paths_json TEXT NOT NULL DEFAULT '[]',
|
|
121
|
+
message_id TEXT,
|
|
122
|
+
reply_message_id TEXT,
|
|
123
|
+
created_at TEXT NOT NULL
|
|
124
|
+
);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_queued_prompts_session_order
|
|
126
|
+
ON queued_prompts(local_session_id, created_at);
|
|
127
|
+
`,
|
|
128
|
+
] as const;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
import { CardRenderer } from "../feishu/CardRenderer.js";
|
|
3
|
+
import type { FeishuOutbound } from "../feishu/types.js";
|
|
4
|
+
import type { StateStore } from "../state/StateStore.js";
|
|
5
|
+
import type { SafeRestartStatus } from "./SafeRestartScheduler.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_INITIAL_CARD_DELAY_MS = 3_000;
|
|
8
|
+
|
|
9
|
+
export interface SafeRestartNotifierOptions {
|
|
10
|
+
initialCardDelayMs?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class SafeRestartNotifier {
|
|
14
|
+
private readonly messageIds = new Map<string, string>();
|
|
15
|
+
private readonly cardHashes = new Map<string, string>();
|
|
16
|
+
private activeScheduleId?: number;
|
|
17
|
+
private pendingInitialStatus?: SafeRestartStatus;
|
|
18
|
+
private initialCardTimer?: NodeJS.Timeout;
|
|
19
|
+
private queue = Promise.resolve();
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly store: StateStore,
|
|
23
|
+
private readonly outbound: FeishuOutbound,
|
|
24
|
+
private readonly renderer: CardRenderer,
|
|
25
|
+
private readonly logger: Pick<Logger, "warn">,
|
|
26
|
+
private readonly options: SafeRestartNotifierOptions = {},
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
update(status: SafeRestartStatus): Promise<void> {
|
|
30
|
+
if (this.activeScheduleId !== status.scheduleId) {
|
|
31
|
+
this.activeScheduleId = status.scheduleId;
|
|
32
|
+
this.messageIds.clear();
|
|
33
|
+
this.cardHashes.clear();
|
|
34
|
+
this.clearInitialCardTimer();
|
|
35
|
+
this.pendingInitialStatus = status;
|
|
36
|
+
const delayMs = this.options.initialCardDelayMs ?? DEFAULT_INITIAL_CARD_DELAY_MS;
|
|
37
|
+
if (delayMs <= 0) return this.publishPendingInitialStatus();
|
|
38
|
+
this.initialCardTimer = setTimeout(() => {
|
|
39
|
+
this.initialCardTimer = undefined;
|
|
40
|
+
void this.publishPendingInitialStatus().catch((error: unknown) => {
|
|
41
|
+
this.logger.warn({ error }, "Failed to publish delayed safe restart status.");
|
|
42
|
+
});
|
|
43
|
+
}, delayMs);
|
|
44
|
+
return Promise.resolve();
|
|
45
|
+
}
|
|
46
|
+
if (this.pendingInitialStatus) {
|
|
47
|
+
this.pendingInitialStatus = status;
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
}
|
|
50
|
+
return this.enqueue(status);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async flush(): Promise<void> {
|
|
54
|
+
await this.publishPendingInitialStatus();
|
|
55
|
+
await this.queue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private publishPendingInitialStatus(): Promise<void> {
|
|
59
|
+
this.clearInitialCardTimer();
|
|
60
|
+
const status = this.pendingInitialStatus;
|
|
61
|
+
this.pendingInitialStatus = undefined;
|
|
62
|
+
return status ? this.enqueue(status) : this.queue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private enqueue(status: SafeRestartStatus): Promise<void> {
|
|
66
|
+
const queued = this.queue.catch(() => undefined).then(async () => {
|
|
67
|
+
if (this.activeScheduleId !== status.scheduleId) return;
|
|
68
|
+
await this.publish(status);
|
|
69
|
+
});
|
|
70
|
+
this.queue = queued;
|
|
71
|
+
return queued;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private clearInitialCardTimer(): void {
|
|
75
|
+
if (this.initialCardTimer) clearTimeout(this.initialCardTimer);
|
|
76
|
+
this.initialCardTimer = undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async publish(status: SafeRestartStatus): Promise<void> {
|
|
80
|
+
const targets = this.store.listChatContexts("p2p");
|
|
81
|
+
if (targets.length === 0) return;
|
|
82
|
+
const waitingTasks = this.store.listAllSessions()
|
|
83
|
+
.filter((session) => session.status === "running" || this.store.countQueuedPrompts(session.localSessionId) > 0)
|
|
84
|
+
.map((session) => ({
|
|
85
|
+
id: session.remoteSessionId ?? session.localSessionId,
|
|
86
|
+
title: session.title,
|
|
87
|
+
}));
|
|
88
|
+
const card = this.renderer.renderSafeRestartStatus({
|
|
89
|
+
reason: status.reason,
|
|
90
|
+
phase: status.phase,
|
|
91
|
+
remainingMs: status.remainingMs,
|
|
92
|
+
pendingFinalDeliveries: status.activity.pendingFinalDeliveries,
|
|
93
|
+
waitingTasks,
|
|
94
|
+
});
|
|
95
|
+
const cardHash = JSON.stringify(card);
|
|
96
|
+
|
|
97
|
+
await Promise.all(targets.map(async ({ contextKey }) => {
|
|
98
|
+
const messageId = this.messageIds.get(contextKey);
|
|
99
|
+
if (messageId) {
|
|
100
|
+
if (this.cardHashes.get(contextKey) === cardHash) return;
|
|
101
|
+
try {
|
|
102
|
+
await this.outbound.updateInteractiveCard(messageId, card);
|
|
103
|
+
this.cardHashes.set(contextKey, cardHash);
|
|
104
|
+
return;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this.logger.warn({ error, contextKey, messageId }, "Failed to update safe restart status card; sending a replacement.");
|
|
107
|
+
this.messageIds.delete(contextKey);
|
|
108
|
+
this.cardHashes.delete(contextKey);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const created = await this.outbound.sendInteractiveCard(contextKey, card);
|
|
113
|
+
if (created && this.activeScheduleId === status.scheduleId) {
|
|
114
|
+
this.messageIds.set(contextKey, created);
|
|
115
|
+
this.cardHashes.set(contextKey, cardHash);
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
this.logger.warn({ error, contextKey }, "Failed to send safe restart status card.");
|
|
119
|
+
}
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export interface ServerActivityState {
|
|
2
|
+
runningSessions: number;
|
|
3
|
+
pendingFinalDeliveries: number;
|
|
4
|
+
latestInboundAt?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type SafeRestartPhase = "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting";
|
|
8
|
+
|
|
9
|
+
export interface SafeRestartStatus {
|
|
10
|
+
scheduleId: number;
|
|
11
|
+
reason: string;
|
|
12
|
+
phase: SafeRestartPhase;
|
|
13
|
+
activity: ServerActivityState;
|
|
14
|
+
remainingMs?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SafeRestartSchedulerOptions {
|
|
18
|
+
readActivity(): ServerActivityState;
|
|
19
|
+
onReady(reason: string): void | Promise<void>;
|
|
20
|
+
onStatus?(status: SafeRestartStatus): void | Promise<void>;
|
|
21
|
+
onStatusError?(error: unknown): void;
|
|
22
|
+
quietPeriodMs?: number;
|
|
23
|
+
pollIntervalMs?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SafeRestartScheduler {
|
|
27
|
+
private timer?: NodeJS.Timeout;
|
|
28
|
+
private polling?: Promise<void>;
|
|
29
|
+
private pollAgain = false;
|
|
30
|
+
private reason?: string;
|
|
31
|
+
private idleSince?: number;
|
|
32
|
+
private idleInboundAt?: string;
|
|
33
|
+
private scheduleId = 0;
|
|
34
|
+
|
|
35
|
+
constructor(private readonly options: SafeRestartSchedulerOptions) {}
|
|
36
|
+
|
|
37
|
+
get scheduled(): boolean {
|
|
38
|
+
return Boolean(this.reason);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get pendingReason(): string | undefined {
|
|
42
|
+
return this.reason;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
schedule(reason: string): boolean {
|
|
46
|
+
const newlyScheduled = !this.reason;
|
|
47
|
+
if (newlyScheduled) this.scheduleId += 1;
|
|
48
|
+
this.reason = reason;
|
|
49
|
+
this.idleSince = undefined;
|
|
50
|
+
this.idleInboundAt = undefined;
|
|
51
|
+
if (!this.timer) {
|
|
52
|
+
this.timer = setInterval(() => this.requestPoll(), this.options.pollIntervalMs ?? 1_000);
|
|
53
|
+
}
|
|
54
|
+
this.requestPoll();
|
|
55
|
+
return newlyScheduled;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
cancel(): void {
|
|
59
|
+
if (this.timer) clearInterval(this.timer);
|
|
60
|
+
this.timer = undefined;
|
|
61
|
+
this.reason = undefined;
|
|
62
|
+
this.idleSince = undefined;
|
|
63
|
+
this.idleInboundAt = undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async poll(): Promise<void> {
|
|
67
|
+
const reason = this.reason;
|
|
68
|
+
if (!reason) return;
|
|
69
|
+
const state = this.options.readActivity();
|
|
70
|
+
if (state.runningSessions > 0 || state.pendingFinalDeliveries > 0) {
|
|
71
|
+
this.idleSince = undefined;
|
|
72
|
+
this.idleInboundAt = undefined;
|
|
73
|
+
await this.emitStatus({
|
|
74
|
+
scheduleId: this.scheduleId,
|
|
75
|
+
reason,
|
|
76
|
+
phase: state.runningSessions > 0 ? "waiting_tasks" : "waiting_delivery",
|
|
77
|
+
activity: state,
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const quietPeriodMs = this.options.quietPeriodMs ?? 15_000;
|
|
82
|
+
const latestInboundAt = state.latestInboundAt ?? "";
|
|
83
|
+
if (this.idleSince === undefined || this.idleInboundAt !== latestInboundAt) {
|
|
84
|
+
this.idleSince = Date.now();
|
|
85
|
+
this.idleInboundAt = latestInboundAt;
|
|
86
|
+
await this.emitStatus({ scheduleId: this.scheduleId, reason, phase: "countdown", activity: state, remainingMs: quietPeriodMs });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const remainingMs = Math.max(0, quietPeriodMs - (Date.now() - this.idleSince));
|
|
90
|
+
if (remainingMs > 0) {
|
|
91
|
+
await this.emitStatus({ scheduleId: this.scheduleId, reason, phase: "countdown", activity: state, remainingMs });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const confirmed = this.options.readActivity();
|
|
95
|
+
if (
|
|
96
|
+
confirmed.runningSessions > 0
|
|
97
|
+
|| confirmed.pendingFinalDeliveries > 0
|
|
98
|
+
|| (confirmed.latestInboundAt ?? "") !== this.idleInboundAt
|
|
99
|
+
) {
|
|
100
|
+
this.idleSince = undefined;
|
|
101
|
+
this.idleInboundAt = undefined;
|
|
102
|
+
if (confirmed.runningSessions > 0 || confirmed.pendingFinalDeliveries > 0) {
|
|
103
|
+
await this.emitStatus({
|
|
104
|
+
scheduleId: this.scheduleId,
|
|
105
|
+
reason,
|
|
106
|
+
phase: confirmed.runningSessions > 0 ? "waiting_tasks" : "waiting_delivery",
|
|
107
|
+
activity: confirmed,
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
await this.emitStatus({
|
|
111
|
+
scheduleId: this.scheduleId,
|
|
112
|
+
reason,
|
|
113
|
+
phase: "countdown",
|
|
114
|
+
activity: confirmed,
|
|
115
|
+
remainingMs: quietPeriodMs,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await this.emitStatus({ scheduleId: this.scheduleId, reason, phase: "restarting", activity: confirmed, remainingMs: 0 });
|
|
121
|
+
this.cancel();
|
|
122
|
+
await this.options.onReady(reason);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private requestPoll(): void {
|
|
126
|
+
if (this.polling) {
|
|
127
|
+
this.pollAgain = true;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.polling = this.drainPolls().finally(() => {
|
|
131
|
+
this.polling = undefined;
|
|
132
|
+
if (this.pollAgain && this.reason) this.requestPoll();
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async drainPolls(): Promise<void> {
|
|
137
|
+
do {
|
|
138
|
+
this.pollAgain = false;
|
|
139
|
+
await this.poll();
|
|
140
|
+
} while (this.pollAgain && this.reason);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async emitStatus(status: SafeRestartStatus): Promise<void> {
|
|
144
|
+
if (!this.options.onStatus) return;
|
|
145
|
+
await Promise.resolve(this.options.onStatus(status)).catch((error: unknown) => {
|
|
146
|
+
this.options.onStatusError?.(error);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function replacementSupervisorEnvironment(
|
|
2
|
+
restartReason: string,
|
|
3
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
4
|
+
): NodeJS.ProcessEnv {
|
|
5
|
+
return {
|
|
6
|
+
...environment,
|
|
7
|
+
AGENT_BOT_START_DELAY_MS: "250",
|
|
8
|
+
AGENT_BOT_RESTART_REASON: restartReason,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const RESTART_EXIT_CODE = 75;
|
|
2
|
+
export const STOP_EXIT_CODE = 76;
|
|
3
|
+
export const INTENTIONAL_RESTART_DELAY_MS = 250;
|
|
4
|
+
export const STABLE_UPTIME_MS = 60_000;
|
|
5
|
+
|
|
6
|
+
export function crashRestartDelayMs(consecutiveFailures: number): number {
|
|
7
|
+
const exponent = Math.max(0, consecutiveFailures - 1);
|
|
8
|
+
return Math.min(30_000, 1_000 * (2 ** exponent));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function describeRestartReason(
|
|
12
|
+
exitCode: number | null,
|
|
13
|
+
signal: NodeJS.Signals | null,
|
|
14
|
+
intentional: boolean,
|
|
15
|
+
): string {
|
|
16
|
+
if (intentional) return "用户执行 /restart 命令";
|
|
17
|
+
if (signal) return `进程收到 ${signal} 信号后退出,Supervisor 自动重启`;
|
|
18
|
+
return `进程退出(exit code ${exitCode ?? "unknown"}),Supervisor 自动重启`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { isServerRunning } from "./cli/LocalControlClient.js";
|
|
4
|
+
import { controlEndpoint } from "./cli/controlProtocol.js";
|
|
5
|
+
import { loadConfig } from "./config/loadConfig.js";
|
|
6
|
+
import {
|
|
7
|
+
crashRestartDelayMs,
|
|
8
|
+
describeRestartReason,
|
|
9
|
+
INTENTIONAL_RESTART_DELAY_MS,
|
|
10
|
+
RESTART_EXIT_CODE,
|
|
11
|
+
STABLE_UPTIME_MS,
|
|
12
|
+
STOP_EXIT_CODE,
|
|
13
|
+
} from "./supervision/restartPolicy.js";
|
|
14
|
+
|
|
15
|
+
const childEntry = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
16
|
+
const sqlitePath = loadConfig().storage.sqlitePath;
|
|
17
|
+
const serverEndpoint = controlEndpoint(sqlitePath);
|
|
18
|
+
let child: ChildProcess | undefined;
|
|
19
|
+
let restartTimer: NodeJS.Timeout | undefined;
|
|
20
|
+
let stopping = false;
|
|
21
|
+
let consecutiveFailures = 0;
|
|
22
|
+
let nextStartReason = process.env.AGENT_BOT_RESTART_REASON?.trim() || "Supervisor 启动";
|
|
23
|
+
|
|
24
|
+
async function startChild(): Promise<void> {
|
|
25
|
+
if (stopping || child) return;
|
|
26
|
+
if (await isServerRunning(serverEndpoint)) {
|
|
27
|
+
writeSupervisorLog("existing_server_detected", { endpoint: serverEndpoint });
|
|
28
|
+
process.exit(0);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const startedAt = Date.now();
|
|
32
|
+
const restartReason = nextStartReason;
|
|
33
|
+
nextStartReason = "Supervisor 重新拉起进程";
|
|
34
|
+
child = spawn(process.execPath, [childEntry], {
|
|
35
|
+
cwd: process.cwd(),
|
|
36
|
+
env: { ...process.env, AGENT_BOT_SUPERVISED: "1", AGENT_BOT_RESTART_REASON: restartReason },
|
|
37
|
+
stdio: "inherit",
|
|
38
|
+
windowsHide: true,
|
|
39
|
+
});
|
|
40
|
+
writeSupervisorLog("started", { pid: child.pid, restartReason });
|
|
41
|
+
|
|
42
|
+
child.once("error", (error) => {
|
|
43
|
+
writeSupervisorLog("spawn_error", { error: error.message });
|
|
44
|
+
});
|
|
45
|
+
child.once("exit", (code, signal) => {
|
|
46
|
+
void handleChildExit(code, signal, startedAt);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function handleChildExit(
|
|
51
|
+
code: number | null,
|
|
52
|
+
signal: NodeJS.Signals | null,
|
|
53
|
+
startedAt: number,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
child = undefined;
|
|
56
|
+
const uptimeMs = Date.now() - startedAt;
|
|
57
|
+
writeSupervisorLog("exited", { code, signal, uptimeMs });
|
|
58
|
+
if (stopping) {
|
|
59
|
+
process.exit(0);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (code === STOP_EXIT_CODE) {
|
|
63
|
+
writeSupervisorLog("stopped_by_request", { code });
|
|
64
|
+
process.exit(0);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (await isServerRunning(serverEndpoint)) {
|
|
68
|
+
writeSupervisorLog("duplicate_supervisor_stopped", { endpoint: serverEndpoint });
|
|
69
|
+
process.exit(0);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const intentional = code === RESTART_EXIT_CODE;
|
|
74
|
+
nextStartReason = describeRestartReason(code, signal, intentional);
|
|
75
|
+
if (intentional || uptimeMs >= STABLE_UPTIME_MS) consecutiveFailures = 0;
|
|
76
|
+
else consecutiveFailures += 1;
|
|
77
|
+
const delayMs = intentional
|
|
78
|
+
? INTENTIONAL_RESTART_DELAY_MS
|
|
79
|
+
: crashRestartDelayMs(consecutiveFailures);
|
|
80
|
+
writeSupervisorLog("restarting", { delayMs, consecutiveFailures, intentional });
|
|
81
|
+
restartTimer = setTimeout(() => void startChild(), delayMs);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function stop(signal: NodeJS.Signals): void {
|
|
85
|
+
if (stopping) return;
|
|
86
|
+
stopping = true;
|
|
87
|
+
if (restartTimer) clearTimeout(restartTimer);
|
|
88
|
+
if (child && !child.killed) {
|
|
89
|
+
child.kill(signal);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeSupervisorLog(event: string, data: Record<string, unknown>): void {
|
|
96
|
+
process.stdout.write(`${JSON.stringify({ component: "agent-bot-supervisor", event, time: new Date().toISOString(), ...data })}\n`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
process.on("SIGINT", () => stop("SIGINT"));
|
|
100
|
+
process.on("SIGTERM", () => stop("SIGTERM"));
|
|
101
|
+
const initialDelayMs = Number(process.env.AGENT_BOT_START_DELAY_MS ?? 0);
|
|
102
|
+
if (Number.isFinite(initialDelayMs) && initialDelayMs > 0) {
|
|
103
|
+
restartTimer = setTimeout(() => void startChild(), initialDelayMs);
|
|
104
|
+
} else {
|
|
105
|
+
void startChild();
|
|
106
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
4
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
5
|
+
|
|
6
|
+
export interface ShellCommandResult {
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
exitCode: number | null;
|
|
10
|
+
timedOut: boolean;
|
|
11
|
+
outputTruncated: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ShellCommandOptions {
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
maxOutputBytes?: number;
|
|
17
|
+
platform?: NodeJS.Platform;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function executeShellCommand(
|
|
21
|
+
command: string,
|
|
22
|
+
cwd: string,
|
|
23
|
+
options: ShellCommandOptions = {},
|
|
24
|
+
): Promise<ShellCommandResult> {
|
|
25
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
26
|
+
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
27
|
+
const prepared = prepareShellCommand(command, options.platform ?? process.platform);
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const child = spawn(prepared.command, prepared.args, {
|
|
31
|
+
cwd,
|
|
32
|
+
env: process.env,
|
|
33
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
});
|
|
36
|
+
const output = new BoundedOutput(maxOutputBytes);
|
|
37
|
+
let timedOut = false;
|
|
38
|
+
const timeout = setTimeout(() => {
|
|
39
|
+
timedOut = true;
|
|
40
|
+
child.kill();
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
|
|
43
|
+
child.stdout.on("data", (chunk: Buffer) => output.append("stdout", chunk));
|
|
44
|
+
child.stderr.on("data", (chunk: Buffer) => output.append("stderr", chunk));
|
|
45
|
+
child.once("error", (error) => {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
reject(error);
|
|
48
|
+
});
|
|
49
|
+
child.once("close", (exitCode) => {
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
resolve({
|
|
52
|
+
...output.result(),
|
|
53
|
+
exitCode,
|
|
54
|
+
timedOut,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function prepareShellCommand(command: string, platform: NodeJS.Platform): { command: string; args: string[] } {
|
|
61
|
+
if (platform !== "win32") return { command: "/bin/sh", args: ["-lc", command] };
|
|
62
|
+
const script = `$ProgressPreference = 'SilentlyContinue'; [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); ${command}`;
|
|
63
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
64
|
+
return {
|
|
65
|
+
command: "powershell.exe",
|
|
66
|
+
args: ["-NoLogo", "-NoProfile", "-NonInteractive", "-OutputFormat", "Text", "-EncodedCommand", encoded],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class BoundedOutput {
|
|
71
|
+
private readonly stdout: Buffer[] = [];
|
|
72
|
+
private readonly stderr: Buffer[] = [];
|
|
73
|
+
private retainedBytes = 0;
|
|
74
|
+
private truncated = false;
|
|
75
|
+
|
|
76
|
+
constructor(private readonly maxBytes: number) {}
|
|
77
|
+
|
|
78
|
+
append(stream: "stdout" | "stderr", chunk: Buffer): void {
|
|
79
|
+
const remaining = Math.max(0, this.maxBytes - this.retainedBytes);
|
|
80
|
+
if (remaining === 0) {
|
|
81
|
+
this.truncated = true;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const retained = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining);
|
|
85
|
+
this[stream].push(retained);
|
|
86
|
+
this.retainedBytes += retained.length;
|
|
87
|
+
if (retained.length < chunk.length) this.truncated = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
result(): Pick<ShellCommandResult, "stdout" | "stderr" | "outputTruncated"> {
|
|
91
|
+
return {
|
|
92
|
+
stdout: Buffer.concat(this.stdout).toString("utf8"),
|
|
93
|
+
stderr: Buffer.concat(this.stderr).toString("utf8"),
|
|
94
|
+
outputTruncated: this.truncated,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/utils/id.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function truncateText(text: string, maxLength = 8000): string {
|
|
2
|
+
if (text.length <= maxLength) return text;
|
|
3
|
+
if (maxLength <= 0) return "";
|
|
4
|
+
if (maxLength <= 3) return ".".repeat(maxLength);
|
|
5
|
+
return `${text.slice(0, maxLength - 3)}...`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function truncateMiddle(text: string, maxLength = 8000, marker = "\n...\n"): string {
|
|
9
|
+
if (text.length <= maxLength) return text;
|
|
10
|
+
if (maxLength <= 0) return "";
|
|
11
|
+
if (maxLength <= marker.length) return ".".repeat(maxLength);
|
|
12
|
+
const retainedLength = maxLength - marker.length;
|
|
13
|
+
const headLength = Math.ceil(retainedLength / 2);
|
|
14
|
+
const tailLength = Math.floor(retainedLength / 2);
|
|
15
|
+
return `${text.slice(0, headLength)}${marker}${text.slice(text.length - tailLength)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function codeBlock(text: string, language = ""): string {
|
|
19
|
+
const fenceSafe = text.replaceAll("```", "`\u200b``");
|
|
20
|
+
return `\`\`\`${language}\n${fenceSafe}\n\`\`\``;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function asInlineCode(text: string): string {
|
|
24
|
+
return `\`${text.replaceAll("`", "'")}\``;
|
|
25
|
+
}
|