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