agent-life-bridge 0.1.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.
Files changed (195) hide show
  1. package/.env.example +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +350 -0
  4. package/bin/agent-life-bridge.mjs +13 -0
  5. package/config.example.json +130 -0
  6. package/config.multi-agent.example.json +88 -0
  7. package/dist/agents/agent-life-session-map-store.d.ts +21 -0
  8. package/dist/agents/agent-life-session-map-store.d.ts.map +1 -0
  9. package/dist/agents/agent-life-session-map-store.js +78 -0
  10. package/dist/agents/agent-life-session-map-store.js.map +1 -0
  11. package/dist/agents/agent-resolver.d.ts +12 -0
  12. package/dist/agents/agent-resolver.d.ts.map +1 -0
  13. package/dist/agents/agent-resolver.js +114 -0
  14. package/dist/agents/agent-resolver.js.map +1 -0
  15. package/dist/agents/agent-router.d.ts +8 -0
  16. package/dist/agents/agent-router.d.ts.map +1 -0
  17. package/dist/agents/agent-router.js +72 -0
  18. package/dist/agents/agent-router.js.map +1 -0
  19. package/dist/agents/cli-backends.d.ts +25 -0
  20. package/dist/agents/cli-backends.d.ts.map +1 -0
  21. package/dist/agents/cli-backends.js +252 -0
  22. package/dist/agents/cli-backends.js.map +1 -0
  23. package/dist/agents/cli-output.d.ts +43 -0
  24. package/dist/agents/cli-output.d.ts.map +1 -0
  25. package/dist/agents/cli-output.js +352 -0
  26. package/dist/agents/cli-output.js.map +1 -0
  27. package/dist/agents/cli-runner.d.ts +32 -0
  28. package/dist/agents/cli-runner.d.ts.map +1 -0
  29. package/dist/agents/cli-runner.js +861 -0
  30. package/dist/agents/cli-runner.js.map +1 -0
  31. package/dist/agents/cli-session-store.d.ts +53 -0
  32. package/dist/agents/cli-session-store.d.ts.map +1 -0
  33. package/dist/agents/cli-session-store.js +263 -0
  34. package/dist/agents/cli-session-store.js.map +1 -0
  35. package/dist/agents/codex-app-server-runtime.d.ts +80 -0
  36. package/dist/agents/codex-app-server-runtime.d.ts.map +1 -0
  37. package/dist/agents/codex-app-server-runtime.js +1049 -0
  38. package/dist/agents/codex-app-server-runtime.js.map +1 -0
  39. package/dist/agents/fch-runtime-context.d.ts +28 -0
  40. package/dist/agents/fch-runtime-context.d.ts.map +1 -0
  41. package/dist/agents/fch-runtime-context.js +65 -0
  42. package/dist/agents/fch-runtime-context.js.map +1 -0
  43. package/dist/agents/model-selection.d.ts +28 -0
  44. package/dist/agents/model-selection.d.ts.map +1 -0
  45. package/dist/agents/model-selection.js +40 -0
  46. package/dist/agents/model-selection.js.map +1 -0
  47. package/dist/agents/models-config.d.ts +28 -0
  48. package/dist/agents/models-config.d.ts.map +1 -0
  49. package/dist/agents/models-config.js +43 -0
  50. package/dist/agents/models-config.js.map +1 -0
  51. package/dist/channels/agent-life.d.ts +9 -0
  52. package/dist/channels/agent-life.d.ts.map +1 -0
  53. package/dist/channels/agent-life.js +407 -0
  54. package/dist/channels/agent-life.js.map +1 -0
  55. package/dist/channels/command-gating.d.ts +20 -0
  56. package/dist/channels/command-gating.d.ts.map +1 -0
  57. package/dist/channels/command-gating.js +43 -0
  58. package/dist/channels/command-gating.js.map +1 -0
  59. package/dist/channels/draft-stream-controls.d.ts +21 -0
  60. package/dist/channels/draft-stream-controls.d.ts.map +1 -0
  61. package/dist/channels/draft-stream-controls.js +43 -0
  62. package/dist/channels/draft-stream-controls.js.map +1 -0
  63. package/dist/channels/draft-stream-loop.d.ts +31 -0
  64. package/dist/channels/draft-stream-loop.d.ts.map +1 -0
  65. package/dist/channels/draft-stream-loop.js +60 -0
  66. package/dist/channels/draft-stream-loop.js.map +1 -0
  67. package/dist/channels/mention-gating.d.ts +18 -0
  68. package/dist/channels/mention-gating.d.ts.map +1 -0
  69. package/dist/channels/mention-gating.js +20 -0
  70. package/dist/channels/mention-gating.js.map +1 -0
  71. package/dist/channels/registry.d.ts +5 -0
  72. package/dist/channels/registry.d.ts.map +1 -0
  73. package/dist/channels/registry.js +14 -0
  74. package/dist/channels/registry.js.map +1 -0
  75. package/dist/channels/run-state-machine.d.ts +21 -0
  76. package/dist/channels/run-state-machine.d.ts.map +1 -0
  77. package/dist/channels/run-state-machine.js +41 -0
  78. package/dist/channels/run-state-machine.js.map +1 -0
  79. package/dist/channels/session-envelope.d.ts +5 -0
  80. package/dist/channels/session-envelope.d.ts.map +1 -0
  81. package/dist/channels/session-envelope.js +16 -0
  82. package/dist/channels/session-envelope.js.map +1 -0
  83. package/dist/channels/session-id.d.ts +3 -0
  84. package/dist/channels/session-id.d.ts.map +1 -0
  85. package/dist/channels/session-id.js +11 -0
  86. package/dist/channels/session-id.js.map +1 -0
  87. package/dist/channels/session.d.ts +18 -0
  88. package/dist/channels/session.d.ts.map +1 -0
  89. package/dist/channels/session.js +35 -0
  90. package/dist/channels/session.js.map +1 -0
  91. package/dist/channels/typing-lifecycle.d.ts +16 -0
  92. package/dist/channels/typing-lifecycle.d.ts.map +1 -0
  93. package/dist/channels/typing-lifecycle.js +31 -0
  94. package/dist/channels/typing-lifecycle.js.map +1 -0
  95. package/dist/cli/cmd-add-agent.d.ts +19 -0
  96. package/dist/cli/cmd-add-agent.d.ts.map +1 -0
  97. package/dist/cli/cmd-add-agent.js +362 -0
  98. package/dist/cli/cmd-add-agent.js.map +1 -0
  99. package/dist/cli/cmd-config.d.ts +3 -0
  100. package/dist/cli/cmd-config.d.ts.map +1 -0
  101. package/dist/cli/cmd-config.js +16 -0
  102. package/dist/cli/cmd-config.js.map +1 -0
  103. package/dist/cli/cmd-doctor.d.ts +3 -0
  104. package/dist/cli/cmd-doctor.d.ts.map +1 -0
  105. package/dist/cli/cmd-doctor.js +127 -0
  106. package/dist/cli/cmd-doctor.js.map +1 -0
  107. package/dist/cli/cmd-logs.d.ts +3 -0
  108. package/dist/cli/cmd-logs.d.ts.map +1 -0
  109. package/dist/cli/cmd-logs.js +26 -0
  110. package/dist/cli/cmd-logs.js.map +1 -0
  111. package/dist/cli/cmd-onboard.d.ts +5 -0
  112. package/dist/cli/cmd-onboard.d.ts.map +1 -0
  113. package/dist/cli/cmd-onboard.js +53 -0
  114. package/dist/cli/cmd-onboard.js.map +1 -0
  115. package/dist/cli/cmd-restart.d.ts +3 -0
  116. package/dist/cli/cmd-restart.d.ts.map +1 -0
  117. package/dist/cli/cmd-restart.js +22 -0
  118. package/dist/cli/cmd-restart.js.map +1 -0
  119. package/dist/cli/cmd-start.d.ts +19 -0
  120. package/dist/cli/cmd-start.d.ts.map +1 -0
  121. package/dist/cli/cmd-start.js +783 -0
  122. package/dist/cli/cmd-start.js.map +1 -0
  123. package/dist/cli/cmd-status.d.ts +3 -0
  124. package/dist/cli/cmd-status.d.ts.map +1 -0
  125. package/dist/cli/cmd-status.js +16 -0
  126. package/dist/cli/cmd-status.js.map +1 -0
  127. package/dist/cli/cmd-stop.d.ts +9 -0
  128. package/dist/cli/cmd-stop.d.ts.map +1 -0
  129. package/dist/cli/cmd-stop.js +59 -0
  130. package/dist/cli/cmd-stop.js.map +1 -0
  131. package/dist/cli/cmd-update.d.ts +3 -0
  132. package/dist/cli/cmd-update.d.ts.map +1 -0
  133. package/dist/cli/cmd-update.js +127 -0
  134. package/dist/cli/cmd-update.js.map +1 -0
  135. package/dist/cli/cmd-version.d.ts +4 -0
  136. package/dist/cli/cmd-version.d.ts.map +1 -0
  137. package/dist/cli/cmd-version.js +12 -0
  138. package/dist/cli/cmd-version.js.map +1 -0
  139. package/dist/cli/pid-file.d.ts +23 -0
  140. package/dist/cli/pid-file.d.ts.map +1 -0
  141. package/dist/cli/pid-file.js +136 -0
  142. package/dist/cli/pid-file.js.map +1 -0
  143. package/dist/cli/run-main.d.ts +16 -0
  144. package/dist/cli/run-main.d.ts.map +1 -0
  145. package/dist/cli/run-main.js +114 -0
  146. package/dist/cli/run-main.js.map +1 -0
  147. package/dist/cli/update-check.d.ts +15 -0
  148. package/dist/cli/update-check.d.ts.map +1 -0
  149. package/dist/cli/update-check.js +187 -0
  150. package/dist/cli/update-check.js.map +1 -0
  151. package/dist/commands/prefix-router.d.ts +17 -0
  152. package/dist/commands/prefix-router.d.ts.map +1 -0
  153. package/dist/commands/prefix-router.js +79 -0
  154. package/dist/commands/prefix-router.js.map +1 -0
  155. package/dist/config/agent-dirs.d.ts +24 -0
  156. package/dist/config/agent-dirs.d.ts.map +1 -0
  157. package/dist/config/agent-dirs.js +71 -0
  158. package/dist/config/agent-dirs.js.map +1 -0
  159. package/dist/config/config.d.ts +4 -0
  160. package/dist/config/config.d.ts.map +1 -0
  161. package/dist/config/config.js +42 -0
  162. package/dist/config/config.js.map +1 -0
  163. package/dist/config/default-config.d.ts +9 -0
  164. package/dist/config/default-config.d.ts.map +1 -0
  165. package/dist/config/default-config.js +94 -0
  166. package/dist/config/default-config.js.map +1 -0
  167. package/dist/config/env-vars.d.ts +15 -0
  168. package/dist/config/env-vars.d.ts.map +1 -0
  169. package/dist/config/env-vars.js +16 -0
  170. package/dist/config/env-vars.js.map +1 -0
  171. package/dist/config/merge-config.d.ts +7 -0
  172. package/dist/config/merge-config.d.ts.map +1 -0
  173. package/dist/config/merge-config.js +45 -0
  174. package/dist/config/merge-config.js.map +1 -0
  175. package/dist/config/paths.d.ts +25 -0
  176. package/dist/config/paths.d.ts.map +1 -0
  177. package/dist/config/paths.js +31 -0
  178. package/dist/config/paths.js.map +1 -0
  179. package/dist/config/zod-schema.d.ts +3114 -0
  180. package/dist/config/zod-schema.d.ts.map +1 -0
  181. package/dist/config/zod-schema.js +217 -0
  182. package/dist/config/zod-schema.js.map +1 -0
  183. package/dist/entry.d.ts +3 -0
  184. package/dist/entry.d.ts.map +1 -0
  185. package/dist/entry.js +70 -0
  186. package/dist/entry.js.map +1 -0
  187. package/dist/logger.d.ts +5 -0
  188. package/dist/logger.d.ts.map +1 -0
  189. package/dist/logger.js +42 -0
  190. package/dist/logger.js.map +1 -0
  191. package/dist/types/index.d.ts +215 -0
  192. package/dist/types/index.d.ts.map +1 -0
  193. package/dist/types/index.js +2 -0
  194. package/dist/types/index.js.map +1 -0
  195. package/package.json +62 -0
@@ -0,0 +1,783 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loadConfig } from '../config/config.js';
4
+ import { ensureDataDirs, ensureAgentStateDirs } from '../config/agent-dirs.js';
5
+ import { createLogger, resolveLogFilePath } from '../logger.js';
6
+ import { getAllChannels } from '../channels/registry.js';
7
+ import { SessionManager } from '../channels/session.js';
8
+ import { LocalCliTurnExecutor } from '../agents/cli-runner.js';
9
+ import { CliSessionStore, makeAgentSessionKey } from '../agents/cli-session-store.js';
10
+ import { AgentLifeSessionMapStore } from '../agents/agent-life-session-map-store.js';
11
+ import { createFchRuntimeContext } from '../agents/fch-runtime-context.js';
12
+ import { resolveAgentConfigs } from '../agents/agent-resolver.js';
13
+ import { resolveAgent } from '../agents/agent-router.js';
14
+ import { cleanupPidFile, cleanupRuntimeState, ensureNoRunningInstance, writePidFile, writeRuntimeState } from './pid-file.js';
15
+ import { maybeNotifyAboutUpdateWithLogger } from './update-check.js';
16
+ import { normalizeAgentLifeChannelConfigs, } from '../config/zod-schema.js';
17
+ import { TypingLifecycle } from '../channels/typing-lifecycle.js';
18
+ import { agentSessionStorePath } from '../config/paths.js';
19
+ import { extractProvider, extractModel, isCliBackend } from '../agents/cli-backends.js';
20
+ import { routeCommandText } from '../commands/prefix-router.js';
21
+ export async function cmdStart(args) {
22
+ const channels = getAllChannels();
23
+ const activeChannels = [];
24
+ const cliExecutors = new Map();
25
+ const sessionStores = new Map();
26
+ const teardownExecutors = async () => {
27
+ for (const executor of cliExecutors.values()) {
28
+ await executor.teardown().catch(() => undefined);
29
+ }
30
+ };
31
+ let logger = null;
32
+ let shutdownPromise = null;
33
+ let startupSettled = false;
34
+ const gracefulShutdown = async (signal) => {
35
+ if (!shutdownPromise) {
36
+ shutdownPromise = (async () => {
37
+ logger?.info({ signal }, 'Shutdown signal received');
38
+ for (const channel of activeChannels) {
39
+ await channel.teardown().catch(() => undefined);
40
+ }
41
+ await teardownExecutors();
42
+ cleanupPidFile();
43
+ cleanupRuntimeState();
44
+ })();
45
+ }
46
+ await shutdownPromise;
47
+ };
48
+ const shutdownAndExit = (signal) => {
49
+ void gracefulShutdown(signal).finally(() => {
50
+ process.exit(0);
51
+ });
52
+ };
53
+ const onSigInt = () => shutdownAndExit('SIGINT');
54
+ const onSigTerm = () => shutdownAndExit('SIGTERM');
55
+ process.on('SIGINT', onSigInt);
56
+ process.on('SIGTERM', onSigTerm);
57
+ await ensureNoRunningInstance({
58
+ waitMs: 25_000,
59
+ expectedCwd: process.cwd(),
60
+ onWait(pid, waitMs) {
61
+ console.log(`Existing instance detected (pid: ${pid}), waiting up to ${Math.round(waitMs / 1000)}s for graceful shutdown...`);
62
+ },
63
+ onWaitComplete(pid) {
64
+ console.log(`Previous instance exited (pid: ${pid}), continuing startup.`);
65
+ },
66
+ });
67
+ if (args.daemon) {
68
+ const { spawn } = await import('node:child_process');
69
+ const childArgs = ['start'];
70
+ if (args.config) {
71
+ childArgs.push('--config', args.config);
72
+ }
73
+ const child = spawn(process.execPath, [process.argv[1], ...childArgs], {
74
+ detached: true,
75
+ stdio: 'ignore',
76
+ env: { ...process.env, AGENT_LIFE_BRIDGE_DAEMON: '1' },
77
+ });
78
+ child.unref();
79
+ console.log(`agent-life-bridge daemon started (pid: ${child.pid})`);
80
+ process.exit(0);
81
+ }
82
+ const config = loadConfig(args.config);
83
+ ensureDataDirs(config.files.temp_dir);
84
+ logger = createLogger(config.logging.level, config.logging.pretty);
85
+ logger.info({ logFilePath: resolveLogFilePath() }, 'agent-life-bridge starting up');
86
+ writePidFile(process.pid);
87
+ writeRuntimeState({
88
+ pid: process.pid,
89
+ argv: process.argv.slice(2),
90
+ cwd: process.cwd(),
91
+ });
92
+ try {
93
+ const sessionManager = new SessionManager(logger);
94
+ const { agents: resolvedAgents, defaultAgentId } = resolveAgentConfigs(config, logger);
95
+ const agentLifeSessionMapStore = new AgentLifeSessionMapStore();
96
+ ensureAgentStateDirs([...resolvedAgents.keys()], defaultAgentId);
97
+ logger.info({ agentCount: resolvedAgents.size, defaultAgentId, agents: [...resolvedAgents.keys()] }, 'Agents resolved');
98
+ for (const [agentId] of resolvedAgents) {
99
+ const sessionStore = new CliSessionStore(agentSessionStorePath(agentId));
100
+ sessionStores.set(agentId, sessionStore);
101
+ const incompleteCodexSessions = await sessionStore.findIncompleteSessions('codex-cli');
102
+ if (incompleteCodexSessions.length > 0) {
103
+ logger.warn({
104
+ agentId,
105
+ count: incompleteCodexSessions.length,
106
+ sessions: incompleteCodexSessions.slice(0, 10).map((entry) => entry.sessionKey),
107
+ }, 'Found persisted Codex sessions without thread ids; those conversations will resume with fresh turns until rebuilt');
108
+ }
109
+ cliExecutors.set(agentId, new LocalCliTurnExecutor({
110
+ config,
111
+ logger,
112
+ sessionStore,
113
+ }));
114
+ }
115
+ const ctx = {
116
+ logger,
117
+ async dispatch(envelope) {
118
+ const agentLifeConfig = resolveAgentLifeAccountConfig(config.channels['agent-life'], envelope);
119
+ const agent = resolveAgent(envelope, config.bindings ?? [], resolvedAgents, defaultAgentId);
120
+ const agentLifeConversationKey = envelope.source.agentLifeSessionKey ?? envelope.sessionId;
121
+ const rootAgentSessionKey = makeAgentSessionKey(agent.id, agentLifeConversationKey);
122
+ const fchContext = createFchRuntimeContext({
123
+ sessionId: envelope.sessionId,
124
+ agentId: agent.id,
125
+ });
126
+ await agentLifeSessionMapStore.upsert({
127
+ agentLifeMsgId: envelope.source.messageId,
128
+ sessionId: envelope.sessionId,
129
+ agentId: agent.id,
130
+ applyId: fchContext.applyId,
131
+ userId: String(envelope.source.userId),
132
+ });
133
+ logger.info({ sessionId: envelope.sessionId, agentId: agent.id, chatType: envelope.source.chatType }, 'Dispatching message');
134
+ sessionManager.getOrCreate(envelope);
135
+ const primaryModelRef = agent.model.primary;
136
+ const provider = extractProvider(primaryModelRef);
137
+ const model = extractModel(primaryModelRef);
138
+ const sessionStore = sessionStores.get(agent.id);
139
+ if (!isCliBackend(primaryModelRef, agent.cliBackends)) {
140
+ logger.warn({ sessionId: envelope.sessionId, agentId: agent.id, model: primaryModelRef }, 'Primary API model runtime is not implemented yet');
141
+ await sendAgentLifeTextReply(agentLifeConfig, envelope, '当前仅支持 CLI 模式模型。');
142
+ return;
143
+ }
144
+ const cliExecutor = cliExecutors.get(agent.id);
145
+ if (!cliExecutor) {
146
+ throw new Error(`No CLI executor found for agent "${agent.id}"`);
147
+ }
148
+ if (!sessionStore) {
149
+ throw new Error(`No session store found for agent "${agent.id}"`);
150
+ }
151
+ const routedCommand = routeCommandText(envelope.content.text);
152
+ if (routedCommand.kind === 'cli') {
153
+ logger.info({
154
+ sessionId: envelope.sessionId,
155
+ agentId: agent.id,
156
+ originalText: envelope.content.text,
157
+ forwardedText: routedCommand.forwardedText,
158
+ }, 'Sanitized CLI command for passthrough');
159
+ }
160
+ if (routedCommand.kind === 'bridge' && routedCommand.command === 'status') {
161
+ await sendAgentLifeTextReply(agentLifeConfig, envelope, [
162
+ 'agent-life-bridge is running',
163
+ `agentId: ${agent.id}`,
164
+ `provider: ${provider}`,
165
+ `sessionId: ${envelope.sessionId}`,
166
+ ].join('\n'));
167
+ return;
168
+ }
169
+ if (routedCommand.kind === 'bridge' && routedCommand.command === 'stop') {
170
+ await sendAgentLifeTextReply(agentLifeConfig, envelope, '已收到停止请求,但当前后端暂不支持强制终止。');
171
+ return;
172
+ }
173
+ if (routedCommand.kind === 'bridge' && routedCommand.command === 'new') {
174
+ const created = await sessionStore.createFreshSession(rootAgentSessionKey);
175
+ if (provider === 'codex-cli') {
176
+ await cliExecutor.rotateCodexRuntime();
177
+ }
178
+ logger.info({
179
+ sessionId: envelope.sessionId,
180
+ agentId: agent.id,
181
+ provider,
182
+ rootAgentSessionKey,
183
+ activeAgentSessionKey: created.sessionKey,
184
+ branchCounter: created.branchCounter,
185
+ legacyAlias: routedCommand.isLegacyAlias,
186
+ }, 'Created fresh chat session via bridge command');
187
+ await sendAgentLifeTextReply(agentLifeConfig, envelope, provider === 'codex-cli'
188
+ ? '已创建新的 Codex 会话,后续消息将连接到新窗口。'
189
+ : '已创建新的会话,后续消息将连接到新会话。');
190
+ return;
191
+ }
192
+ const agentSessionKey = await sessionStore.resolveActiveSessionKey(rootAgentSessionKey);
193
+ // Seed CLI session ID from agent config (one-time, does not overwrite runtime values)
194
+ if (agent.cliSessionId && agentSessionKey === rootAgentSessionKey) {
195
+ const existing = await sessionStore.getCliSessionId(agentSessionKey, provider);
196
+ if (!existing) {
197
+ await sessionStore.setCliSessionId(agentSessionKey, provider, agent.cliSessionId, { model });
198
+ logger.info({ agentId: agent.id, provider, cliSessionId: agent.cliSessionId }, 'Seeded CLI session ID from agent config');
199
+ }
200
+ }
201
+ let typing = null;
202
+ const shouldStreamDraftReply = shouldUseAgentLifeDraftReply(provider, envelope.content.text);
203
+ const draftReply = shouldStreamDraftReply
204
+ ? createAgentLifeDraftReply(agentLifeConfig, envelope, logger, {
205
+ resendOnMessageNotFound: provider !== 'claude-cli',
206
+ })
207
+ : createNoopDraftReply();
208
+ try {
209
+ typing = new TypingLifecycle(() => sendAgentLifeChatAction(agentLifeConfig, envelope, 'typing'), 4000, logger);
210
+ const result = await cliExecutor.run({
211
+ envelope: {
212
+ ...envelope,
213
+ sessionId: agentSessionKey,
214
+ content: {
215
+ ...envelope.content,
216
+ text: routedCommand.kind === 'cli' ? routedCommand.forwardedText : envelope.content.text,
217
+ promptMode: routedCommand.kind === 'cli' ? 'cli_passthrough' : 'default',
218
+ },
219
+ },
220
+ provider,
221
+ model: model || primaryModelRef,
222
+ workspaceDir: agent.workspace,
223
+ timeoutMs: 15 * 60 * 1_000,
224
+ systemPrompt: agent.systemPrompt,
225
+ agentEnv: agent.env,
226
+ agentClearEnv: agent.clearEnv,
227
+ fchContext,
228
+ cliBackends: agent.cliBackends,
229
+ callbacks: {
230
+ onProcessing() {
231
+ logger.info({ sessionId: envelope.sessionId, agentId: agent.id, provider }, 'CLI turn entered processing state');
232
+ typing?.start();
233
+ void draftReply.update('正在处理你的消息...').catch((error) => {
234
+ logger.warn({
235
+ sessionId: envelope.sessionId,
236
+ agentId: agent.id,
237
+ provider,
238
+ err: error instanceof Error ? error.message : String(error),
239
+ }, 'Failed to send initial draft progress');
240
+ });
241
+ },
242
+ async onProgress(text) {
243
+ await draftReply.update(text);
244
+ },
245
+ async onAssistantMessage(text) {
246
+ logger.info({
247
+ sessionId: envelope.sessionId,
248
+ agentId: agent.id,
249
+ provider,
250
+ textPreview: text.length > 200 ? `${text.slice(0, 200)}...` : text,
251
+ }, 'Streaming assistant message to agent-life');
252
+ await draftReply.appendMessage(text);
253
+ },
254
+ },
255
+ });
256
+ typing?.stop();
257
+ logger.info({
258
+ sessionId: envelope.sessionId,
259
+ agentId: agent.id,
260
+ provider: result.provider,
261
+ model: result.model,
262
+ cliSessionId: result.cliSessionId,
263
+ isResume: result.isResume,
264
+ replyLength: result.text.trim().length,
265
+ diagnostics: result.diagnostics,
266
+ }, 'CLI turn completed');
267
+ logger.debug({ sessionId: envelope.sessionId, text: result.text }, 'CLI turn output');
268
+ const replyText = result.text.trim() || 'CLI 已执行完成,但没有返回可显示的文本内容。';
269
+ const replyMediaFiles = await resolveReplyMediaFiles(replyText, envelope.content.text);
270
+ if (draftReply.hasDraft()) {
271
+ if (replyMediaFiles.length > 0) {
272
+ const cleanedText = stripMediaPathsFromText(replyText, replyMediaFiles.map((file) => file.path)).trim();
273
+ const draftSummary = cleanedText || buildMediaSentSummary(replyMediaFiles);
274
+ await draftReply.finalize(draftSummary);
275
+ for (const file of replyMediaFiles) {
276
+ await sendAgentLifeMediaReply(agentLifeConfig, envelope, file);
277
+ }
278
+ }
279
+ else {
280
+ await draftReply.finalize(replyText);
281
+ }
282
+ }
283
+ else {
284
+ await sendAgentLifeReply(agentLifeConfig, envelope, replyText);
285
+ }
286
+ }
287
+ catch (error) {
288
+ typing?.stop();
289
+ logger.error({
290
+ sessionId: envelope.sessionId,
291
+ agentId: agent.id,
292
+ provider,
293
+ err: error instanceof Error ? error.message : String(error),
294
+ }, 'CLI turn failed');
295
+ const errorText = `执行失败:${error instanceof Error ? error.message : String(error)}`;
296
+ if (draftReply.hasDraft()) {
297
+ await draftReply.finalize(errorText);
298
+ }
299
+ else {
300
+ await sendAgentLifeTextReply(agentLifeConfig, envelope, errorText);
301
+ }
302
+ }
303
+ finally {
304
+ typing?.stop();
305
+ }
306
+ },
307
+ };
308
+ if (channels.length === 0) {
309
+ throw new Error('No channels registered — agent-life channel import is missing');
310
+ }
311
+ for (const channel of channels) {
312
+ const channelConfig = config.channels[channel.id];
313
+ if (!channelConfig) {
314
+ logger.warn({ channelId: channel.id }, 'No config found for channel, skipping');
315
+ continue;
316
+ }
317
+ logger.info({ channelId: channel.id }, 'Setting up channel');
318
+ activeChannels.push(channel);
319
+ await channel.setup(channelConfig, ctx);
320
+ }
321
+ logger.info('agent-life-bridge started');
322
+ startupSettled = true;
323
+ maybeNotifyAboutUpdateWithLogger(args, logger);
324
+ }
325
+ catch (error) {
326
+ await teardownExecutors();
327
+ cleanupPidFile();
328
+ cleanupRuntimeState();
329
+ throw error;
330
+ }
331
+ finally {
332
+ if (!startupSettled) {
333
+ process.off('SIGINT', onSigInt);
334
+ process.off('SIGTERM', onSigTerm);
335
+ }
336
+ }
337
+ }
338
+ async function sendAgentLifeChatAction(agentLife, envelope, action) {
339
+ await postAgentLifeMethod(agentLife, 'sendChatAction', {
340
+ chat_id: envelope.source.chatId,
341
+ ...(typeof envelope.source.messageThreadId === 'number'
342
+ ? { message_thread_id: envelope.source.messageThreadId }
343
+ : {}),
344
+ action,
345
+ });
346
+ }
347
+ async function sendAgentLifeTextReply(agentLife, envelope, text, options) {
348
+ const result = await postAgentLifeMethod(agentLife, 'sendMessage', {
349
+ chat_id: envelope.source.chatId,
350
+ ...(typeof envelope.source.messageThreadId === 'number'
351
+ ? { message_thread_id: envelope.source.messageThreadId }
352
+ : {}),
353
+ text,
354
+ reply_to_message_id: envelope.source.messageId,
355
+ ...(options?.suppressBotStatusRead ? { suppress_bot_status_read: true } : {}),
356
+ ...(options?.draftProgress ? { draft_progress: true } : {}),
357
+ });
358
+ return typeof result?.message_id === 'number' ? result.message_id : null;
359
+ }
360
+ async function sendAgentLifeReply(agentLife, envelope, text) {
361
+ const mediaFiles = await resolveReplyMediaFiles(text, envelope.content.text);
362
+ if (mediaFiles.length === 0) {
363
+ await sendAgentLifeTextReply(agentLife, envelope, text);
364
+ return;
365
+ }
366
+ const cleanedText = stripMediaPathsFromText(text, mediaFiles.map((file) => file.path)).trim();
367
+ const caption = cleanedText || '截图结果';
368
+ for (const [index, file] of mediaFiles.entries()) {
369
+ await sendAgentLifeMediaReply(agentLife, envelope, file, index === 0 ? caption : undefined);
370
+ }
371
+ }
372
+ async function sendAgentLifeMediaReply(agentLife, envelope, file, caption) {
373
+ const method = file.kind === 'photo' ? 'sendPhoto' : 'sendDocument';
374
+ const field = file.kind === 'photo' ? 'photo' : 'document';
375
+ const form = new FormData();
376
+ form.append('chat_id', String(envelope.source.chatId));
377
+ if (typeof envelope.source.messageThreadId === 'number') {
378
+ form.append('message_thread_id', String(envelope.source.messageThreadId));
379
+ }
380
+ form.append('reply_to_message_id', String(envelope.source.messageId));
381
+ if (caption) {
382
+ form.append('caption', caption);
383
+ }
384
+ const rawBuffer = await readFile(file.path);
385
+ const buffer = shouldPrefixUtf8Bom(file.mimeType, file.path) ? ensureUtf8Bom(rawBuffer) : rawBuffer;
386
+ const blob = new Blob([buffer], { type: file.mimeType });
387
+ form.append(field, blob, path.basename(file.path));
388
+ await postAgentLifeFormMethod(agentLife, method, form);
389
+ }
390
+ async function postAgentLifeFormMethod(agentLife, method, payload) {
391
+ const baseUrl = agentLife.gateway_url.replace(/\/+$/, '');
392
+ const response = await fetch(`${baseUrl}/bot${agentLife.bot_token}/${method}`, {
393
+ method: 'POST',
394
+ body: payload,
395
+ });
396
+ if (!response.ok) {
397
+ const body = await response.text().catch(() => '');
398
+ throw new Error(`agent-life ${method} failed with HTTP ${response.status}${body ? `: ${body}` : ''}`);
399
+ }
400
+ const result = await response.json();
401
+ if (!result.ok) {
402
+ throw new Error(`agent-life ${method} failed: ${result.description ?? 'unknown error'}`);
403
+ }
404
+ }
405
+ async function resolveReplyMediaFiles(replyText, requestText) {
406
+ const replyCandidates = extractLocalFilePaths(replyText);
407
+ const requestCandidates = getRequestedTmpFilePaths(requestText ?? '');
408
+ const candidates = dedupeStringValues([...replyCandidates, ...requestCandidates]);
409
+ const resolved = [];
410
+ for (const candidate of candidates) {
411
+ try {
412
+ const fileUrl = candidate.startsWith('file://') ? new URL(candidate) : null;
413
+ const filePath = fileUrl ? decodeURIComponent(fileUrl.pathname) : candidate;
414
+ const buffer = await readFile(filePath);
415
+ if (buffer.length === 0)
416
+ continue;
417
+ resolved.push({
418
+ path: filePath,
419
+ kind: isPhotoPath(filePath) ? 'photo' : 'document',
420
+ mimeType: inferMimeType(filePath),
421
+ });
422
+ }
423
+ catch {
424
+ continue;
425
+ }
426
+ }
427
+ return dedupeMediaFiles(resolved);
428
+ }
429
+ export function extractLocalFilePaths(text) {
430
+ const matches = text.match(/(?:file:\/\/\/|\/)[^\s"'`]+/g) ?? [];
431
+ return matches
432
+ .flatMap((entry) => entry.split(/[、,]/))
433
+ .map((entry) => entry.replace(/[),.;:!?]+$/g, ''))
434
+ .filter(Boolean)
435
+ .filter((entry) => entry.startsWith('/tmp/') || entry.startsWith('file:///tmp/'));
436
+ }
437
+ export function shouldAttachRequestedTmpFiles(text) {
438
+ if (!text.trim()) {
439
+ return false;
440
+ }
441
+ if (extractLocalFilePaths(text).length === 0) {
442
+ return false;
443
+ }
444
+ return /(发送给我|发给我|传给我|回传给我|发送一下|发一下|上传给我|把.*给我)/.test(text);
445
+ }
446
+ export function shouldUseAgentLifeDraftReply(_provider, requestText) {
447
+ if (shouldAttachRequestedTmpFiles(requestText)) {
448
+ return false;
449
+ }
450
+ return true;
451
+ }
452
+ export function getRequestedTmpFilePaths(text) {
453
+ if (!shouldAttachRequestedTmpFiles(text)) {
454
+ return [];
455
+ }
456
+ return extractLocalFilePaths(text);
457
+ }
458
+ function stripMediaPathsFromText(text, filePaths) {
459
+ let next = text;
460
+ for (const filePath of filePaths) {
461
+ const escaped = escapeRegExp(filePath);
462
+ next = next.replace(new RegExp(escaped, 'g'), '');
463
+ next = next.replace(new RegExp(escapeRegExp(`file://${filePath}`), 'g'), '');
464
+ }
465
+ return next.replace(/\n{3,}/g, '\n\n');
466
+ }
467
+ function dedupeMediaFiles(files) {
468
+ const seen = new Set();
469
+ return files.filter((file) => {
470
+ if (seen.has(file.path))
471
+ return false;
472
+ seen.add(file.path);
473
+ return true;
474
+ });
475
+ }
476
+ function dedupeStringValues(values) {
477
+ return [...new Set(values)];
478
+ }
479
+ function buildMediaSentSummary(files) {
480
+ if (files.length === 1) {
481
+ return `已发送文件:${path.basename(files[0].path)}`;
482
+ }
483
+ return `已发送 ${files.length} 个文件`;
484
+ }
485
+ function isPhotoPath(filePath) {
486
+ return /\.(png|jpe?g|webp|gif|bmp)$/i.test(filePath);
487
+ }
488
+ function inferMimeType(filePath) {
489
+ const lower = filePath.toLowerCase();
490
+ if (lower.endsWith('.png'))
491
+ return 'image/png';
492
+ if (lower.endsWith('.jpg') || lower.endsWith('.jpeg'))
493
+ return 'image/jpeg';
494
+ if (lower.endsWith('.webp'))
495
+ return 'image/webp';
496
+ if (lower.endsWith('.gif'))
497
+ return 'image/gif';
498
+ if (lower.endsWith('.bmp'))
499
+ return 'image/bmp';
500
+ if (lower.endsWith('.pdf'))
501
+ return 'application/pdf';
502
+ if (lower.endsWith('.md') || lower.endsWith('.markdown'))
503
+ return 'text/markdown; charset=utf-8';
504
+ if (lower.endsWith('.txt'))
505
+ return 'text/plain; charset=utf-8';
506
+ if (lower.endsWith('.json'))
507
+ return 'application/json; charset=utf-8';
508
+ if (lower.endsWith('.csv'))
509
+ return 'text/csv; charset=utf-8';
510
+ if (lower.endsWith('.html') || lower.endsWith('.htm'))
511
+ return 'text/html; charset=utf-8';
512
+ if (lower.endsWith('.xml'))
513
+ return 'application/xml; charset=utf-8';
514
+ return 'application/octet-stream';
515
+ }
516
+ function shouldPrefixUtf8Bom(mimeType, filePath) {
517
+ if (/;\s*charset=utf-8/i.test(mimeType)) {
518
+ return true;
519
+ }
520
+ return /\.(md|markdown|txt|json|csv|html?|xml)$/i.test(filePath);
521
+ }
522
+ function ensureUtf8Bom(buffer) {
523
+ const utf8Bom = Buffer.from([0xef, 0xbb, 0xbf]);
524
+ if (buffer.length >= 3 && buffer.subarray(0, 3).equals(utf8Bom)) {
525
+ return buffer;
526
+ }
527
+ return Buffer.concat([utf8Bom, buffer]);
528
+ }
529
+ function escapeRegExp(value) {
530
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
531
+ }
532
+ export function createAgentLifeDraftReply(agentLife, envelope, logger, options) {
533
+ let messageId = null;
534
+ let editUnsupported = false;
535
+ let messageNotFound = false;
536
+ let lastRendered = '';
537
+ const progressLines = [];
538
+ const assistantMessages = [];
539
+ let operationChain = Promise.resolve();
540
+ const resendOnMessageNotFound = options?.resendOnMessageNotFound ?? true;
541
+ const renderProgress = () => {
542
+ const content = progressLines.slice(-12).join('\n').trim() || '正在输入...';
543
+ return truncateForTelegram(content);
544
+ };
545
+ const runExclusive = async (operation) => {
546
+ const next = operationChain.then(operation, operation);
547
+ operationChain = next.catch(() => undefined);
548
+ await next;
549
+ };
550
+ const sendDraftMessage = async (text, isFinal = false) => {
551
+ messageId = await sendAgentLifeTextReply(agentLife, envelope, text, isFinal
552
+ ? undefined
553
+ : {
554
+ suppressBotStatusRead: true,
555
+ draftProgress: true,
556
+ });
557
+ logger.info({
558
+ sessionId: envelope.sessionId,
559
+ chatId: envelope.source.chatId,
560
+ messageId,
561
+ isFinal,
562
+ textPreview: text.length > 200 ? `${text.slice(0, 200)}...` : text,
563
+ }, 'agent-life draft sendMessage sent');
564
+ };
565
+ const editDraftMessage = async (text, isFinal = false) => {
566
+ if (messageId === null) {
567
+ await sendDraftMessage(text, isFinal);
568
+ return true;
569
+ }
570
+ if (editUnsupported) {
571
+ if (!isFinal) {
572
+ return false;
573
+ }
574
+ await sendDraftMessage(text, true);
575
+ return true;
576
+ }
577
+ if (messageNotFound) {
578
+ return false;
579
+ }
580
+ try {
581
+ await postAgentLifeMethod(agentLife, 'editMessageText', {
582
+ chat_id: envelope.source.chatId,
583
+ ...(typeof envelope.source.messageThreadId === 'number'
584
+ ? { message_thread_id: envelope.source.messageThreadId }
585
+ : {}),
586
+ message_id: messageId,
587
+ text,
588
+ ...(isFinal ? { mark_bot_status_read: true } : {}),
589
+ });
590
+ logger.info({
591
+ sessionId: envelope.sessionId,
592
+ chatId: envelope.source.chatId,
593
+ messageId,
594
+ isFinal,
595
+ textPreview: text.length > 200 ? `${text.slice(0, 200)}...` : text,
596
+ }, 'agent-life draft editMessageText sent');
597
+ return true;
598
+ }
599
+ catch (error) {
600
+ if (isAgentLifeEditUnsupportedError(error)) {
601
+ editUnsupported = true;
602
+ logger.warn({
603
+ sessionId: envelope.sessionId,
604
+ chatId: envelope.source.chatId,
605
+ messageId,
606
+ isFinal,
607
+ err: error instanceof Error ? error.message : String(error),
608
+ }, 'agent-life draft edit unsupported, falling back to sendMessage-only mode');
609
+ if (!isFinal) {
610
+ return false;
611
+ }
612
+ await sendDraftMessage(text, true);
613
+ return true;
614
+ }
615
+ if (!isAgentLifeMessageNotFoundError(error)) {
616
+ throw error;
617
+ }
618
+ logger.warn({
619
+ sessionId: envelope.sessionId,
620
+ chatId: envelope.source.chatId,
621
+ messageId,
622
+ isFinal,
623
+ err: error instanceof Error ? error.message : String(error),
624
+ }, 'agent-life draft edit failed with message not found, resending draft');
625
+ messageNotFound = true;
626
+ if (!resendOnMessageNotFound) {
627
+ logger.warn({
628
+ sessionId: envelope.sessionId,
629
+ chatId: envelope.source.chatId,
630
+ messageId,
631
+ isFinal,
632
+ }, 'agent-life draft resend disabled after message not found; keeping single visible draft message');
633
+ return false;
634
+ }
635
+ await sendDraftMessage(text, isFinal);
636
+ messageNotFound = false;
637
+ return true;
638
+ }
639
+ };
640
+ const renderAssistantMessages = () => {
641
+ const content = assistantMessages.join('\n\n').trim();
642
+ return truncateForTelegram(content);
643
+ };
644
+ return {
645
+ hasDraft() {
646
+ return messageId !== null;
647
+ },
648
+ async update(text) {
649
+ await runExclusive(async () => {
650
+ if (assistantMessages.length > 0) {
651
+ return;
652
+ }
653
+ const normalized = normalizeProgressLine(text);
654
+ if (!normalized) {
655
+ return;
656
+ }
657
+ if (progressLines[progressLines.length - 1] !== normalized) {
658
+ progressLines.push(normalized);
659
+ }
660
+ const rendered = renderProgress();
661
+ if (!rendered || rendered === lastRendered) {
662
+ return;
663
+ }
664
+ if (await editDraftMessage(rendered)) {
665
+ lastRendered = rendered;
666
+ }
667
+ });
668
+ },
669
+ async appendMessage(text) {
670
+ await runExclusive(async () => {
671
+ const normalized = text.trim();
672
+ if (!normalized) {
673
+ return;
674
+ }
675
+ const lastAssistantMessage = assistantMessages[assistantMessages.length - 1];
676
+ if (lastAssistantMessage === normalized) {
677
+ return;
678
+ }
679
+ if (lastAssistantMessage && normalized.startsWith(lastAssistantMessage)) {
680
+ assistantMessages[assistantMessages.length - 1] = normalized;
681
+ }
682
+ else {
683
+ assistantMessages.push(normalized);
684
+ }
685
+ const rendered = renderAssistantMessages();
686
+ if (!rendered || rendered === lastRendered) {
687
+ return;
688
+ }
689
+ logger.info({
690
+ sessionId: envelope.sessionId,
691
+ chatId: envelope.source.chatId,
692
+ assistantMessageCount: assistantMessages.length,
693
+ textPreview: rendered.length > 200 ? `${rendered.slice(0, 200)}...` : rendered,
694
+ }, 'agent-life draft aggregated assistant messages');
695
+ if (await editDraftMessage(rendered)) {
696
+ lastRendered = rendered;
697
+ }
698
+ });
699
+ },
700
+ async finalize(text) {
701
+ await runExclusive(async () => {
702
+ const finalText = truncateForTelegram(text.trim() || 'CLI 已执行完成,但没有返回可显示的文本内容。');
703
+ if (finalText === lastRendered) {
704
+ if (messageId !== null && !editUnsupported) {
705
+ await editDraftMessage(finalText, true);
706
+ }
707
+ return;
708
+ }
709
+ if (await editDraftMessage(finalText, true)) {
710
+ lastRendered = finalText;
711
+ }
712
+ });
713
+ },
714
+ };
715
+ }
716
+ function createNoopDraftReply() {
717
+ return {
718
+ hasDraft() {
719
+ return false;
720
+ },
721
+ async update(_text) {
722
+ return;
723
+ },
724
+ async appendMessage(_text) {
725
+ return;
726
+ },
727
+ async finalize(_text) {
728
+ return;
729
+ },
730
+ };
731
+ }
732
+ function normalizeProgressLine(text) {
733
+ return text.replace(/\s+/g, ' ').trim();
734
+ }
735
+ function truncateForTelegram(text, maxLength = 3800) {
736
+ if (text.length <= maxLength) {
737
+ return text;
738
+ }
739
+ return `...${text.slice(-(maxLength - 3))}`;
740
+ }
741
+ function isAgentLifeMessageNotFoundError(error) {
742
+ if (!(error instanceof Error)) {
743
+ return false;
744
+ }
745
+ return /message not found/i.test(error.message);
746
+ }
747
+ export function isAgentLifeEditUnsupportedError(error) {
748
+ if (!(error instanceof Error)) {
749
+ return false;
750
+ }
751
+ return /editMessageText failed with HTTP 405/i.test(error.message)
752
+ || /method not allowed/i.test(error.message);
753
+ }
754
+ async function postAgentLifeMethod(agentLife, method, payload) {
755
+ const baseUrl = agentLife.gateway_url.replace(/\/+$/, '');
756
+ const response = await fetch(`${baseUrl}/bot${agentLife.bot_token}/${method}`, {
757
+ method: 'POST',
758
+ headers: { 'content-type': 'application/json' },
759
+ body: JSON.stringify(payload),
760
+ });
761
+ if (!response.ok) {
762
+ const body = await response.text().catch(() => '');
763
+ throw new Error(`agent-life ${method} failed with HTTP ${response.status}${body ? `: ${body}` : ''}`);
764
+ }
765
+ const result = await response.json();
766
+ if (!result.ok) {
767
+ throw new Error(`agent-life ${method} failed: ${result.description ?? 'unknown error'}`);
768
+ }
769
+ return result.result;
770
+ }
771
+ export function resolveAgentLifeAccountConfig(agentLife, envelope) {
772
+ const accounts = normalizeAgentLifeChannelConfigs(agentLife);
773
+ const matched = accounts.find((entry) => entry.accountId === envelope.source.accountId);
774
+ if (matched) {
775
+ return matched.config;
776
+ }
777
+ const availableAccountIds = accounts.map((entry) => entry.accountId);
778
+ if (availableAccountIds.length === 0) {
779
+ throw new Error('No agent-life account config found for reply');
780
+ }
781
+ throw new Error(`No agent-life account config found for reply: accountId "${String(envelope.source.accountId ?? '')}" is not configured. Available accounts: ${availableAccountIds.join(', ')}`);
782
+ }
783
+ //# sourceMappingURL=cmd-start.js.map