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