@larktask/aamp-feishu-task-agent 0.1.0-dev.174 → 0.1.1-dev.3

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.
@@ -8,8 +8,24 @@ import { ReadStream, WriteStream } from 'node:tty';
8
8
  import { emitKeypressEvents } from 'node:readline';
9
9
  import { spawn } from 'node:child_process';
10
10
  import { EventEmitter } from 'node:events';
11
+ import { fileURLToPath } from 'node:url';
12
+ import {
13
+ TASK_AGENT_TYPES,
14
+ resolveTaskAgentMetadata,
15
+ } from './agent-metadata.mjs';
16
+ import {
17
+ createKeyedSerialExecutor,
18
+ createSerializedRunner,
19
+ runLayeredStarts,
20
+ } from './runtime-concurrency.mjs';
21
+ import {
22
+ createPackageExecutableLauncher,
23
+ npmExecutableResolverArgs,
24
+ parseResolvedPackageExecutable,
25
+ } from './runtime-package-executable.mjs';
11
26
  import {
12
27
  agentStartRetryError,
28
+ agentStartFailureMessage,
13
29
  bridgeAuthenticationRetryError,
14
30
  classifyNetworkError,
15
31
  createSerializedLineWriter,
@@ -18,6 +34,7 @@ import {
18
34
  launchDetachedDiagnostic,
19
35
  networkEnvironmentSummary,
20
36
  probeEndpoint,
37
+ preserveAgentStartFailure,
21
38
  safeDiagnosticUrl,
22
39
  withNetworkRetry,
23
40
  } from './runtime-network.mjs';
@@ -43,8 +60,8 @@ const NPM_BIN = process.env.AAMP_TASK_NPM_BIN || 'npm';
43
60
  const NPM_REGISTRY = process.env.AAMP_TASK_NPM_REGISTRY || 'https://registry.npmjs.org/';
44
61
  const FEISHU_API_PROBE_URL = 'https://open.feishu.cn/';
45
62
  const NPM_CACHE_DIR = process.env.AAMP_TASK_NPM_CACHE_DIR || path.join(os.tmpdir(), 'aamp-one-click-npm-cache');
46
- const ACP_PACKAGE = process.env.AAMP_TASK_ACP_BRIDGE_PKG || '@zengxingyuan/aamp-acp-bridge@0.1.28-dev.20';
47
- const FEISHU_PACKAGE = process.env.AAMP_TASK_FEISHU_BRIDGE_PKG || '@zengxingyuan/aamp-feishu-bridge@0.1.51';
63
+ const ACP_PACKAGE = process.env.AAMP_TASK_ACP_BRIDGE_PKG || '@luckyterry/aamp-acp-bridge@0.1.29-dev.0';
64
+ const FEISHU_PACKAGE = process.env.AAMP_TASK_FEISHU_BRIDGE_PKG || '@iluolyx/aamp-feishu-bridge@0.1.52-dev.5';
48
65
  const INSTALL_COMMAND = process.env.AAMP_TASK_INSTALL_COMMAND
49
66
  || 'npx -y --package @larktask/aamp-feishu-task-agent@dev feishu-task-agent install';
50
67
  const DEFAULT_AGENT = process.env.AAMP_TASK_DEFAULT_AGENT || '';
@@ -54,24 +71,71 @@ const READY_TIMEOUT_MS = Number(process.env.AAMP_TASK_READY_TIMEOUT_MS || 90_000
54
71
  const NETWORK_MAX_ATTEMPTS = Math.max(1, Number(process.env.AAMP_TASK_NETWORK_MAX_ATTEMPTS || 3));
55
72
  const NETWORK_RETRY_BASE_DELAY_MS = Math.max(0, Number(process.env.AAMP_TASK_NETWORK_RETRY_BASE_DELAY_MS || 500));
56
73
  const NETWORK_PROBE_TIMEOUT_MS = Math.max(1_000, Number(process.env.AAMP_TASK_NETWORK_PROBE_TIMEOUT_MS || 10_000));
74
+ const FEISHU_START_CONCURRENCY = 4;
57
75
  const CONFIG_SCHEMA = 'aamp.feishu-task-agent.bindings';
58
76
  const CONFIG_VERSION = 1;
59
- const AGENT_TYPES = ['codex', 'cursor'];
60
- const PROFILE_DOMAINS = [
61
- 'base', 'calendar', 'contact', 'docs', 'im', 'mail', 'mindnotes', 'minutes',
62
- 'note', 'sheets', 'slides', 'task', 'vc', 'wiki',
63
- ];
77
+ const PROFILE_DOMAINS = ['task'];
78
+ const OPEN_API_DOMAIN_BY_TENANT_BRAND = Object.freeze({
79
+ feishu: 'https://open.feishu.cn',
80
+ lark: 'https://open.larksuite.com',
81
+ });
64
82
 
65
83
  const secrets = new Set();
66
84
  const managedProcesses = new Set();
67
85
  const transientProcesses = new Set();
68
86
  const heldLeases = new Set();
69
87
  const bindingStatuses = new Map();
88
+ const runPairingSerially = createKeyedSerialExecutor();
89
+ const errorLogWriter = createSerializedLineWriter((content) => appendPrivate(ERRORS_LOG, content));
90
+ const manifestWriter = createSerializedRunner(async () => {
91
+ const statuses = [...bindingStatuses.entries()].map(([bindingId, status]) => ({
92
+ binding_id: bindingId,
93
+ ...status,
94
+ }));
95
+ await writeJsonAtomic(MANIFEST_FILE, {
96
+ schema: 'aamp.local_logs.run.v2',
97
+ run_id: RUN_ID,
98
+ task_agent_version: process.env.AAMP_TASK_AGENT_VERSION || '',
99
+ command: COMMAND,
100
+ started_at: process.env.AAMP_TASK_RUN_STARTED_AT || RUN_STARTED_AT,
101
+ config_file: CONFIG_FILE,
102
+ runtime_home: RUNTIME_HOME,
103
+ bindings: statuses,
104
+ errors_log: ERRORS_LOG,
105
+ log_dir: RUN_LOG_DIR,
106
+ });
107
+ });
70
108
  let stopRequested = false;
71
109
  let stopSignal = '';
72
- let cleanupPromise;
73
110
  let terminal;
74
111
 
112
+ function createResourceCleanup(drain) {
113
+ const runner = createSerializedRunner(drain);
114
+ return () => runner.run();
115
+ }
116
+
117
+ function createPromptInterrupter() {
118
+ let activeCancel;
119
+ return {
120
+ activate(cancel) {
121
+ if (activeCancel) throw new Error('已有交互提示正在等待输入');
122
+ activeCancel = cancel;
123
+ return () => {
124
+ if (activeCancel === cancel) activeCancel = undefined;
125
+ };
126
+ },
127
+ interrupt(error) {
128
+ const cancel = activeCancel;
129
+ if (!cancel) return false;
130
+ activeCancel = undefined;
131
+ cancel(error);
132
+ return true;
133
+ },
134
+ };
135
+ }
136
+
137
+ const promptInterrupter = createPromptInterrupter();
138
+
75
139
  function nowIso() {
76
140
  return new Date().toISOString();
77
141
  }
@@ -116,13 +180,258 @@ function addSecret(value) {
116
180
  function redact(value) {
117
181
  let output = String(value ?? '');
118
182
  for (const secret of secrets) output = output.split(secret).join('[REDACTED]');
183
+ output = output.replace(/\b(Bearer|Basic)\s+[^\s,}]+/gi, '$1 [REDACTED]');
119
184
  output = output
120
185
  .replace(/([?&]pair_code=)[^&\s"']+/gi, '$1[REDACTED]')
121
- .replace(/("?(?:app_secret|appSecret|smtpPassword|mailboxToken|access_token|device_code|pairCode)"?\s*[:=]\s*"?)[^",\s}]+/gi, '$1[REDACTED]')
186
+ .replace(/("?(?:app_secret|appSecret|smtpPassword|mailboxToken|access_token|accessToken|refresh_token|refreshToken|id_token|idToken|session_token|sessionToken|device_code|pairCode|api_key|apiKey|api-key|private_key|privateKey|private-key|auth_token|auth-token|password|authorization|cookie|credential|secret|token|session)"?\s*[:=]\s*"?)(?:(?:Bearer|Basic)\s+)?[^",\s}]+/gi, '$1[REDACTED]')
122
187
  .replace(/(--app-secret\s+)[^\s]+/gi, '$1[REDACTED]');
123
188
  return output;
124
189
  }
125
190
 
191
+ const REMOTE_EVENT_PATH_PREFIX = 'aamp-runtime:';
192
+ const REMOTE_AGENT_FAILED = 'REMOTE_AGENT_FAILED: Remote Agent execution failed.';
193
+ const REMOTE_AGENT_PREPARATION_FAILED = 'REMOTE_AGENT_PREPARATION_FAILED: Remote Agent preparation failed.';
194
+ const REMOTE_FAILURE_CODES = new Set([
195
+ 'AIME_ACCESS_DENIED',
196
+ 'AIME_EMPTY_RESPONSE',
197
+ 'AIME_MODEL_NOT_FOUND',
198
+ 'AIME_NETWORK_UNREACHABLE',
199
+ 'AIME_PROTOCOL_DRIFT',
200
+ 'AIME_SDK_INCOMPATIBLE',
201
+ 'AIME_SEND_FAILED',
202
+ 'AIME_SESSION_NOT_FOUND',
203
+ 'AIME_STREAM_INTERRUPTED',
204
+ 'AIME_UNSUPPORTED_CONTENT',
205
+ 'AUTH_CONFIGURATION_UNSUPPORTED',
206
+ 'AUTH_IDENTITY_CHANGED',
207
+ 'AUTH_IDENTITY_UNAVAILABLE',
208
+ 'AUTH_REQUIRED',
209
+ 'AUTH_SOURCE_UNSUPPORTED',
210
+ 'REMOTE_AGENT_FAILED',
211
+ 'REMOTE_ARTIFACT_UNSUPPORTED',
212
+ ]);
213
+
214
+ function isRemoteExecution(options) {
215
+ return options?.executionLocation === 'remote';
216
+ }
217
+
218
+ function encodeRemoteEventPath(value, options = {}) {
219
+ const eventPathRoot = options.eventPathRoot;
220
+ const eventPathHandles = options.eventPathHandles;
221
+ if (!eventPathRoot || !(eventPathHandles instanceof Map)
222
+ || typeof value !== 'string' || !path.isAbsolute(value)) return '';
223
+ const root = path.resolve(eventPathRoot);
224
+ const resolved = path.resolve(value);
225
+ if (!isPathInside(root, resolved)) return '';
226
+ let handle;
227
+ do {
228
+ handle = `${REMOTE_EVENT_PATH_PREFIX}${crypto.randomBytes(24).toString('base64url')}`;
229
+ } while (eventPathHandles.has(handle));
230
+ eventPathHandles.set(handle, { root, resolved });
231
+ return handle;
232
+ }
233
+
234
+ function safeRemoteFailure(value) {
235
+ const message = redact(value);
236
+ const candidate = /\b(?:AIME|AUTH|REMOTE)_[A-Z0-9_]+\b/.exec(message)?.[0];
237
+ const code = candidate && REMOTE_FAILURE_CODES.has(candidate) ? candidate : 'REMOTE_AGENT_FAILED';
238
+ if (message.trim()) return { code, message };
239
+ return { code, message: code === 'REMOTE_AGENT_FAILED' ? REMOTE_AGENT_FAILED : `${code}: Remote Agent execution failed.` };
240
+ }
241
+
242
+ function trustedAgentExecutionLocations(entries = []) {
243
+ const locations = new Map();
244
+ const ambiguous = new Set();
245
+ const values = entries instanceof Map ? entries.entries() : entries;
246
+ for (const entry of values || []) {
247
+ if (!Array.isArray(entry) || entry.length < 2) continue;
248
+ const [name, executionLocation] = entry;
249
+ if (typeof name !== 'string' || !['local', 'remote'].includes(executionLocation)) continue;
250
+ if (locations.has(name) || ambiguous.has(name)) {
251
+ locations.delete(name);
252
+ ambiguous.add(name);
253
+ continue;
254
+ }
255
+ locations.set(name, executionLocation);
256
+ }
257
+ return locations;
258
+ }
259
+
260
+ function trustedAgentIdentity(value, options, executionLocation) {
261
+ if (typeof value !== 'string') return '';
262
+ const actual = options.agentExecutionLocations?.get(value);
263
+ return actual && (!executionLocation || actual === executionLocation) ? value : '';
264
+ }
265
+
266
+ function allowedStructuralIdentity(value, allowedValues) {
267
+ return typeof value === 'string' && new Set(allowedValues || []).has(value) ? value : '';
268
+ }
269
+
270
+ function safeRemoteDuration(value) {
271
+ return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
272
+ }
273
+
274
+ function projectTrustedLocalAgentEvent(document, options = {}) {
275
+ if (!document || typeof document !== 'object' || Array.isArray(document)) return undefined;
276
+ const type = typeof document.type === 'string' ? document.type : '';
277
+ const agent = trustedAgentIdentity(document.agent, options, 'local');
278
+ if (!agent || !['agent.starting', 'agent.started', 'agent.identity', 'agent.failed'].includes(type)) {
279
+ return undefined;
280
+ }
281
+ const event = {
282
+ type,
283
+ ...(document.bridge === 'acp-bridge' ? { bridge: 'acp-bridge' } : {}),
284
+ agent,
285
+ };
286
+ if (type === 'agent.starting') return event;
287
+ const durationMs = safeRemoteDuration(document.durationMs);
288
+ if (type === 'agent.started') {
289
+ return {
290
+ ...event,
291
+ ...(typeof document.email === 'string' ? { email: redact(document.email) } : {}),
292
+ connected: document.connected === true,
293
+ pollingFallback: document.pollingFallback === true,
294
+ ...(durationMs === undefined ? {} : { durationMs }),
295
+ };
296
+ }
297
+ if (type === 'agent.identity') {
298
+ return {
299
+ ...event,
300
+ ...(typeof document.email === 'string' ? { email: redact(document.email) } : {}),
301
+ ...(typeof document.acpCommand === 'string' ? { acpCommand: redact(document.acpCommand) } : {}),
302
+ };
303
+ }
304
+ return {
305
+ ...event,
306
+ message: redact(document.message || `${agent} Agent Bridge 启动失败`),
307
+ ...(typeof document.code === 'string'
308
+ ? { code: redact(document.code) }
309
+ : typeof document.code === 'number' ? { code: document.code } : {}),
310
+ ...(durationMs === undefined ? {} : { durationMs }),
311
+ };
312
+ }
313
+
314
+ function projectRemoteOperationalEvent(document, options = {}) {
315
+ if (!document || typeof document !== 'object' || Array.isArray(document)) return undefined;
316
+ const type = typeof document.type === 'string' ? document.type : '';
317
+ if (type === 'bridge.process') {
318
+ if (!['started', 'exited'].includes(document.status)) return undefined;
319
+ const event = { type, status: document.status };
320
+ const durationMs = safeRemoteDuration(document.durationMs);
321
+ if (durationMs !== undefined) event.durationMs = durationMs;
322
+ if (document.status === 'exited') {
323
+ event.code = Number.isInteger(document.code) ? document.code : null;
324
+ event.signal = typeof document.signal === 'string' && /^SIG[A-Z0-9]+$/.test(document.signal)
325
+ ? document.signal
326
+ : null;
327
+ event.expectedStop = document.expectedStop === true;
328
+ if (document.error) event.error = REMOTE_AGENT_FAILED;
329
+ }
330
+ return event;
331
+ }
332
+ if (type === 'bridge.running') {
333
+ const agents = Array.isArray(document.agents)
334
+ ? document.agents.flatMap((agent) => {
335
+ const name = trustedAgentIdentity(agent?.name, options);
336
+ return name ? [{ name }] : [];
337
+ })
338
+ : [];
339
+ return { type, agentCount: agents.length, agents };
340
+ }
341
+ if (type === 'bridge.task_runtime.starting') {
342
+ const appId = allowedStructuralIdentity(document.appId, options.allowedAppIds);
343
+ const imConfigDir = encodeRemoteEventPath(document.imConfigDir, options);
344
+ const taskConfigDir = encodeRemoteEventPath(document.taskConfigDir, options);
345
+ if (!appId || !imConfigDir || !taskConfigDir) return undefined;
346
+ return { type, appId, imConfigDir, taskConfigDir };
347
+ }
348
+ if (type === 'bridge.task_runtime.running') {
349
+ return { type };
350
+ }
351
+ if (type === 'agent.starting') {
352
+ const agent = trustedAgentIdentity(document.agent, options, 'remote');
353
+ return agent ? { type, agent } : undefined;
354
+ }
355
+ if (type === 'agent.started') {
356
+ const agent = trustedAgentIdentity(document.agent, options, 'remote');
357
+ if (!agent) return undefined;
358
+ const durationMs = safeRemoteDuration(document.durationMs);
359
+ return {
360
+ type,
361
+ agent,
362
+ connected: document.connected === true,
363
+ pollingFallback: document.pollingFallback === true,
364
+ ...(durationMs === undefined ? {} : { durationMs }),
365
+ };
366
+ }
367
+ if (type === 'agent.identity') {
368
+ const agent = trustedAgentIdentity(document.agent, options, 'remote');
369
+ if (!agent) return undefined;
370
+ return {
371
+ type,
372
+ agent,
373
+ executionLocation: 'remote',
374
+ acpCommandConfigured: document.acpCommandConfigured === true,
375
+ };
376
+ }
377
+ if (type === 'agent.failed') {
378
+ const agent = trustedAgentIdentity(document.agent, options, 'remote');
379
+ if (!agent) return undefined;
380
+ const failure = safeRemoteFailure(document.message);
381
+ const durationMs = safeRemoteDuration(document.durationMs);
382
+ return {
383
+ type,
384
+ agent,
385
+ ...failure,
386
+ ...(durationMs === undefined ? {} : { durationMs }),
387
+ };
388
+ }
389
+ return undefined;
390
+ }
391
+
392
+ function safeOperationalLine(line, options = {}) {
393
+ if (!String(line || '').trim()) return '';
394
+ const text = String(line).trim();
395
+ if (!isRemoteExecution(options)) return redact(text);
396
+ try {
397
+ const document = JSON.parse(text);
398
+ const localEvent = projectTrustedLocalAgentEvent(document, options);
399
+ if (localEvent) return JSON.stringify(localEvent);
400
+ const projected = projectRemoteOperationalEvent(document, options);
401
+ return projected ? JSON.stringify(projected) : redact(text);
402
+ } catch {
403
+ return redact(text);
404
+ }
405
+ }
406
+
407
+ function safeOperationalOutput(value, options = {}) {
408
+ return String(value ?? '')
409
+ .split(/\r?\n/)
410
+ .map((line) => safeOperationalLine(line, options))
411
+ .join('\n');
412
+ }
413
+
414
+ function safeCapturedLog(stdout, stderr, options = {}) {
415
+ const chunks = [stdout, stderr]
416
+ .map((value) => safeOperationalOutput(value, options).replace(/\n+$/g, ''))
417
+ .filter(Boolean);
418
+ return chunks.length ? `${chunks.join('\n')}\n` : '';
419
+ }
420
+
421
+ function resolveRemoteEventPath(value, record, eventPathRoot) {
422
+ const isHandle = typeof value === 'string' && value.startsWith(REMOTE_EVENT_PATH_PREFIX);
423
+ if (!isHandle) {
424
+ if (record?.eventPathHandles instanceof Map) throw new Error('Feishu Bridge 返回了无效的安全路径');
425
+ return value;
426
+ }
427
+ const entry = record?.eventPathHandles?.get(value);
428
+ const root = path.resolve(eventPathRoot);
429
+ if (!entry || entry.root !== root || !isPathInside(root, entry.resolved)) {
430
+ throw new Error('Feishu Bridge 返回了无效的安全路径');
431
+ }
432
+ return entry.resolved;
433
+ }
434
+
126
435
  async function ensurePrivateDir(dir) {
127
436
  await fsp.mkdir(dir, { recursive: true, mode: 0o700 });
128
437
  await fsp.chmod(dir, 0o700).catch(() => {});
@@ -380,6 +689,10 @@ async function acquireRuntimeSessionLease(action) {
380
689
  }
381
690
  const lease = { lockDir: RUNTIME_SESSION_LOCK, release };
382
691
  heldLeases.add(lease);
692
+ if (stopRequested) {
693
+ await releaseLease(lease);
694
+ throwIfStopping();
695
+ }
383
696
  return lease;
384
697
  }
385
698
 
@@ -428,18 +741,56 @@ async function assertNoSymlinkPath(root, candidate) {
428
741
  }
429
742
  }
430
743
 
744
+ async function resolveConfiguredPendingPairingFile(group, agentType) {
745
+ const configuredAgents = (group?.agents || []).filter((agent) => agent?.name === agentType);
746
+ if (configuredAgents.length !== 1 || typeof configuredAgents[0].pairingFile !== 'string'
747
+ || !configuredAgents[0].pairingFile.trim()) {
748
+ throw new Error('ACP Bridge 私有配对文件配置缺失或 Agent 不唯一');
749
+ }
750
+ if (typeof group?.home !== 'string' || !path.isAbsolute(group.home)
751
+ || !isPathInside(RUNTIME_HOME, group.home)) {
752
+ throw new Error('ACP Bridge runtime 不属于 Task Agent 私有目录');
753
+ }
754
+ if (!path.isAbsolute(configuredAgents[0].pairingFile)
755
+ || !isPathInside(group.home, configuredAgents[0].pairingFile)) {
756
+ throw new Error('ACP Bridge 私有配对文件不属于当前 Agent Bridge runtime');
757
+ }
758
+ const configuredPairingFile = path.resolve(configuredAgents[0].pairingFile);
759
+ await assertNoSymlinkPath(RUNTIME_HOME, configuredPairingFile);
760
+ return configuredPairingFile;
761
+ }
762
+
763
+ async function resolvePendingPairingFile(group, agentType, pairing) {
764
+ const configuredPairingFile = await resolveConfiguredPendingPairingFile(group, agentType);
765
+ const executionLocation = resolveTaskAgentMetadata(agentType).executionLocation;
766
+ if (executionLocation === 'remote' && pairing?.pairingFileConfigured !== true) {
767
+ throw new Error('ACP Bridge 未确认远程 Agent 的私有配对文件配置');
768
+ }
769
+ if (executionLocation !== 'remote' && (typeof pairing?.pairingFile !== 'string'
770
+ || path.resolve(pairing.pairingFile) !== configuredPairingFile)) {
771
+ throw new Error('ACP Bridge 返回的本地配对文件与私有配置不一致');
772
+ }
773
+ return configuredPairingFile;
774
+ }
775
+
431
776
  function validateBinding(binding, index) {
432
777
  if (!binding || typeof binding !== 'object') throw new Error(`bindings[${index}] 无效`);
433
778
  assertString(binding.binding_id, `bindings[${index}].binding_id`);
434
779
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(binding.binding_id)) {
435
780
  throw new Error(`bindings[${index}].binding_id 必须是 UUID`);
436
781
  }
437
- if (!AGENT_TYPES.includes(binding.agent_type)) throw new Error(`bindings[${index}].agent_type 仅支持 codex/cursor`);
782
+ if (!TASK_AGENT_TYPES.includes(binding.agent_type)) {
783
+ throw new Error(`bindings[${index}].agent_type 仅支持 codex/cursor/coco/traex/traecli/workbuddy/workbuddy_ai/aime`);
784
+ }
438
785
  assertString(binding.aamp_host, `bindings[${index}].aamp_host`);
439
786
  assertString(binding.environment?.name, `bindings[${index}].environment.name`);
440
787
  assertString(binding.bot?.app_id, `bindings[${index}].bot.app_id`);
441
788
  assertString(binding.bot?.app_secret, `bindings[${index}].bot.app_secret`);
442
- assertString(binding.bot?.lark_cli_profile, `bindings[${index}].bot.lark_cli_profile`);
789
+ normalizeTenantBrand(binding.bot?.tenant_brand, `bindings[${index}].bot.tenant_brand`);
790
+ const metadata = resolveTaskAgentMetadata(binding.agent_type);
791
+ if (metadata.executionLocation === 'local') {
792
+ assertString(binding.bot?.lark_cli_profile, `bindings[${index}].bot.lark_cli_profile`);
793
+ }
443
794
  assertString(binding.feishu_config_dir, `bindings[${index}].feishu_config_dir`);
444
795
  const expectedConfigDir = expectedFeishuConfigDir(binding.binding_id);
445
796
  if (path.resolve(binding.feishu_config_dir) !== path.resolve(expectedConfigDir)) {
@@ -488,20 +839,71 @@ async function loadStore() {
488
839
  }
489
840
  }
490
841
 
491
- async function replaceBindings(bindings) {
492
- await withConfigLock(async () => {
493
- await writeJsonAtomic(CONFIG_FILE, { ...emptyStore(), bindings });
494
- });
842
+ function bindingExpectation(binding) {
843
+ return {
844
+ binding_id: binding.binding_id,
845
+ updated_at: binding.updated_at,
846
+ };
495
847
  }
496
848
 
497
- async function appendBinding(binding) {
498
- await withConfigLock(async () => {
849
+ function normalizeTenantBrand(value, field = 'tenant_brand') {
850
+ if (value === undefined || value === null || value === '') return 'feishu';
851
+ if (value !== 'feishu' && value !== 'lark') {
852
+ throw new Error(`${field} 仅支持 feishu/lark`);
853
+ }
854
+ return value;
855
+ }
856
+
857
+ function openApiDomainForTenantBrand(value) {
858
+ return OPEN_API_DOMAIN_BY_TENANT_BRAND[normalizeTenantBrand(value)];
859
+ }
860
+
861
+ function sameBindingRelationship(existing, candidate) {
862
+ return Boolean(existing && candidate
863
+ && existing.agent_type === candidate.agent_type
864
+ && existing.aamp_host === candidate.aamp_host
865
+ && existing.environment?.name === candidate.environment?.name
866
+ && existing.bot?.app_id === candidate.bot?.app_id
867
+ && normalizeTenantBrand(existing.bot?.tenant_brand)
868
+ === normalizeTenantBrand(candidate.bot?.tenant_brand));
869
+ }
870
+
871
+ async function upsertBindings(intents) {
872
+ return withConfigLock(async () => {
499
873
  const store = await loadStore();
500
- if (store.bindings.some((item) => item.bot.app_id === binding.bot.app_id)) {
501
- throw new Error(`Bot ${binding.bot.app_id} 已绑定,不能重复选择`);
874
+ const intentByAppId = new Map();
875
+ for (const intent of intents) {
876
+ const appId = intent.binding.bot.app_id;
877
+ if (intentByAppId.has(appId)) throw new Error(`Bot ${appId} 在本次操作中重复选择`);
878
+ intentByAppId.set(appId, intent);
502
879
  }
503
- validateBinding(binding, store.bindings.length);
504
- await writeJsonAtomic(CONFIG_FILE, { ...emptyStore(), bindings: [...store.bindings, binding] });
880
+
881
+ let replacedCount = 0;
882
+ const consumed = new Set();
883
+ const bindings = store.bindings.map((current) => {
884
+ const appId = current.bot.app_id;
885
+ const intent = intentByAppId.get(appId);
886
+ if (!intent) return current;
887
+ const expected = intent.expected;
888
+ if (!expected
889
+ || current.binding_id !== expected.binding_id
890
+ || current.updated_at !== expected.updated_at) {
891
+ throw new Error(`Bot ${appId} 的绑定已发生变化,请重新执行`);
892
+ }
893
+ consumed.add(appId);
894
+ replacedCount += 1;
895
+ return intent.binding;
896
+ });
897
+
898
+ for (const [appId, intent] of intentByAppId) {
899
+ if (consumed.has(appId)) continue;
900
+ if (intent.expected) throw new Error(`Bot ${appId} 的绑定已发生变化,请重新执行`);
901
+ bindings.push(intent.binding);
902
+ }
903
+
904
+ const next = validateStore({ ...emptyStore(), bindings });
905
+ await writeJsonAtomic(CONFIG_FILE, next);
906
+ return { bindings: intents.map(({ binding }) => binding), replacedCount };
505
907
  });
506
908
  }
507
909
 
@@ -520,50 +922,207 @@ async function updateBinding(binding) {
520
922
  });
521
923
  }
522
924
 
523
- function bindingLabel(binding) {
925
+ function agentSelectionDisplayName(agent) {
926
+ return agent;
927
+ }
928
+
929
+ function agentBindingDisplayName(agent) {
930
+ return agent;
931
+ }
932
+
933
+ function bindingLabel(binding, resolvedAgentType = binding.agent_type) {
524
934
  const botName = binding.bot?.display_name || binding.bot?.app_id || 'unknown Bot';
525
- return `${binding.agent_type} ↔ ${botName} (${binding.bot?.app_id || 'unknown'})`;
935
+ return `${agentBindingDisplayName(resolvedAgentType)} ↔ ${botName} (${binding.bot?.app_id || 'unknown'})`;
936
+ }
937
+
938
+ function pairingQueueKey(prepared) {
939
+ return `${prepared.group.host}\u0000${prepared.binding.agent_type}`;
940
+ }
941
+
942
+ function orderStartupItems(bindings, items) {
943
+ const order = new Map(bindings.map((binding, index) => [binding.binding_id, index]));
944
+ return [...items].sort((left, right) => (
945
+ (order.get(left.binding.binding_id) ?? Number.MAX_SAFE_INTEGER)
946
+ - (order.get(right.binding.binding_id) ?? Number.MAX_SAFE_INTEGER)
947
+ ));
948
+ }
949
+
950
+ function startupSummaryLines({ title, plannedCount, running = [], failed = [], cancelled = [] }) {
951
+ const lines = [`${title} ${running.length}/${plannedCount} 个配置。`];
952
+ if (running.length) {
953
+ lines.push('启动成功:');
954
+ for (const item of running) {
955
+ lines.push(`- ${bindingLabel(item.binding, item.runtimeAgentType)}`);
956
+ }
957
+ }
958
+ if (failed.length) {
959
+ lines.push('启动失败:');
960
+ for (const item of failed) {
961
+ lines.push(`- ${bindingLabel(item.binding, item.runtimeAgentType)}`);
962
+ lines.push(` 原因:${safeBindingFailureReason(item.binding, item.reason)}`);
963
+ }
964
+ }
965
+ if (cancelled.length) {
966
+ lines.push('已取消:');
967
+ for (const item of cancelled) {
968
+ lines.push(`- ${bindingLabel(item.binding, item.runtimeAgentType)}`);
969
+ lines.push(` 原因:${redact(item.reason)}`);
970
+ }
971
+ }
972
+ return lines;
973
+ }
974
+
975
+ function printStartupSummary(options) {
976
+ console.log(`\n${startupSummaryLines(options).join('\n')}`);
526
977
  }
527
978
 
528
- function printBindingStarted(binding) {
529
- console.log(`[aamp-one-click] 启动成功:${bindingLabel(binding)}`);
979
+ function installHasOnlyCancellations({ cancelled = [], failures = [], selectionFailures = [] }) {
980
+ return cancelled.length > 0 && failures.length === 0 && selectionFailures.length === 0;
981
+ }
982
+
983
+ function agentFailureMessage(agentType, message) {
984
+ const text = safeAgentFailureReason(agentType, message || 'Agent Bridge 启动失败');
985
+ if (agentType === 'traecli') {
986
+ return `${text}\n请执行 'traecli doctor --json' 检查 TraeCode CLI,修复后重试。`;
987
+ }
988
+ const productName = agentType === 'workbuddy'
989
+ ? 'WorkBuddy'
990
+ : agentType === 'workbuddy_ai'
991
+ ? 'WorkBuddy AI'
992
+ : '';
993
+ if (!productName) return text;
994
+ if (text.startsWith(`${productName} is not logged in.`)
995
+ || text.startsWith(`${productName} login expired.`)) return text;
996
+ return `${text}\n如果尚未登录,请打开 ${productName} 完成登录后重试。`;
997
+ }
998
+
999
+ function safeAgentFailureReason(agentType, message) {
1000
+ try {
1001
+ if (typeof resolveTaskAgentMetadata === 'function'
1002
+ && resolveTaskAgentMetadata(agentType).executionLocation === 'remote') {
1003
+ return safeRemoteFailure(message).message;
1004
+ }
1005
+ } catch {
1006
+ // Unknown Agent validation remains authoritative at its existing call sites.
1007
+ }
1008
+ return String(message ?? '');
1009
+ }
1010
+
1011
+ function safeBindingFailureReason(binding, message) {
1012
+ try {
1013
+ if (resolveTaskAgentMetadata(binding?.agent_type).executionLocation === 'remote') {
1014
+ return safeRemoteFailure(message).message;
1015
+ }
1016
+ } catch {
1017
+ // Binding validation remains authoritative at its existing call sites.
1018
+ }
1019
+ return redact(message);
1020
+ }
1021
+
1022
+ function resolvePreparedAgentBindings(bindings, host, requestedAgentType, preparedAgentType) {
1023
+ const runtimeAgentType = preparedAgentType || requestedAgentType;
1024
+ if (!TASK_AGENT_TYPES.includes(runtimeAgentType)) {
1025
+ throw new Error(`unexpected prepared Agent type: ${runtimeAgentType}`);
1026
+ }
1027
+ if (runtimeAgentType === requestedAgentType) {
1028
+ return {
1029
+ requestedAgentType,
1030
+ runtimeAgentType,
1031
+ stableAgentType: requestedAgentType,
1032
+ stableAgentTypes: [requestedAgentType],
1033
+ bindingsToNormalize: [],
1034
+ };
1035
+ }
1036
+ if (requestedAgentType !== 'coco' || !['traex', 'traecli'].includes(runtimeAgentType)) {
1037
+ throw new Error(`unexpected prepared Agent type: ${requestedAgentType} -> ${runtimeAgentType}`);
1038
+ }
1039
+ const matching = bindings.filter((binding) => (
1040
+ binding.aamp_host === host && binding.agent_type === requestedAgentType
1041
+ ));
1042
+ const bindingsToNormalize = matching.filter((binding) => (
1043
+ binding.state === 'pending' && !binding.agent_target_email
1044
+ ));
1045
+ const keepHistoricalIdentity = matching.some((binding) => !bindingsToNormalize.includes(binding));
1046
+ const stableAgentTypes = [
1047
+ ...(keepHistoricalIdentity || !bindingsToNormalize.length ? [requestedAgentType] : []),
1048
+ ...(bindingsToNormalize.length ? [runtimeAgentType] : []),
1049
+ ];
1050
+ return {
1051
+ requestedAgentType,
1052
+ runtimeAgentType,
1053
+ stableAgentType: stableAgentTypes[0],
1054
+ stableAgentTypes,
1055
+ bindingsToNormalize,
1056
+ };
1057
+ }
1058
+
1059
+ function commitPreparedAgentBindings(plan) {
1060
+ for (const binding of plan.bindingsToNormalize) binding.agent_type = plan.runtimeAgentType;
1061
+ return plan.stableAgentType;
1062
+ }
1063
+
1064
+ async function prepareAndCommitAgentBindings(plan, prepare) {
1065
+ const results = [];
1066
+ for (const stableAgentType of plan.stableAgentTypes) {
1067
+ results.push(await prepare(stableAgentType));
1068
+ }
1069
+ commitPreparedAgentBindings(plan);
1070
+ return results;
1071
+ }
1072
+
1073
+ function recordPreparationFailure(group, requestedAgentType, runtimeAgentType, error) {
1074
+ const reason = agentFailureMessage(runtimeAgentType || requestedAgentType, error?.message || error);
1075
+ group.failures.set(requestedAgentType, reason);
1076
+ return reason;
1077
+ }
1078
+
1079
+ function recordStableAgentFailure(group, stableAgentType, message) {
1080
+ const runtimeAgentType = group.runtimeAgentTypes.get(stableAgentType) || stableAgentType;
1081
+ const reason = agentFailureMessage(runtimeAgentType, message);
1082
+ group.failures.set(stableAgentType, reason);
1083
+ return reason;
1084
+ }
1085
+
1086
+ function printBindingStarted(binding, runtimeAgentType = binding.agent_type) {
1087
+ console.log(`[aamp-one-click] 启动成功:${bindingLabel(binding, runtimeAgentType)}`);
1088
+ }
1089
+
1090
+ function bindingCancellationReason(groups, binding) {
1091
+ return groups.get(binding.aamp_host)?.cancellations?.get(binding.agent_type) || '';
1092
+ }
1093
+
1094
+ function printBindingCancelled(binding, reason) {
1095
+ console.log(`\n🟡 已取消:${bindingLabel(binding)}`);
1096
+ console.log(` 原因:${reason}`);
530
1097
  }
531
1098
 
532
1099
  async function recordError(component, message, binding) {
533
- await appendPrivate(ERRORS_LOG, `${JSON.stringify({
1100
+ const safeMessage = binding ? safeBindingFailureReason(binding, message) : redact(message);
1101
+ await errorLogWriter.write(`${JSON.stringify({
534
1102
  timestamp: nowIso(),
535
1103
  level: 'error',
536
1104
  component,
537
1105
  binding_id: binding?.binding_id,
538
1106
  app_id: binding?.bot?.app_id,
539
- message: redact(message),
1107
+ message: safeMessage,
540
1108
  })}\n`);
541
1109
  }
542
1110
 
543
1111
  async function writeManifest() {
544
- const statuses = [...bindingStatuses.entries()].map(([bindingId, status]) => ({ binding_id: bindingId, ...status }));
545
- await writeJsonAtomic(MANIFEST_FILE, {
546
- schema: 'aamp.local_logs.run.v2',
547
- run_id: RUN_ID,
548
- task_agent_version: process.env.AAMP_TASK_AGENT_VERSION || '',
549
- command: COMMAND,
550
- started_at: process.env.AAMP_TASK_RUN_STARTED_AT || RUN_STARTED_AT,
551
- config_file: CONFIG_FILE,
552
- runtime_home: RUNTIME_HOME,
553
- bindings: statuses,
554
- errors_log: ERRORS_LOG,
555
- log_dir: RUN_LOG_DIR,
556
- });
1112
+ await manifestWriter.run();
557
1113
  }
558
1114
 
559
1115
  async function setBindingStatus(binding, phase, status, reason = '') {
1116
+ const safeReason = status === 'failed'
1117
+ ? safeBindingFailureReason(binding, reason)
1118
+ : redact(reason);
560
1119
  bindingStatuses.set(binding.binding_id, {
561
1120
  agent_type: binding.agent_type,
562
1121
  app_id: binding.bot.app_id,
563
1122
  bot_name: binding.bot.display_name || binding.bot.app_id,
564
1123
  phase,
565
1124
  status,
566
- ...(reason ? { reason: redact(reason) } : {}),
1125
+ ...(reason ? { reason: safeReason } : {}),
567
1126
  updated_at: nowIso(),
568
1127
  });
569
1128
  await writeManifest();
@@ -592,7 +1151,12 @@ async function chooseOne(title, items, render, initialIndex = 0) {
592
1151
 
593
1152
  draw();
594
1153
  return new Promise((resolve, reject) => {
1154
+ let finished = false;
1155
+ let releasePrompt = () => {};
595
1156
  const finish = (error) => {
1157
+ if (finished) return;
1158
+ finished = true;
1159
+ releasePrompt();
596
1160
  input.off('keypress', onKeypress);
597
1161
  input.setRawMode(wasRaw);
598
1162
  input.pause();
@@ -618,6 +1182,7 @@ async function chooseOne(title, items, render, initialIndex = 0) {
618
1182
  }
619
1183
  draw(true);
620
1184
  };
1185
+ releasePrompt = promptInterrupter.activate(finish);
621
1186
  input.on('keypress', onKeypress);
622
1187
  });
623
1188
  }
@@ -642,12 +1207,17 @@ async function chooseMany(title, items, render) {
642
1207
  const mark = checked.has(index) ? 'x' : ' ';
643
1208
  output.write(`\x1b[2K\r ${pointer} [${mark}] ${option.label}\n`);
644
1209
  });
645
- output.write('\x1b[2K\r使用 ↑/↓ 移动,空格多选,回车确认;选择“全部”会忽略其他选项。\n');
1210
+ output.write('\x1b[2K\r使用 ↑/↓ 移动,按空格键选择(支持多选),按回车键确认;选择“全部”会取消其他选项的选中状态。\n');
646
1211
  };
647
1212
 
648
1213
  draw();
649
1214
  return new Promise((resolve, reject) => {
1215
+ let finished = false;
1216
+ let releasePrompt = () => {};
650
1217
  const finish = (error) => {
1218
+ if (finished) return;
1219
+ finished = true;
1220
+ releasePrompt();
651
1221
  input.off('keypress', onKeypress);
652
1222
  input.setRawMode(wasRaw);
653
1223
  input.pause();
@@ -683,6 +1253,7 @@ async function chooseMany(title, items, render) {
683
1253
  }
684
1254
  draw(true);
685
1255
  };
1256
+ releasePrompt = promptInterrupter.activate(finish);
686
1257
  input.on('keypress', onKeypress);
687
1258
  });
688
1259
  }
@@ -709,10 +1280,28 @@ function helperArgs(action, bindingOrAgent) {
709
1280
  async function runBootstrapHelper(action, bindingOrAgent, extraEnv = {}) {
710
1281
  if (!BOOTSTRAP) throw new Error('Bootstrap path is unavailable');
711
1282
  throwIfStopping();
712
- const { input } = terminalStreams();
1283
+ const helperAgent = typeof bindingOrAgent === 'object'
1284
+ ? bindingOrAgent?.agent_type
1285
+ : bindingOrAgent;
1286
+ let executionLocation = 'local';
1287
+ if (helperAgent) executionLocation = resolveTaskAgentMetadata(helperAgent).executionLocation;
1288
+ const input = executionLocation === 'remote' && !process.stdin.isTTY
1289
+ ? process.stdin
1290
+ : terminalStreams().input;
713
1291
  const helperEnv = { ...extraEnv };
714
1292
  const inputPayload = helperEnv.AAMP_TASK_INTERNAL_BINDING_JSON || '';
715
1293
  delete helperEnv.AAMP_TASK_INTERNAL_BINDING_JSON;
1294
+ const remoteHelper = executionLocation === 'remote';
1295
+ const helperProcessGroup = remoteHelper && process.platform !== 'win32';
1296
+ // node (v25) aborts at startup when spawned detached with a /dev/tty stdin.
1297
+ // Remote helpers never read stdin (interactive prompts use /dev/tty directly).
1298
+ const helperStdin = remoteHelper ? 'ignore' : input;
1299
+ const helperOutputOptions = {
1300
+ executionLocation,
1301
+ agentExecutionLocations: trustedAgentExecutionLocations(
1302
+ helperAgent ? [[helperAgent, executionLocation]] : [],
1303
+ ),
1304
+ };
716
1305
  const child = spawn('bash', helperArgs(action, bindingOrAgent), {
717
1306
  env: {
718
1307
  ...process.env,
@@ -720,21 +1309,52 @@ async function runBootstrapHelper(action, bindingOrAgent, extraEnv = {}) {
720
1309
  AAMP_TASK_INTERNAL: 'true',
721
1310
  AAMP_TASK_INTERNAL_RESULT_FD: '3',
722
1311
  AAMP_TASK_INTERNAL_INPUT_FD: '4',
1312
+ ...(remoteHelper ? {
1313
+ AAMP_TASK_INTERNAL_EXECUTION_LOCATION: 'remote',
1314
+ ONE_CLICK_LOG: '/dev/null',
1315
+ ERRORS_LOG: '/dev/null',
1316
+ } : {}),
723
1317
  },
724
- stdio: [input, 'inherit', 'inherit', 'pipe', 'pipe'],
1318
+ stdio: [helperStdin, remoteHelper ? 'pipe' : 'inherit', remoteHelper ? 'pipe' : 'inherit', 'pipe', 'pipe'],
1319
+ ...(helperProcessGroup ? { detached: true } : {}),
725
1320
  });
726
- const processRecord = trackTransientProcess(child, `Bootstrap helper ${action}`, false);
1321
+ const processRecord = trackTransientProcess(child, `Bootstrap helper ${action}`, helperProcessGroup);
727
1322
  let result = '';
1323
+ const relayWrites = [];
1324
+ const remoteDiagnostics = [];
1325
+ const relayRemoteLine = (streamName, line) => {
1326
+ if (!String(line || '').trim()) return;
1327
+ const safeLine = safeOperationalLine(line, helperOutputOptions);
1328
+ remoteDiagnostics.push(safeLine);
1329
+ const target = streamName === 'stderr' ? process.stderr : process.stdout;
1330
+ target.write(`${safeLine}\n`);
1331
+ if (process.env.ONE_CLICK_LOG && process.env.ONE_CLICK_LOG !== '/dev/null') {
1332
+ relayWrites.push(appendPrivate(process.env.ONE_CLICK_LOG, `${safeLine}\n`));
1333
+ }
1334
+ };
1335
+ if (remoteHelper) {
1336
+ createLineReader(child.stdout, (line) => relayRemoteLine('stdout', line));
1337
+ createLineReader(child.stderr, (line) => relayRemoteLine('stderr', line));
1338
+ }
728
1339
  child.stdio[3].setEncoding('utf8');
729
1340
  child.stdio[3].on('data', (chunk) => { result += chunk; });
730
1341
  child.stdio[4].end(inputPayload ? `${inputPayload}\n` : '');
731
1342
  if (stopRequested) await stopManagedProcess(processRecord);
732
1343
  const exit = await processRecord.exitPromise;
1344
+ await Promise.allSettled(relayWrites);
733
1345
  throwIfStopping();
734
- if (exit.code !== 0) throw exit.error || new Error(`Bootstrap helper ${action} failed${exit.signal ? ` (${exit.signal})` : ''}`);
1346
+ if (exit.code !== 0) {
1347
+ if (remoteHelper) {
1348
+ throw new Error(remoteDiagnostics.at(-1) || REMOTE_AGENT_PREPARATION_FAILED);
1349
+ }
1350
+ throw exit.error || new Error(`Bootstrap helper ${action} failed${exit.signal ? ` (${exit.signal})` : ''}`);
1351
+ }
735
1352
  try {
736
1353
  return JSON.parse(result.trim() || '{}');
737
1354
  } catch {
1355
+ if (remoteHelper) {
1356
+ throw new Error(remoteDiagnostics.at(-1) || REMOTE_AGENT_PREPARATION_FAILED);
1357
+ }
738
1358
  throw new Error(`Bootstrap helper ${action} returned invalid result`);
739
1359
  }
740
1360
  }
@@ -746,7 +1366,7 @@ function npmExecArgs(packageSpec, executable, args) {
746
1366
  ];
747
1367
  }
748
1368
 
749
- async function runCapture(packageSpec, executable, args, options = {}) {
1369
+ async function runNpmExecCapture(packageSpec, executable, args, options = {}) {
750
1370
  throwIfStopping();
751
1371
  const child = spawn(NPM_BIN, npmExecArgs(packageSpec, executable, args), {
752
1372
  env: options.env || process.env,
@@ -766,10 +1386,67 @@ async function runCapture(packageSpec, executable, args, options = {}) {
766
1386
  const exit = await processRecord.exitPromise;
767
1387
  throwIfStopping();
768
1388
  if (options.logFile) {
769
- await appendPrivate(options.logFile, `${stdout}${stderr ? `\n${stderr}` : ''}`);
1389
+ await appendPrivate(options.logFile, safeCapturedLog(stdout, stderr, options));
1390
+ }
1391
+ if (exit.code !== 0) {
1392
+ const detail = safeOperationalOutput(
1393
+ stderr.trim() || stdout.trim() || exit.error?.message || `exit ${exit.code}`,
1394
+ options,
1395
+ );
1396
+ throw new Error(`${executable} failed: ${detail.split('\n').slice(-8).join('\n')}`);
1397
+ }
1398
+ return { stdout, stderr };
1399
+ }
1400
+
1401
+ const packageExecutableLauncher = createPackageExecutableLauncher({
1402
+ materialize: async (packageSpec, executable, options = {}) => {
1403
+ const result = await runNpmExecCapture(
1404
+ packageSpec,
1405
+ process.execPath,
1406
+ npmExecutableResolverArgs(executable),
1407
+ options,
1408
+ );
1409
+ return parseResolvedPackageExecutable(result.stdout, executable);
1410
+ },
1411
+ });
1412
+
1413
+ async function runCapture(packageSpec, executable, args, options = {}) {
1414
+ throwIfStopping();
1415
+ const preparedExecutable = await packageExecutableLauncher.resolve(
1416
+ packageSpec,
1417
+ executable,
1418
+ options,
1419
+ );
1420
+ throwIfStopping();
1421
+ const child = packageExecutableLauncher.launchPrepared({
1422
+ preparedExecutable,
1423
+ args,
1424
+ spawnOptions: {
1425
+ env: options.env || process.env,
1426
+ stdio: ['pipe', 'pipe', 'pipe'],
1427
+ detached: process.platform !== 'win32',
1428
+ },
1429
+ });
1430
+ const processRecord = trackTransientProcess(child, executable, process.platform !== 'win32');
1431
+ let stdout = '';
1432
+ let stderr = '';
1433
+ child.stdout.setEncoding('utf8');
1434
+ child.stderr.setEncoding('utf8');
1435
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
1436
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
1437
+ if (options.input !== undefined) child.stdin.end(options.input);
1438
+ else child.stdin.end();
1439
+ if (stopRequested) await stopManagedProcess(processRecord);
1440
+ const exit = await processRecord.exitPromise;
1441
+ throwIfStopping();
1442
+ if (options.logFile) {
1443
+ await appendPrivate(options.logFile, safeCapturedLog(stdout, stderr, options));
770
1444
  }
771
1445
  if (exit.code !== 0) {
772
- const detail = redact(stderr.trim() || stdout.trim() || exit.error?.message || `exit ${exit.code}`);
1446
+ const detail = safeOperationalOutput(
1447
+ stderr.trim() || stdout.trim() || exit.error?.message || `exit ${exit.code}`,
1448
+ options,
1449
+ );
773
1450
  throw new Error(`${executable} failed: ${detail.split('\n').slice(-8).join('\n')}`);
774
1451
  }
775
1452
  return { stdout, stderr };
@@ -829,15 +1506,42 @@ function createLineReader(stream, onLine) {
829
1506
  });
830
1507
  }
831
1508
 
832
- async function startManagedProcess({ label, packageSpec, executable, args, env, logFile }) {
1509
+ async function startManagedProcess({
1510
+ label,
1511
+ packageSpec,
1512
+ executable,
1513
+ args,
1514
+ env,
1515
+ logFile,
1516
+ preparedExecutable,
1517
+ executionLocation = 'local',
1518
+ eventPathRoot,
1519
+ agentExecutionLocations = [],
1520
+ allowedAppIds = [],
1521
+ }) {
833
1522
  await ensurePrivateDir(path.dirname(logFile));
834
1523
  throwIfStopping();
835
1524
  await fsp.writeFile(logFile, '', { mode: 0o600, flag: 'a' });
836
1525
  throwIfStopping();
837
- const child = spawn(NPM_BIN, npmExecArgs(packageSpec, executable, args), {
838
- env: env || process.env,
839
- stdio: ['ignore', 'pipe', 'pipe'],
840
- detached: process.platform !== 'win32',
1526
+ const executableDescriptor = preparedExecutable || await packageExecutableLauncher.resolve(
1527
+ packageSpec,
1528
+ executable,
1529
+ {
1530
+ env: env || process.env,
1531
+ executionLocation,
1532
+ eventPathRoot,
1533
+ ...(executionLocation === 'remote' ? { logFile } : {}),
1534
+ },
1535
+ );
1536
+ throwIfStopping();
1537
+ const child = packageExecutableLauncher.launchPrepared({
1538
+ preparedExecutable: executableDescriptor,
1539
+ args,
1540
+ spawnOptions: {
1541
+ env: env || process.env,
1542
+ stdio: ['ignore', 'pipe', 'pipe'],
1543
+ detached: process.platform !== 'win32',
1544
+ },
841
1545
  });
842
1546
  const processStartedAt = Date.now();
843
1547
  const logWriter = createSerializedLineWriter((content) => appendPrivate(logFile, content));
@@ -852,11 +1556,19 @@ async function startManagedProcess({ label, packageSpec, executable, args, env,
852
1556
  expectedStop: false,
853
1557
  processGroup: process.platform !== 'win32',
854
1558
  outputTail: [],
1559
+ eventPathHandles: executionLocation === 'remote' && eventPathRoot ? new Map() : undefined,
855
1560
  logWriter,
856
1561
  logWriteError: undefined,
857
1562
  };
858
1563
  managedProcesses.add(record);
859
- void logWriter.write(`${JSON.stringify({
1564
+ const outputOptions = {
1565
+ executionLocation,
1566
+ eventPathRoot,
1567
+ eventPathHandles: record.eventPathHandles,
1568
+ agentExecutionLocations: trustedAgentExecutionLocations(agentExecutionLocations),
1569
+ allowedAppIds,
1570
+ };
1571
+ void logWriter.write(`${safeOperationalLine(JSON.stringify({
860
1572
  timestamp: nowIso(),
861
1573
  type: 'bridge.process',
862
1574
  status: 'started',
@@ -865,17 +1577,21 @@ async function startManagedProcess({ label, packageSpec, executable, args, env,
865
1577
  package: packageSpec,
866
1578
  pid: child.pid || null,
867
1579
  node: process.version,
868
- })}\n`).catch((error) => {
1580
+ }), outputOptions)}\n`).catch((error) => {
869
1581
  record.logWriteError ??= error;
870
1582
  });
871
1583
  const handleLine = (streamName, line) => {
872
- const safeLine = redact(line);
1584
+ const safeLine = safeOperationalLine(line, outputOptions);
873
1585
  record.outputTail.push(`[${streamName}] ${safeLine}`);
874
1586
  if (record.outputTail.length > 30) record.outputTail.shift();
875
1587
  record.emitter.emit('output', safeLine);
876
- if (streamName === 'stdout' && line.trim()) {
1588
+ if (streamName === 'stdout' && safeLine.trim()) {
877
1589
  try {
878
- const event = JSON.parse(line.trim());
1590
+ const rawEvent = JSON.parse(String(line).trim());
1591
+ const event = executionLocation === 'remote'
1592
+ ? (projectTrustedLocalAgentEvent(rawEvent, outputOptions)
1593
+ || projectRemoteOperationalEvent(rawEvent, outputOptions))
1594
+ : JSON.parse(safeLine.trim());
879
1595
  if (event && typeof event.type === 'string') {
880
1596
  record.events.push(event);
881
1597
  record.emitter.emit('event', event);
@@ -884,7 +1600,7 @@ async function startManagedProcess({ label, packageSpec, executable, args, env,
884
1600
  // Feishu task mode can mix human-readable lines with JSON events.
885
1601
  }
886
1602
  }
887
- void logWriter.write(`${line}\n`).catch((error) => {
1603
+ void logWriter.write(`${safeLine}\n`).catch((error) => {
888
1604
  record.logWriteError ??= error;
889
1605
  });
890
1606
  };
@@ -895,7 +1611,7 @@ async function startManagedProcess({ label, packageSpec, executable, args, env,
895
1611
  const finish = async (exit) => {
896
1612
  if (settled) return;
897
1613
  settled = true;
898
- await logWriter.write(`${JSON.stringify({
1614
+ await logWriter.write(`${safeOperationalLine(JSON.stringify({
899
1615
  timestamp: nowIso(),
900
1616
  type: 'bridge.process',
901
1617
  status: 'exited',
@@ -908,12 +1624,13 @@ async function startManagedProcess({ label, packageSpec, executable, args, env,
908
1624
  signal: exit.signal || null,
909
1625
  expectedStop: record.expectedStop,
910
1626
  ...(exit.error ? { error: describeNetworkError(exit.error) } : {}),
911
- })}\n`).catch((error) => {
1627
+ }), outputOptions)}\n`).catch((error) => {
912
1628
  record.logWriteError ??= error;
913
1629
  });
914
1630
  await logWriter.flush().catch((error) => {
915
1631
  record.logWriteError ??= error;
916
1632
  });
1633
+ record.eventPathHandles?.clear();
917
1634
  record.exited = true;
918
1635
  record.exit = {
919
1636
  ...exit,
@@ -1047,7 +1764,29 @@ async function releaseLease(lease) {
1047
1764
  await lease.release();
1048
1765
  }
1049
1766
 
1050
- async function setupAgentGroups(bindings) {
1767
+ function acpBridgeAgentPolicy(stableAgentType) {
1768
+ const metadata = resolveTaskAgentMetadata(stableAgentType);
1769
+ return {
1770
+ executionLocation: metadata.executionLocation,
1771
+ ...(metadata.attachmentPolicy ? { attachmentPolicy: metadata.attachmentPolicy } : {}),
1772
+ ...(metadata.taskDispatchConcurrency
1773
+ ? { taskDispatchConcurrency: metadata.taskDispatchConcurrency }
1774
+ : {}),
1775
+ };
1776
+ }
1777
+
1778
+ function agentExecutionLocations(agents) {
1779
+ return new Set((agents || []).map((agent) => (
1780
+ resolveTaskAgentMetadata(agent.name).executionLocation
1781
+ )));
1782
+ }
1783
+
1784
+ function groupProcessExecutionLocation(agents) {
1785
+ return agentExecutionLocations(agents).has('remote') ? 'remote' : 'local';
1786
+ }
1787
+
1788
+ async function initializeAgentGroups(bindings, operations = {}) {
1789
+ const prepareAgent = operations.runBootstrapHelper || runBootstrapHelper;
1051
1790
  const byHost = new Map();
1052
1791
  for (const binding of bindings) {
1053
1792
  if (!byHost.has(binding.aamp_host)) byHost.set(binding.aamp_host, new Map());
@@ -1074,45 +1813,88 @@ async function setupAgentGroups(bindings) {
1074
1813
  identities: new Map(),
1075
1814
  availableAgents: new Set(),
1076
1815
  failures: new Map(),
1816
+ cancellations: new Map(),
1817
+ runtimeAgentTypes: new Map(),
1077
1818
  leases: new Map(),
1819
+ agents: [],
1820
+ bridgeEnv,
1078
1821
  process: undefined,
1079
1822
  };
1080
1823
  groups.set(host, group);
1081
1824
  const agents = [];
1082
1825
  for (const [agentType, sampleBinding] of agentBindings) {
1083
1826
  throwIfStopping();
1827
+ let runtimeAgentType;
1828
+ let stableAgentTypes = [agentType];
1084
1829
  try {
1085
1830
  const lease = await acquireAgentLease(host, agentType);
1086
1831
  throwIfStopping();
1087
1832
  group.leases.set(agentType, lease);
1088
- console.log(`[aamp-one-click] 正在检查 ${agentType} 本地智能体...`);
1089
- const prepared = await runBootstrapHelper('__prepare-agent', sampleBinding);
1833
+ const metadata = resolveTaskAgentMetadata(agentType);
1834
+ console.log(`[aamp-one-click] 正在检查 ${agentSelectionDisplayName(agentType)} ${metadata.executionLocation === 'remote' ? '远程智能体' : '本地智能体'}...`);
1835
+ const prepared = await prepareAgent('__prepare-agent', sampleBinding);
1090
1836
  throwIfStopping();
1091
- if (path.resolve(prepared.lark_cli_config_dir || '') !== path.resolve(bridgeEnv.LARKSUITE_CLI_CONFIG_DIR)) {
1092
- throw new Error(`Agent 使用的 lark-cli 配置目录与 Online 配置不一致:${prepared.lark_cli_config_dir || 'unknown'}`);
1837
+ runtimeAgentType = prepared.agent_type || agentType;
1838
+ if (prepared.cancelled === true) {
1839
+ const reason = redact(prepared.reason || '用户取消了 Agent 准备流程');
1840
+ group.cancellations.set(agentType, reason);
1841
+ await releaseLease(group.leases.get(agentType));
1842
+ group.leases.delete(agentType);
1843
+ continue;
1093
1844
  }
1094
- const agentHome = path.join(home, 'agents', agentType);
1095
- await assertNoSymlinkPath(RUNTIME_HOME, agentHome);
1096
- throwIfStopping();
1097
- await ensurePrivateDir(agentHome);
1098
- throwIfStopping();
1099
- agents.push({
1100
- name: agentType,
1101
- acpCommand: prepared.acp_command,
1102
- credentialsFile: path.join(agentHome, 'credentials.json'),
1103
- pairingFile: path.join(agentHome, 'pairing.json'),
1104
- senderPoliciesFile: path.join(agentHome, 'sender-policies.json'),
1105
- createPairing: false,
1845
+ if (prepared.agent_type && prepared.agent_type !== agentType && agentBindings.has(prepared.agent_type)) {
1846
+ throw new Error(`Agent 类型归一化后发生重复:${agentType} -> ${prepared.agent_type}`);
1847
+ }
1848
+ const bindingPlan = resolvePreparedAgentBindings(
1849
+ bindings,
1850
+ host,
1851
+ agentType,
1852
+ runtimeAgentType,
1853
+ );
1854
+ stableAgentTypes = bindingPlan.stableAgentTypes;
1855
+ const preparedAgents = await prepareAndCommitAgentBindings(bindingPlan, async (stableAgentType) => {
1856
+ if (!group.leases.has(stableAgentType)) {
1857
+ group.leases.set(stableAgentType, await acquireAgentLease(host, stableAgentType));
1858
+ }
1859
+ const stableMetadata = resolveTaskAgentMetadata(stableAgentType);
1860
+ if (stableMetadata.executionLocation === 'local'
1861
+ && path.resolve(prepared.lark_cli_config_dir || '') !== path.resolve(bridgeEnv.LARKSUITE_CLI_CONFIG_DIR)) {
1862
+ throw new Error(`Agent 使用的 lark-cli 配置目录与 Online 配置不一致:${prepared.lark_cli_config_dir || 'unknown'}`);
1863
+ }
1864
+ const agentHome = path.join(home, 'agents', stableAgentType);
1865
+ await assertNoSymlinkPath(RUNTIME_HOME, agentHome);
1866
+ throwIfStopping();
1867
+ await ensurePrivateDir(agentHome);
1868
+ throwIfStopping();
1869
+ return {
1870
+ name: stableAgentType,
1871
+ acpCommand: prepared.acp_command,
1872
+ credentialsFile: path.join(agentHome, 'credentials.json'),
1873
+ pairingFile: path.join(agentHome, 'pairing.json'),
1874
+ senderPoliciesFile: path.join(agentHome, 'sender-policies.json'),
1875
+ createPairing: false,
1876
+ ...acpBridgeAgentPolicy(stableAgentType),
1877
+ };
1106
1878
  });
1879
+ if (!stableAgentTypes.includes(agentType)) {
1880
+ await releaseLease(group.leases.get(agentType));
1881
+ group.leases.delete(agentType);
1882
+ }
1883
+ agents.push(...preparedAgents);
1884
+ for (const stableAgentType of stableAgentTypes) {
1885
+ group.runtimeAgentTypes.set(stableAgentType, runtimeAgentType);
1886
+ }
1107
1887
  } catch (error) {
1108
- const reason = redact(error.message || error);
1109
- group.failures.set(agentType, reason);
1110
- await releaseLease(group.leases.get(agentType));
1111
- group.leases.delete(agentType);
1888
+ recordPreparationFailure(group, agentType, runtimeAgentType, redact(error.message || error));
1889
+ for (const stableAgentType of new Set([...stableAgentTypes, agentType])) {
1890
+ await releaseLease(group.leases.get(stableAgentType));
1891
+ group.leases.delete(stableAgentType);
1892
+ }
1112
1893
  if (stopRequested) throw error;
1113
1894
  }
1114
1895
  }
1115
1896
  if (!agents.length) continue;
1897
+ group.executionLocation = groupProcessExecutionLocation(agents);
1116
1898
  try {
1117
1899
  throwIfStopping();
1118
1900
  const initResult = await runNetworkStage(async () => {
@@ -1120,7 +1902,16 @@ async function setupAgentGroups(bindings) {
1120
1902
  ACP_PACKAGE,
1121
1903
  'aamp-acp-bridge',
1122
1904
  ['init', '--json', '--config', group.configFile, '--input', '-'],
1123
- { input: JSON.stringify({ aampHost: host, agents }), env: bridgeEnv, logFile },
1905
+ { input: JSON.stringify({ aampHost: host, agents }),
1906
+ env: bridgeEnv,
1907
+ logFile,
1908
+ executionLocation: group.executionLocation,
1909
+ eventPathRoot: group.home,
1910
+ agentExecutionLocations: trustedAgentExecutionLocations(agents.map((agent) => [
1911
+ agent.name,
1912
+ resolveTaskAgentMetadata(agent.name).executionLocation,
1913
+ ])),
1914
+ },
1124
1915
  );
1125
1916
  }, {
1126
1917
  stage: 'acp-init',
@@ -1132,20 +1923,63 @@ async function setupAgentGroups(bindings) {
1132
1923
  throwIfStopping();
1133
1924
  const initialized = parseJsonDocument(initResult.stdout, 'ACP init');
1134
1925
  for (const agent of initialized.agents || []) group.identities.set(agent.name, agent.email);
1135
- console.log(`[aamp-one-click] 正在启动本地 Agent Bridge (${agents.map((agent) => agent.name).join(', ')})...`);
1136
- const started = await runNetworkStage(async ({ attempt, maxAttempts }) => {
1137
- const process = await startManagedProcess({
1138
- label: `ACP Bridge ${host}`,
1139
- packageSpec: ACP_PACKAGE,
1140
- executable: 'aamp-acp-bridge',
1141
- args: ['start', '--config', group.configFile, '--json', ...(DEBUG_MODE ? ['--debug'] : [])],
1142
- env: bridgeEnv,
1143
- logFile,
1144
- });
1145
- group.process = process;
1146
- try {
1147
- const running = await waitForEvent(process, (event) => event.type === 'bridge.running');
1148
- const retryError = agentStartRetryError(
1926
+ group.agents = agents;
1927
+ } catch (error) {
1928
+ for (const agent of agents) {
1929
+ const message = agentStartFailureMessage(
1930
+ error?.agentStartEvents || group.process?.events,
1931
+ agent.name,
1932
+ redact(error.message || error),
1933
+ );
1934
+ recordStableAgentFailure(group, agent.name, message);
1935
+ }
1936
+ if (group.process) await stopManagedProcess(group.process);
1937
+ for (const lease of group.leases.values()) await releaseLease(lease);
1938
+ group.leases.clear();
1939
+ if (stopRequested) throw error;
1940
+ }
1941
+ }
1942
+ return groups;
1943
+ }
1944
+
1945
+ async function startAgentGroups(groups) {
1946
+ for (const group of groups.values()) {
1947
+ const agents = group.agents || [];
1948
+ if (!agents.length) continue;
1949
+ const bridgeEnv = group.bridgeEnv;
1950
+ try {
1951
+ throwIfStopping();
1952
+ const runtimeAgentNames = agents
1953
+ .map((agent) => group.runtimeAgentTypes.get(agent.name) || agent.name)
1954
+ .map(agentSelectionDisplayName);
1955
+ const executionLocations = agentExecutionLocations(agents);
1956
+ if (executionLocations.size === 1 && executionLocations.has('remote')) {
1957
+ console.log(`[aamp-one-click] 正在启动远程 Agent Bridge (${runtimeAgentNames.join(', ')})...`);
1958
+ } else if (executionLocations.size === 1 && executionLocations.has('local')) {
1959
+ console.log(`[aamp-one-click] 正在启动本地 Agent Bridge (${runtimeAgentNames.join(', ')})...`);
1960
+ } else {
1961
+ console.log(`[aamp-one-click] 正在启动 Agent Bridge (${runtimeAgentNames.join(', ')})...`);
1962
+ }
1963
+ const executionLocation = executionLocations.has('remote') ? 'remote' : 'local';
1964
+ const started = await runNetworkStage(async ({ attempt, maxAttempts }) => {
1965
+ const process = await startManagedProcess({
1966
+ label: `ACP Bridge ${group.host}`,
1967
+ packageSpec: ACP_PACKAGE,
1968
+ executable: 'aamp-acp-bridge',
1969
+ args: ['start', '--config', group.configFile, '--json', ...(DEBUG_MODE ? ['--debug'] : [])],
1970
+ env: bridgeEnv,
1971
+ logFile: group.logFile,
1972
+ executionLocation,
1973
+ eventPathRoot: group.home,
1974
+ agentExecutionLocations: agents.map((agent) => [
1975
+ agent.name,
1976
+ resolveTaskAgentMetadata(agent.name).executionLocation,
1977
+ ]),
1978
+ });
1979
+ group.process = process;
1980
+ try {
1981
+ const running = await waitForEvent(process, (event) => event.type === 'bridge.running');
1982
+ const retryError = agentStartRetryError(
1149
1983
  process.events,
1150
1984
  agents.map((agent) => agent.name),
1151
1985
  attempt,
@@ -1154,16 +1988,17 @@ async function setupAgentGroups(bindings) {
1154
1988
  if (retryError) throw retryError;
1155
1989
  return { process, running };
1156
1990
  } catch (error) {
1991
+ const failure = preserveAgentStartFailure(error, process.events);
1157
1992
  await stopManagedProcess(process);
1158
1993
  managedProcesses.delete(process);
1159
1994
  if (group.process === process) group.process = undefined;
1160
- throw error;
1995
+ throw failure;
1161
1996
  }
1162
1997
  }, {
1163
1998
  stage: 'acp-start',
1164
1999
  label: 'Agent Bridge 启动',
1165
- host,
1166
- logFile,
2000
+ host: group.host,
2001
+ logFile: group.logFile,
1167
2002
  environment: bridgeEnv,
1168
2003
  });
1169
2004
  group.process = started.process;
@@ -1174,13 +2009,24 @@ async function setupAgentGroups(bindings) {
1174
2009
  for (const agent of agents) {
1175
2010
  if (!group.availableAgents.has(agent.name)) {
1176
2011
  const failed = group.process.events.find((event) => event.type === 'agent.failed' && event.agent === agent.name);
1177
- group.failures.set(agent.name, failed?.message || `${agent.name} Agent Bridge 启动失败`);
2012
+ recordStableAgentFailure(
2013
+ group,
2014
+ agent.name,
2015
+ failed?.message || `${agent.name} Agent Bridge 启动失败`,
2016
+ );
1178
2017
  await releaseLease(group.leases.get(agent.name));
1179
2018
  group.leases.delete(agent.name);
1180
2019
  }
1181
2020
  }
1182
2021
  } catch (error) {
1183
- for (const agent of agents) group.failures.set(agent.name, redact(error.message || error));
2022
+ for (const agent of agents) {
2023
+ const message = agentStartFailureMessage(
2024
+ error?.agentStartEvents || group.process?.events,
2025
+ agent.name,
2026
+ redact(error.message || error),
2027
+ );
2028
+ recordStableAgentFailure(group, agent.name, message);
2029
+ }
1184
2030
  if (group.process) await stopManagedProcess(group.process);
1185
2031
  for (const lease of group.leases.values()) await releaseLease(lease);
1186
2032
  group.leases.clear();
@@ -1190,11 +2036,31 @@ async function setupAgentGroups(bindings) {
1190
2036
  return groups;
1191
2037
  }
1192
2038
 
1193
- function resolveGroup(groups, binding) {
2039
+ async function setupAgentGroups(bindings) {
2040
+ const groups = await initializeAgentGroups(bindings);
2041
+ await startAgentGroups(groups);
2042
+ return groups;
2043
+ }
2044
+
2045
+ function resolveInitializedGroup(groups, binding) {
1194
2046
  const group = groups.get(binding.aamp_host);
1195
2047
  if (!group) throw new Error(`Agent Bridge group is unavailable for ${binding.aamp_host}`);
2048
+ const agentCancellation = group.cancellations.get(binding.agent_type);
2049
+ if (agentCancellation) throw new Error(agentCancellation);
1196
2050
  const agentFailure = group.failures.get(binding.agent_type);
1197
- if ((!group.process || group.process.exited) && agentFailure) throw new Error(agentFailure);
2051
+ if (agentFailure) throw new Error(agentFailure);
2052
+ const email = group.identities.get(binding.agent_type);
2053
+ if (!email) throw new Error(`${binding.agent_type} Agent mailbox is unavailable`);
2054
+ if (binding.agent_target_email && binding.agent_target_email !== email) {
2055
+ throw new Error(`Agent mailbox 已变化(配置=${binding.agent_target_email},当前=${email}),请使用 add 或 install 重新绑定`);
2056
+ }
2057
+ const runtimeAgentType = group.runtimeAgentTypes.get(binding.agent_type) || binding.agent_type;
2058
+ return { group, email, runtimeAgentType };
2059
+ }
2060
+
2061
+ function resolveGroup(groups, binding) {
2062
+ const resolved = resolveInitializedGroup(groups, binding);
2063
+ const { group } = resolved;
1198
2064
  if (!group.process || group.process.exited) {
1199
2065
  const tail = group.process?.outputTail?.slice(-10).join('\n');
1200
2066
  throw new Error(`Agent Bridge 已退出${tail ? `:\n${tail}` : ''}`);
@@ -1202,12 +2068,7 @@ function resolveGroup(groups, binding) {
1202
2068
  if (!group.availableAgents.has(binding.agent_type)) {
1203
2069
  throw new Error(group.failures.get(binding.agent_type) || `${binding.agent_type} Agent Bridge 未启动`);
1204
2070
  }
1205
- const email = group.identities.get(binding.agent_type);
1206
- if (!email) throw new Error(`${binding.agent_type} Agent mailbox is unavailable`);
1207
- if (binding.agent_target_email && binding.agent_target_email !== email) {
1208
- throw new Error(`Agent mailbox 已变化(配置=${binding.agent_target_email},当前=${email}),请使用 add 或 install 重新绑定`);
1209
- }
1210
- return { group, email };
2071
+ return resolved;
1211
2072
  }
1212
2073
 
1213
2074
  async function writeFeishuRuntimeProfile(binding) {
@@ -1215,14 +2076,15 @@ async function writeFeishuRuntimeProfile(binding) {
1215
2076
  const instancesDir = path.join(binding.feishu_config_dir, 'task-runtime', 'instances');
1216
2077
  await assertNoSymlinkPath(RUNTIME_HOME, profileFile);
1217
2078
  await assertNoSymlinkPath(RUNTIME_HOME, instancesDir);
2079
+ const metadata = resolveTaskAgentMetadata(binding.agent_type);
1218
2080
  await writeJsonAtomic(profileFile, {
1219
2081
  version: 1,
1220
2082
  profiles: [{
1221
2083
  app_id: binding.bot.app_id,
1222
2084
  app_secret: binding.bot.app_secret,
1223
- profile: binding.bot.lark_cli_profile,
1224
2085
  display_name: binding.bot.display_name,
1225
- auth_mode: 'lark-cli',
2086
+ auth_mode: metadata.executionLocation === 'remote' ? 'app-secret' : 'lark-cli',
2087
+ ...(metadata.executionLocation === 'local' ? { profile: binding.bot.lark_cli_profile } : {}),
1226
2088
  capabilities: ['im', 'task'],
1227
2089
  domains: PROFILE_DOMAINS,
1228
2090
  updated_at: nowIso(),
@@ -1236,7 +2098,55 @@ async function ensureBindingProfile(binding) {
1236
2098
  });
1237
2099
  }
1238
2100
 
2101
+ async function probeBindingProfile(binding) {
2102
+ return runBootstrapHelper('__probe-profile', binding, {
2103
+ AAMP_TASK_INTERNAL_BINDING_JSON: JSON.stringify({
2104
+ agent_type: binding.agent_type,
2105
+ aamp_host: binding.aamp_host,
2106
+ bot: {
2107
+ app_id: binding.bot.app_id,
2108
+ lark_cli_profile: binding.bot.lark_cli_profile,
2109
+ },
2110
+ }),
2111
+ });
2112
+ }
2113
+
2114
+ const readyProfileProbeOperations = Object.freeze({
2115
+ probeBindingProfile,
2116
+ throwIfStopping,
2117
+ });
2118
+
2119
+ async function probeReadyBindingProfiles(
2120
+ bindings,
2121
+ operations = readyProfileProbeOperations,
2122
+ ) {
2123
+ const probes = new Map();
2124
+ for (const binding of bindings) {
2125
+ if (bindingNeedsInitialStart(binding)
2126
+ || resolveTaskAgentMetadata(binding.agent_type).executionLocation === 'remote') continue;
2127
+ operations.throwIfStopping();
2128
+ try {
2129
+ const profile = await operations.probeBindingProfile(binding);
2130
+ operations.throwIfStopping();
2131
+ if (profile?.ready === true && profile.lark_cli_bin) probes.set(binding, profile);
2132
+ } catch {
2133
+ operations.throwIfStopping();
2134
+ // The normal binding preparation remains authoritative for misses and failures.
2135
+ }
2136
+ }
2137
+ return probes;
2138
+ }
2139
+
2140
+ function prewarmFeishuExecutable(binding) {
2141
+ return packageExecutableLauncher.resolve(
2142
+ FEISHU_PACKAGE,
2143
+ 'aamp-feishu-bridge',
2144
+ { env: onlineEnvironment(binding) },
2145
+ );
2146
+ }
2147
+
1239
2148
  function feishuArgs(binding, larkCliBin, target) {
2149
+ const metadata = resolveTaskAgentMetadata(binding.agent_type);
1240
2150
  const targetArgs = target.pairingUrl
1241
2151
  ? ['--pairing-url', target.pairingUrl]
1242
2152
  : ['--target-agent', target.agentTargetEmail];
@@ -1245,48 +2155,105 @@ function feishuArgs(binding, larkCliBin, target) {
1245
2155
  '--config-dir', binding.feishu_config_dir,
1246
2156
  '--aamp-host', binding.aamp_host,
1247
2157
  '--agent', binding.agent_type,
2158
+ '--agent-execution-location', metadata.executionLocation,
1248
2159
  ...targetArgs,
1249
2160
  '--app-id', binding.bot.app_id,
1250
2161
  '--bot-name', binding.bot.display_name || binding.bot.app_id,
1251
- '--use-feishu-cli',
1252
- '--feishu-cli-profile', binding.bot.lark_cli_profile,
1253
- '--feishu-cli-bin', larkCliBin,
2162
+ '--domain', openApiDomainForTenantBrand(binding.bot.tenant_brand),
2163
+ ...(metadata.executionLocation === 'local' ? [
2164
+ '--use-feishu-cli',
2165
+ '--feishu-cli-profile', binding.bot.lark_cli_profile,
2166
+ '--feishu-cli-bin', larkCliBin,
2167
+ ] : []),
1254
2168
  '--json',
1255
2169
  ...(DEBUG_MODE ? ['--debug'] : []),
1256
2170
  ];
1257
2171
  }
1258
2172
 
1259
- async function prepareFeishuProcess(binding, phase) {
1260
- throwIfStopping();
1261
- await writeFeishuRuntimeProfile(binding);
1262
- throwIfStopping();
1263
- const profile = await ensureBindingProfile(binding);
1264
- throwIfStopping();
1265
- if (!profile.lark_cli_bin) throw new Error(`lark-cli profile ${binding.bot.lark_cli_profile} is unavailable`);
2173
+ const feishuPreparationOperations = Object.freeze({
2174
+ ensureBindingProfile,
2175
+ onlineEnvironment,
2176
+ throwIfStopping,
2177
+ writeFeishuRuntimeProfile,
2178
+ });
2179
+
2180
+ async function prepareFeishuProcess(
2181
+ binding,
2182
+ phase,
2183
+ runtimeAgentType = binding.agent_type,
2184
+ profileProbes,
2185
+ operations = feishuPreparationOperations,
2186
+ ) {
2187
+ operations.throwIfStopping();
2188
+ await operations.writeFeishuRuntimeProfile(binding);
2189
+ operations.throwIfStopping();
2190
+ const environment = operations.onlineEnvironment(binding);
2191
+ const metadata = resolveTaskAgentMetadata(binding.agent_type);
2192
+ const probedProfile = profileProbes?.get(binding);
2193
+ const profile = metadata.executionLocation === 'remote'
2194
+ ? {}
2195
+ : (probedProfile?.ready === true
2196
+ && probedProfile.lark_cli_bin
2197
+ && path.resolve(probedProfile.lark_cli_config_dir || '')
2198
+ === path.resolve(environment.LARKSUITE_CLI_CONFIG_DIR || '')
2199
+ ? probedProfile
2200
+ : await operations.ensureBindingProfile(binding));
2201
+ operations.throwIfStopping();
2202
+ if (metadata.executionLocation === 'local' && !profile.lark_cli_bin) throw new Error(`lark-cli profile ${binding.bot.lark_cli_profile} is unavailable`);
2203
+ const logFile = path.join(RUN_LOG_DIR, `feishu-bridge-${safeId(binding.binding_id)}-${phase}.jsonl`);
2204
+ const preparedExecutable = operations.resolveFeishuExecutable
2205
+ ? await operations.resolveFeishuExecutable(environment)
2206
+ : await packageExecutableLauncher.resolve(
2207
+ FEISHU_PACKAGE,
2208
+ 'aamp-feishu-bridge',
2209
+ {
2210
+ env: environment,
2211
+ executionLocation: metadata.executionLocation,
2212
+ ...(metadata.executionLocation === 'remote' ? { logFile } : {}),
2213
+ },
2214
+ );
2215
+ operations.throwIfStopping();
1266
2216
  return {
1267
2217
  binding,
1268
2218
  phase,
2219
+ runtimeAgentType,
1269
2220
  larkCliBin: profile.lark_cli_bin,
1270
- logFile: path.join(RUN_LOG_DIR, `feishu-bridge-${safeId(binding.binding_id)}-${phase}.jsonl`),
2221
+ logFile,
2222
+ preparedExecutable,
1271
2223
  };
1272
2224
  }
1273
2225
 
1274
2226
  async function startPreparedFeishuProcess(prepared, target, environment = onlineEnvironment(prepared.binding)) {
1275
2227
  throwIfStopping();
1276
- const { binding, phase, larkCliBin, logFile } = prepared;
2228
+ const {
2229
+ binding,
2230
+ phase,
2231
+ runtimeAgentType,
2232
+ larkCliBin,
2233
+ logFile,
2234
+ preparedExecutable,
2235
+ } = prepared;
1277
2236
  const phaseMessage = phase === 'install'
1278
2237
  ? '正在建立绑定并启动飞书任务 Bridge'
1279
2238
  : phase === 'add'
1280
2239
  ? '正在验证飞书任务绑定'
1281
2240
  : '正在启动飞书任务 Bridge';
1282
- console.log(`[aamp-one-click] ${phaseMessage}:${bindingLabel(binding)}...`);
2241
+ console.log(`[aamp-one-click] ${phaseMessage}:${bindingLabel(binding, runtimeAgentType)}...`);
1283
2242
  return startManagedProcess({
1284
- label: `Feishu Bridge ${bindingLabel(binding)}`,
2243
+ label: `Feishu Bridge ${bindingLabel(binding, runtimeAgentType)}`,
1285
2244
  packageSpec: FEISHU_PACKAGE,
1286
2245
  executable: 'aamp-feishu-bridge',
1287
2246
  args: feishuArgs(binding, larkCliBin, target),
1288
2247
  env: environment,
1289
2248
  logFile,
2249
+ preparedExecutable,
2250
+ executionLocation: resolveTaskAgentMetadata(binding.agent_type).executionLocation,
2251
+ eventPathRoot: binding.feishu_config_dir,
2252
+ agentExecutionLocations: [[
2253
+ binding.agent_type,
2254
+ resolveTaskAgentMetadata(binding.agent_type).executionLocation,
2255
+ ]],
2256
+ allowedAppIds: [binding.bot.app_id],
1290
2257
  });
1291
2258
  }
1292
2259
 
@@ -1366,9 +2333,11 @@ async function waitForInitialBinding(record, pairingFile) {
1366
2333
  async function readInitialRuntimeMetadata(binding, record, expectedAgentEmail) {
1367
2334
  const starting = record.events.find((event) => event.type === 'bridge.task_runtime.starting' && event.appId === binding.bot.app_id);
1368
2335
  if (!starting?.imConfigDir || !starting?.taskConfigDir) throw new Error('Feishu Bridge 未返回实例配置目录');
1369
- const imFile = path.join(starting.imConfigDir, 'config.json');
1370
- const taskFile = path.join(starting.taskConfigDir, 'config.json');
1371
- if (!isPathInside(binding.feishu_config_dir, starting.imConfigDir) || !isPathInside(binding.feishu_config_dir, starting.taskConfigDir)) {
2336
+ const imConfigDir = resolveRemoteEventPath(starting.imConfigDir, record, binding.feishu_config_dir);
2337
+ const taskConfigDir = resolveRemoteEventPath(starting.taskConfigDir, record, binding.feishu_config_dir);
2338
+ const imFile = path.join(imConfigDir, 'config.json');
2339
+ const taskFile = path.join(taskConfigDir, 'config.json');
2340
+ if (!isPathInside(binding.feishu_config_dir, imConfigDir) || !isPathInside(binding.feishu_config_dir, taskConfigDir)) {
1372
2341
  throw new Error('Feishu Bridge 返回了新流程 runtime-v1 之外的配置目录');
1373
2342
  }
1374
2343
  await assertNoSymlinkPath(RUNTIME_HOME, imFile);
@@ -1381,8 +2350,8 @@ async function readInitialRuntimeMetadata(binding, record, expectedAgentEmail) {
1381
2350
  throw new Error('Feishu Bridge 实例的 Bot App ID 与本次配对不一致');
1382
2351
  }
1383
2352
  return {
1384
- im_config_dir: starting.imConfigDir,
1385
- task_config_dir: starting.taskConfigDir,
2353
+ im_config_dir: imConfigDir,
2354
+ task_config_dir: taskConfigDir,
1386
2355
  feishu_bridge_email: imConfig.mailbox?.email || '',
1387
2356
  };
1388
2357
  }
@@ -1417,130 +2386,601 @@ async function validateSavedRuntime(binding) {
1417
2386
  }
1418
2387
  }
1419
2388
 
1420
- async function bindOneDraft(draft, groups, mode) {
1421
- await setBindingStatus(draft, 'bind', 'starting');
1422
- throwIfStopping();
1423
- const { group, email } = resolveGroup(groups, draft);
1424
- const binding = { ...draft, state: 'ready', agent_target_email: email, updated_at: nowIso() };
1425
- const preparedFeishu = await prepareFeishuProcess(binding, mode);
1426
- throwIfStopping();
1427
- const pairResult = await runCapture(
1428
- ACP_PACKAGE,
1429
- 'aamp-acp-bridge',
1430
- ['pair', '--agent', draft.agent_type, '--config', group.configFile, '--json', '--no-start'],
1431
- { logFile: group.logFile },
2389
+ const bindingStartOperations = Object.freeze({
2390
+ // User-visible success output is deferred until layered results are back in selection order.
2391
+ printBindingStarted: () => {},
2392
+ readInitialRuntimeMetadata,
2393
+ runCapture,
2394
+ parseJsonDocument,
2395
+ resolveConfiguredPendingPairingFile,
2396
+ resolvePendingPairingFile,
2397
+ setBindingStatus,
2398
+ startPreparedFeishuUntilReady,
2399
+ stopManagedProcess,
2400
+ throwIfStopping,
2401
+ updateBinding,
2402
+ validateSavedRuntime,
2403
+ });
2404
+
2405
+ const bindingPreparationOperations = Object.freeze({
2406
+ nowIso,
2407
+ prepareFeishuProcess,
2408
+ resolveGroup,
2409
+ resolveInitializedGroup,
2410
+ setBindingStatus,
2411
+ throwIfStopping,
2412
+ validateSavedRuntime,
2413
+ });
2414
+
2415
+ async function prepareBindingStart(
2416
+ binding,
2417
+ groups,
2418
+ mode,
2419
+ operations = bindingPreparationOperations,
2420
+ options = {},
2421
+ ) {
2422
+ operations.throwIfStopping();
2423
+ const pending = bindingNeedsInitialStart(binding);
2424
+ await operations.setBindingStatus(binding, pending ? 'bind' : 'start', 'starting');
2425
+ operations.throwIfStopping();
2426
+ const resolve = options.allowAgentStarting && !pending
2427
+ ? operations.resolveInitializedGroup
2428
+ : operations.resolveGroup;
2429
+ const { group, email, runtimeAgentType } = resolve(groups, binding);
2430
+ const activeBinding = pending
2431
+ ? { ...binding, state: 'ready', agent_target_email: email, updated_at: operations.nowIso() }
2432
+ : binding;
2433
+ if (!pending) {
2434
+ if (email !== binding.agent_target_email) {
2435
+ throw new Error('当前 Agent mailbox 与绑定记录不一致,请重新绑定');
2436
+ }
2437
+ await operations.validateSavedRuntime(binding);
2438
+ }
2439
+ operations.throwIfStopping();
2440
+ const preparedFeishu = await operations.prepareFeishuProcess(
2441
+ activeBinding,
2442
+ mode,
2443
+ runtimeAgentType,
2444
+ options.profileProbes,
1432
2445
  );
1433
- throwIfStopping();
1434
- const pairing = parseJsonDocument(pairResult.stdout, 'ACP pairing');
1435
- if (!pairing.connectUrl || !pairing.pairingFile || pairing.mailbox !== email) {
1436
- throw new Error('ACP Bridge 返回的配对信息不完整或 mailbox 不一致');
2446
+ return {
2447
+ originalBinding: binding,
2448
+ binding: activeBinding,
2449
+ group,
2450
+ mode,
2451
+ pending,
2452
+ email,
2453
+ runtimeAgentType,
2454
+ preparedFeishu,
2455
+ deferReadyCommit: Boolean(options.deferReadyCommit && !pending),
2456
+ };
2457
+ }
2458
+
2459
+ async function executePreparedReadyBindingStart(prepared, operations = bindingStartOperations) {
2460
+ const { binding, group, runtimeAgentType, preparedFeishu } = prepared;
2461
+ let feishu;
2462
+ try {
2463
+ feishu = await operations.startPreparedFeishuUntilReady(
2464
+ preparedFeishu,
2465
+ { agentTargetEmail: binding.agent_target_email },
2466
+ { stage: 'feishu-start' },
2467
+ );
2468
+ operations.throwIfStopping();
2469
+ if (!prepared.deferReadyCommit) {
2470
+ await operations.setBindingStatus(binding, 'start', 'running');
2471
+ operations.throwIfStopping();
2472
+ operations.printBindingStarted(binding, runtimeAgentType);
2473
+ }
2474
+ return { binding, process: feishu, group, runtimeAgentType };
2475
+ } catch (error) {
2476
+ if (feishu) await operations.stopManagedProcess(feishu);
2477
+ throw error;
1437
2478
  }
2479
+ }
2480
+
2481
+ async function executePreparedPendingBindingStart(prepared, operations = bindingStartOperations) {
2482
+ const {
2483
+ originalBinding,
2484
+ binding,
2485
+ group,
2486
+ mode,
2487
+ email,
2488
+ runtimeAgentType,
2489
+ preparedFeishu,
2490
+ } = prepared;
1438
2491
  let feishu;
1439
- let keepRunning = false;
1440
2492
  try {
1441
- feishu = await startPreparedFeishuUntilReady(
2493
+ const configuredPairingFile = await operations.resolveConfiguredPendingPairingFile(
2494
+ group,
2495
+ originalBinding.agent_type,
2496
+ );
2497
+ operations.throwIfStopping();
2498
+ const pairResult = await operations.runCapture(
2499
+ ACP_PACKAGE,
2500
+ 'aamp-acp-bridge',
2501
+ ['pair', '--agent', originalBinding.agent_type, '--config', group.configFile, '--json', '--no-start'],
2502
+ {
2503
+ logFile: group.logFile,
2504
+ executionLocation: resolveTaskAgentMetadata(originalBinding.agent_type).executionLocation,
2505
+ },
2506
+ );
2507
+ operations.throwIfStopping();
2508
+ const pairing = operations.parseJsonDocument(pairResult.stdout, 'ACP pairing');
2509
+ if (!pairing.connectUrl || pairing.mailbox !== email) {
2510
+ throw new Error('ACP Bridge 返回的配对信息不完整或 mailbox 不一致');
2511
+ }
2512
+ const pairingFile = await operations.resolvePendingPairingFile(
2513
+ group,
2514
+ originalBinding.agent_type,
2515
+ pairing,
2516
+ );
2517
+ if (pairingFile !== configuredPairingFile) {
2518
+ throw new Error('ACP Bridge 私有配对文件在配对过程中发生变化');
2519
+ }
2520
+ feishu = await operations.startPreparedFeishuUntilReady(
1442
2521
  preparedFeishu,
1443
2522
  { pairingUrl: pairing.connectUrl },
1444
- { stage: mode === 'install' ? 'feishu-install-bind' : 'feishu-add-bind', pairingFile: pairing.pairingFile },
2523
+ { stage: mode === 'install' ? 'feishu-install-bind' : 'feishu-add-bind', pairingFile },
1445
2524
  );
1446
- throwIfStopping();
1447
- binding.runtime = await readInitialRuntimeMetadata(binding, feishu, email);
1448
- throwIfStopping();
1449
- await validateSavedRuntime(binding);
1450
- throwIfStopping();
1451
- const startsBridge = mode === 'install' || mode === 'start';
1452
- await setBindingStatus(binding, startsBridge ? 'start' : 'bind', startsBridge ? 'running' : 'succeeded');
1453
- throwIfStopping();
1454
- keepRunning = startsBridge;
1455
- return { binding, process: keepRunning ? feishu : undefined, group };
1456
- } finally {
1457
- if (!keepRunning) await stopManagedProcess(feishu);
2525
+ operations.throwIfStopping();
2526
+ binding.runtime = await operations.readInitialRuntimeMetadata(binding, feishu, email);
2527
+ operations.throwIfStopping();
2528
+ await operations.validateSavedRuntime(binding);
2529
+ operations.throwIfStopping();
2530
+ await operations.updateBinding(binding);
2531
+ operations.throwIfStopping();
2532
+ await operations.setBindingStatus(binding, 'start', 'running');
2533
+ operations.throwIfStopping();
2534
+ return { binding, process: feishu, group, runtimeAgentType };
2535
+ } catch (error) {
2536
+ if (feishu) await operations.stopManagedProcess(feishu);
2537
+ throw error;
1458
2538
  }
1459
2539
  }
1460
2540
 
1461
- async function startOneBinding(binding, groups) {
1462
- await setBindingStatus(binding, 'start', 'starting');
1463
- throwIfStopping();
1464
- const { email } = resolveGroup(groups, binding);
1465
- if (email !== binding.agent_target_email) throw new Error('当前 Agent mailbox 与绑定记录不一致,请重新绑定');
1466
- await validateSavedRuntime(binding);
1467
- throwIfStopping();
1468
- const preparedFeishu = await prepareFeishuProcess(binding, 'start');
1469
- const feishu = await startPreparedFeishuUntilReady(
1470
- preparedFeishu,
1471
- { agentTargetEmail: binding.agent_target_email },
1472
- { stage: 'feishu-start' },
1473
- );
1474
- throwIfStopping();
1475
- await setBindingStatus(binding, 'start', 'running');
1476
- throwIfStopping();
1477
- printBindingStarted(binding);
1478
- return feishu;
2541
+ async function executePreparedBindingStart(prepared, operations = bindingStartOperations) {
2542
+ if (!prepared.pending) return executePreparedReadyBindingStart(prepared, operations);
2543
+ return runPairingSerially(pairingQueueKey(prepared), async () => {
2544
+ return executePreparedPendingBindingStart(prepared, operations);
2545
+ });
1479
2546
  }
1480
2547
 
1481
- async function startSelectedBindings(bindings, existingGroups) {
2548
+ async function runPreparedBindingStarts(preparedItems, start = executePreparedBindingStart) {
2549
+ const lanes = [];
2550
+ const pendingLanes = new Map();
2551
+ for (const item of preparedItems) {
2552
+ if (!item.prepared.pending) {
2553
+ lanes.push([item]);
2554
+ continue;
2555
+ }
2556
+ const key = pairingQueueKey(item.prepared);
2557
+ let lane = pendingLanes.get(key);
2558
+ if (!lane) {
2559
+ lane = [];
2560
+ pendingLanes.set(key, lane);
2561
+ lanes.push(lane);
2562
+ }
2563
+ lane.push(item);
2564
+ }
2565
+
2566
+ const laneOutcomes = await runLayeredStarts(lanes, {
2567
+ concurrency: FEISHU_START_CONCURRENCY,
2568
+ prepare: async (lane) => lane,
2569
+ start: async (lane) => {
2570
+ const outcomes = [];
2571
+ for (const item of lane) {
2572
+ try {
2573
+ outcomes.push({
2574
+ status: 'fulfilled',
2575
+ phase: 'start',
2576
+ value: await start(item.prepared),
2577
+ item: item.prepared.originalBinding || item.prepared.binding,
2578
+ index: item.index,
2579
+ });
2580
+ } catch (reason) {
2581
+ if (stopRequested) throw reason;
2582
+ outcomes.push({
2583
+ status: 'rejected',
2584
+ phase: 'start',
2585
+ reason,
2586
+ item: item.prepared.originalBinding || item.prepared.binding,
2587
+ index: item.index,
2588
+ });
2589
+ }
2590
+ }
2591
+ return outcomes;
2592
+ },
2593
+ });
2594
+ const rejectedLane = laneOutcomes.find((outcome) => outcome.status === 'rejected');
2595
+ if (rejectedLane && stopRequested) throw rejectedLane.reason;
2596
+ const byIndex = new Map(laneOutcomes.flatMap((outcome) => (
2597
+ outcome.status === 'fulfilled' ? outcome.value : []
2598
+ )).map((outcome) => [outcome.index, outcome]));
2599
+ return preparedItems.map(({ index }) => byIndex.get(index));
2600
+ }
2601
+
2602
+ function reportBindingFailure(binding, runtimeAgentType, reason, mode) {
2603
+ const safeReason = safeBindingFailureReason(binding, reason);
2604
+ console.error(`🔴 启动失败:${bindingLabel(binding, runtimeAgentType)}\n 原因:${safeReason}`);
2605
+ if (mode === 'install') {
2606
+ console.error(' 绑定配置已保存,可稍后运行 feishu-task-agent start 重试。');
2607
+ } else {
2608
+ console.error(' 已跳过该项,继续启动下一项。');
2609
+ }
2610
+ }
2611
+
2612
+ const bindingLauncherOperations = Object.freeze({
2613
+ bindingCancellationReason,
2614
+ executePreparedBindingStart,
2615
+ prepareBindingStart: (binding, groups, mode, options) => (
2616
+ prepareBindingStart(binding, groups, mode, bindingPreparationOperations, options)
2617
+ ),
2618
+ printBindingCancelled,
2619
+ printBindingStarted,
2620
+ recordError,
2621
+ reportBindingFailure,
2622
+ setBindingStatus,
2623
+ throwIfStopping,
2624
+ });
2625
+
2626
+ async function finalizeDeferredLaunchResults(launched, mode, operations = bindingLauncherOperations) {
2627
+ for (const item of launched.cancelled) {
2628
+ await operations.setBindingStatus(
2629
+ item.binding,
2630
+ mode === 'install' ? 'bind' : 'start',
2631
+ 'cancelled',
2632
+ item.reason,
2633
+ );
2634
+ operations.printBindingCancelled(item.binding, item.reason);
2635
+ }
2636
+ for (const item of launched.failed) {
2637
+ await operations.setBindingStatus(item.binding, 'start', 'failed', item.reason);
2638
+ await operations.recordError('startup', item.reason, item.binding);
2639
+ operations.reportBindingFailure(
2640
+ item.binding,
2641
+ item.runtimeAgentType,
2642
+ item.reason,
2643
+ mode,
2644
+ );
2645
+ }
2646
+ }
2647
+
2648
+ async function startBindingsWithGroups(
2649
+ bindings,
2650
+ groups,
2651
+ mode,
2652
+ operations = bindingLauncherOperations,
2653
+ options = {},
2654
+ ) {
2655
+ const cancelled = [];
2656
+ const candidates = [];
2657
+ for (const binding of bindings) {
2658
+ operations.throwIfStopping();
2659
+ const reason = operations.bindingCancellationReason(groups, binding);
2660
+ if (reason) {
2661
+ const runtimeAgentType = groups.get(binding.aamp_host)?.runtimeAgentTypes?.get(binding.agent_type)
2662
+ || binding.agent_type;
2663
+ cancelled.push({ binding, reason, runtimeAgentType });
2664
+ } else {
2665
+ candidates.push(binding);
2666
+ }
2667
+ }
2668
+
2669
+ const outcomes = new Array(candidates.length);
2670
+ const preparedItems = [];
2671
+ for (let index = 0; index < candidates.length; index += 1) {
2672
+ const binding = candidates[index];
2673
+ try {
2674
+ preparedItems.push({
2675
+ index,
2676
+ prepared: await operations.prepareBindingStart(binding, groups, mode, options),
2677
+ });
2678
+ } catch (reason) {
2679
+ if (stopRequested) throw reason;
2680
+ outcomes[index] = { status: 'rejected', phase: 'prepare', reason, item: binding, index };
2681
+ }
2682
+ }
2683
+ const started = await runPreparedBindingStarts(preparedItems, (prepared) => {
2684
+ operations.throwIfStopping();
2685
+ return operations.executePreparedBindingStart(prepared);
2686
+ });
2687
+ started.forEach((outcome, preparedIndex) => {
2688
+ outcomes[preparedItems[preparedIndex].index] = outcome;
2689
+ });
2690
+ operations.throwIfStopping();
2691
+
1482
2692
  const running = [];
1483
2693
  const failed = [];
2694
+ for (const outcome of outcomes) {
2695
+ if (outcome.status === 'fulfilled') {
2696
+ running.push(outcome.value);
2697
+ if (mode === 'start' && !options.deferReadyCommit) {
2698
+ operations.printBindingStarted(outcome.value.binding, outcome.value.runtimeAgentType);
2699
+ }
2700
+ continue;
2701
+ }
2702
+ const binding = outcome.item;
2703
+ const reason = safeBindingFailureReason(binding, outcome.reason?.message || outcome.reason);
2704
+ const runtimeAgentType = groups.get(binding.aamp_host)?.runtimeAgentTypes?.get(binding.agent_type)
2705
+ || binding.agent_type;
2706
+ failed.push({ binding, reason, runtimeAgentType });
2707
+ }
2708
+ const launched = {
2709
+ running: orderStartupItems(bindings, running),
2710
+ failed: orderStartupItems(bindings, failed),
2711
+ cancelled: orderStartupItems(bindings, cancelled),
2712
+ };
2713
+ if (!options.deferReadyCommit) {
2714
+ await finalizeDeferredLaunchResults(launched, mode, operations);
2715
+ }
2716
+ return launched;
2717
+ }
2718
+
2719
+ async function stopRunningBindings(running) {
2720
+ for (const item of [...running].reverse()) {
2721
+ if (item.process && !item.process.exited) await stopManagedProcess(item.process);
2722
+ }
2723
+ }
2724
+
2725
+ const overlappedReadyReconcileOperations = Object.freeze({
2726
+ printBindingCancelled,
2727
+ printBindingStarted,
2728
+ recordError,
2729
+ reportBindingFailure,
2730
+ setBindingStatus,
2731
+ stopManagedProcess,
2732
+ throwIfStopping,
2733
+ });
2734
+
2735
+ async function reconcileOverlappedReadyBindings(
2736
+ bindings,
2737
+ launched,
2738
+ groups,
2739
+ operations = overlappedReadyReconcileOperations,
2740
+ ) {
2741
+ const alive = [];
2742
+ const failed = [];
2743
+ const cancelled = [];
2744
+ const outcomes = orderStartupItems(bindings, [
2745
+ ...launched.running.map((item) => ({ ...item, launchStatus: 'running' })),
2746
+ ...launched.failed.map((item) => ({ ...item, launchStatus: 'failed' })),
2747
+ ...launched.cancelled.map((item) => ({ ...item, launchStatus: 'cancelled' })),
2748
+ ]);
2749
+ for (const item of outcomes) {
2750
+ operations.throwIfStopping();
2751
+ const { binding, process: feishu } = item;
2752
+ const group = item.group || groups.get(binding.aamp_host);
2753
+ const runtimeAgentType = item.runtimeAgentType
2754
+ || group?.runtimeAgentTypes?.get(binding.agent_type)
2755
+ || binding.agent_type;
2756
+ const cancellation = group?.cancellations?.get(binding.agent_type) || '';
2757
+ if (cancellation) {
2758
+ if (feishu && !feishu.exited) await operations.stopManagedProcess(feishu);
2759
+ await operations.setBindingStatus(binding, 'start', 'cancelled', cancellation);
2760
+ operations.printBindingCancelled(binding, cancellation);
2761
+ cancelled.push({ binding, reason: cancellation, runtimeAgentType });
2762
+ continue;
2763
+ }
2764
+
2765
+ let reason = group?.failures?.get(binding.agent_type) || '';
2766
+ if (!reason && (!group?.process || group.process.exited)) {
2767
+ reason = `${bindingLabel(binding, runtimeAgentType)} 的 Agent Bridge 在启动期间已退出:${group?.host || binding.aamp_host}`;
2768
+ }
2769
+ if (!reason && !group.availableAgents?.has(binding.agent_type)) {
2770
+ reason = `${binding.agent_type} Agent Bridge 未启动`;
2771
+ }
2772
+ if (!reason && item.launchStatus === 'failed') {
2773
+ reason = item.reason;
2774
+ }
2775
+ if (!reason && item.launchStatus === 'running' && (!feishu || feishu.exited)) {
2776
+ reason = `${bindingLabel(binding, runtimeAgentType)} 的 Feishu Bridge 在进入监督前已退出 (${feishu?.exit?.signal || feishu?.exit?.code || 'unknown'})`;
2777
+ }
2778
+ if (reason) {
2779
+ const redactedReason = safeBindingFailureReason(binding, reason);
2780
+ if (feishu && !feishu.exited) await operations.stopManagedProcess(feishu);
2781
+ await operations.setBindingStatus(binding, 'start', 'failed', redactedReason);
2782
+ await operations.recordError('startup', redactedReason, binding);
2783
+ operations.reportBindingFailure(binding, runtimeAgentType, redactedReason, 'start');
2784
+ failed.push({ binding, reason: redactedReason, runtimeAgentType });
2785
+ continue;
2786
+ }
2787
+ if (item.launchStatus === 'cancelled') {
2788
+ const launchCancellation = redact(item.reason);
2789
+ await operations.setBindingStatus(binding, 'start', 'cancelled', launchCancellation);
2790
+ operations.printBindingCancelled(binding, launchCancellation);
2791
+ cancelled.push({ binding, reason: launchCancellation, runtimeAgentType });
2792
+ continue;
2793
+ }
2794
+
2795
+ await operations.setBindingStatus(binding, 'start', 'running');
2796
+ operations.throwIfStopping();
2797
+ operations.printBindingStarted(binding, runtimeAgentType);
2798
+ alive.push(item);
2799
+ }
2800
+ return { alive, failed, cancelled };
2801
+ }
2802
+
2803
+ async function runOverlappedStartup(bindings, groups, operations, profileProbes) {
2804
+ const readyBindings = bindings.filter((binding) => !bindingNeedsInitialStart(binding));
2805
+ const pendingBindings = bindings.filter((binding) => bindingNeedsInitialStart(binding));
2806
+ const emptyLaunch = { running: [], failed: [], cancelled: [] };
2807
+ const agentBranch = Promise.resolve().then(() => operations.startAgentGroups(groups));
2808
+ const readyBranch = readyBindings.length
2809
+ ? Promise.resolve().then(() => operations.startBindingsWithGroups(
2810
+ readyBindings,
2811
+ groups,
2812
+ 'start',
2813
+ { allowAgentStarting: true, deferReadyCommit: true, profileProbes },
2814
+ ))
2815
+ : Promise.resolve(emptyLaunch);
2816
+
2817
+ const [agentOutcome, readyOutcome] = await Promise.allSettled([agentBranch, readyBranch]);
2818
+ const earlyRunning = readyOutcome.status === 'fulfilled' ? readyOutcome.value.running : [];
2819
+ if (agentOutcome.status === 'rejected' || readyOutcome.status === 'rejected') {
2820
+ await operations.stopRunningBindings(earlyRunning);
2821
+ operations.throwIfStopping();
2822
+ throw (agentOutcome.status === 'rejected' ? agentOutcome.reason : readyOutcome.reason);
2823
+ }
2824
+ operations.throwIfStopping();
2825
+
2826
+ const reconciledReady = await operations.reconcileOverlappedReadyBindings(
2827
+ readyBindings,
2828
+ readyOutcome.value,
2829
+ groups,
2830
+ );
2831
+ operations.throwIfStopping();
2832
+ const pendingLaunch = pendingBindings.length
2833
+ ? await operations.startBindingsWithGroups(pendingBindings, groups, 'start')
2834
+ : emptyLaunch;
2835
+ operations.throwIfStopping();
2836
+
2837
+ return {
2838
+ running: orderStartupItems(bindings, [
2839
+ ...reconciledReady.alive,
2840
+ ...pendingLaunch.running,
2841
+ ]),
2842
+ failed: orderStartupItems(bindings, [
2843
+ ...reconciledReady.failed,
2844
+ ...pendingLaunch.failed,
2845
+ ]),
2846
+ cancelled: orderStartupItems(bindings, [
2847
+ ...reconciledReady.cancelled,
2848
+ ...pendingLaunch.cancelled,
2849
+ ]),
2850
+ };
2851
+ }
2852
+
2853
+ function startupDisposition({ running = [], failed = [], cancelled = [] }, extraFailureCount = 0) {
2854
+ if (running.length) return 'supervise';
2855
+ if (cancelled.length && !failed.length && extraFailureCount === 0) return 'only-cancel';
2856
+ return 'all-failed';
2857
+ }
2858
+
2859
+ async function reconcileStartupResults(
2860
+ bindings,
2861
+ launched,
2862
+ validationFailures = [],
2863
+ reconcile = reconcileRetainedBindings,
2864
+ ) {
2865
+ const reconciled = await reconcile(launched.running);
2866
+ const result = {
2867
+ running: orderStartupItems(bindings, reconciled.alive),
2868
+ failed: orderStartupItems(bindings, [
2869
+ ...validationFailures,
2870
+ ...launched.failed,
2871
+ ...reconciled.failed,
2872
+ ]),
2873
+ cancelled: orderStartupItems(bindings, launched.cancelled),
2874
+ };
2875
+ return { ...result, disposition: startupDisposition(result) };
2876
+ }
2877
+
2878
+ async function recordOnlineValidationFailure(binding, error) {
2879
+ const reason = safeBindingFailureReason(binding, error.message || error);
2880
+ await setBindingStatus(binding, 'start', 'failed', reason);
2881
+ await recordError('startup', reason, binding);
2882
+ console.error(`\n🔴 启动失败:${bindingLabel(binding)}\n 原因:${reason}`);
2883
+ console.error(' 已跳过该项,继续启动下一项。');
2884
+ return { binding, reason };
2885
+ }
2886
+
2887
+ function startBindingsForOverlap(bindings, groups, mode, options) {
2888
+ return startBindingsWithGroups(bindings, groups, mode, bindingLauncherOperations, options);
2889
+ }
2890
+
2891
+ const startupOrchestrationOperations = Object.freeze({
2892
+ finalizeDeferredLaunchResults: (launched) => finalizeDeferredLaunchResults(launched, 'start'),
2893
+ initializeAgentGroups,
2894
+ prewarmFeishuExecutable,
2895
+ probeReadyBindingProfiles,
2896
+ reconcileRetainedBindings,
2897
+ reconcileOverlappedReadyBindings,
2898
+ recordValidationFailure: recordOnlineValidationFailure,
2899
+ startAgentGroups,
2900
+ startBindingsWithGroups: startBindingsForOverlap,
2901
+ stopRunningBindings,
2902
+ throwIfStopping,
2903
+ validateBinding: assertOnlineBinding,
2904
+ });
2905
+
2906
+ async function orchestrateStartupBindings(
2907
+ bindings,
2908
+ existingGroups,
2909
+ operations = startupOrchestrationOperations,
2910
+ ) {
2911
+ const validationFailures = [];
1484
2912
  const onlineBindings = [];
1485
2913
  for (const binding of bindings) {
1486
2914
  try {
1487
- assertOnlineBinding(binding);
2915
+ operations.validateBinding(binding);
1488
2916
  onlineBindings.push(binding);
1489
2917
  } catch (error) {
1490
- const reason = redact(error.message || error);
1491
- failed.push({ binding, reason });
1492
- await setBindingStatus(binding, 'start', 'failed', reason);
1493
- await recordError('startup', reason, binding);
1494
- console.error(`\n🔴 启动失败:${bindingLabel(binding)}\n 原因:${reason}`);
1495
- console.error(' 已跳过该项,继续启动下一项。');
2918
+ validationFailures.push(await operations.recordValidationFailure(binding, error));
1496
2919
  }
1497
2920
  }
1498
- const groups = existingGroups || await setupAgentGroups(onlineBindings);
1499
- for (const binding of onlineBindings) {
1500
- throwIfStopping();
2921
+ if (onlineBindings.length && operations.prewarmFeishuExecutable) {
1501
2922
  try {
1502
- let activeBinding = binding;
1503
- let group = groups.get(binding.aamp_host);
1504
- let processRecord;
1505
- if (bindingNeedsInitialStart(binding)) {
1506
- const paired = await bindOneDraft(binding, groups, 'start');
1507
- if (!paired.process) throw new Error('首次启动完成配对后未获得可监督的 Feishu Bridge 进程');
1508
- try {
1509
- await updateBinding(paired.binding);
1510
- } catch (error) {
1511
- await stopManagedProcess(paired.process);
1512
- throw error;
1513
- }
1514
- activeBinding = paired.binding;
1515
- group = paired.group;
1516
- processRecord = paired.process;
1517
- printBindingStarted(activeBinding);
1518
- } else {
1519
- processRecord = await startOneBinding(binding, groups);
1520
- }
1521
- throwIfStopping();
1522
- running.push({ binding: activeBinding, process: processRecord, group });
1523
- } catch (error) {
1524
- if (stopRequested) throw error;
1525
- const reason = redact(error.message || error);
1526
- failed.push({ binding, reason });
1527
- await setBindingStatus(binding, 'start', 'failed', reason);
1528
- await recordError('startup', reason, binding);
1529
- console.error(`\n🔴 启动失败:${bindingLabel(binding)}\n 原因:${reason}`);
1530
- console.error(' 已跳过该项,继续启动下一项。');
2923
+ void Promise.resolve(operations.prewarmFeishuExecutable(onlineBindings[0])).catch(() => {});
2924
+ } catch {
2925
+ // This is speculative only. Binding preparation performs the authoritative resolve.
1531
2926
  }
1532
2927
  }
1533
- const reconciled = await reconcileRetainedBindings(running);
1534
- running.splice(0, running.length, ...reconciled.alive);
1535
- failed.push(...reconciled.failed);
1536
- if (!running.length) {
1537
- await shutdownGroups(groups);
1538
- throw new Error('全部配置启动失败');
2928
+ let groups = existingGroups;
2929
+ let profileProbes;
2930
+ if (!groups) {
2931
+ const initialization = Promise.resolve().then(() => operations.initializeAgentGroups(onlineBindings));
2932
+ const probing = operations.probeReadyBindingProfiles
2933
+ ? Promise.resolve().then(() => operations.probeReadyBindingProfiles(onlineBindings))
2934
+ : Promise.resolve(new Map());
2935
+ const [initializationOutcome, probingOutcome] = await Promise.allSettled([
2936
+ initialization,
2937
+ probing,
2938
+ ]);
2939
+ operations.throwIfStopping();
2940
+ if (initializationOutcome.status === 'rejected') throw initializationOutcome.reason;
2941
+ groups = initializationOutcome.value;
2942
+ profileProbes = probingOutcome.status === 'fulfilled' ? probingOutcome.value : new Map();
1539
2943
  }
1540
- console.log(`\n已成功启动 ${running.length}/${bindings.length} 个配置。`);
1541
- console.log('🟢 保持终端打开,你可以给 agent 派发飞书任务');
1542
- if (failed.length) console.log(`另有 ${failed.length} 个配置启动失败,详情见上方信息和本地日志。`);
1543
- await supervise(running, groups);
2944
+ const launched = existingGroups
2945
+ ? await operations.startBindingsWithGroups(onlineBindings, groups, 'start')
2946
+ : await runOverlappedStartup(onlineBindings, groups, operations, profileProbes);
2947
+ const result = await reconcileStartupResults(
2948
+ bindings,
2949
+ launched,
2950
+ validationFailures,
2951
+ operations.reconcileRetainedBindings,
2952
+ );
2953
+ return { groups, ...result };
2954
+ }
2955
+
2956
+ async function dispatchStartupResult(result, operations) {
2957
+ const disposition = startupDisposition(result, operations.extraFailureCount || 0);
2958
+ if (disposition === 'supervise') {
2959
+ return operations.supervise(result.running, result.groups);
2960
+ }
2961
+ await operations.shutdown(result.groups);
2962
+ if (disposition === 'only-cancel') return operations.onlyCancelled();
2963
+ return operations.allFailed();
2964
+ }
2965
+
2966
+ async function startSelectedBindings(bindings, existingGroups) {
2967
+ const result = await orchestrateStartupBindings(bindings, existingGroups);
2968
+ printStartupSummary({
2969
+ title: '已成功启动',
2970
+ plannedCount: bindings.length,
2971
+ running: result.running,
2972
+ failed: result.failed,
2973
+ cancelled: result.cancelled,
2974
+ });
2975
+ await dispatchStartupResult(result, {
2976
+ supervise: async (running, groups) => {
2977
+ console.log('🟢 保持终端打开,你可以给 agent 派发飞书任务');
2978
+ await supervise(running, groups);
2979
+ },
2980
+ shutdown: shutdownGroups,
2981
+ onlyCancelled: async () => {},
2982
+ allFailed: async () => { throw new Error('全部配置启动失败'); },
2983
+ });
1544
2984
  }
1545
2985
 
1546
2986
  async function markRuntimeFailed(binding, reason, component) {
@@ -1560,12 +3000,12 @@ async function reconcileRetainedBindings(running) {
1560
3000
  continue;
1561
3001
  }
1562
3002
  const reason = !feishuAlive
1563
- ? `${bindingLabel(item.binding)} 的 Feishu Bridge 在进入监督前已退出 (${item.process?.exit?.signal || item.process?.exit?.code || 'unknown'})`
1564
- : `${bindingLabel(item.binding)} 的 Agent Bridge 在进入监督前已退出:${item.group?.host || item.binding.aamp_host}`;
3003
+ ? `${bindingLabel(item.binding, item.runtimeAgentType)} 的 Feishu Bridge 在进入监督前已退出 (${item.process?.exit?.signal || item.process?.exit?.code || 'unknown'})`
3004
+ : `${bindingLabel(item.binding, item.runtimeAgentType)} 的 Agent Bridge 在进入监督前已退出:${item.group?.host || item.binding.aamp_host}`;
1565
3005
  if (feishuAlive) await stopManagedProcess(item.process);
1566
3006
  await markRuntimeFailed(item.binding, reason, 'startup');
1567
- failed.push({ binding: item.binding, reason });
1568
- console.error(`\n🔴 启动失败:${bindingLabel(item.binding)}\n 原因:${reason}`);
3007
+ failed.push({ binding: item.binding, reason, runtimeAgentType: item.runtimeAgentType });
3008
+ console.error(`\n🔴 启动失败:${bindingLabel(item.binding, item.runtimeAgentType)}\n 原因:${reason}`);
1569
3009
  }
1570
3010
  return { alive, failed };
1571
3011
  }
@@ -1609,23 +3049,27 @@ async function shutdownGroups(groups) {
1609
3049
  }
1610
3050
  }
1611
3051
 
1612
- async function cleanupAll() {
1613
- if (cleanupPromise) return cleanupPromise;
1614
- cleanupPromise = (async () => {
1615
- while (managedProcesses.size || transientProcesses.size || heldLeases.size) {
1616
- const records = [...managedProcesses].reverse();
1617
- for (const record of records) {
1618
- managedProcesses.delete(record);
1619
- await stopManagedProcess(record).catch(() => {});
1620
- }
1621
- for (const record of [...transientProcesses].reverse()) {
1622
- transientProcesses.delete(record);
1623
- await stopManagedProcess(record).catch(() => {});
1624
- }
1625
- for (const lease of [...heldLeases]) await releaseLease(lease).catch(() => {});
3052
+ const cleanupRuntimeResources = createResourceCleanup(async () => {
3053
+ while (managedProcesses.size || transientProcesses.size || heldLeases.size) {
3054
+ const records = [...managedProcesses].reverse();
3055
+ for (const record of records) {
3056
+ managedProcesses.delete(record);
3057
+ await stopManagedProcess(record).catch(() => {});
1626
3058
  }
1627
- })();
1628
- return cleanupPromise;
3059
+ for (const record of [...transientProcesses].reverse()) {
3060
+ transientProcesses.delete(record);
3061
+ await stopManagedProcess(record).catch(() => {});
3062
+ }
3063
+ for (const lease of [...heldLeases]) await releaseLease(lease).catch(() => {});
3064
+ }
3065
+ });
3066
+
3067
+ async function cleanupAll() {
3068
+ await cleanupRuntimeResources();
3069
+ await Promise.allSettled([
3070
+ (async () => await manifestWriter.flush())(),
3071
+ (async () => await errorLogWriter.flush())(),
3072
+ ]);
1629
3073
  }
1630
3074
 
1631
3075
  function printLogHints(detailed = false) {
@@ -1644,7 +3088,7 @@ function displayBindings(bindings) {
1644
3088
  }
1645
3089
  const rows = bindings.map((binding, index) => ({
1646
3090
  index: String(index + 1),
1647
- agent: binding.agent_type,
3091
+ agent: agentBindingDisplayName(binding.agent_type),
1648
3092
  bot: binding.bot.display_name || binding.bot.app_id,
1649
3093
  appId: binding.bot.app_id,
1650
3094
  environment: binding.environment.name,
@@ -1662,24 +3106,15 @@ function displayBindings(bindings) {
1662
3106
 
1663
3107
  async function discoverAgents() {
1664
3108
  const result = await runBootstrapHelper('__discover-agents', '');
1665
- const agents = (result.agents || []).filter((agent) => AGENT_TYPES.includes(agent));
1666
- if (!agents.length) throw new Error('暂未检测到本地智能体。请先安装并登录 Codex 或 Cursor CLI 后重试。');
3109
+ const agents = (result.agents || []).filter((agent) => TASK_AGENT_TYPES.includes(agent));
3110
+ if (!agents.length) {
3111
+ throw new Error('暂未检测到智能体。请先安装 Codex、Cursor、Trae CLI、TraeCode CLI、WorkBuddy 或 WorkBuddy AI,或连接公司内网使用 AIME。');
3112
+ }
1667
3113
  return agents;
1668
3114
  }
1669
3115
 
1670
- async function createDraft(agents, unavailableAppIds) {
1671
- const agent = DEFAULT_AGENT || await chooseOne('请选择要绑定的本地智能体:', agents, (item) => item);
1672
- const registered = await runBootstrapHelper('__register-binding', agent);
1673
- addSecret(registered.app_secret);
1674
- if (!registered.app_id || !registered.app_secret || !registered.lark_cli_profile) {
1675
- throw new Error('飞书应用授权结果不完整');
1676
- }
1677
- if (unavailableAppIds.has(registered.app_id)) {
1678
- throw new Error(`Bot ${registered.app_id} 已经选择过,不能重复绑定`);
1679
- }
1680
- unavailableAppIds.add(registered.app_id);
1681
- const bindingId = randomId();
1682
- const timestamp = nowIso();
3116
+ function buildPendingBinding(agent, registered, bindingId = randomId(), timestamp = nowIso()) {
3117
+ const metadata = resolveTaskAgentMetadata(agent);
1683
3118
  return {
1684
3119
  binding_id: bindingId,
1685
3120
  agent_type: agent,
@@ -1687,7 +3122,8 @@ async function createDraft(agents, unavailableAppIds) {
1687
3122
  app_id: registered.app_id,
1688
3123
  app_secret: registered.app_secret,
1689
3124
  display_name: registered.display_name || registered.app_id,
1690
- lark_cli_profile: registered.lark_cli_profile,
3125
+ tenant_brand: normalizeTenantBrand(registered.tenant_brand, 'registered.tenant_brand'),
3126
+ ...(metadata.executionLocation === 'local' ? { lark_cli_profile: registered.lark_cli_profile } : {}),
1691
3127
  },
1692
3128
  environment: { name: 'online' },
1693
3129
  state: 'pending',
@@ -1698,27 +3134,74 @@ async function createDraft(agents, unavailableAppIds) {
1698
3134
  };
1699
3135
  }
1700
3136
 
3137
+ async function createDraft(agents, selectedAppIds) {
3138
+ const agent = DEFAULT_AGENT || await chooseOne('请选择要绑定的智能体:', agents, agentSelectionDisplayName);
3139
+ const registered = await runBootstrapHelper('__register-binding', agent);
3140
+ addSecret(registered.app_secret);
3141
+ const metadata = resolveTaskAgentMetadata(agent);
3142
+ if (!registered.app_id || !registered.app_secret
3143
+ || (metadata.executionLocation === 'local' && !registered.lark_cli_profile)) {
3144
+ throw new Error('飞书应用授权结果不完整');
3145
+ }
3146
+ if (selectedAppIds.has(registered.app_id)) {
3147
+ throw new Error(`Bot ${registered.app_id} 已经选择过,不能重复绑定`);
3148
+ }
3149
+ selectedAppIds.add(registered.app_id);
3150
+ return buildPendingBinding(agent, registered);
3151
+ }
3152
+
1701
3153
  async function runBindingSession(mode) {
1702
3154
  const store = await loadStore();
1703
3155
  throwIfStopping();
1704
- const unavailableAppIds = new Set(mode === 'add' ? store.bindings.map((binding) => binding.bot.app_id) : []);
3156
+ const existingByAppId = new Map(store.bindings.map((binding) => [binding.bot.app_id, binding]));
3157
+ const selectedAppIds = new Set();
1705
3158
  const agents = DEFAULT_AGENT ? [DEFAULT_AGENT] : await discoverAgents();
1706
3159
  throwIfStopping();
1707
- const sessionDrafts = [];
3160
+ const bindingIntents = [];
3161
+ const acceptedBindings = [];
3162
+ const selectedBindings = [];
1708
3163
  const succeeded = [];
1709
3164
  const failed = [];
3165
+ const cancelled = [];
1710
3166
  const selectionFailures = [];
1711
3167
  const running = [];
3168
+ let selectedCount = 0;
1712
3169
 
1713
3170
  console.log('\n=== 选择绑定配置 ===');
1714
3171
  let keepGoing = true;
1715
3172
  while (keepGoing) {
1716
3173
  throwIfStopping();
1717
3174
  try {
1718
- const draft = await createDraft(agents, unavailableAppIds);
3175
+ const draft = await createDraft(agents, selectedAppIds);
1719
3176
  throwIfStopping();
1720
- sessionDrafts.push(draft);
1721
- console.log(`已选择:${bindingLabel(draft)}`);
3177
+ selectedCount += 1;
3178
+ const existing = existingByAppId.get(draft.bot.app_id);
3179
+ let accepted = true;
3180
+ let acceptedBinding = draft;
3181
+ if (existing && sameBindingRelationship(existing, draft)) {
3182
+ acceptedBinding = existing;
3183
+ } else if (existing) {
3184
+ console.log(`Bot ${draft.bot.app_id} 已存在绑定:${bindingLabel(existing)}`);
3185
+ console.log(`拟替换为:${bindingLabel(draft)}`);
3186
+ if (!await confirm('是否替换绑定?', false)) {
3187
+ accepted = false;
3188
+ const reason = '用户取消替换已有绑定';
3189
+ cancelled.push({ binding: draft, reason });
3190
+ await setBindingStatus(draft, 'bind', 'cancelled', reason);
3191
+ printBindingCancelled(draft, reason);
3192
+ }
3193
+ }
3194
+ selectedBindings.push(accepted ? acceptedBinding : draft);
3195
+ if (accepted) {
3196
+ acceptedBindings.push(acceptedBinding);
3197
+ if (acceptedBinding === draft) {
3198
+ bindingIntents.push({
3199
+ binding: draft,
3200
+ expected: existing ? bindingExpectation(existing) : undefined,
3201
+ });
3202
+ }
3203
+ console.log(`已选择:${bindingLabel(acceptedBinding)}`);
3204
+ }
1722
3205
  } catch (error) {
1723
3206
  if (stopRequested) throw error;
1724
3207
  const reason = redact(error.message || error);
@@ -1727,97 +3210,147 @@ async function runBindingSession(mode) {
1727
3210
  console.error(`🔴 本次选择未完成:${reason}`);
1728
3211
  }
1729
3212
  throwIfStopping();
1730
- keepGoing = await confirm('是否继续选择本地智能体和 Bot?', false);
3213
+ keepGoing = await confirm('是否继续选择智能体和 Bot?', false);
1731
3214
  throwIfStopping();
1732
3215
  }
1733
3216
 
1734
- if (!sessionDrafts.length) {
1735
- return { groups: new Map(), succeeded, failed, selectionFailures, running, selectedCount: 0 };
3217
+ if (!acceptedBindings.length) {
3218
+ return {
3219
+ groups: new Map(),
3220
+ saved: [],
3221
+ acceptedBindings,
3222
+ selectedBindings,
3223
+ succeeded,
3224
+ failed,
3225
+ cancelled,
3226
+ selectionFailures,
3227
+ running,
3228
+ selectedCount,
3229
+ replacedCount: 0,
3230
+ };
1736
3231
  }
1737
3232
 
1738
- if (mode === 'add') {
3233
+ let persisted = { bindings: [], replacedCount: 0 };
3234
+ if (bindingIntents.length) {
1739
3235
  console.log('\n=== 保存绑定配置 ===');
1740
- for (const draft of sessionDrafts) {
1741
- try {
1742
- await appendBinding(draft);
1743
- throwIfStopping();
1744
- succeeded.push(draft);
1745
- await setBindingStatus(draft, 'bind', 'saved');
1746
- console.log(`🟢 已保存:${bindingLabel(draft)}`);
1747
- } catch (error) {
1748
- if (stopRequested) throw error;
1749
- const reason = redact(error.message || error);
1750
- failed.push({ binding: draft, reason });
1751
- await setBindingStatus(draft, 'bind', 'failed', reason);
1752
- await recordError('binding', reason, draft);
1753
- console.error(`🔴 配置保存失败:${bindingLabel(draft)}\n 原因:${reason}`);
1754
- console.error(' 已跳过该项,继续处理下一项。');
1755
- }
3236
+ persisted = await upsertBindings(bindingIntents);
3237
+ for (const binding of persisted.bindings) {
3238
+ await setBindingStatus(binding, 'bind', 'saved');
3239
+ console.log(`已保存:${bindingLabel(binding)}`);
1756
3240
  }
1757
- return { groups: new Map(), succeeded, failed, selectionFailures, running, selectedCount: sessionDrafts.length };
3241
+ }
3242
+ const saved = persisted.bindings;
3243
+
3244
+ if (mode === 'add') {
3245
+ succeeded.push(...acceptedBindings);
3246
+ return {
3247
+ groups: new Map(),
3248
+ saved,
3249
+ acceptedBindings,
3250
+ selectedBindings,
3251
+ succeeded,
3252
+ failed,
3253
+ cancelled,
3254
+ selectionFailures,
3255
+ running,
3256
+ selectedCount,
3257
+ replacedCount: persisted.replacedCount,
3258
+ };
1758
3259
  }
1759
3260
 
1760
3261
  console.log('\n=== 建立绑定并启动 ===');
1761
3262
  throwIfStopping();
1762
- const groups = await setupAgentGroups(sessionDrafts);
3263
+ const groups = await setupAgentGroups(acceptedBindings);
1763
3264
  throwIfStopping();
1764
- for (const draft of sessionDrafts) {
1765
- throwIfStopping();
1766
- try {
1767
- const paired = await bindOneDraft(draft, groups, mode);
1768
- throwIfStopping();
1769
- if (mode === 'install' && !paired.process) throw new Error('完成绑定后未获得可监督的 Feishu Bridge 进程');
1770
- succeeded.push(paired.binding);
1771
- if (mode === 'install') {
1772
- running.push({ binding: paired.binding, process: paired.process, group: paired.group });
1773
- }
1774
- } catch (error) {
1775
- if (stopRequested) throw error;
1776
- const reason = redact(error.message || error);
1777
- failed.push({ binding: draft, reason });
1778
- await setBindingStatus(draft, 'bind', 'failed', reason);
1779
- await recordError('binding', reason, draft);
1780
- console.error(`🔴 绑定失败:${bindingLabel(draft)}\n 原因:${reason}`);
1781
- console.error(' 已跳过该项,继续处理下一项。');
1782
- }
1783
- }
1784
- return { groups, succeeded, failed, selectionFailures, running, selectedCount: sessionDrafts.length };
3265
+ const launched = await startBindingsWithGroups(acceptedBindings, groups, mode);
3266
+ running.push(...launched.running);
3267
+ succeeded.push(...launched.running.map(({ binding }) => binding));
3268
+ failed.push(...launched.failed);
3269
+ cancelled.push(...launched.cancelled);
3270
+ return {
3271
+ groups,
3272
+ saved,
3273
+ acceptedBindings,
3274
+ selectedBindings,
3275
+ succeeded,
3276
+ failed,
3277
+ cancelled,
3278
+ selectionFailures,
3279
+ running,
3280
+ selectedCount,
3281
+ replacedCount: persisted.replacedCount,
3282
+ };
1785
3283
  }
1786
3284
 
1787
3285
  async function runInstall() {
1788
3286
  const result = await withMutationLock('install 绑定流程', async () => {
1789
3287
  const bound = await runBindingSession('install');
1790
3288
  throwIfStopping();
1791
- if (!bound.succeeded.length) {
1792
- await shutdownGroups(bound.groups);
1793
- throw new Error('没有配置完成绑定,现有新流程配置保持不变');
1794
- }
1795
- const reconciled = await reconcileRetainedBindings(bound.running);
1796
- throwIfStopping();
1797
- bound.running = reconciled.alive;
1798
- bound.runtimeFailures = reconciled.failed;
1799
- await replaceBindings(bound.succeeded);
3289
+ const composed = await reconcileStartupResults(bound.selectedBindings, {
3290
+ running: bound.running,
3291
+ failed: bound.failed,
3292
+ cancelled: bound.cancelled,
3293
+ });
1800
3294
  throwIfStopping();
1801
- if (!bound.running.length) {
1802
- await shutdownGroups(bound.groups);
1803
- throw new Error(`全部已绑定配置启动失败;${bound.succeeded.length} 个真实配对配置已写入 ${CONFIG_FILE}`);
1804
- }
3295
+ bound.running = composed.running;
3296
+ bound.failed = composed.failed;
3297
+ bound.cancelled = composed.cancelled;
3298
+ bound.disposition = composed.disposition;
1805
3299
  return bound;
1806
3300
  });
1807
- console.log(`\n已成功建立绑定并启动 ${result.running.length}/${result.selectedCount} 个配置。`);
1808
- console.log('🟢 保持终端打开,你可以给 agent 派发飞书任务');
1809
- const failureCount = result.failed.length + result.selectionFailures.length + result.runtimeFailures.length;
1810
- if (failureCount) console.log(`另有 ${failureCount} 次选择或绑定失败,详情见上方信息和本地日志。`);
1811
- await supervise(result.running, result.groups);
3301
+
3302
+ if (!result.acceptedBindings.length) {
3303
+ if (result.cancelled.length && !result.selectionFailures.length) {
3304
+ printStartupSummary({
3305
+ title: '已成功建立绑定并启动',
3306
+ plannedCount: result.selectedCount,
3307
+ running: [],
3308
+ failed: [],
3309
+ cancelled: result.cancelled,
3310
+ });
3311
+ return;
3312
+ }
3313
+ throw new Error('没有配置完成绑定,现有配置保持不变');
3314
+ }
3315
+ printStartupSummary({
3316
+ title: '已成功建立绑定并启动',
3317
+ plannedCount: result.selectedCount,
3318
+ running: result.running,
3319
+ failed: result.failed,
3320
+ cancelled: result.cancelled,
3321
+ });
3322
+ await dispatchStartupResult(result, {
3323
+ extraFailureCount: result.selectionFailures.length,
3324
+ supervise: async (running, groups) => {
3325
+ console.log('🟢 保持终端打开,你可以给 agent 派发飞书任务');
3326
+ if (result.selectionFailures.length) {
3327
+ console.log(`另有 ${result.selectionFailures.length} 次选择未完成,详情见上方信息和本地日志。`);
3328
+ }
3329
+ await supervise(running, groups);
3330
+ },
3331
+ shutdown: shutdownGroups,
3332
+ onlyCancelled: async () => {},
3333
+ allFailed: async () => {
3334
+ throw new Error(`全部配置启动失败;${result.acceptedBindings.length} 个绑定配置已保存,可稍后运行 feishu-task-agent start 重试`);
3335
+ },
3336
+ });
1812
3337
  }
1813
3338
 
1814
3339
  async function runAdd() {
1815
- await withMutationLock('add 绑定流程', async () => {
1816
- const bound = await runBindingSession('add');
1817
- if (!bound.succeeded.length) throw new Error('没有配置完成绑定');
1818
- return bound;
3340
+ const result = await withMutationLock('add 绑定流程', async () => {
3341
+ return runBindingSession('add');
1819
3342
  });
1820
- console.log('配置添加成功,运行feishu-task-agent start启动时生效');
3343
+ if (!result.acceptedBindings.length) {
3344
+ if (result.cancelled.length && !result.selectionFailures.length) {
3345
+ console.log(`已取消 ${result.cancelled.length} 个配置的绑定;现有配置保持不变。`);
3346
+ return;
3347
+ }
3348
+ throw new Error('没有配置完成绑定');
3349
+ }
3350
+ if (result.replacedCount > 0 && await hasActiveAgentLease()) {
3351
+ console.log('当前已运行的 Bridge 不受影响;替换将在下一次 feishu-task-agent start 时生效。');
3352
+ }
3353
+ console.log('配置添加成功,运行 feishu-task-agent start 启动时生效');
1821
3354
  }
1822
3355
 
1823
3356
  async function runList() {
@@ -1892,26 +3425,81 @@ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
1892
3425
  if (stopRequested) return;
1893
3426
  stopRequested = true;
1894
3427
  stopSignal = signal;
1895
- if (terminal?.input?.isRaw) terminal.input.setRawMode(false);
1896
- terminal?.output?.write('\x1b[?25h');
1897
- void cleanupAll().finally(() => {
1898
- console.log(`\n已收到 ${signal},本次启动的 Bridge 已停止。`);
1899
- process.exit(0);
1900
- });
3428
+ const interruptedPrompt = promptInterrupter.interrupt(new Error(`已收到 ${signal}`));
3429
+ if (!interruptedPrompt) {
3430
+ if (terminal?.input?.isRaw) terminal.input.setRawMode(false);
3431
+ terminal?.output?.write('\x1b[?25h');
3432
+ }
3433
+ void cleanupAll().catch(() => {});
1901
3434
  });
1902
3435
  }
1903
3436
 
1904
- main()
1905
- .catch(async (error) => {
1906
- if (!stopRequested) {
1907
- const reason = redact(error?.message || error);
1908
- await recordError('controller', reason).catch(() => {});
1909
- console.error(`\n🔴 运行失败:${reason}`);
1910
- printLogHints(true);
1911
- process.exitCode = 1;
1912
- }
1913
- })
1914
- .finally(async () => {
1915
- await cleanupAll();
1916
- if (stopRequested && stopSignal) console.log(`\n已收到 ${stopSignal},本次启动的 Bridge 已停止。`);
1917
- });
3437
+ let isMainModule = false;
3438
+ if (process.argv[1]) {
3439
+ try {
3440
+ isMainModule = fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
3441
+ } catch {
3442
+ isMainModule = path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
3443
+ }
3444
+ }
3445
+
3446
+ if (isMainModule) {
3447
+ main()
3448
+ .catch(async (error) => {
3449
+ if (!stopRequested) {
3450
+ const reason = redact(error?.message || error);
3451
+ await recordError('controller', reason).catch(() => {});
3452
+ console.error(`\n🔴 运行失败:${reason}`);
3453
+ printLogHints(true);
3454
+ process.exitCode = 1;
3455
+ }
3456
+ })
3457
+ .finally(async () => {
3458
+ await cleanupAll();
3459
+ if (stopRequested && stopSignal) console.log(`\n已收到 ${stopSignal},本次启动的 Bridge 已停止。`);
3460
+ });
3461
+ }
3462
+
3463
+ export {
3464
+ buildPendingBinding,
3465
+ bindingExpectation,
3466
+ acpBridgeAgentPolicy,
3467
+ commitPreparedAgentBindings,
3468
+ createPromptInterrupter,
3469
+ createResourceCleanup,
3470
+ dispatchStartupResult,
3471
+ executePreparedBindingStart,
3472
+ executePreparedPendingBindingStart,
3473
+ executePreparedReadyBindingStart,
3474
+ installHasOnlyCancellations,
3475
+ initializeAgentGroups,
3476
+ orderStartupItems,
3477
+ prepareBindingStart,
3478
+ prepareFeishuProcess,
3479
+ readInitialRuntimeMetadata,
3480
+ resolveConfiguredPendingPairingFile,
3481
+ resolvePendingPairingFile,
3482
+ feishuArgs,
3483
+ prepareAndCommitAgentBindings,
3484
+ probeReadyBindingProfiles,
3485
+ reconcileStartupResults,
3486
+ reconcileOverlappedReadyBindings,
3487
+ recordPreparationFailure,
3488
+ recordError,
3489
+ recordStableAgentFailure,
3490
+ resolvePreparedAgentBindings,
3491
+ orchestrateStartupBindings,
3492
+ runOverlappedStartup,
3493
+ runPreparedBindingStarts,
3494
+ sameBindingRelationship,
3495
+ startBindingsWithGroups,
3496
+ startAgentGroups,
3497
+ startManagedProcess,
3498
+ runBootstrapHelper,
3499
+ setBindingStatus,
3500
+ startupSummaryLines,
3501
+ cleanupAll,
3502
+ upsertBindings,
3503
+ writeFeishuRuntimeProfile,
3504
+ writeManifest,
3505
+ };