@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.
@@ -0,0 +1,71 @@
1
+ export async function settleWithConcurrency(items, limit, worker) {
2
+ if (!Number.isInteger(limit) || limit < 1) throw new Error('concurrency limit must be a positive integer')
3
+ const results = new Array(items.length)
4
+ let cursor = 0
5
+ const run = async () => {
6
+ while (cursor < items.length) {
7
+ const index = cursor
8
+ cursor += 1
9
+ try {
10
+ results[index] = { status: 'fulfilled', value: await worker(items[index], index) }
11
+ } catch (reason) {
12
+ results[index] = { status: 'rejected', reason }
13
+ }
14
+ }
15
+ }
16
+ await Promise.all(Array.from(
17
+ { length: Math.min(limit, items.length) },
18
+ () => run(),
19
+ ))
20
+ return results
21
+ }
22
+
23
+ export async function runLayeredStarts(items, { prepare, start, concurrency }) {
24
+ const outcomes = new Array(items.length)
25
+ const prepared = []
26
+ for (let index = 0; index < items.length; index += 1) {
27
+ try {
28
+ prepared.push({ index, value: await prepare(items[index], index) })
29
+ } catch (reason) {
30
+ outcomes[index] = { status: 'rejected', phase: 'prepare', reason, item: items[index], index }
31
+ }
32
+ }
33
+ const started = await settleWithConcurrency(
34
+ prepared,
35
+ concurrency,
36
+ ({ index, value }) => start(value, index, items[index]),
37
+ )
38
+ started.forEach((result, preparedIndex) => {
39
+ const { index } = prepared[preparedIndex]
40
+ outcomes[index] = { ...result, phase: 'start', item: items[index], index }
41
+ })
42
+ return outcomes
43
+ }
44
+
45
+ export function createKeyedSerialExecutor() {
46
+ const tails = new Map()
47
+ return async (key, operation) => {
48
+ const previous = tails.get(key) || Promise.resolve()
49
+ const current = previous.catch(() => {}).then(operation)
50
+ tails.set(key, current)
51
+ try {
52
+ return await current
53
+ } finally {
54
+ if (tails.get(key) === current) tails.delete(key)
55
+ }
56
+ }
57
+ }
58
+
59
+ export function createSerializedRunner(operation) {
60
+ let tail = Promise.resolve()
61
+ return {
62
+ run() {
63
+ const current = tail.catch(() => {}).then(operation)
64
+ tail = current
65
+ return current
66
+ },
67
+ flush() {
68
+ return tail
69
+ },
70
+ }
71
+ }
@@ -190,6 +190,19 @@ export function agentStartRetryError(events, expectedAgentNames, attempt, maxAtt
190
190
  return undefined;
191
191
  }
192
192
 
193
+ export function preserveAgentStartFailure(error, events) {
194
+ const failure = error instanceof Error ? error : new Error(String(error));
195
+ failure.agentStartEvents = [...(events || [])];
196
+ return failure;
197
+ }
198
+
199
+ export function agentStartFailureMessage(events, agentName, fallbackMessage) {
200
+ const failed = [...(events || [])]
201
+ .reverse()
202
+ .find((event) => event?.type === 'agent.failed' && event.agent === agentName);
203
+ return String(failed?.message || fallbackMessage || `${agentName} Agent Bridge 启动失败`);
204
+ }
205
+
193
206
  export function safeDiagnosticUrl(rawUrl) {
194
207
  const parsed = new URL(rawUrl);
195
208
  parsed.username = '';
@@ -0,0 +1,254 @@
1
+ import { spawn } from 'node:child_process'
2
+
3
+ const NPM_EXEC_ENVIRONMENT_KEYS = [
4
+ 'npm_lifecycle_event',
5
+ 'npm_package_json',
6
+ 'npm_command',
7
+ 'npm_execpath',
8
+ 'npm_node_execpath',
9
+ 'INIT_CWD',
10
+ ]
11
+
12
+ const RESOLVE_EXECUTABLE_SOURCE = String.raw`
13
+ import fs from 'node:fs'
14
+ import path from 'node:path'
15
+
16
+ const executable = process.argv[1]
17
+ const environmentKeys = ${JSON.stringify(NPM_EXEC_ENVIRONMENT_KEYS)}
18
+
19
+ function environmentValue(name) {
20
+ return Object.entries(process.env)
21
+ .find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1] || ''
22
+ }
23
+
24
+ const pathValue = environmentValue('PATH')
25
+ const pathEntries = pathValue.split(path.delimiter).filter(Boolean)
26
+ const environment = {}
27
+ for (const key of environmentKeys) {
28
+ if (Object.hasOwn(process.env, key) && typeof process.env[key] === 'string') {
29
+ environment[key] = process.env[key]
30
+ }
31
+ }
32
+ const windowsExtensions = (environmentValue('PATHEXT') || '.COM;.EXE;.BAT;.CMD')
33
+ .split(';')
34
+ .map((extension) => extension.trim())
35
+ .filter(Boolean)
36
+
37
+ function executableCandidates(binDir) {
38
+ const candidate = path.join(binDir, executable)
39
+ if (process.platform !== 'win32') return [candidate]
40
+ if (windowsExtensions.some((extension) => executable.toLowerCase().endsWith(extension.toLowerCase()))) {
41
+ return [candidate]
42
+ }
43
+ return windowsExtensions.map((extension) => candidate + extension.toLowerCase())
44
+ }
45
+
46
+ for (const binDir of pathEntries) {
47
+ for (const candidate of executableCandidates(binDir)) {
48
+ try {
49
+ const stat = fs.statSync(candidate)
50
+ if (!stat.isFile()) continue
51
+ if (process.platform !== 'win32') fs.accessSync(candidate, fs.constants.X_OK)
52
+ const extension = path.extname(candidate).toLowerCase()
53
+ const kind = process.platform === 'win32' && (extension === '.cmd' || extension === '.bat')
54
+ ? 'cmd'
55
+ : 'direct'
56
+ process.stdout.write(JSON.stringify({
57
+ executable,
58
+ kind,
59
+ command: candidate,
60
+ pathValue,
61
+ environment,
62
+ }))
63
+ process.exit(0)
64
+ } catch {
65
+ // Keep following npm's PATH order until its executable shim is found.
66
+ }
67
+ }
68
+ }
69
+
70
+ process.stderr.write('unable to resolve npm executable shim: ' + executable + '\\n')
71
+ process.exit(1)
72
+ `
73
+
74
+ export function npmExecutableResolverArgs(executable) {
75
+ return ['--input-type=module', '--eval', RESOLVE_EXECUTABLE_SOURCE, executable]
76
+ }
77
+
78
+ function validatePreparedExecutable(value, executable = value?.executable) {
79
+ if (!value || typeof value !== 'object') {
80
+ throw new Error(`${executable || 'package'} resolved an invalid npm executable descriptor`)
81
+ }
82
+ if (value.executable !== executable || !['direct', 'cmd'].includes(value.kind)) {
83
+ throw new Error(`${executable} resolved an invalid npm executable descriptor`)
84
+ }
85
+ if (typeof value.command !== 'string' || !value.command) {
86
+ throw new Error(`${executable} resolved an invalid npm executable command`)
87
+ }
88
+ if (typeof value.pathValue !== 'string' || !value.pathValue) {
89
+ throw new Error(`${executable} resolved an invalid npm executable PATH`)
90
+ }
91
+ if (!value.environment || typeof value.environment !== 'object' || Array.isArray(value.environment)) {
92
+ throw new Error(`${executable} resolved an invalid npm executable environment`)
93
+ }
94
+ const environment = {}
95
+ for (const [key, environmentValue] of Object.entries(value.environment)) {
96
+ if (!NPM_EXEC_ENVIRONMENT_KEYS.includes(key) || typeof environmentValue !== 'string') {
97
+ throw new Error(`${executable} resolved an invalid npm executable environment`)
98
+ }
99
+ environment[key] = environmentValue
100
+ }
101
+ return {
102
+ executable: value.executable,
103
+ kind: value.kind,
104
+ command: value.command,
105
+ pathValue: value.pathValue,
106
+ environment,
107
+ }
108
+ }
109
+
110
+ export function parseResolvedPackageExecutable(stdout, executable) {
111
+ const lines = String(stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
112
+ for (const line of lines.reverse()) {
113
+ try {
114
+ const result = JSON.parse(line)
115
+ if (result?.executable === executable) return validatePreparedExecutable(result, executable)
116
+ } catch {
117
+ // npm can print unrelated output; continue looking for the resolver record.
118
+ }
119
+ }
120
+ throw new Error(`${executable} package did not expose a runnable npm executable shim`)
121
+ }
122
+
123
+ function replacePath(environment, pathValue, platform) {
124
+ const next = { ...(environment || process.env) }
125
+ const existingKeys = Object.keys(next).filter((key) => key.toLowerCase() === 'path')
126
+ const pathKey = existingKeys[0] || (platform === 'win32' ? 'Path' : 'PATH')
127
+ for (const key of existingKeys) {
128
+ if (key !== pathKey) delete next[key]
129
+ }
130
+ next[pathKey] = pathValue
131
+ return next
132
+ }
133
+
134
+ function mergePreparedEnvironment(environment, descriptor, platform) {
135
+ const next = replacePath(environment, descriptor.pathValue, platform)
136
+ for (const [key, value] of Object.entries(descriptor.environment)) {
137
+ for (const existingKey of Object.keys(next)) {
138
+ if (existingKey.toLowerCase() === key.toLowerCase()) delete next[existingKey]
139
+ }
140
+ next[key] = value
141
+ }
142
+ return next
143
+ }
144
+
145
+ // Adapted from the escaping used by @npmcli/promise-spawn for cmd.exe. User
146
+ // arguments need two escaping passes because a generated .cmd shim adds a
147
+ // second cmd.exe parsing layer.
148
+ function escapeCmdArgument(input, doubleEscape = false) {
149
+ const value = String(input)
150
+ if (!value.length) return '""'
151
+ let result
152
+ if (!/[ \t\n\v"]/.test(value)) {
153
+ result = value
154
+ } else {
155
+ result = '"'
156
+ for (let index = 0; index <= value.length; index += 1) {
157
+ let slashCount = 0
158
+ while (value[index] === '\\') {
159
+ index += 1
160
+ slashCount += 1
161
+ }
162
+ if (index === value.length) {
163
+ result += '\\'.repeat(slashCount * 2)
164
+ break
165
+ }
166
+ if (value[index] === '"') {
167
+ result += '\\'.repeat(slashCount * 2 + 1)
168
+ result += value[index]
169
+ } else {
170
+ result += '\\'.repeat(slashCount)
171
+ result += value[index]
172
+ }
173
+ }
174
+ result += '"'
175
+ }
176
+ result = result.replace(/[ !%^&()<>|"]/g, '^$&')
177
+ if (doubleEscape) result = result.replace(/[ !%^&()<>|"]/g, '^$&')
178
+ return result
179
+ }
180
+
181
+ export function createPackageExecutableLauncher({
182
+ materialize,
183
+ spawnProcess = spawn,
184
+ platform = process.platform,
185
+ } = {}) {
186
+ if (typeof materialize !== 'function') throw new Error('materialize must be a function')
187
+ const resolved = new Map()
188
+ const packageTails = new Map()
189
+
190
+ function resolve(packageSpec, executable, context = {}) {
191
+ const key = JSON.stringify([packageSpec, executable])
192
+ const existing = resolved.get(key)
193
+ if (existing) return existing
194
+
195
+ const previous = packageTails.get(packageSpec) || Promise.resolve()
196
+ const pending = previous.catch(() => {}).then(async () => (
197
+ validatePreparedExecutable(
198
+ await materialize(packageSpec, executable, context),
199
+ executable,
200
+ )
201
+ ))
202
+ packageTails.set(packageSpec, pending)
203
+ resolved.set(key, pending)
204
+ pending.then(
205
+ () => {
206
+ if (packageTails.get(packageSpec) === pending) packageTails.delete(packageSpec)
207
+ },
208
+ () => {
209
+ if (resolved.get(key) === pending) resolved.delete(key)
210
+ if (packageTails.get(packageSpec) === pending) packageTails.delete(packageSpec)
211
+ },
212
+ )
213
+ return pending
214
+ }
215
+
216
+ function launchPrepared({ preparedExecutable, args = [], spawnOptions = {} }) {
217
+ const descriptor = validatePreparedExecutable(preparedExecutable)
218
+ const options = {
219
+ ...spawnOptions,
220
+ env: mergePreparedEnvironment(spawnOptions.env, descriptor, platform),
221
+ shell: false,
222
+ }
223
+ if (descriptor.kind === 'direct') {
224
+ return spawnProcess(descriptor.command, args, options)
225
+ }
226
+ if (platform !== 'win32') {
227
+ throw new Error(`${descriptor.executable} resolved a Windows command shim on ${platform}`)
228
+ }
229
+ const commandShell = Object.entries(options.env)
230
+ .find(([key]) => key.toLowerCase() === 'comspec')?.[1] || 'cmd.exe'
231
+ const script = [
232
+ escapeCmdArgument(descriptor.command),
233
+ ...args.map((argument) => escapeCmdArgument(argument, true)),
234
+ ].join(' ')
235
+ return spawnProcess(commandShell, ['/d', '/s', '/c', script], {
236
+ ...options,
237
+ windowsVerbatimArguments: true,
238
+ })
239
+ }
240
+
241
+ async function launch({
242
+ packageSpec,
243
+ executable,
244
+ args = [],
245
+ context = {},
246
+ spawnOptions = {},
247
+ preparedExecutable,
248
+ }) {
249
+ const descriptor = preparedExecutable || await resolve(packageSpec, executable, context)
250
+ return launchPrepared({ preparedExecutable: descriptor, args, spawnOptions })
251
+ }
252
+
253
+ return { launch, launchPrepared, resolve }
254
+ }
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ const MAX_OUTPUT_BYTES = 1024 * 1024
6
+ const EXIT = Object.freeze({ unsupported: 3, model: 10, blocked: 11, invalid: 65, execution: 70, timeout: 124, missing: 127 })
7
+
8
+ function cleanText(value, homeDir = '') {
9
+ if (typeof value !== 'string') return undefined
10
+ let text = value.replace(/\s+/g, ' ').trim()
11
+ if (homeDir) text = text.split(homeDir).join('~')
12
+ if (!text) return undefined
13
+ return text.slice(0, 400)
14
+ }
15
+
16
+ export function supportsTraeCodeAcpHelp(output) {
17
+ const text = String(output || '')
18
+ return /\bacp\s+serve\b/i.test(text) && /Start the ACP server/i.test(text)
19
+ }
20
+
21
+ export function parseTraeCodeDoctor(raw, homeDir = '') {
22
+ let document
23
+ try {
24
+ document = JSON.parse(String(raw || ''))
25
+ } catch {
26
+ throw new Error('TraeCode doctor did not return valid JSON')
27
+ }
28
+ if (!document || typeof document !== 'object' || !Array.isArray(document.checks)) {
29
+ throw new Error('TraeCode doctor JSON is missing a checks array')
30
+ }
31
+
32
+ const checks = document.checks.map((value, index) => {
33
+ if (!value || typeof value !== 'object') {
34
+ throw new Error(`TraeCode doctor check ${index} is invalid`)
35
+ }
36
+ const name = cleanText(value.name, homeDir)
37
+ const severity = cleanText(value.severity, homeDir)?.toLowerCase()
38
+ const message = cleanText(value.message, homeDir)
39
+ const fix = cleanText(value.fix, homeDir)
40
+ if (!name || !['info', 'warning', 'error'].includes(severity) || !message) {
41
+ throw new Error(`TraeCode doctor check ${index} is invalid`)
42
+ }
43
+ return { name, severity, message, ...(fix ? { fix } : {}) }
44
+ })
45
+ const warnings = checks.filter((check) => check.severity === 'warning')
46
+ const errors = checks.filter((check) => check.severity === 'error')
47
+ return {
48
+ status: errors.length === 0
49
+ ? 'ready'
50
+ : errors.some((check) => check.name.toLowerCase() === 'model')
51
+ ? 'model_required'
52
+ : 'blocked',
53
+ warnings,
54
+ errors,
55
+ }
56
+ }
57
+
58
+ function formatChecks(checks) {
59
+ return checks.map((check) => {
60
+ const suffix = check.fix ? `;建议:${check.fix}` : ''
61
+ return `${check.name}: ${check.message}${suffix}`
62
+ }).join('\n')
63
+ }
64
+
65
+ function runBounded(command, args, timeoutSeconds) {
66
+ const timeoutMs = Math.max(1, Number.isFinite(timeoutSeconds) ? timeoutSeconds : 10) * 1000
67
+ return new Promise((resolve) => {
68
+ let stdout = ''
69
+ let stderr = ''
70
+ let total = 0
71
+ let timedOut = false
72
+ let outputLimited = false
73
+ let settled = false
74
+ let timer
75
+ const child = spawn(command, args, {
76
+ stdio: ['ignore', 'pipe', 'pipe'],
77
+ windowsHide: true,
78
+ })
79
+ let killTimer
80
+ const finish = (result) => {
81
+ if (settled) return
82
+ settled = true
83
+ clearTimeout(timer)
84
+ clearTimeout(killTimer)
85
+ resolve(result)
86
+ }
87
+ const signal = (name) => {
88
+ try {
89
+ child.kill(name)
90
+ } catch (error) {
91
+ if (error?.code === 'ESRCH') return
92
+ throw error
93
+ }
94
+ }
95
+ const terminate = () => {
96
+ signal('SIGTERM')
97
+ if (!killTimer) killTimer = setTimeout(() => {
98
+ signal('SIGKILL')
99
+ child.stdout.destroy()
100
+ child.stderr.destroy()
101
+ finish({ code: child.exitCode, signal: child.signalCode, stdout, stderr, timedOut, outputLimited })
102
+ }, 500)
103
+ killTimer.unref()
104
+ }
105
+ const append = (target, chunk) => {
106
+ const text = chunk.toString('utf8')
107
+ total += Buffer.byteLength(text)
108
+ if (total > MAX_OUTPUT_BYTES) {
109
+ if (!outputLimited) {
110
+ outputLimited = true
111
+ clearTimeout(timer)
112
+ terminate()
113
+ }
114
+ return target
115
+ }
116
+ return target + text
117
+ }
118
+ child.stdout.on('data', (chunk) => { stdout = append(stdout, chunk) })
119
+ child.stderr.on('data', (chunk) => { stderr = append(stderr, chunk) })
120
+ timer = setTimeout(() => {
121
+ timedOut = true
122
+ terminate()
123
+ }, timeoutMs)
124
+ child.once('error', (error) => {
125
+ finish({ spawnError: error, stdout, stderr, timedOut, outputLimited })
126
+ })
127
+ child.once('close', (code, signal) => {
128
+ finish({ code, signal, stdout, stderr, timedOut, outputLimited })
129
+ })
130
+ })
131
+ }
132
+
133
+ async function main() {
134
+ const [action, command, timeoutRaw] = process.argv.slice(2)
135
+ if (!['probe-acp', 'doctor'].includes(action) || !command) process.exit(64)
136
+ const result = await runBounded(command, action === 'probe-acp'
137
+ ? ['acp', 'serve', '--help']
138
+ : ['doctor', '--json'], Number(timeoutRaw || '10'))
139
+ if (result.timedOut) process.exit(EXIT.timeout)
140
+ if (result.outputLimited) process.exit(EXIT.execution)
141
+ if (result.spawnError) process.exit(result.spawnError.code === 'ENOENT' ? EXIT.missing : EXIT.execution)
142
+
143
+ if (action === 'probe-acp') {
144
+ process.exit(result.code === 0 && supportsTraeCodeAcpHelp(`${result.stdout}\n${result.stderr}`)
145
+ ? 0
146
+ : EXIT.unsupported)
147
+ }
148
+
149
+ if (![0, 1, 2].includes(result.code)) {
150
+ process.stderr.write('TraeCode doctor failed with an unsupported exit code')
151
+ process.exit(EXIT.execution)
152
+ }
153
+ let parsed
154
+ try {
155
+ parsed = parseTraeCodeDoctor(result.stdout, process.env.HOME || '')
156
+ } catch (error) {
157
+ process.stderr.write(error.message)
158
+ process.exit(EXIT.invalid)
159
+ }
160
+ if (parsed.status === 'ready') {
161
+ if (parsed.warnings.length) process.stdout.write(formatChecks(parsed.warnings))
162
+ process.exit(0)
163
+ }
164
+ process.stdout.write(formatChecks(parsed.errors))
165
+ process.exit(parsed.status === 'model_required' ? EXIT.model : EXIT.blocked)
166
+ }
167
+
168
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
169
+ main().catch((error) => {
170
+ process.stderr.write(cleanText(error?.message || error, process.env.HOME) || 'TraeCode readiness check failed')
171
+ process.exit(EXIT.execution)
172
+ })
173
+ }