@larktask/aamp-feishu-task-agent 0.1.0-dev.171

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,3834 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ umask 077
4
+
5
+ AGENT=""
6
+ APP_ID=""
7
+ APP_SECRET=""
8
+ BOT_NAME=""
9
+ LARK_CLI_PROFILE=""
10
+ AAMP_HOST="https://meshmail.ai"
11
+ DEBUG_MODE="false"
12
+ AAMP_TASK_START_MODE="install"
13
+ AAMP_TASK_ACTION=""
14
+ AAMP_TASK_ENTRY="${AAMP_TASK_ENTRY:-}"
15
+ AAMP_TASK_INTERNAL="${AAMP_TASK_INTERNAL:-false}"
16
+ AAMP_TASK_INTERNAL_RESULT_FD="${AAMP_TASK_INTERNAL_RESULT_FD:-3}"
17
+ AAMP_TASK_INTERNAL_INPUT_FD="${AAMP_TASK_INTERNAL_INPUT_FD:-4}"
18
+ NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org/}"
19
+ NPM_CACHE_DIR="${NPM_CONFIG_CACHE:-${npm_config_cache:-${TMPDIR:-/tmp}/aamp-one-click-npm-cache}}"
20
+ NPM_GLOBAL_PREFIX="${NPM_GLOBAL_PREFIX:-$HOME/.aamp/npm-global}"
21
+ AAMP_LOG_DIR="${AAMP_LOG_DIR:-$HOME/.aamp/logs}"
22
+ AAMP_BIN_DIR="${AAMP_BIN_DIR:-$HOME/.aamp/bin}"
23
+ AAMP_TASK_COMMAND_NAME="${AAMP_TASK_COMMAND_NAME:-feishu-task-agent}"
24
+ AAMP_TASK_COMMAND_PATH="${AAMP_TASK_COMMAND_PATH:-$AAMP_BIN_DIR/$AAMP_TASK_COMMAND_NAME}"
25
+ AAMP_TASK_SHIM_DIR="${AAMP_TASK_SHIM_DIR:-$HOME/.local/bin}"
26
+ AAMP_TASK_AUTO_UPDATE="${AAMP_TASK_AUTO_UPDATE:-true}"
27
+ AAMP_TASK_AUTO_UPDATE_DONE="${AAMP_TASK_AUTO_UPDATE_DONE:-false}"
28
+ AAMP_TASK_UPDATE_CACHE_FILE="${AAMP_TASK_UPDATE_CACHE_FILE:-$HOME/.aamp/feishu-task-agent/update-cache.json}"
29
+ AAMP_TASK_UPDATE_CACHE_TTL_SECONDS="${AAMP_TASK_UPDATE_CACHE_TTL_SECONDS:-86400}"
30
+ AAMP_TAIL_BRIDGE_LOGS="${AAMP_TAIL_BRIDGE_LOGS:-false}"
31
+ AAMP_ONE_CLICK_VERBOSE="${AAMP_ONE_CLICK_VERBOSE:-false}"
32
+ AAMP_ONE_CLICK_MOCK_FAIL_STAGE="${AAMP_ONE_CLICK_MOCK_FAIL_STAGE:-}"
33
+ BOT_CONFIG_FILE="${BOT_CONFIG_FILE:-$HOME/.aamp/feishu-bridge/task-runtime/task-profiles-v2.json}"
34
+ CURRENT_RUN_FILE="${CURRENT_RUN_FILE:-$HOME/.aamp/feishu-bridge/task-runtime/runs/current.json}"
35
+ ACTIVE_RUNS_FILE="${ACTIVE_RUNS_FILE:-$HOME/.aamp/feishu-bridge/task-runtime/runs/active.json}"
36
+ BOT_RESERVATIONS_FILE="${BOT_RESERVATIONS_FILE:-$HOME/.aamp/feishu-bridge/task-runtime/runs/reservations.json}"
37
+ BOT_SELECTION_LOCK_DIR="${BOT_SELECTION_LOCK_DIR:-$HOME/.aamp/feishu-bridge/task-runtime/runs/selection.lock}"
38
+ LARK_CLI_INSTALL_LOCK_DIR="${LARK_CLI_INSTALL_LOCK_DIR:-$HOME/.aamp/locks/lark-cli-install.lock}"
39
+ LARK_CLI_CONFIG_LOCK_DIR="${LARK_CLI_CONFIG_LOCK_DIR:-$HOME/.aamp/locks/lark-cli-config.lock}"
40
+ TASK_AGENT_UPDATE_LOCK_DIR="${TASK_AGENT_UPDATE_LOCK_DIR:-$HOME/.aamp/locks/feishu-task-agent-update.lock}"
41
+ CODEX_UPDATE_LOCK_DIR="${CODEX_UPDATE_LOCK_DIR:-$HOME/.aamp/locks/codex-cli-update.lock}"
42
+ AAMP_LARK_CLI_BIN="${AAMP_LARK_CLI_BIN:-}"
43
+ LARK_CLI_CMD=""
44
+ AAMP_LARK_CLI_CONFIG_DIR="${AAMP_LARK_CLI_CONFIG_DIR:-${LARKSUITE_CLI_CONFIG_DIR:-$HOME/.lark-cli-aamp-one-click-v1}}"
45
+ CODEM_SERVER_URL="${CODEM_SERVER_URL:-https://codem.feishu.cn}"
46
+ CODEM_INSTALLER_URL="${CODEM_INSTALLER_URL:-https://sf-unpkg-src.bytedance.net/@byted-meego/codem-installer@latest/install.sh}"
47
+ CODEM_INSTALLER_CONFIRM="${CODEM_INSTALLER_CONFIRM:-}"
48
+ CODEM_PROVIDER_PREFLIGHT="${CODEM_PROVIDER_PREFLIGHT:-true}"
49
+ CODEM_PROVIDER_PREFLIGHT_TIMEOUT_SECONDS="${CODEM_PROVIDER_PREFLIGHT_TIMEOUT_SECONDS:-60}"
50
+ CODEX_APP_CLI="/Applications/Codex.app/Contents/Resources/codex"
51
+ CODEX_AUTO_UPDATE="${CODEX_AUTO_UPDATE:-true}"
52
+ CODEX_NPM_PACKAGE="${CODEX_NPM_PACKAGE:-@openai/codex}"
53
+ CODEX_ACP_PKG="${CODEX_ACP_PKG:-@agentclientprotocol/codex-acp@1.0.2}"
54
+ LARK_REGISTER_APP_SDK="${LARK_REGISTER_APP_SDK:-@larksuiteoapi/node-sdk@1.68.0}"
55
+ LARK_CLI_MIN_VERSION="${LARK_CLI_MIN_VERSION:-1.0.64}"
56
+ FEISHU_APP_SCOPES_TENANT="${FEISHU_APP_SCOPES_TENANT:-im:message,im:message:send_as_bot,im:message:readonly,im:resource,cardkit:card:write,task:task,task:comment,task:task:readonly,task:comment:readonly,task:attachment:delete,task:attachment:file:download,task:attachment:read,task:attachment:upload,task:attachment:write,task:comment:delete,task:comment:read,task:comment:write,task:comment:writeonly,task:task:delete,task:task:read,task:task:write,task:task:writeonly,task:tasklist:delete,task:tasklist:read,task:tasklist:write,task:tasklist:writeonly,search:docs:read,base:app:copy,base:app:create,base:app:read,base:app:update,base:block:create,base:block:delete,base:block:read,base:block:update,base:dashboard:create,base:dashboard:delete,base:dashboard:read,base:dashboard:update,base:field:create,base:field:delete,base:field:read,base:field:update,base:form:create,base:form:delete,base:form:read,base:form:update,base:history:read,base:record:create,base:record:delete,base:record:read,base:record:update,base:role:create,base:role:delete,base:role:read,base:role:update,base:table:create,base:table:delete,base:table:read,base:table:update,base:view:read,base:view:write_only,base:workflow:create,base:workflow:read,base:workflow:update,board:whiteboard:node:create,board:whiteboard:node:read,calendar:calendar.event:create,calendar:calendar.event:delete,calendar:calendar.event:read,calendar:calendar.event:reply,calendar:calendar.event:update,calendar:calendar.free_busy:read,calendar:calendar:create,calendar:calendar:delete,calendar:calendar:read,calendar:calendar:update,contact:user.base:readonly,contact:user.basic_profile:readonly,docs:document.media:download,docs:document.media:upload,docs:document:export,docs:document:import,docx:document:create,docx:document:readonly,docx:document:write_only,drive:drive.metadata:readonly,drive:file:download,drive:file:upload,im:chat.managers:write_only,im:chat.members:read,im:chat.members:write_only,im:chat.moderation:read,im:chat:moderation:write_only,im:message.pins:read,im:message.pins:write_only,im:message.reactions:read,im:message.reactions:write_only,im:message:recall,mail:user_mailbox.event.mail_address:read,mail:user_mailbox.mail_contact:read,mail:user_mailbox.message.address:read,mail:user_mailbox.message.body:read,mail:user_mailbox.message.subject:read,mindnote:node:create,mindnote:node:read,minutes:minutes.basic:read,minutes:minutes.media:export,minutes:minutes:readonly,sheets:spreadsheet.meta:read,sheets:spreadsheet.meta:write_only,sheets:spreadsheet:create,sheets:spreadsheet:read,sheets:spreadsheet:write_only,slides:presentation:create,slides:presentation:read,slides:presentation:update,slides:presentation:write_only,task:custom_field:read,task:custom_field:write,task:section:read,task:section:write,vc:meeting.bot.join:write,vc:meeting.meetingevent:read,vc:meeting.message:write,vc:record:readonly,wiki:member:create,wiki:member:retrieve,wiki:member:update,wiki:node:copy,wiki:node:create,wiki:node:move,wiki:node:read,wiki:node:retrieve,wiki:space:read,wiki:space:retrieve,wiki:space:write_only}"
57
+ FEISHU_APP_SCOPES_USER="${FEISHU_APP_SCOPES_USER:-im:message,im:message:readonly,im:resource,cardkit:card:write,task:task,task:comment,task:task:readonly,task:comment:readonly,task:attachment:delete,task:attachment:file:download,task:attachment:read,task:attachment:upload,task:attachment:write,task:comment:delete,task:comment:read,task:comment:write,task:comment:writeonly,task:task:delete,task:task:read,task:task:write,task:task:writeonly,task:tasklist:delete,task:tasklist:read,task:tasklist:write,task:tasklist:writeonly,search:docs:read,search:message,base:app:copy,base:app:create,base:app:read,base:app:update,base:block:create,base:block:delete,base:block:read,base:block:update,base:dashboard:create,base:dashboard:delete,base:dashboard:read,base:dashboard:update,base:field:create,base:field:delete,base:field:read,base:field:update,base:form:create,base:form:delete,base:form:read,base:form:update,base:history:read,base:record:create,base:record:delete,base:record:read,base:record:update,base:role:create,base:role:delete,base:role:read,base:role:update,base:table:create,base:table:delete,base:table:read,base:table:update,base:view:read,base:view:write_only,base:workflow:create,base:workflow:read,base:workflow:update,board:whiteboard:node:create,board:whiteboard:node:read,calendar:calendar.event:create,calendar:calendar.event:delete,calendar:calendar.event:read,calendar:calendar.event:reply,calendar:calendar.event:update,calendar:calendar.free_busy:read,calendar:calendar:create,calendar:calendar:delete,calendar:calendar:read,calendar:calendar:update,contact:user.base:readonly,contact:user.basic_profile:readonly,contact:user:search,docs:document.media:download,docs:document.media:upload,docs:document:export,docs:document:import,docx:document:create,docx:document:readonly,docx:document:write_only,drive:drive.metadata:readonly,drive:file:download,drive:file:upload,im:chat.managers:write_only,im:chat.members:read,im:chat.members:write_only,im:chat.moderation:read,im:chat.nickname:read,im:chat.nickname:write,im:chat.user_setting:read,im:chat.user_setting:write,im:chat:read,im:chat:update,im:chat:create_by_user,im:chat:moderation:write_only,im:feed.flag:read,im:feed.flag:write,im:feed.shortcut:read,im:feed.shortcut:write,im:feed_group_v1:read,im:feed_group_v1:write,im:message.group_msg:get_as_user,im:message.p2p_msg:get_as_user,im:message.pins:read,im:message.pins:write_only,im:message.reactions:read,im:message.reactions:write_only,im:message:recall,mail:event,mail:user_mailbox.event.mail_address:read,mail:user_mailbox.mail_contact:read,mail:user_mailbox.message.address:read,mail:user_mailbox.message.body:read,mail:user_mailbox.message.subject:read,mindnote:node:create,mindnote:node:read,minutes:minutes.artifacts:read,minutes:minutes.basic:read,minutes:minutes.media:export,minutes:minutes.search:read,minutes:minutes.upload:write,minutes:minutes:readonly,minutes:minutes:update,profile:user_profile:read,sheets:spreadsheet.meta:read,sheets:spreadsheet.meta:write_only,sheets:spreadsheet:create,sheets:spreadsheet:read,sheets:spreadsheet:write_only,slides:presentation:create,slides:presentation:read,slides:presentation:update,slides:presentation:write_only,task:custom_field:read,task:custom_field:write,task:section:read,task:section:write,vc:meeting.bot.join:write,vc:meeting.meetingevent:read,vc:meeting.message:write,vc:meeting.search:read,vc:note:read,vc:record:readonly,wiki:member:create,wiki:member:retrieve,wiki:member:update,wiki:node:copy,wiki:node:create,wiki:node:move,wiki:node:read,wiki:node:retrieve,wiki:space:read,wiki:space:retrieve,wiki:space:write_only}"
58
+ FEISHU_APP_EVENTS_TENANT="${FEISHU_APP_EVENTS_TENANT:-task.task.update_user_access_v2}"
59
+ FEISHU_APP_EVENTS_USER="${FEISHU_APP_EVENTS_USER:-task.task.update_user_access_v2}"
60
+ FEISHU_USER_AUTH_DOMAINS="${FEISHU_USER_AUTH_DOMAINS:-base,calendar,contact,docs,im,mail,mindnotes,minutes,note,sheets,slides,task,vc,wiki}"
61
+ FEISHU_USER_AUTH_EXCLUDES="${FEISHU_USER_AUTH_EXCLUDES:-im:message.send_as_user,mail:user_mailbox.message:send,mail:user_mailbox.rule:read,mail:user_mailbox.folder:write,mail:user_mailbox.rule:write,mail:user_mailbox.message:modify,mail:user_mailbox.message:readonly,mail:user_mailbox.folder:read,mail:user_mailbox.mail_contact:write,mail:user_mailbox:readonly}"
62
+ FEISHU_USER_AUTH_REQUIRED_SCOPES="${FEISHU_USER_AUTH_REQUIRED_SCOPES:-im:message im:message:readonly im:resource cardkit:card:write task:task task:comment task:task:readonly task:comment:readonly task:attachment:delete task:attachment:file:download task:attachment:read task:attachment:upload task:attachment:write task:comment:delete task:comment:read task:comment:write task:comment:writeonly task:task:delete task:task:read task:task:write task:task:writeonly task:tasklist:delete task:tasklist:read task:tasklist:write task:tasklist:writeonly search:docs:read search:message base:app:copy base:app:create base:app:read base:app:update base:block:create base:block:delete base:block:read base:block:update base:dashboard:create base:dashboard:delete base:dashboard:read base:dashboard:update base:field:create base:field:delete base:field:read base:field:update base:form:create base:form:delete base:form:read base:form:update base:history:read base:record:create base:record:delete base:record:read base:record:update base:role:create base:role:delete base:role:read base:role:update base:table:create base:table:delete base:table:read base:table:update base:view:read base:view:write_only base:workflow:create base:workflow:read base:workflow:update board:whiteboard:node:create board:whiteboard:node:read calendar:calendar.event:create calendar:calendar.event:delete calendar:calendar.event:read calendar:calendar.event:reply calendar:calendar.event:update calendar:calendar.free_busy:read calendar:calendar:create calendar:calendar:delete calendar:calendar:read calendar:calendar:update contact:user.base:readonly contact:user.basic_profile:readonly contact:user:search docs:document.media:download docs:document.media:upload docs:document:export docs:document:import docx:document:create docx:document:readonly docx:document:write_only drive:drive.metadata:readonly drive:file:download drive:file:upload im:chat.managers:write_only im:chat.members:read im:chat.members:write_only im:chat.moderation:read im:chat.nickname:read im:chat.nickname:write im:chat.user_setting:read im:chat.user_setting:write im:chat:read im:chat:update im:chat:create_by_user im:chat:moderation:write_only im:feed.flag:read im:feed.flag:write im:feed.shortcut:read im:feed.shortcut:write im:feed_group_v1:read im:feed_group_v1:write im:message.group_msg:get_as_user im:message.p2p_msg:get_as_user im:message.pins:read im:message.pins:write_only im:message.reactions:read im:message.reactions:write_only im:message:recall mail:event mail:user_mailbox.event.mail_address:read mail:user_mailbox.mail_contact:read mail:user_mailbox.message.address:read mail:user_mailbox.message.body:read mail:user_mailbox.message.subject:read mindnote:node:create mindnote:node:read minutes:minutes.artifacts:read minutes:minutes.basic:read minutes:minutes.media:export minutes:minutes.search:read minutes:minutes.upload:write minutes:minutes:readonly minutes:minutes:update profile:user_profile:read sheets:spreadsheet.meta:read sheets:spreadsheet.meta:write_only sheets:spreadsheet:create sheets:spreadsheet:read sheets:spreadsheet:write_only slides:presentation:create slides:presentation:read slides:presentation:update slides:presentation:write_only task:custom_field:read task:custom_field:write task:section:read task:section:write vc:meeting.bot.join:write vc:meeting.meetingevent:read vc:meeting.message:write vc:meeting.search:read vc:note:read vc:record:readonly wiki:member:create wiki:member:retrieve wiki:member:update wiki:node:copy wiki:node:create wiki:node:move wiki:node:read wiki:node:retrieve wiki:space:read wiki:space:retrieve wiki:space:write_only}"
63
+ ACP_BRIDGE_PKG="${ACP_BRIDGE_PKG:-@zengxingyuan/aamp-acp-bridge@0.1.28-dev.20}"
64
+ CLI_BRIDGE_PKG="${CLI_BRIDGE_PKG:-@zengxingyuan/aamp-cli-bridge@0.1.7-dev.14}"
65
+ FEISHU_BRIDGE_PKG="${FEISHU_BRIDGE_PKG:-@zengxingyuan/aamp-feishu-bridge@0.1.51}"
66
+ AAMP_TASK_AGENT_NAME="${AAMP_TASK_AGENT_NAME:-@larktask/aamp-feishu-task-agent}"
67
+ AAMP_TASK_AGENT_VERSION="0.1.0-dev.171"
68
+ AAMP_TASK_AGENT_CHANNEL="${AAMP_TASK_AGENT_CHANNEL:-dev}"
69
+ AAMP_STALE_PROCESS_CLEANUP="${AAMP_STALE_PROCESS_CLEANUP:-false}"
70
+ AAMP_STALE_PROCESS_SECONDS="${AAMP_STALE_PROCESS_SECONDS:-86400}"
71
+
72
+ ACP_PID=""
73
+ CLI_PID=""
74
+ FEISHU_PID=""
75
+ ACP_TAIL_PID=""
76
+ CLI_TAIL_PID=""
77
+ FEISHU_TAIL_PID=""
78
+ DETECTED_AGENTS=()
79
+ ACP_LOG=""
80
+ CLI_LOG=""
81
+ FEISHU_LOG=""
82
+ CODEM_LOGIN_LOG=""
83
+ CODEM_SERVICE_START_OUTPUT=""
84
+ CODEM_AUTO_UPDATE_DONE="false"
85
+ CODEM_FORCE_LOGIN_DONE="false"
86
+ CODEM_PROVIDER_RECOVERY_DONE="false"
87
+ PAIRING_URL=""
88
+ ACP_AGENT_COMMAND=""
89
+ STARTED_BRIDGE_PID=""
90
+ ONE_CLICK_RUN_ID="$(date +%s)-$$"
91
+ BOT_RESERVED="false"
92
+ NPM_BIN=""
93
+ NPX_BIN=""
94
+ CURSOR_LOCAL_BIN="$HOME/.local/bin"
95
+ CODEM_LOCAL_BIN="$HOME/.codem/bin"
96
+ AAMP_RUN_LOG_DIR="${AAMP_RUN_LOG_DIR:-}"
97
+ AAMP_RUN_ID="${AAMP_RUN_ID:-}"
98
+ ONE_CLICK_LOG="${ONE_CLICK_LOG:-}"
99
+ ERRORS_LOG="${ERRORS_LOG:-}"
100
+ AAMP_LOGS_BIN="$AAMP_BIN_DIR/aamp-logs"
101
+ AGENT_BRIDGE_EMAIL=""
102
+ FEISHU_BRIDGE_EMAIL=""
103
+ RESTART_ARGS=()
104
+
105
+ sanitize_inherited_npm_exec_env() {
106
+ local key lower
107
+ while IFS='=' read -r key _; do
108
+ lower="$(printf '%s' "$key" | tr '[:upper:]' '[:lower:]')"
109
+ case "$lower" in
110
+ npm_config_*|npm_package_*|npm_lifecycle_*|npm_command|npm_execpath|npm_node_execpath|init_cwd)
111
+ unset "$key"
112
+ ;;
113
+ esac
114
+ done < <(env)
115
+ }
116
+
117
+ usage() {
118
+ cat <<'USAGE'
119
+ Usage:
120
+ feishu-task-agent install # bind multiple Agent-Bot pairs, then start them
121
+ feishu-task-agent start # choose saved pairs to start
122
+ feishu-task-agent list # list saved pairs
123
+ feishu-task-agent add # bind and save more pairs without leaving bridges running
124
+ feishu-task-agent remove # remove saved pairs without stopping running bridges
125
+ feishu-task-agent update # update the short command now
126
+ feishu-task-agent help # show this help
127
+
128
+ Running the installed short command without a subcommand shows this help.
129
+ Running the standalone one-click script without a subcommand is the same as
130
+ "feishu-task-agent install".
131
+
132
+ Options:
133
+ --agent codex|cursor Use this Agent for every new binding in the command.
134
+ --aamp-host URL AAMP service URL. Default: https://meshmail.ai
135
+ --debug Enable debug mode for bridge processes
136
+ -h, --help Show this help
137
+
138
+ 日志命令:
139
+ aamp-logs list-runs
140
+ aamp-logs collect --run-dir <运行日志目录>
141
+ aamp-logs collect --task-id xxx
142
+ aamp-logs collect --task-guid yyy
143
+ USAGE
144
+ }
145
+
146
+ write_one_click_log() {
147
+ local line="$1"
148
+ if [ -n "$ONE_CLICK_LOG" ]; then
149
+ printf '%s %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$line" >>"$ONE_CLICK_LOG" 2>/dev/null || true
150
+ fi
151
+ }
152
+
153
+ agent_log() {
154
+ local line
155
+ line="[aamp-one-click] $*"
156
+ printf '%s\n' "$line"
157
+ write_one_click_log "$line"
158
+ }
159
+
160
+ agent_detail() {
161
+ local line
162
+ line="[aamp-one-click] $*"
163
+ write_one_click_log "$line"
164
+ if [ "$AAMP_ONE_CLICK_VERBOSE" = "true" ]; then
165
+ printf '%s\n' "$line"
166
+ fi
167
+ }
168
+
169
+ record_version_line() {
170
+ local line
171
+ line="当前版本:$AAMP_TASK_AGENT_VERSION"
172
+ write_one_click_log "[aamp-one-click] $line"
173
+ if [ "$AAMP_TASK_START_MODE" = "start" ]; then
174
+ printf '%s\n' "$line"
175
+ fi
176
+ }
177
+
178
+ print_local_log_hints() {
179
+ local stream="${1:-stdout}"
180
+ [ -n "$AAMP_RUN_LOG_DIR" ] || return 0
181
+ if [ "$stream" = "stderr" ]; then
182
+ printf ' 运行日志目录:%s\n' "$AAMP_RUN_LOG_DIR" >&2
183
+ printf ' 运行日志打包:%s collect --run-dir %s\n' "$AAMP_LOGS_BIN" "$AAMP_RUN_LOG_DIR" >&2
184
+ printf ' 特定任务日志打包:%s collect --task-id xxx\n' "$AAMP_LOGS_BIN" >&2
185
+ printf ' 特定任务日志打包:%s collect --task-guid yyy\n' "$AAMP_LOGS_BIN" >&2
186
+ else
187
+ printf ' 运行日志目录:%s\n' "$AAMP_RUN_LOG_DIR"
188
+ printf ' 运行日志打包:%s collect --run-dir %s\n' "$AAMP_LOGS_BIN" "$AAMP_RUN_LOG_DIR"
189
+ printf ' 特定任务日志打包:%s collect --task-id xxx\n' "$AAMP_LOGS_BIN"
190
+ printf ' 特定任务日志打包:%s collect --task-guid yyy\n' "$AAMP_LOGS_BIN"
191
+ fi
192
+ }
193
+
194
+ agent_success() {
195
+ printf '\n🟢 %s 已接入飞书任务,可以开始对话 & 派发任务。\n' "$AGENT"
196
+ if [ -n "$BOT_NAME" ]; then
197
+ printf ' 飞书 Bot:%s\n' "$BOT_NAME"
198
+ fi
199
+ printf '🟢 保持终端打开,你可以给 agent 派发飞书任务\n'
200
+ printf '\n'
201
+ write_one_click_log "[aamp-one-click] SUCCESS: $AGENT connected to Feishu tasks"
202
+ }
203
+
204
+ agent_fail() {
205
+ local reason="$*"
206
+ local line
207
+ line="[aamp-one-click] ERROR: $reason"
208
+ write_one_click_log "$line"
209
+ if [ -n "$ERRORS_LOG" ]; then
210
+ printf '{"timestamp":"%s","level":"error","component":"one-click","message":"%s"}\n' \
211
+ "$(date '+%Y-%m-%dT%H:%M:%S%z')" \
212
+ "$(json_escape "$reason")" >>"$ERRORS_LOG" 2>/dev/null || true
213
+ fi
214
+ if [ "$AAMP_TASK_INTERNAL" = "true" ]; then
215
+ printf '\n🔴 当前配置处理失败:%s\n\n' "$reason" >&2
216
+ exit 1
217
+ fi
218
+ printf '\n🔴 运行失败,本地飞书任务连接没有启动成功。\n' >&2
219
+ printf ' 原因:%s\n' "$reason" >&2
220
+ print_local_log_hints stderr
221
+ printf '\n' >&2
222
+ exit 1
223
+ }
224
+
225
+ mock_fail_stage_known() {
226
+ case "$1" in
227
+ node-toolchain|agent-login|feishu-bot|agent-bridge|feishu-bridge)
228
+ return 0
229
+ ;;
230
+ *)
231
+ return 1
232
+ ;;
233
+ esac
234
+ }
235
+
236
+ validate_mock_fail_stage() {
237
+ [ -n "$AAMP_ONE_CLICK_MOCK_FAIL_STAGE" ] || return 0
238
+ mock_fail_stage_known "$AAMP_ONE_CLICK_MOCK_FAIL_STAGE" || agent_fail "unknown mock failure stage: $AAMP_ONE_CLICK_MOCK_FAIL_STAGE"
239
+ }
240
+
241
+ maybe_mock_fail() {
242
+ local stage="$1"
243
+ [ "$AAMP_ONE_CLICK_MOCK_FAIL_STAGE" = "$stage" ] || return 0
244
+ agent_fail "模拟启动失败:$stage"
245
+ }
246
+
247
+ json_escape() {
248
+ local value="$1"
249
+ value="${value//\\/\\\\}"
250
+ value="${value//\"/\\\"}"
251
+ value="${value//$'\n'/\\n}"
252
+ printf '%s' "$value"
253
+ }
254
+
255
+ write_run_manifest() {
256
+ [ -n "$AAMP_RUN_LOG_DIR" ] || return 0
257
+ local agent_bridge_type="acp"
258
+ local agent_bridge_name="aamp-acp-bridge"
259
+ if uses_cli_bridge; then
260
+ agent_bridge_type="cli"
261
+ agent_bridge_name="aamp-cli-bridge"
262
+ fi
263
+ cat >"$AAMP_RUN_LOG_DIR/manifest.json" <<EOF
264
+ {
265
+ "schema": "aamp.local_logs.run.v1",
266
+ "run_id": "$(json_escape "$AAMP_RUN_ID")",
267
+ "started_at": "$(date '+%Y-%m-%dT%H:%M:%S%z')",
268
+ "agent": "$(json_escape "$AGENT")",
269
+ "env": "online",
270
+ "aamp_host": "$(json_escape "$AAMP_HOST")",
271
+ "app_id": "$(json_escape "$APP_ID")",
272
+ "bot_name": "$(json_escape "$BOT_NAME")",
273
+ "feishu_bridge_name": "aamp-feishu-bridge",
274
+ "feishu_bridge_email": "$(json_escape "$FEISHU_BRIDGE_EMAIL")",
275
+ "agent_bridge_type": "$(json_escape "$agent_bridge_type")",
276
+ "agent_bridge_name": "$(json_escape "$agent_bridge_name")",
277
+ "agent_bridge_email": "$(json_escape "$AGENT_BRIDGE_EMAIL")",
278
+ "log_dir": "$(json_escape "$AAMP_RUN_LOG_DIR")",
279
+ "errors_log": "$(json_escape "$ERRORS_LOG")"
280
+ }
281
+ EOF
282
+ }
283
+
284
+ extract_email_from_pairing_url() {
285
+ local pairing_url="$1"
286
+ PAIRING_URL_VALUE="$pairing_url" node -e '
287
+ try {
288
+ const parsed = new URL(String(process.env.PAIRING_URL_VALUE || ""));
289
+ process.stdout.write(parsed.searchParams.get("mailbox") || "");
290
+ } catch {
291
+ process.stdout.write("");
292
+ }
293
+ '
294
+ }
295
+
296
+ extract_agent_email_from_acp_init_output() {
297
+ node -e '
298
+ let input = "";
299
+ process.stdin.on("data", (chunk) => { input += chunk; });
300
+ process.stdin.on("end", () => {
301
+ const start = input.indexOf("{");
302
+ const end = input.lastIndexOf("}");
303
+ if (start < 0 || end < start) return;
304
+ try {
305
+ const data = JSON.parse(input.slice(start, end + 1));
306
+ process.stdout.write(data.agents?.[0]?.email || data.agents?.[0]?.pairing?.mailbox || "");
307
+ } catch {}
308
+ });
309
+ '
310
+ }
311
+
312
+ extract_feishu_email_from_log() {
313
+ local log_file="$1"
314
+ [ -f "$log_file" ] || return 0
315
+ LOG_FILE="$log_file" node -e '
316
+ const fs = require("node:fs");
317
+ const file = process.env.LOG_FILE;
318
+ let content = "";
319
+ try {
320
+ content = fs.readFileSync(file, "utf8");
321
+ } catch {
322
+ process.exit(0);
323
+ }
324
+ for (const line of content.split(/\r?\n/)) {
325
+ if (!line.trim()) continue;
326
+ let message = line;
327
+ try {
328
+ const record = JSON.parse(line);
329
+ message = String(record.message ?? record.msg ?? line);
330
+ } catch {}
331
+ const match = /\bmailbox=([^\s,]+)/.exec(message);
332
+ if (match?.[1]) {
333
+ process.stdout.write(match[1]);
334
+ break;
335
+ }
336
+ }
337
+ '
338
+ }
339
+
340
+ init_log_run() {
341
+ mkdir -p "$AAMP_LOG_DIR/runs" "$AAMP_BIN_DIR"
342
+ AAMP_RUN_ID="$(date '+%Y%m%dT%H%M%S')-$$"
343
+ AAMP_RUN_LOG_DIR="$AAMP_LOG_DIR/runs/$AAMP_RUN_ID"
344
+ ONE_CLICK_LOG="$AAMP_RUN_LOG_DIR/one-click.log"
345
+ ERRORS_LOG="$AAMP_RUN_LOG_DIR/errors.jsonl"
346
+ mkdir -p "$AAMP_RUN_LOG_DIR"
347
+ : >"$ONE_CLICK_LOG"
348
+ : >"$ERRORS_LOG"
349
+ ln -sfn "$AAMP_RUN_LOG_DIR" "$AAMP_LOG_DIR/latest" 2>/dev/null || true
350
+ export AAMP_LOG_DIR AAMP_RUN_LOG_DIR AAMP_RUN_ID ONE_CLICK_LOG ERRORS_LOG AAMP_LOGS_BIN
351
+ write_run_manifest
352
+ agent_detail "logs directory: $AAMP_RUN_LOG_DIR"
353
+ }
354
+
355
+ run_log_file() {
356
+ local filename="$1"
357
+ if [ -n "$AAMP_RUN_LOG_DIR" ]; then
358
+ printf '%s\n' "$AAMP_RUN_LOG_DIR/$filename"
359
+ else
360
+ mktemp "${TMPDIR:-/tmp}/${filename}.XXXXXX"
361
+ fi
362
+ }
363
+
364
+ install_aamp_logs_bin() {
365
+ local expected_version="${1:-$AAMP_TASK_AGENT_VERSION}"
366
+ [ -n "$NPM_BIN" ] || return 0
367
+ mkdir -p "$AAMP_BIN_DIR"
368
+
369
+ ensure_task_agent_global_install "$expected_version" || {
370
+ agent_log "warning: failed to synchronize aamp-logs $expected_version; logs still saved at $AAMP_RUN_LOG_DIR"
371
+ return 1
372
+ }
373
+
374
+ local tmp="${AAMP_LOGS_BIN}.tmp.$$"
375
+ rm -f "$tmp" 2>/dev/null || true
376
+ ln -s "$NPM_GLOBAL_PREFIX/bin/aamp-logs" "$tmp" 2>>"$ONE_CLICK_LOG" || return 1
377
+ mv -f "$tmp" "$AAMP_LOGS_BIN" 2>>"$ONE_CLICK_LOG" || {
378
+ rm -f "$tmp" 2>/dev/null || true
379
+ return 1
380
+ }
381
+ install_aamp_logs_shim
382
+ agent_detail "logs command: $AAMP_LOGS_BIN -> $NPM_GLOBAL_PREFIX/bin/aamp-logs"
383
+ }
384
+
385
+ install_aamp_logs_shim() {
386
+ install_command_shim "$AAMP_LOGS_BIN" "aamp-logs"
387
+ }
388
+
389
+ kill_process_tree() {
390
+ local pid="$1"
391
+ [ -n "$pid" ] || return 0
392
+ kill -0 "$pid" 2>/dev/null || return 0
393
+
394
+ local children child
395
+ children="$(pgrep -P "$pid" 2>/dev/null || true)"
396
+ for child in $children; do
397
+ kill_process_tree "$child"
398
+ done
399
+
400
+ kill "$pid" 2>/dev/null || true
401
+ }
402
+
403
+ force_kill_process_tree() {
404
+ local pid="$1"
405
+ [ -n "$pid" ] || return 0
406
+ kill -0 "$pid" 2>/dev/null || return 0
407
+
408
+ kill_process_tree "$pid"
409
+ sleep 0.5
410
+ if kill -0 "$pid" 2>/dev/null; then
411
+ local children child
412
+ children="$(pgrep -P "$pid" 2>/dev/null || true)"
413
+ for child in $children; do
414
+ force_kill_process_tree "$child"
415
+ done
416
+ kill -KILL "$pid" 2>/dev/null || true
417
+ fi
418
+ }
419
+
420
+ start_logged_bridge() {
421
+ local log_file="$1"
422
+ local tail_var="$2"
423
+ local mode="$3"
424
+ shift 3
425
+
426
+ local log_level="info"
427
+ if [ "$DEBUG_MODE" = "true" ]; then
428
+ log_level="debug"
429
+ fi
430
+
431
+ if [ "$mode" = "append" ]; then
432
+ AAMP_LOG_FILE="$log_file" AAMP_LOG_LEVEL="$log_level" "$@" >>"$log_file" 2>&1 &
433
+ else
434
+ AAMP_LOG_FILE="$log_file" AAMP_LOG_LEVEL="$log_level" "$@" >"$log_file" 2>&1 &
435
+ fi
436
+ STARTED_BRIDGE_PID=$!
437
+
438
+ if [ "$AAMP_TAIL_BRIDGE_LOGS" = "true" ]; then
439
+ if [ "$mode" = "append" ]; then
440
+ tail -n 0 -f "$log_file" &
441
+ else
442
+ tail -n +1 -f "$log_file" &
443
+ fi
444
+ printf -v "$tail_var" '%s' "$!"
445
+ else
446
+ printf -v "$tail_var" ''
447
+ fi
448
+ }
449
+
450
+ acquire_bot_selection_lock() {
451
+ local attempt owner owner_pid
452
+ mkdir -p "$(dirname "$BOT_SELECTION_LOCK_DIR")"
453
+ for attempt in $(seq 1 300); do
454
+ if mkdir "$BOT_SELECTION_LOCK_DIR" 2>/dev/null; then
455
+ printf '%s\n' "$ONE_CLICK_RUN_ID" >"$BOT_SELECTION_LOCK_DIR/owner" 2>/dev/null || true
456
+ return 0
457
+ fi
458
+ owner="$(cat "$BOT_SELECTION_LOCK_DIR/owner" 2>/dev/null || true)"
459
+ owner_pid="${owner##*-}"
460
+ if [ -n "$owner_pid" ] && [ "$owner_pid" != "$owner" ] && ! kill -0 "$owner_pid" 2>/dev/null; then
461
+ rm -f "$BOT_SELECTION_LOCK_DIR/owner" 2>/dev/null || true
462
+ rmdir "$BOT_SELECTION_LOCK_DIR" 2>/dev/null || true
463
+ continue
464
+ fi
465
+ sleep 0.2
466
+ done
467
+ agent_fail "timed out waiting for Feishu bot selection lock"
468
+ }
469
+
470
+ release_bot_selection_lock() {
471
+ local owner
472
+ owner="$(cat "$BOT_SELECTION_LOCK_DIR/owner" 2>/dev/null || true)"
473
+ [ "$owner" = "$ONE_CLICK_RUN_ID" ] || return 0
474
+ rm -f "$BOT_SELECTION_LOCK_DIR/owner" 2>/dev/null || true
475
+ rmdir "$BOT_SELECTION_LOCK_DIR" 2>/dev/null || true
476
+ }
477
+
478
+ acquire_dir_lock() {
479
+ local lock_dir="$1"
480
+ local label="$2"
481
+ local attempt owner owner_pid
482
+ mkdir -p "$(dirname "$lock_dir")"
483
+ for attempt in $(seq 1 600); do
484
+ if mkdir "$lock_dir" 2>/dev/null; then
485
+ printf '%s\n' "$ONE_CLICK_RUN_ID" >"$lock_dir/owner" 2>/dev/null || true
486
+ return 0
487
+ fi
488
+ owner="$(cat "$lock_dir/owner" 2>/dev/null || true)"
489
+ owner_pid="${owner##*-}"
490
+ if [ -n "$owner_pid" ] && [ "$owner_pid" != "$owner" ] && ! kill -0 "$owner_pid" 2>/dev/null; then
491
+ rm -f "$lock_dir/owner" 2>/dev/null || true
492
+ rmdir "$lock_dir" 2>/dev/null || true
493
+ continue
494
+ fi
495
+ sleep 0.2
496
+ done
497
+ agent_fail "timed out waiting for $label lock"
498
+ }
499
+
500
+ release_dir_lock() {
501
+ local lock_dir="$1"
502
+ local owner
503
+ owner="$(cat "$lock_dir/owner" 2>/dev/null || true)"
504
+ [ "$owner" = "$ONE_CLICK_RUN_ID" ] || return 0
505
+ rm -f "$lock_dir/owner" 2>/dev/null || true
506
+ rmdir "$lock_dir" 2>/dev/null || true
507
+ }
508
+
509
+ with_lark_cli_install_lock() {
510
+ acquire_dir_lock "$LARK_CLI_INSTALL_LOCK_DIR" "lark-cli install"
511
+ set +e
512
+ "$@"
513
+ local status=$?
514
+ set -e
515
+ release_dir_lock "$LARK_CLI_INSTALL_LOCK_DIR"
516
+ return "$status"
517
+ }
518
+
519
+ with_lark_cli_config_lock() {
520
+ acquire_dir_lock "$LARK_CLI_CONFIG_LOCK_DIR" "lark-cli config"
521
+ set +e
522
+ "$@"
523
+ local status=$?
524
+ set -e
525
+ release_dir_lock "$LARK_CLI_CONFIG_LOCK_DIR"
526
+ return "$status"
527
+ }
528
+
529
+ with_task_agent_update_lock() {
530
+ acquire_dir_lock "$TASK_AGENT_UPDATE_LOCK_DIR" "Feishu task agent update"
531
+ set +e
532
+ "$@"
533
+ local status=$?
534
+ set -e
535
+ release_dir_lock "$TASK_AGENT_UPDATE_LOCK_DIR"
536
+ return "$status"
537
+ }
538
+
539
+ cleanup_stale_one_click_processes() {
540
+ [ "$AAMP_STALE_PROCESS_CLEANUP" = "true" ] || return 0
541
+ command -v ps >/dev/null 2>&1 || return 0
542
+
543
+ local pass stale line pid command
544
+ for pass in 1 2; do
545
+ stale=()
546
+ while IFS= read -r line; do
547
+ [ -n "$line" ] || continue
548
+ stale+=("$line")
549
+ done < <(
550
+ ps -axo pid=,ppid=,etime=,command= 2>/dev/null | awk -v min_age="$AAMP_STALE_PROCESS_SECONDS" -v self="$$" '
551
+ function etime_seconds(value, days, parts, count, time_parts) {
552
+ days = 0
553
+ if (index(value, "-") > 0) {
554
+ count = split(value, parts, "-")
555
+ days = parts[1] + 0
556
+ value = parts[count]
557
+ }
558
+ count = split(value, time_parts, ":")
559
+ if (count == 3) return days * 86400 + time_parts[1] * 3600 + time_parts[2] * 60 + time_parts[3]
560
+ if (count == 2) return days * 86400 + time_parts[1] * 60 + time_parts[2]
561
+ return days * 86400 + value
562
+ }
563
+ {
564
+ pid = $1
565
+ ppid = $2
566
+ etime = $3
567
+ $1 = ""; $2 = ""; $3 = ""
568
+ sub(/^[[:space:]]+/, "")
569
+ command = $0
570
+
571
+ if (pid == self) next
572
+ if (ppid != 1) next
573
+
574
+ age = etime_seconds(etime)
575
+ if (age < min_age) next
576
+
577
+ if (command ~ /aamp-one-click-npm-cache/ && command ~ /(aamp-acp-bridge|aamp-feishu-bridge|aamp-feishu-task-bridge|aamp-cli-bridge|codex-acp|acpx)/) {
578
+ print pid "\t" command
579
+ next
580
+ }
581
+
582
+ if (command ~ /node_modules\/\.bin\/(aamp-acp-bridge|aamp-feishu-bridge|aamp-feishu-task-bridge|aamp-cli-bridge|codex-acp)/) {
583
+ print pid "\t" command
584
+ next
585
+ }
586
+
587
+ if (command ~ /acpx.*__queue-owner/) {
588
+ print pid "\t" command
589
+ next
590
+ }
591
+ }
592
+ '
593
+ )
594
+
595
+ [ "${#stale[@]}" -gt 0 ] || return 0
596
+
597
+ agent_log "cleaning stale one-click orphan process(es) older than ${AAMP_STALE_PROCESS_SECONDS}s"
598
+ for line in "${stale[@]}"; do
599
+ pid="${line%%$'\t'*}"
600
+ command="${line#*$'\t'}"
601
+ agent_log "stopping stale process pid=$pid command=$command"
602
+ kill "$pid" 2>/dev/null || true
603
+ done
604
+
605
+ [ "$pass" -eq 1 ] && sleep 1
606
+ done
607
+ }
608
+
609
+ os_name() {
610
+ uname -s 2>/dev/null || printf 'unknown'
611
+ }
612
+
613
+ is_macos() {
614
+ [ "$(os_name)" = "Darwin" ]
615
+ }
616
+
617
+ path_prepend() {
618
+ local path_entry="$1"
619
+ [ -n "$path_entry" ] || return 0
620
+ case ":$PATH:" in
621
+ *":$path_entry:"*) ;;
622
+ *) export PATH="$path_entry:$PATH" ;;
623
+ esac
624
+ }
625
+
626
+ run_brew() {
627
+ # Feed one "yes" for Homebrew prompts such as dependency installation confirmation.
628
+ printf 'y\n' | HOMEBREW_NO_ENV_HINTS=1 brew "$@"
629
+ }
630
+
631
+ refresh_node_toolchain_bins() {
632
+ NPM_BIN="$(command -v npm || true)"
633
+ NPX_BIN="$(command -v npx || true)"
634
+ }
635
+
636
+ print_node_toolchain_help() {
637
+ cat >&2 <<'HELP'
638
+
639
+ Automatic Node.js/npm installation did not complete.
640
+ Install it manually with one of these commands, then reopen the terminal or fix PATH:
641
+ Homebrew: brew install node
642
+ Volta: volta install node npm
643
+ fnm: fnm install --lts
644
+ nvm: nvm install --lts
645
+
646
+ HELP
647
+ }
648
+
649
+ try_install_node_toolchain() {
650
+ local node_path
651
+ node_path="$(command -v node || true)"
652
+
653
+ if command -v volta >/dev/null 2>&1 && { [ -z "$node_path" ] || [[ "$node_path" == *"/.volta/"* ]]; }; then
654
+ agent_log "installing Node.js/npm with Volta"
655
+ volta install node npm
656
+ return $?
657
+ fi
658
+
659
+ if command -v fnm >/dev/null 2>&1 && { [ -z "$node_path" ] || [[ "$node_path" == *"/.fnm/"* ]]; }; then
660
+ agent_log "installing Node.js/npm with fnm"
661
+ fnm install --lts
662
+ eval "$(fnm env --shell bash)"
663
+ return 0
664
+ fi
665
+
666
+ local nvm_dir="${NVM_DIR:-$HOME/.nvm}"
667
+ if [ -s "$nvm_dir/nvm.sh" ] && { [ -z "$node_path" ] || [[ "$node_path" == *"/.nvm/"* ]]; }; then
668
+ agent_log "installing Node.js/npm with nvm"
669
+ # shellcheck disable=SC1090
670
+ source "$nvm_dir/nvm.sh"
671
+ nvm install --lts
672
+ nvm use --lts
673
+ return 0
674
+ fi
675
+
676
+ if command -v brew >/dev/null 2>&1; then
677
+ local formula="node"
678
+ if brew list --versions node@22 >/dev/null 2>&1; then
679
+ formula="node@22"
680
+ elif brew list --versions node >/dev/null 2>&1; then
681
+ formula="node"
682
+ fi
683
+
684
+ if brew list --versions "$formula" >/dev/null 2>&1; then
685
+ agent_log "reinstalling $formula with Homebrew to restore npm/npx"
686
+ run_brew reinstall "$formula"
687
+ else
688
+ agent_log "installing Node.js/npm with Homebrew"
689
+ run_brew install "$formula"
690
+ fi
691
+ return $?
692
+ fi
693
+
694
+ agent_log "no supported Node.js installer found; tried Volta, fnm, nvm, and Homebrew"
695
+ return 1
696
+ }
697
+
698
+ ensure_node_toolchain() {
699
+ refresh_node_toolchain_bins
700
+ if [ -n "$NPM_BIN" ] && [ -n "$NPX_BIN" ]; then
701
+ configure_npm_registry
702
+ return 0
703
+ fi
704
+
705
+ agent_log "npm/npx not found; installing Node.js/npm automatically"
706
+ if ! try_install_node_toolchain; then
707
+ print_node_toolchain_help
708
+ agent_fail "failed to install Node.js/npm automatically"
709
+ fi
710
+
711
+ hash -r 2>/dev/null || true
712
+ refresh_node_toolchain_bins
713
+ [ -n "$NPM_BIN" ] && [ -n "$NPX_BIN" ] || {
714
+ print_node_toolchain_help
715
+ agent_fail "npm/npx is still unavailable after installation"
716
+ }
717
+ configure_npm_registry
718
+ }
719
+
720
+ configure_npm_registry() {
721
+ sanitize_inherited_npm_exec_env
722
+ mkdir -p "$NPM_CACHE_DIR"
723
+ mkdir -p "$NPM_GLOBAL_PREFIX/bin"
724
+ path_prepend "$NPM_GLOBAL_PREFIX/bin"
725
+ export npm_config_cache="$NPM_CACHE_DIR"
726
+ export NPM_CONFIG_CACHE="$NPM_CACHE_DIR"
727
+ export npm_config_registry="$NPM_REGISTRY"
728
+ export NPM_CONFIG_REGISTRY="$NPM_REGISTRY"
729
+ export npm_config_prefix="$NPM_GLOBAL_PREFIX"
730
+ export NPM_CONFIG_PREFIX="$NPM_GLOBAL_PREFIX"
731
+ "$NPM_BIN" config set registry "$NPM_REGISTRY" >/dev/null 2>&1 || true
732
+ agent_detail "using npm registry: $NPM_REGISTRY"
733
+ agent_detail "using npm cache: $NPM_CACHE_DIR"
734
+ agent_detail "using npm global prefix: $NPM_GLOBAL_PREFIX"
735
+ }
736
+
737
+ script_file_task_agent_version() {
738
+ local file="$1"
739
+ [ -f "$file" ] || return 1
740
+ sed -n 's/^AAMP_TASK_AGENT_VERSION="\([^"]*\)".*/\1/p' "$file" | head -n 1
741
+ }
742
+
743
+ task_agent_global_package_dir() {
744
+ printf '%s/lib/node_modules/%s' "$NPM_GLOBAL_PREFIX" "$AAMP_TASK_AGENT_NAME"
745
+ }
746
+
747
+ task_agent_global_package_version() {
748
+ local package_json
749
+ package_json="$(task_agent_global_package_dir)/package.json"
750
+ [ -r "$package_json" ] || return 1
751
+ PACKAGE_JSON="$package_json" node -e '
752
+ const fs = require("fs");
753
+ try {
754
+ const data = JSON.parse(fs.readFileSync(process.env.PACKAGE_JSON, "utf8"));
755
+ if (typeof data.version !== "string" || data.version.length === 0) process.exit(1);
756
+ process.stdout.write(data.version);
757
+ } catch {
758
+ process.exit(1);
759
+ }
760
+ ' 2>>"$ONE_CLICK_LOG"
761
+ }
762
+
763
+ task_agent_global_install_is_complete() {
764
+ local package_dir
765
+ package_dir="$(task_agent_global_package_dir)"
766
+ [ -x "$NPM_GLOBAL_PREFIX/bin/aamp-logs" ] || return 1
767
+ [ -r "$package_dir/bootstrap/aamp-feishu-task-agent-bootstrap.sh" ] || return 1
768
+ [ -r "$package_dir/bin/feishu-task-agent-controller.mjs" ] || return 1
769
+ }
770
+
771
+ task_agent_global_install_is_current() {
772
+ local expected_version="$1"
773
+ local installed_version
774
+ installed_version="$(task_agent_global_package_version || true)"
775
+ [ "$installed_version" = "$expected_version" ] || return 1
776
+ task_agent_global_install_is_complete
777
+ }
778
+
779
+ ensure_task_agent_global_install_locked() {
780
+ local expected_version="$1"
781
+ if task_agent_global_install_is_current "$expected_version"; then
782
+ return 0
783
+ fi
784
+
785
+ local installed_version
786
+ installed_version="$(task_agent_global_package_version || true)"
787
+ if [ -n "$installed_version" ] && task_agent_version_is_newer "$expected_version" "$installed_version" && task_agent_global_install_is_complete; then
788
+ agent_detail "keeping newer task-agent npm package: installed=$installed_version requested=$expected_version"
789
+ return 0
790
+ fi
791
+ agent_detail "synchronizing task-agent npm package: current=${installed_version:-missing} expected=$expected_version"
792
+ npm_install_global "$AAMP_TASK_AGENT_NAME@$expected_version" >>"$ONE_CLICK_LOG" 2>&1 || return 1
793
+ task_agent_global_install_is_current "$expected_version"
794
+ }
795
+
796
+ ensure_task_agent_global_install() {
797
+ local expected_version="$1"
798
+ if task_agent_global_install_is_current "$expected_version"; then
799
+ return 0
800
+ fi
801
+ with_task_agent_update_lock ensure_task_agent_global_install_locked "$expected_version"
802
+ }
803
+
804
+ short_command_is_current() {
805
+ local target="$1"
806
+ local installed_version
807
+ [ -x "$target" ] || return 1
808
+ installed_version="$(script_file_task_agent_version "$target" || true)"
809
+ [ "$installed_version" = "$AAMP_TASK_AGENT_VERSION" ]
810
+ }
811
+
812
+ write_task_update_cache() {
813
+ local latest="${1:-$AAMP_TASK_AGENT_VERSION}"
814
+ mkdir -p "$(dirname "$AAMP_TASK_UPDATE_CACHE_FILE")"
815
+ CACHE_FILE="$AAMP_TASK_UPDATE_CACHE_FILE" CURRENT_VERSION="$AAMP_TASK_AGENT_VERSION" LATEST_VERSION="$latest" node -e '
816
+ const fs = require("fs");
817
+ const file = process.env.CACHE_FILE;
818
+ const payload = {
819
+ version: 1,
820
+ checked_at: Math.floor(Date.now() / 1000),
821
+ current_version: process.env.CURRENT_VERSION || "",
822
+ latest_version: process.env.LATEST_VERSION || process.env.CURRENT_VERSION || "",
823
+ };
824
+ fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n");
825
+ ' 2>>"$ONE_CLICK_LOG" || true
826
+ }
827
+
828
+ task_update_cache_is_fresh() {
829
+ [ -f "$AAMP_TASK_UPDATE_CACHE_FILE" ] || return 1
830
+ CACHE_FILE="$AAMP_TASK_UPDATE_CACHE_FILE" TTL="$AAMP_TASK_UPDATE_CACHE_TTL_SECONDS" CURRENT_VERSION="$AAMP_TASK_AGENT_VERSION" node -e '
831
+ const fs = require("fs");
832
+ let data;
833
+ try { data = JSON.parse(fs.readFileSync(process.env.CACHE_FILE, "utf8")); } catch { process.exit(1); }
834
+ const checkedAt = Number(data.checked_at || 0);
835
+ const ttl = Number(process.env.TTL || 86400);
836
+ if (!Number.isFinite(checkedAt) || checkedAt <= 0 || !Number.isFinite(ttl) || ttl <= 0) process.exit(1);
837
+ if (String(data.current_version || "") !== String(process.env.CURRENT_VERSION || "")) process.exit(1);
838
+ const age = Math.floor(Date.now() / 1000) - checkedAt;
839
+ process.exit(age >= 0 && age < ttl ? 0 : 1);
840
+ ' 2>>"$ONE_CLICK_LOG"
841
+ }
842
+
843
+ install_short_command_from_version() {
844
+ local version="$1"
845
+ local target="$2"
846
+ local package_dir source_file tmp
847
+ tmp="${target}.tmp.$$"
848
+ mkdir -p "$(dirname "$target")"
849
+ ensure_task_agent_global_install "$version" || return 1
850
+ package_dir="$(task_agent_global_package_dir)"
851
+ source_file="$package_dir/bootstrap/aamp-feishu-task-agent-bootstrap.sh"
852
+ [ -r "$source_file" ] || return 1
853
+ agent_detail "installing short command from task-agent $version to $target"
854
+ cp "$source_file" "$tmp" 2>>"$ONE_CLICK_LOG" || return 1
855
+ chmod +x "$tmp" 2>/dev/null || true
856
+ mv "$tmp" "$target"
857
+ }
858
+
859
+ install_command_shim() {
860
+ local target="$1"
861
+ local command_name="$2"
862
+ mkdir -p "$AAMP_TASK_SHIM_DIR" 2>/dev/null || return 0
863
+ local shim="$AAMP_TASK_SHIM_DIR/$command_name"
864
+ local tmp="${shim}.tmp.$$"
865
+ cat >"$tmp" <<EOF
866
+ #!/usr/bin/env bash
867
+ AAMP_TASK_ENTRY=short exec "$target" "\$@"
868
+ EOF
869
+ chmod +x "$tmp" 2>/dev/null || true
870
+ if [ -f "$shim" ] && cmp -s "$tmp" "$shim"; then
871
+ rm -f "$tmp" 2>/dev/null || true
872
+ chmod +x "$shim" 2>/dev/null || true
873
+ return 0
874
+ fi
875
+ mv "$tmp" "$shim" 2>/dev/null || rm -f "$tmp" 2>/dev/null || true
876
+ }
877
+
878
+ install_short_command_shim() {
879
+ install_command_shim "$1" "$AAMP_TASK_COMMAND_NAME"
880
+ }
881
+
882
+
883
+ install_short_command_current() {
884
+ local target="$1"
885
+ local source_file="${BASH_SOURCE[0]:-}"
886
+ local tmp="${target}.tmp.$$"
887
+ if short_command_is_current "$target"; then
888
+ agent_detail "short command already current: $target"
889
+ return 0
890
+ fi
891
+ case "$source_file" in
892
+ ""|/dev/fd/*|/private/dev/fd/*|/proc/*) source_file="" ;;
893
+ esac
894
+ if [ -n "$source_file" ] && [ -f "$source_file" ] && [ -r "$source_file" ]; then
895
+ mkdir -p "$(dirname "$target")"
896
+ cp "$source_file" "$tmp" 2>>"$ONE_CLICK_LOG" || return 1
897
+ chmod +x "$tmp" 2>/dev/null || true
898
+ mv "$tmp" "$target"
899
+ return 0
900
+ fi
901
+ install_short_command_from_version "$AAMP_TASK_AGENT_VERSION" "$target"
902
+ }
903
+
904
+ install_short_command() {
905
+ install_short_command_current "$AAMP_TASK_COMMAND_PATH" || {
906
+ agent_detail "failed to install short command at $AAMP_TASK_COMMAND_PATH; continuing current run"
907
+ return 1
908
+ }
909
+ install_short_command_shim "$AAMP_TASK_COMMAND_PATH"
910
+ agent_detail "short command installed: $AAMP_TASK_COMMAND_PATH"
911
+ agent_detail "short command shim: $AAMP_TASK_SHIM_DIR/$AAMP_TASK_COMMAND_NAME"
912
+ }
913
+
914
+ record_install_success() {
915
+ write_one_click_log "[aamp-one-click] installed: $AAMP_TASK_COMMAND_PATH"
916
+ write_one_click_log "[aamp-one-click] start with: $AAMP_TASK_COMMAND_NAME"
917
+ }
918
+
919
+ ensure_selected_agent_for_start() {
920
+ if [ -z "$AGENT" ]; then
921
+ select_agent_interactively
922
+ fi
923
+ validate_agent_name "$AGENT"
924
+ }
925
+
926
+ resolve_latest_task_agent_version() {
927
+ sanitize_inherited_npm_exec_env
928
+ "$NPM_BIN" view "$AAMP_TASK_AGENT_NAME@$AAMP_TASK_AGENT_CHANNEL" version \
929
+ --registry "$NPM_REGISTRY" \
930
+ --cache "$NPM_CACHE_DIR" \
931
+ --prefix "$NPM_GLOBAL_PREFIX" 2>/dev/null | tail -n 1 | tr -d '[:space:]'
932
+ }
933
+
934
+ task_agent_version_is_newer() {
935
+ VERSION_CURRENT="$1" VERSION_LATEST="$2" node -e '
936
+ function parse(value) {
937
+ const match = String(value || "").match(/^(\d+)\.(\d+)\.(\d+)(?:-dev\.(\d+))?$/);
938
+ if (!match) return null;
939
+ return [Number(match[1]), Number(match[2]), Number(match[3]), match[4] === undefined ? Number.MAX_SAFE_INTEGER : Number(match[4])];
940
+ }
941
+ const current = parse(process.env.VERSION_CURRENT);
942
+ const latest = parse(process.env.VERSION_LATEST);
943
+ if (!process.env.VERSION_LATEST || process.env.VERSION_LATEST === process.env.VERSION_CURRENT) process.exit(1);
944
+ if (!current || !latest) process.exit(0);
945
+ for (let index = 0; index < current.length; index += 1) {
946
+ if (latest[index] > current[index]) process.exit(0);
947
+ if (latest[index] < current[index]) process.exit(1);
948
+ }
949
+ process.exit(1);
950
+ '
951
+ }
952
+
953
+ auto_update_short_command_if_needed() {
954
+ local mode="${1:-normal}"
955
+ [ "$AAMP_TASK_AUTO_UPDATE" = "true" ] || return 0
956
+ [ "$AAMP_TASK_AUTO_UPDATE_DONE" != "true" ] || return 0
957
+ [ -n "$NPM_BIN" ] || return 0
958
+ if [ "$mode" != "force" ] && task_update_cache_is_fresh; then
959
+ agent_detail "task-agent auto-update skipped: cache fresh"
960
+ return 0
961
+ fi
962
+
963
+ local latest
964
+ latest="$(resolve_latest_task_agent_version || true)"
965
+ if [ -z "$latest" ]; then
966
+ agent_detail "task-agent auto-update check skipped: cannot resolve latest version"
967
+ return 0
968
+ fi
969
+ agent_detail "task-agent auto-update check: current=$AAMP_TASK_AGENT_VERSION latest=$latest channel=$AAMP_TASK_AGENT_CHANNEL mode=$mode"
970
+ if ! task_agent_version_is_newer "$AAMP_TASK_AGENT_VERSION" "$latest"; then
971
+ write_task_update_cache "$latest"
972
+ return 0
973
+ fi
974
+
975
+ if ensure_task_agent_global_install "$latest" && \
976
+ install_aamp_logs_bin "$latest" && \
977
+ install_short_command_from_version "$latest" "$AAMP_TASK_COMMAND_PATH"; then
978
+ install_short_command_shim "$AAMP_TASK_COMMAND_PATH"
979
+ write_task_update_cache "$latest"
980
+ agent_detail "task-agent auto-updated from $AAMP_TASK_AGENT_VERSION to $latest"
981
+ agent_detail "restarting with updated task-agent $latest"
982
+ AAMP_TASK_AUTO_UPDATE_DONE=true exec "$AAMP_TASK_COMMAND_PATH" "${RESTART_ARGS[@]}"
983
+ fi
984
+ agent_detail "task-agent auto-update to $latest failed; continuing current version"
985
+ }
986
+
987
+ adopt_newer_global_task_agent_if_available() {
988
+ local installed_version
989
+ installed_version="$(task_agent_global_package_version || true)"
990
+ [ -n "$installed_version" ] || return 0
991
+ task_agent_version_is_newer "$AAMP_TASK_AGENT_VERSION" "$installed_version" || return 0
992
+
993
+ agent_detail "adopting newer task-agent from npm prefix: current=$AAMP_TASK_AGENT_VERSION installed=$installed_version"
994
+ install_aamp_logs_bin "$installed_version" || return 1
995
+ install_short_command_from_version "$installed_version" "$AAMP_TASK_COMMAND_PATH" || return 1
996
+ install_short_command_shim "$AAMP_TASK_COMMAND_PATH"
997
+ write_task_update_cache "$installed_version"
998
+ AAMP_TASK_AUTO_UPDATE_DONE=true exec "$AAMP_TASK_COMMAND_PATH" "${RESTART_ARGS[@]}"
999
+ }
1000
+
1001
+ run_task_agent_update_command() {
1002
+ local installed_version
1003
+ auto_update_short_command_if_needed force
1004
+ installed_version="$(script_file_task_agent_version "$AAMP_TASK_COMMAND_PATH" 2>/dev/null || printf '%s' "$AAMP_TASK_AGENT_VERSION")"
1005
+ install_aamp_logs_bin "$installed_version" || agent_fail "failed to synchronize aamp-logs $installed_version"
1006
+ printf '当前版本:%s\n' "$installed_version"
1007
+ }
1008
+ npm_install_global() {
1009
+ local npm_log
1010
+ npm_log="$(mktemp "${TMPDIR:-/tmp}/aamp-npm-install.XXXXXX")"
1011
+ sanitize_inherited_npm_exec_env
1012
+ if "$NPM_BIN" install -g \
1013
+ --registry "$NPM_REGISTRY" \
1014
+ --cache "$NPM_CACHE_DIR" \
1015
+ --prefix "$NPM_GLOBAL_PREFIX" \
1016
+ "$@" >"$npm_log" 2>&1; then
1017
+ cat "$npm_log"
1018
+ hash -r 2>/dev/null || true
1019
+ return 0
1020
+ fi
1021
+
1022
+ if ! npm_log_indicates_cache_error "$npm_log"; then
1023
+ cat "$npm_log" >&2
1024
+ return 1
1025
+ fi
1026
+
1027
+ reset_npm_cache_for_retry
1028
+ npm_log="$(mktemp "${TMPDIR:-/tmp}/aamp-npm-install-retry.XXXXXX")"
1029
+ sanitize_inherited_npm_exec_env
1030
+ "$NPM_BIN" install -g \
1031
+ --registry "$NPM_REGISTRY" \
1032
+ --cache "$NPM_CACHE_DIR" \
1033
+ --prefix "$NPM_GLOBAL_PREFIX" \
1034
+ "$@" >"$npm_log" 2>&1 || {
1035
+ cat "$npm_log" >&2
1036
+ return 1
1037
+ }
1038
+ cat "$npm_log"
1039
+ hash -r 2>/dev/null || true
1040
+ }
1041
+
1042
+ npx_package() {
1043
+ local npm_log
1044
+ local status
1045
+ local package_spec=""
1046
+ local package_args=()
1047
+ if [ "${1:-}" = "--package" ]; then
1048
+ [ -n "${2:-}" ] || agent_fail "npx_package requires a package value after --package"
1049
+ package_spec="$2"
1050
+ package_args=(--package "$package_spec")
1051
+ shift 2
1052
+ fi
1053
+ npm_log="$(mktemp "${TMPDIR:-/tmp}/aamp-npx.XXXXXX")"
1054
+ sanitize_inherited_npm_exec_env
1055
+ set +e
1056
+ "$NPM_BIN" exec --yes --registry "$NPM_REGISTRY" --cache "$NPM_CACHE_DIR" "${package_args[@]}" -- "$@" 2>&1 | tee "$npm_log"
1057
+ status=${PIPESTATUS[0]}
1058
+ set -e
1059
+ if [ "$status" -eq 0 ]; then
1060
+ return 0
1061
+ fi
1062
+
1063
+ if [ -n "$package_spec" ] && npm_log_indicates_exec_bin_error "$npm_log"; then
1064
+ run_global_package_bin "$package_spec" "$@"
1065
+ return $?
1066
+ fi
1067
+
1068
+ if ! npm_log_indicates_cache_error "$npm_log"; then
1069
+ return "$status"
1070
+ fi
1071
+
1072
+ reset_npm_cache_for_retry
1073
+ npm_log="$(mktemp "${TMPDIR:-/tmp}/aamp-npx-retry.XXXXXX")"
1074
+ sanitize_inherited_npm_exec_env
1075
+ set +e
1076
+ "$NPM_BIN" exec --yes --registry "$NPM_REGISTRY" --cache "$NPM_CACHE_DIR" "${package_args[@]}" -- "$@" 2>&1 | tee "$npm_log"
1077
+ status=${PIPESTATUS[0]}
1078
+ set -e
1079
+ if [ "$status" -ne 0 ] && [ -n "$package_spec" ] && npm_log_indicates_exec_bin_error "$npm_log"; then
1080
+ run_global_package_bin "$package_spec" "$@"
1081
+ return $?
1082
+ fi
1083
+ return "$status"
1084
+ }
1085
+
1086
+ npm_log_indicates_exec_bin_error() {
1087
+ local npm_log="$1"
1088
+ grep -Eiq '(^|[[:space:]])(sh|bash|zsh): .*command not found|not found: .*|could not determine executable to run' "$npm_log"
1089
+ }
1090
+
1091
+ run_global_package_bin() {
1092
+ local package_spec="$1"
1093
+ local bin_name="$2"
1094
+ shift 2
1095
+ local bin_path="$NPM_GLOBAL_PREFIX/bin/$bin_name"
1096
+
1097
+ agent_log "npm exec could not resolve $bin_name; installing $package_spec into one-click npm prefix"
1098
+ npm_install_global "$package_spec" || return $?
1099
+ hash -r 2>/dev/null || true
1100
+ [ -x "$bin_path" ] || {
1101
+ agent_log "expected executable not found after install: $bin_path"
1102
+ return 127
1103
+ }
1104
+ "$bin_path" "$@"
1105
+ }
1106
+
1107
+ npm_log_indicates_cache_error() {
1108
+ local npm_log="$1"
1109
+ grep -Eiq 'Invalid response body|_cacache|ENOENT.*(_cacache|content-v2)|tarball data.*corrupted|TAR_BAD_ARCHIVE|zlib: unexpected end of file' "$npm_log"
1110
+ }
1111
+
1112
+ reset_npm_cache_for_retry() {
1113
+ NPM_CACHE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/aamp-one-click-npm-cache.XXXXXX")"
1114
+ export npm_config_cache="$NPM_CACHE_DIR"
1115
+ export NPM_CONFIG_CACHE="$NPM_CACHE_DIR"
1116
+ agent_log "npm cache appears corrupted; retrying with fresh npm cache: $NPM_CACHE_DIR"
1117
+ }
1118
+
1119
+ npm_install_register_helper() {
1120
+ local workdir="$1"
1121
+ local npm_log
1122
+ npm_log="$(mktemp "${TMPDIR:-/tmp}/aamp-register-helper-install.XXXXXX")"
1123
+
1124
+ sanitize_inherited_npm_exec_env
1125
+ if "$NPM_BIN" install \
1126
+ --prefix "$workdir" \
1127
+ --registry "$NPM_REGISTRY" \
1128
+ --cache "$NPM_CACHE_DIR" \
1129
+ "$LARK_REGISTER_APP_SDK" >"$npm_log" 2>&1; then
1130
+ return 0
1131
+ fi
1132
+
1133
+ if ! npm_log_indicates_cache_error "$npm_log"; then
1134
+ cat "$npm_log" >&2
1135
+ return 1
1136
+ fi
1137
+
1138
+ reset_npm_cache_for_retry
1139
+ npm_log="$(mktemp "${TMPDIR:-/tmp}/aamp-register-helper-install-retry.XXXXXX")"
1140
+ sanitize_inherited_npm_exec_env
1141
+ "$NPM_BIN" install \
1142
+ --prefix "$workdir" \
1143
+ --registry "$NPM_REGISTRY" \
1144
+ --cache "$NPM_CACHE_DIR" \
1145
+ "$LARK_REGISTER_APP_SDK" >"$npm_log" 2>&1 || {
1146
+ cat "$npm_log" >&2
1147
+ return 1
1148
+ }
1149
+ }
1150
+
1151
+ validate_agent_name() {
1152
+ case "$1" in
1153
+ codex|cursor) ;;
1154
+ *) agent_fail "--agent must be codex or cursor" ;;
1155
+ esac
1156
+ }
1157
+
1158
+ read_tty_line() {
1159
+ local prompt="$1"
1160
+ local value
1161
+ if [ ! -r /dev/tty ]; then
1162
+ agent_fail "interactive input requires a terminal"
1163
+ fi
1164
+ printf '%s' "$prompt" >/dev/tty
1165
+ IFS= read -r value </dev/tty
1166
+ printf '%s' "$value"
1167
+ }
1168
+
1169
+ agent_cli_detected() {
1170
+ case "$1" in
1171
+ codex)
1172
+ resolve_codex_cli_for_acp >/dev/null 2>&1
1173
+ ;;
1174
+ cursor)
1175
+ find_cursor_agent_cli >/dev/null 2>&1
1176
+ ;;
1177
+ *)
1178
+ return 1
1179
+ ;;
1180
+ esac
1181
+ }
1182
+
1183
+ discover_interactive_agents() {
1184
+ DETECTED_AGENTS=()
1185
+ if agent_cli_detected codex; then
1186
+ DETECTED_AGENTS+=("codex")
1187
+ fi
1188
+ if agent_cli_detected cursor; then
1189
+ DETECTED_AGENTS+=("cursor")
1190
+ fi
1191
+
1192
+ if [ "${#DETECTED_AGENTS[@]}" -eq 0 ]; then
1193
+ agent_fail "暂未检测到本地智能体。请先安装并登录 Codex 或 Cursor CLI 后重试。"
1194
+ fi
1195
+ }
1196
+
1197
+ move_agent_menu_cursor_up() {
1198
+ local line_count
1199
+ line_count=$(( ${#DETECTED_AGENTS[@]} + 2 ))
1200
+ printf '\033[%dA' "$line_count" >&3
1201
+ }
1202
+
1203
+ render_agent_menu() {
1204
+ local selected="$1"
1205
+ local index
1206
+
1207
+ printf '\033[?25l' >&3
1208
+ printf '\033[2K\r请选择要启动的 Agent:\n' >&3
1209
+ for index in "${!DETECTED_AGENTS[@]}"; do
1210
+ printf '\033[2K\r' >&3
1211
+ if [ "$index" -eq "$selected" ]; then
1212
+ printf ' > %s\n' "${DETECTED_AGENTS[$index]}" >&3
1213
+ else
1214
+ printf ' %s\n' "${DETECTED_AGENTS[$index]}" >&3
1215
+ fi
1216
+ done
1217
+ printf '\033[2K\r使用 ↑/↓ 选择,回车确认。也可按数字键或 j/k。\n' >&3
1218
+ }
1219
+
1220
+ select_agent_interactively() {
1221
+ local selected=0
1222
+ local key rest
1223
+ local tty_state
1224
+
1225
+ discover_interactive_agents
1226
+
1227
+ if ! exec 3<>/dev/tty; then
1228
+ agent_fail "missing --agent and no interactive terminal is available; pass --agent codex|cursor"
1229
+ fi
1230
+
1231
+ tty_state="$(stty -g <&3)"
1232
+ stty -echo -icanon min 1 time 0 <&3
1233
+ render_agent_menu "$selected"
1234
+ while true; do
1235
+ IFS= read -rsn1 -u 3 key || {
1236
+ stty "$tty_state" <&3
1237
+ printf '\033[?25h\n' >&3
1238
+ exec 3>&-
1239
+ agent_fail "failed to read interactive agent selection"
1240
+ }
1241
+
1242
+ case "$key" in
1243
+ ""|$'\n'|$'\r')
1244
+ AGENT="${DETECTED_AGENTS[$selected]}"
1245
+ stty "$tty_state" <&3
1246
+ printf '\033[?25h\n' >&3
1247
+ exec 3>&-
1248
+ return 0
1249
+ ;;
1250
+ $'\033')
1251
+ IFS= read -rsn2 -u 3 rest || rest=""
1252
+ case "$rest" in
1253
+ "[A")
1254
+ selected=$(( (selected + ${#DETECTED_AGENTS[@]} - 1) % ${#DETECTED_AGENTS[@]} ))
1255
+ move_agent_menu_cursor_up
1256
+ render_agent_menu "$selected"
1257
+ ;;
1258
+ "[B")
1259
+ selected=$(( (selected + 1) % ${#DETECTED_AGENTS[@]} ))
1260
+ move_agent_menu_cursor_up
1261
+ render_agent_menu "$selected"
1262
+ ;;
1263
+ "[C"|"[D")
1264
+ ;;
1265
+ esac
1266
+ ;;
1267
+ k)
1268
+ selected=$(( (selected + ${#DETECTED_AGENTS[@]} - 1) % ${#DETECTED_AGENTS[@]} ))
1269
+ move_agent_menu_cursor_up
1270
+ render_agent_menu "$selected"
1271
+ ;;
1272
+ j)
1273
+ selected=$(( (selected + 1) % ${#DETECTED_AGENTS[@]} ))
1274
+ move_agent_menu_cursor_up
1275
+ render_agent_menu "$selected"
1276
+ ;;
1277
+ [1-9])
1278
+ if [ "$key" -le "${#DETECTED_AGENTS[@]}" ]; then
1279
+ selected=$(( key - 1 ))
1280
+ AGENT="${DETECTED_AGENTS[$selected]}"
1281
+ stty "$tty_state" <&3
1282
+ move_agent_menu_cursor_up
1283
+ render_agent_menu "$selected"
1284
+ printf '\033[?25h\n' >&3
1285
+ exec 3>&-
1286
+ return 0
1287
+ fi
1288
+ ;;
1289
+ esac
1290
+ done
1291
+ }
1292
+
1293
+ render_bot_menu() {
1294
+ local selected="$1"
1295
+ local index
1296
+ local columns max_label_width label prefix
1297
+
1298
+ printf '\033[?25l' >&3
1299
+ printf '\033[2K\r请选择飞书 Bot 应用:\n' >&3
1300
+ columns="$(tput cols 2>/dev/null || printf '80')"
1301
+ case "$columns" in
1302
+ ''|*[!0-9]*) columns=80 ;;
1303
+ esac
1304
+ max_label_width=$((columns - 6))
1305
+ [ "$max_label_width" -gt 20 ] || max_label_width=20
1306
+ for index in "${!bot_labels[@]}"; do
1307
+ printf '\033[2K\r' >&3
1308
+ label="${bot_labels[$index]}"
1309
+ if [ "${#label}" -gt "$max_label_width" ]; then
1310
+ label="${label:0:$((max_label_width - 3))}..."
1311
+ fi
1312
+ if [ "$index" -eq "$selected" ]; then
1313
+ prefix=' > '
1314
+ else
1315
+ prefix=' '
1316
+ fi
1317
+ printf '%s%s\n' "$prefix" "$label" >&3
1318
+ done
1319
+ printf '\033[2K\r使用 ↑/↓ 选择,回车确认。也可按数字键或 j/k。\n' >&3
1320
+ }
1321
+
1322
+ select_bot_menu() {
1323
+ local selected=0
1324
+ local key rest
1325
+ local tty_state
1326
+ local line_count
1327
+
1328
+ if ! exec 3<>/dev/tty; then
1329
+ agent_fail "interactive bot selection requires a terminal"
1330
+ fi
1331
+
1332
+ line_count=$((${#bot_labels[@]} + 2))
1333
+ tty_state="$(stty -g <&3)"
1334
+ stty -echo -icanon min 1 time 0 <&3
1335
+ render_bot_menu "$selected"
1336
+ while true; do
1337
+ IFS= read -rsn1 -u 3 key || {
1338
+ stty "$tty_state" <&3
1339
+ printf '\033[?25h\n' >&3
1340
+ exec 3>&-
1341
+ agent_fail "failed to read interactive bot selection"
1342
+ }
1343
+
1344
+ case "$key" in
1345
+ ""|$'\n'|$'\r')
1346
+ stty "$tty_state" <&3
1347
+ printf '\033[?25h\n' >&3
1348
+ exec 3>&-
1349
+ return "$selected"
1350
+ ;;
1351
+ $'\033')
1352
+ IFS= read -rsn2 -u 3 rest || rest=""
1353
+ case "$rest" in
1354
+ "[A")
1355
+ selected=$(( (selected + ${#bot_labels[@]} - 1) % ${#bot_labels[@]} ))
1356
+ printf '\033[%dA' "$line_count" >&3
1357
+ render_bot_menu "$selected"
1358
+ ;;
1359
+ "[B")
1360
+ selected=$(( (selected + 1) % ${#bot_labels[@]} ))
1361
+ printf '\033[%dA' "$line_count" >&3
1362
+ render_bot_menu "$selected"
1363
+ ;;
1364
+ "[C"|"[D")
1365
+ ;;
1366
+ esac
1367
+ ;;
1368
+ k)
1369
+ selected=$(( (selected + ${#bot_labels[@]} - 1) % ${#bot_labels[@]} ))
1370
+ printf '\033[%dA' "$line_count" >&3
1371
+ render_bot_menu "$selected"
1372
+ ;;
1373
+ j)
1374
+ selected=$(( (selected + 1) % ${#bot_labels[@]} ))
1375
+ printf '\033[%dA' "$line_count" >&3
1376
+ render_bot_menu "$selected"
1377
+ ;;
1378
+ [1-9])
1379
+ if [ "$key" -le "${#bot_labels[@]}" ]; then
1380
+ selected=$(( key - 1 ))
1381
+ stty "$tty_state" <&3
1382
+ printf '\033[%dA' "$line_count" >&3
1383
+ render_bot_menu "$selected"
1384
+ printf '\033[?25h\n' >&3
1385
+ exec 3>&-
1386
+ return "$selected"
1387
+ fi
1388
+ ;;
1389
+ esac
1390
+ done
1391
+ }
1392
+
1393
+ parse_args() {
1394
+ local restart_source=()
1395
+ local restart_arg
1396
+ local restart_count=0
1397
+ local restart_index=0
1398
+ local restart_output_index=0
1399
+
1400
+ # Bash 3.2 with `set -u` treats an expanded empty array as unbound. Copy the
1401
+ # original positional parameters one by one so the no-argument help path
1402
+ # never expands an empty array.
1403
+ for restart_arg in "$@"; do
1404
+ restart_source[$restart_count]="$restart_arg"
1405
+ restart_count=$((restart_count + 1))
1406
+ done
1407
+
1408
+ # dev.150 restarted an updated launcher with its internal mode argument.
1409
+ if [ "${1:-}" = "normal" ]; then
1410
+ shift
1411
+ fi
1412
+
1413
+ case "${1:-}" in
1414
+ install|start|list|add|remove|update|help|__discover-agents|__register-binding|__prepare-agent|__ensure-profile)
1415
+ AAMP_TASK_ACTION="$1"
1416
+ shift
1417
+ ;;
1418
+ esac
1419
+
1420
+ if [ -z "$AAMP_TASK_ACTION" ]; then
1421
+ AAMP_TASK_ACTION="${AAMP_TASK_DEFAULT_ACTION:-install}"
1422
+ fi
1423
+
1424
+ case "$AAMP_TASK_ACTION" in
1425
+ install|start|add)
1426
+ AAMP_TASK_START_MODE="start"
1427
+ ;;
1428
+ *)
1429
+ AAMP_TASK_START_MODE="install"
1430
+ ;;
1431
+ esac
1432
+
1433
+ while [ "$#" -gt 0 ]; do
1434
+ case "$1" in
1435
+ --agent)
1436
+ AGENT="${2:-}"
1437
+ shift 2
1438
+ ;;
1439
+ --aamp-host)
1440
+ AAMP_HOST="${2:-}"
1441
+ shift 2
1442
+ ;;
1443
+ --debug)
1444
+ DEBUG_MODE="true"
1445
+ shift
1446
+ ;;
1447
+ --mock-fail-stage)
1448
+ AAMP_ONE_CLICK_MOCK_FAIL_STAGE="${2:-}"
1449
+ shift 2
1450
+ ;;
1451
+ -h|--help)
1452
+ AAMP_TASK_ACTION="help"
1453
+ shift
1454
+ ;;
1455
+ *)
1456
+ agent_fail "unknown argument: $1"
1457
+ ;;
1458
+ esac
1459
+ done
1460
+
1461
+ if [ -n "$AGENT" ]; then
1462
+ validate_agent_name "$AGENT"
1463
+ fi
1464
+
1465
+ if [ "${restart_source[0]:-}" = "normal" ]; then
1466
+ restart_index=1
1467
+ fi
1468
+ RESTART_ARGS=()
1469
+ case "${restart_source[$restart_index]:-}" in
1470
+ install|start|list|add|remove|update|help)
1471
+ ;;
1472
+ *)
1473
+ RESTART_ARGS[0]="$AAMP_TASK_ACTION"
1474
+ restart_output_index=1
1475
+ ;;
1476
+ esac
1477
+ while [ "$restart_index" -lt "$restart_count" ]; do
1478
+ RESTART_ARGS[$restart_output_index]="${restart_source[$restart_index]}"
1479
+ restart_index=$((restart_index + 1))
1480
+ restart_output_index=$((restart_output_index + 1))
1481
+ done
1482
+ }
1483
+
1484
+ source_lark_env() {
1485
+ unset HTTPS_PROXY https_proxy HTTP_PROXY http_proxy ALL_PROXY all_proxy
1486
+ mkdir -p "$AAMP_LARK_CLI_CONFIG_DIR"
1487
+ export LARKSUITE_CLI_CONFIG_DIR="$AAMP_LARK_CLI_CONFIG_DIR"
1488
+ unset LARK_CLI_NO_PROXY
1489
+ agent_detail "online enabled, proxy env cleared, LARKSUITE_CLI_CONFIG_DIR=$LARKSUITE_CLI_CONFIG_DIR"
1490
+ }
1491
+
1492
+ load_bot_configs() {
1493
+ [ -f "$BOT_CONFIG_FILE" ] || return 0
1494
+ BOT_CONFIG_FILE="$BOT_CONFIG_FILE" CURRENT_RUN_FILE="$CURRENT_RUN_FILE" ACTIVE_RUNS_FILE="$ACTIVE_RUNS_FILE" BOT_RESERVATIONS_FILE="$BOT_RESERVATIONS_FILE" ONE_CLICK_RUN_ID="$ONE_CLICK_RUN_ID" node -e '
1495
+ const fs = require("fs");
1496
+ const file = process.env.BOT_CONFIG_FILE;
1497
+ const runFile = process.env.CURRENT_RUN_FILE;
1498
+ const activeRunsFile = process.env.ACTIVE_RUNS_FILE;
1499
+ const reservationsFile = process.env.BOT_RESERVATIONS_FILE;
1500
+ const currentRunId = String(process.env.ONE_CLICK_RUN_ID || "");
1501
+ let parsed;
1502
+ try {
1503
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
1504
+ } catch {
1505
+ process.exit(0);
1506
+ }
1507
+ let currentRun;
1508
+ try {
1509
+ currentRun = JSON.parse(fs.readFileSync(runFile, "utf8"));
1510
+ } catch {}
1511
+ let activeRunStore;
1512
+ try {
1513
+ activeRunStore = JSON.parse(fs.readFileSync(activeRunsFile, "utf8"));
1514
+ } catch {}
1515
+ let reservationStore;
1516
+ try {
1517
+ reservationStore = JSON.parse(fs.readFileSync(reservationsFile, "utf8"));
1518
+ } catch {}
1519
+ function isPidAlive(pid) {
1520
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1521
+ try {
1522
+ process.kill(pid, 0);
1523
+ return true;
1524
+ } catch (error) {
1525
+ return error?.code === "EPERM";
1526
+ }
1527
+ }
1528
+ function pidFromRunId(runId) {
1529
+ const match = /-(\d+)$/.exec(String(runId || ""));
1530
+ if (!match) return 0;
1531
+ const pid = Number(match[1]);
1532
+ return Number.isInteger(pid) && pid > 0 ? pid : 0;
1533
+ }
1534
+ function isRunProcessAlive(runId) {
1535
+ return isPidAlive(pidFromRunId(runId));
1536
+ }
1537
+ function reservationPids(record) {
1538
+ return [record?.pid, record?.main_pid, record?.acp_pid, record?.cli_pid, record?.feishu_pid]
1539
+ .map((value) => Number(value))
1540
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
1541
+ }
1542
+ function isReservationActive(record) {
1543
+ const pids = reservationPids(record);
1544
+ if (pids.some(isPidAlive)) return true;
1545
+ return isRunProcessAlive(record?.run_id);
1546
+ }
1547
+ const candidateRuns = [
1548
+ ...(Array.isArray(activeRunStore?.runs) ? activeRunStore.runs : []),
1549
+ ...(currentRun?.run_id ? [currentRun] : []),
1550
+ ];
1551
+ const activeAppIds = new Set();
1552
+ for (const run of candidateRuns) {
1553
+ if (!isRunProcessAlive(run?.run_id)) continue;
1554
+ for (const pair of Array.isArray(run?.pairs) ? run.pairs : []) {
1555
+ const appId = String(pair?.app_id || "").trim();
1556
+ if (appId) activeAppIds.add(appId);
1557
+ }
1558
+ }
1559
+ for (const reservation of Array.isArray(reservationStore?.reservations) ? reservationStore.reservations : []) {
1560
+ const runId = String(reservation?.run_id || "");
1561
+ if (runId === currentRunId || !isReservationActive(reservation)) continue;
1562
+ const appId = String(reservation?.app_id || "").trim();
1563
+ if (appId) activeAppIds.add(appId);
1564
+ }
1565
+ const bots = Array.isArray(parsed?.profiles) ? parsed.profiles : [];
1566
+ const seen = new Set();
1567
+ for (const bot of bots) {
1568
+ const appId = String(bot?.app_id || "").trim();
1569
+ const profile = String(bot?.profile || "").trim();
1570
+ const name = String(bot?.display_name || appId).trim();
1571
+ const appSecret = String(bot?.app_secret || "").trim();
1572
+ if (!appId || !profile || seen.has(appId) || activeAppIds.has(appId)) continue;
1573
+ if (!appSecret) {
1574
+ continue;
1575
+ }
1576
+ seen.add(appId);
1577
+ console.log([appId, name, profile, appSecret].join("\t"));
1578
+ }
1579
+ '
1580
+ }
1581
+
1582
+ reserve_selected_bot() {
1583
+ local app_id="$1"
1584
+ local profile="$2"
1585
+ local bot_name="$3"
1586
+ [ -n "$app_id" ] || agent_fail "cannot reserve empty Feishu bot app id"
1587
+ mkdir -p "$(dirname "$BOT_RESERVATIONS_FILE")"
1588
+ set +e
1589
+ BOT_RESERVATIONS_FILE="$BOT_RESERVATIONS_FILE" CURRENT_RUN_FILE="$CURRENT_RUN_FILE" ACTIVE_RUNS_FILE="$ACTIVE_RUNS_FILE" ONE_CLICK_RUN_ID="$ONE_CLICK_RUN_ID" BOT_APP_ID="$app_id" BOT_PROFILE="$profile" BOT_NAME="$bot_name" node -e '
1590
+ const fs = require("fs");
1591
+ const path = require("path");
1592
+ const reservationsFile = process.env.BOT_RESERVATIONS_FILE;
1593
+ const runFile = process.env.CURRENT_RUN_FILE;
1594
+ const activeRunsFile = process.env.ACTIVE_RUNS_FILE;
1595
+ const runId = String(process.env.ONE_CLICK_RUN_ID || "");
1596
+ const appId = String(process.env.BOT_APP_ID || "").trim();
1597
+ const profile = String(process.env.BOT_PROFILE || "").trim();
1598
+ const name = String(process.env.BOT_NAME || appId).trim();
1599
+ if (!runId || !appId) process.exit(2);
1600
+ function readJson(file, fallback) {
1601
+ try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return fallback; }
1602
+ }
1603
+ function isPidAlive(pid) {
1604
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1605
+ try {
1606
+ process.kill(pid, 0);
1607
+ return true;
1608
+ } catch (error) {
1609
+ return error?.code === "EPERM";
1610
+ }
1611
+ }
1612
+ function pidFromRunId(runId) {
1613
+ const match = /-(\d+)$/.exec(String(runId || ""));
1614
+ if (!match) return 0;
1615
+ const pid = Number(match[1]);
1616
+ return Number.isInteger(pid) && pid > 0 ? pid : 0;
1617
+ }
1618
+ function isRunProcessAlive(runId) {
1619
+ return isPidAlive(pidFromRunId(runId));
1620
+ }
1621
+ function reservationPids(record) {
1622
+ return [record?.pid, record?.main_pid, record?.acp_pid, record?.cli_pid, record?.feishu_pid]
1623
+ .map((value) => Number(value))
1624
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
1625
+ }
1626
+ function isReservationActive(record) {
1627
+ const pids = reservationPids(record);
1628
+ if (pids.some(isPidAlive)) return true;
1629
+ return isRunProcessAlive(record?.run_id);
1630
+ }
1631
+ const currentRun = readJson(runFile, null);
1632
+ const activeRunStore = readJson(activeRunsFile, { version: 1, runs: [] });
1633
+ const reservationStore = readJson(reservationsFile, { version: 1, reservations: [] });
1634
+ for (const run of [
1635
+ ...(Array.isArray(activeRunStore?.runs) ? activeRunStore.runs : []),
1636
+ ...(currentRun?.run_id ? [currentRun] : []),
1637
+ ]) {
1638
+ if (!isRunProcessAlive(run?.run_id)) continue;
1639
+ for (const pair of Array.isArray(run?.pairs) ? run.pairs : []) {
1640
+ if (String(pair?.app_id || "").trim() === appId) {
1641
+ console.error(`Feishu bot ${appId} is already running in another one-click script.`);
1642
+ process.exit(3);
1643
+ }
1644
+ }
1645
+ }
1646
+ const reservations = [];
1647
+ for (const reservation of Array.isArray(reservationStore?.reservations) ? reservationStore.reservations : []) {
1648
+ const reservationRunId = String(reservation?.run_id || "");
1649
+ if (reservationRunId === runId || isReservationActive(reservation)) {
1650
+ reservations.push(reservation);
1651
+ }
1652
+ }
1653
+ if (reservations.some((reservation) => String(reservation?.app_id || "").trim() === appId && String(reservation?.run_id || "") !== runId)) {
1654
+ console.error(`Feishu bot ${appId} is already selected by another one-click script.`);
1655
+ process.exit(4);
1656
+ }
1657
+ const next = [
1658
+ ...reservations.filter((reservation) => String(reservation?.run_id || "") !== runId),
1659
+ {
1660
+ run_id: runId,
1661
+ pid: pidFromRunId(runId),
1662
+ main_pid: pidFromRunId(runId),
1663
+ app_id: appId,
1664
+ profile,
1665
+ display_name: name,
1666
+ reserved_at: new Date().toISOString(),
1667
+ },
1668
+ ];
1669
+ fs.mkdirSync(path.dirname(reservationsFile), { recursive: true });
1670
+ fs.writeFileSync(reservationsFile, JSON.stringify({ version: 1, reservations: next }, null, 2) + "\n");
1671
+ '
1672
+ local status=$?
1673
+ set -e
1674
+ if [ "$status" -ne 0 ]; then
1675
+ agent_fail "Feishu bot $app_id is already selected or running; choose another bot or start a new app."
1676
+ fi
1677
+ BOT_RESERVED="true"
1678
+ }
1679
+
1680
+ release_selected_bot_reservation() {
1681
+ [ "$BOT_RESERVED" = "true" ] || return 0
1682
+ [ -f "$BOT_RESERVATIONS_FILE" ] || return 0
1683
+ BOT_RESERVATIONS_FILE="$BOT_RESERVATIONS_FILE" ONE_CLICK_RUN_ID="$ONE_CLICK_RUN_ID" node -e '
1684
+ const fs = require("fs");
1685
+ const file = process.env.BOT_RESERVATIONS_FILE;
1686
+ const runId = String(process.env.ONE_CLICK_RUN_ID || "");
1687
+ let parsed;
1688
+ try {
1689
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
1690
+ } catch {
1691
+ process.exit(0);
1692
+ }
1693
+ const reservations = Array.isArray(parsed?.reservations) ? parsed.reservations : [];
1694
+ const next = reservations.filter((reservation) => String(reservation?.run_id || "") !== runId);
1695
+ if (next.length !== reservations.length) {
1696
+ fs.writeFileSync(file, JSON.stringify({ version: 1, reservations: next }, null, 2) + "\n");
1697
+ }
1698
+ '
1699
+ }
1700
+
1701
+ update_selected_bot_reservation_pids() {
1702
+ [ "$BOT_RESERVED" = "true" ] || return 0
1703
+ [ -f "$BOT_RESERVATIONS_FILE" ] || return 0
1704
+ BOT_RESERVATIONS_FILE="$BOT_RESERVATIONS_FILE" ONE_CLICK_RUN_ID="$ONE_CLICK_RUN_ID" ACP_PID="$ACP_PID" CLI_PID="$CLI_PID" FEISHU_PID="$FEISHU_PID" ACP_TAIL_PID="$ACP_TAIL_PID" CLI_TAIL_PID="$CLI_TAIL_PID" FEISHU_TAIL_PID="$FEISHU_TAIL_PID" node -e '
1705
+ const fs = require("fs");
1706
+ const file = process.env.BOT_RESERVATIONS_FILE;
1707
+ const runId = String(process.env.ONE_CLICK_RUN_ID || "");
1708
+ function numberOrUndefined(value) {
1709
+ const number = Number(value);
1710
+ return Number.isInteger(number) && number > 0 ? number : undefined;
1711
+ }
1712
+ let parsed;
1713
+ try {
1714
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
1715
+ } catch {
1716
+ process.exit(0);
1717
+ }
1718
+ const reservations = Array.isArray(parsed?.reservations) ? parsed.reservations : [];
1719
+ let changed = false;
1720
+ const next = reservations.map((reservation) => {
1721
+ if (String(reservation?.run_id || "") !== runId) return reservation;
1722
+ changed = true;
1723
+ return {
1724
+ ...reservation,
1725
+ pid: numberOrUndefined(reservation?.pid) || numberOrUndefined(reservation?.main_pid),
1726
+ main_pid: numberOrUndefined(reservation?.main_pid) || numberOrUndefined(reservation?.pid),
1727
+ acp_pid: numberOrUndefined(process.env.ACP_PID),
1728
+ cli_pid: numberOrUndefined(process.env.CLI_PID),
1729
+ feishu_pid: numberOrUndefined(process.env.FEISHU_PID),
1730
+ acp_tail_pid: numberOrUndefined(process.env.ACP_TAIL_PID),
1731
+ cli_tail_pid: numberOrUndefined(process.env.CLI_TAIL_PID),
1732
+ feishu_tail_pid: numberOrUndefined(process.env.FEISHU_TAIL_PID),
1733
+ updated_at: new Date().toISOString(),
1734
+ };
1735
+ });
1736
+ if (changed) {
1737
+ fs.writeFileSync(file, JSON.stringify({ version: 1, reservations: next }, null, 2) + "\n");
1738
+ }
1739
+ '
1740
+ }
1741
+
1742
+ has_saved_bot_configs() {
1743
+ [ -f "$BOT_CONFIG_FILE" ] || return 1
1744
+ BOT_CONFIG_FILE="$BOT_CONFIG_FILE" node -e '
1745
+ const fs = require("fs");
1746
+ const file = process.env.BOT_CONFIG_FILE;
1747
+ let parsed;
1748
+ try {
1749
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
1750
+ } catch {
1751
+ process.exit(1);
1752
+ }
1753
+ const bots = Array.isArray(parsed?.profiles) ? parsed.profiles : [];
1754
+ process.exit(bots.some((bot) => String(bot?.app_id || "").trim() && String(bot?.profile || "").trim()) ? 0 : 1);
1755
+ '
1756
+ }
1757
+
1758
+ task_profile_name_for_app_id() {
1759
+ local app_id="$1"
1760
+ printf 'aamp-feishu-task-%s' "$app_id"
1761
+ }
1762
+
1763
+ save_bot_config() {
1764
+ local bot_name="$1"
1765
+ local app_id="$2"
1766
+ local profile="$3"
1767
+ local app_secret="${4:-}"
1768
+ mkdir -p "$(dirname "$BOT_CONFIG_FILE")"
1769
+ BOT_CONFIG_FILE="$BOT_CONFIG_FILE" BOT_NAME="$bot_name" BOT_APP_ID="$app_id" BOT_PROFILE="$profile" BOT_APP_SECRET="$app_secret" FEISHU_USER_AUTH_DOMAINS="$FEISHU_USER_AUTH_DOMAINS" node -e '
1770
+ const fs = require("fs");
1771
+ const file = process.env.BOT_CONFIG_FILE;
1772
+ const appSecret = String(process.env.BOT_APP_SECRET || "").trim();
1773
+ const next = {
1774
+ display_name: process.env.BOT_NAME || process.env.BOT_APP_ID,
1775
+ app_id: process.env.BOT_APP_ID,
1776
+ ...(appSecret ? { app_secret: appSecret } : {}),
1777
+ profile: process.env.BOT_PROFILE,
1778
+ auth_mode: "lark-cli",
1779
+ capabilities: ["im", "task"],
1780
+ domains: String(process.env.FEISHU_USER_AUTH_DOMAINS || "").split(",").map((item) => item.trim()).filter(Boolean),
1781
+ updated_at: new Date().toISOString(),
1782
+ };
1783
+ let parsed = { version: 1, profiles: [] };
1784
+ try {
1785
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
1786
+ } catch {}
1787
+ const bots = Array.isArray(parsed?.profiles) ? parsed.profiles : [];
1788
+ const byAppId = new Map();
1789
+ for (const bot of bots) {
1790
+ const appId = String(bot?.app_id || "").trim();
1791
+ if (!appId) continue;
1792
+ byAppId.set(appId, bot);
1793
+ }
1794
+ const existing = byAppId.get(next.app_id) || {};
1795
+ byAppId.set(next.app_id, {
1796
+ ...existing,
1797
+ ...next,
1798
+ app_secret: next.app_secret || existing.app_secret,
1799
+ });
1800
+ fs.writeFileSync(file, JSON.stringify({ version: 1, profiles: [...byAppId.values()] }, null, 2) + "\n");
1801
+ '
1802
+ }
1803
+
1804
+ remove_bot_config() {
1805
+ local app_id="$1"
1806
+ [ -n "$app_id" ] || return 0
1807
+ [ -f "$BOT_CONFIG_FILE" ] || return 0
1808
+ BOT_CONFIG_FILE="$BOT_CONFIG_FILE" BOT_APP_ID="$app_id" node -e '
1809
+ const fs = require("fs");
1810
+ const file = process.env.BOT_CONFIG_FILE;
1811
+ const appIdToRemove = process.env.BOT_APP_ID;
1812
+ let parsed;
1813
+ try {
1814
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
1815
+ } catch {
1816
+ process.exit(0);
1817
+ }
1818
+ const bots = Array.isArray(parsed?.profiles) ? parsed.profiles : [];
1819
+ const filtered = bots.filter((bot) => String(bot?.app_id || "").trim() !== appIdToRemove);
1820
+ if (filtered.length !== bots.length) {
1821
+ fs.writeFileSync(file, JSON.stringify({ version: 1, profiles: filtered }, null, 2) + "\n");
1822
+ }
1823
+ '
1824
+ }
1825
+
1826
+ clean_bot_instance_configs_preserve_mailbox() {
1827
+ local app_id="$1"
1828
+ local profile="$2"
1829
+ [ -n "$app_id" ] || return 0
1830
+ BOT_APP_ID="$app_id" BOT_PROFILE="$profile" HOME_DIR="$HOME" node -e '
1831
+ const fs = require("fs");
1832
+ const path = require("path");
1833
+
1834
+ const appId = String(process.env.BOT_APP_ID || "").trim();
1835
+ const profile = String(process.env.BOT_PROFILE || "").trim();
1836
+ const home = process.env.HOME_DIR || process.env.HOME;
1837
+ const instancesDir = path.join(home, ".aamp", "feishu-bridge", "task-runtime", "instances");
1838
+ if (!appId || !fs.existsSync(instancesDir)) process.exit(0);
1839
+
1840
+ function normalizeId(value) {
1841
+ return String(value || "")
1842
+ .toLowerCase()
1843
+ .replace(/[^a-z0-9]+/g, "-")
1844
+ .replace(/-+/g, "-")
1845
+ .replace(/^-|-$/g, "")
1846
+ .slice(0, 32) || "instance";
1847
+ }
1848
+
1849
+ function readJson(file) {
1850
+ try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return undefined; }
1851
+ }
1852
+
1853
+ function writeJson(file, value) {
1854
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n");
1855
+ }
1856
+
1857
+ function matchesApp(instanceName, config) {
1858
+ if (String(config?.feishu?.appId || "").trim() === appId) return true;
1859
+ const normalizedAppId = normalizeId(appId);
1860
+ return String(instanceName || "").includes(normalizedAppId);
1861
+ }
1862
+
1863
+ let cleaned = 0;
1864
+ for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) {
1865
+ if (!entry.isDirectory()) continue;
1866
+ const instanceName = entry.name;
1867
+ const instanceDir = path.join(instancesDir, instanceName);
1868
+ for (const kind of ["im", "task"]) {
1869
+ const configFile = path.join(instanceDir, kind, "config.json");
1870
+ const config = readJson(configFile);
1871
+ if (!config || !matchesApp(instanceName, config)) continue;
1872
+ if (!config.mailbox) continue;
1873
+
1874
+ const cliProfile = profile || String(config?.feishu?.cliProfile || "").trim();
1875
+ if (kind === "task") {
1876
+ config.feishu = {
1877
+ appId,
1878
+ authMode: "lark-cli",
1879
+ ...(cliProfile ? { cliProfile } : {}),
1880
+ userIdType: config?.feishu?.userIdType || "open_id",
1881
+ eventNames: Array.isArray(config?.feishu?.eventNames) && config.feishu.eventNames.length
1882
+ ? config.feishu.eventNames
1883
+ : ["task.task.update_user_access_v2"],
1884
+ };
1885
+ } else {
1886
+ config.feishu = {
1887
+ appId,
1888
+ authMode: "lark-cli",
1889
+ ...(cliProfile ? { cliProfile } : {}),
1890
+ };
1891
+ }
1892
+ writeJson(configFile, config);
1893
+ cleaned += 1;
1894
+ }
1895
+ }
1896
+ if (cleaned) {
1897
+ console.log(`[aamp-one-click] cleaned ${cleaned} Feishu instance config file(s) for ${appId}, mailbox preserved`);
1898
+ }
1899
+ '
1900
+ }
1901
+
1902
+ ensure_lark_cli() {
1903
+ select_lark_cli_bin
1904
+ }
1905
+
1906
+ lark_cli_candidate_paths() {
1907
+ if [ -n "$AAMP_LARK_CLI_BIN" ]; then
1908
+ printf '%s\n' "$AAMP_LARK_CLI_BIN"
1909
+ fi
1910
+ if [ -x "$NPM_GLOBAL_PREFIX/bin/lark-cli" ]; then
1911
+ printf '%s\n' "$NPM_GLOBAL_PREFIX/bin/lark-cli"
1912
+ fi
1913
+ type -P -a lark-cli 2>/dev/null || true
1914
+ }
1915
+
1916
+ lark_cli_version_for() {
1917
+ local cli_bin="$1"
1918
+ [ -x "$cli_bin" ] || return 1
1919
+ "$cli_bin" --version 2>/dev/null | sed -E 's/.*version[[:space:]]+([0-9]+([.][0-9]+){0,2}).*/\1/' | head -n 1
1920
+ }
1921
+
1922
+ lark_cli_version() {
1923
+ [ -n "$LARK_CLI_CMD" ] || return 1
1924
+ lark_cli_version_for "$LARK_CLI_CMD"
1925
+ }
1926
+
1927
+ version_lt() {
1928
+ VERSION_LEFT="$1" VERSION_RIGHT="$2" node -e '
1929
+ const left = String(process.env.VERSION_LEFT || "0").split(".").map((part) => Number.parseInt(part, 10) || 0);
1930
+ const right = String(process.env.VERSION_RIGHT || "0").split(".").map((part) => Number.parseInt(part, 10) || 0);
1931
+ const length = Math.max(left.length, right.length);
1932
+ for (let index = 0; index < length; index += 1) {
1933
+ const l = left[index] || 0;
1934
+ const r = right[index] || 0;
1935
+ if (l < r) process.exit(0);
1936
+ if (l > r) process.exit(1);
1937
+ }
1938
+ process.exit(1);
1939
+ '
1940
+ }
1941
+
1942
+ select_lark_cli_bin_from_candidates() {
1943
+ local candidate version seen=""
1944
+ while IFS= read -r candidate; do
1945
+ [ -n "$candidate" ] || continue
1946
+ case "$candidate" in
1947
+ /*) ;;
1948
+ *)
1949
+ agent_detail "lark-cli candidate is not an absolute path: $candidate"
1950
+ continue
1951
+ ;;
1952
+ esac
1953
+ case "\n$seen\n" in
1954
+ *"\n$candidate\n"*) continue ;;
1955
+ esac
1956
+ seen="$seen\n$candidate"
1957
+ version="$(lark_cli_version_for "$candidate" || true)"
1958
+ if [ -n "$version" ]; then
1959
+ agent_detail "lark-cli candidate: $candidate version=$version"
1960
+ if ! version_lt "$version" "$LARK_CLI_MIN_VERSION"; then
1961
+ LARK_CLI_CMD="$candidate"
1962
+ export AAMP_LARK_CLI_BIN="$LARK_CLI_CMD"
1963
+ path_prepend "$(dirname "$LARK_CLI_CMD")"
1964
+ agent_detail "using lark-cli: $LARK_CLI_CMD ($version)"
1965
+ agent_detail "lark-cli config dir: ${LARKSUITE_CLI_CONFIG_DIR:-$AAMP_LARK_CLI_CONFIG_DIR}"
1966
+ return 0
1967
+ fi
1968
+ else
1969
+ agent_detail "lark-cli candidate unavailable: $candidate"
1970
+ fi
1971
+ done < <(lark_cli_candidate_paths)
1972
+ return 1
1973
+ }
1974
+
1975
+ install_lark_cli_in_isolated_prefix() {
1976
+ agent_log "installing lark-cli to isolated prefix: $NPM_GLOBAL_PREFIX"
1977
+ npm_install_global @larksuite/cli@latest
1978
+ }
1979
+
1980
+ ensure_lark_cli_after_install_lock() {
1981
+ if select_lark_cli_bin_from_candidates; then
1982
+ return 0
1983
+ fi
1984
+ install_lark_cli_in_isolated_prefix
1985
+ }
1986
+
1987
+ select_lark_cli_bin() {
1988
+ if [ -n "$LARK_CLI_CMD" ] && [ -x "$LARK_CLI_CMD" ]; then
1989
+ return 0
1990
+ fi
1991
+
1992
+ if select_lark_cli_bin_from_candidates; then
1993
+ return 0
1994
+ fi
1995
+
1996
+ if [ -n "$AAMP_LARK_CLI_BIN" ]; then
1997
+ agent_fail "AAMP_LARK_CLI_BIN does not point to lark-cli >= $LARK_CLI_MIN_VERSION: $AAMP_LARK_CLI_BIN"
1998
+ fi
1999
+
2000
+ with_lark_cli_install_lock ensure_lark_cli_after_install_lock || agent_fail "failed to install lark-cli to $NPM_GLOBAL_PREFIX"
2001
+ hash -r 2>/dev/null || true
2002
+ if select_lark_cli_bin_from_candidates; then
2003
+ return 0
2004
+ fi
2005
+
2006
+ agent_fail "failed to select lark-cli >= $LARK_CLI_MIN_VERSION after isolated install"
2007
+ }
2008
+
2009
+ ensure_lark_cli_min_version() {
2010
+ select_lark_cli_bin
2011
+ }
2012
+
2013
+ lark_cli_user_auth_satisfied() {
2014
+ local profile="$1"
2015
+ local status_json auth_check
2016
+ LARK_CLI_CMD="${LARK_CLI_CMD:-lark-cli}"
2017
+
2018
+ status_json="$("$LARK_CLI_CMD" --profile "$profile" auth status --json 2>/dev/null)" || return 1
2019
+
2020
+ set +e
2021
+ auth_check="$(AUTH_STATUS_JSON="$status_json" FEISHU_USER_AUTH_REQUIRED_SCOPES="$FEISHU_USER_AUTH_REQUIRED_SCOPES" FEISHU_USER_AUTH_EXCLUDES="$FEISHU_USER_AUTH_EXCLUDES" node -e '
2022
+ function parseJsonOutput(value, label) {
2023
+ const raw = String(value || "");
2024
+ const start = raw.indexOf("{");
2025
+ if (start < 0) throw new Error(`${label} did not contain JSON`);
2026
+ return JSON.parse(raw.slice(start));
2027
+ }
2028
+
2029
+ const status = parseJsonOutput(process.env.AUTH_STATUS_JSON, "auth status");
2030
+ const user = status?.identities?.user;
2031
+ const usableTokenStatuses = new Set(["valid", "needs_refresh"]);
2032
+ if (!user?.available || !usableTokenStatuses.has(user?.tokenStatus)) {
2033
+ console.log("lark-cli user auth is not ready or token cannot be refreshed automatically");
2034
+ process.exit(1);
2035
+ }
2036
+
2037
+ const excludedScopes = new Set(String(process.env.FEISHU_USER_AUTH_EXCLUDES || "").split(",").map((item) => item.trim()).filter(Boolean));
2038
+ const requiredScopes = String(process.env.FEISHU_USER_AUTH_REQUIRED_SCOPES || "")
2039
+ .split(/[\s,]+/)
2040
+ .map((item) => item.trim())
2041
+ .filter(Boolean)
2042
+ .filter((scope) => !excludedScopes.has(scope));
2043
+ const grantedScopes = new Set(String(user.scope || "").split(/\s+/).filter(Boolean));
2044
+ const missing = [...new Set(requiredScopes)].filter((scope) => !grantedScopes.has(scope));
2045
+ if (missing.length > 0) {
2046
+ const sample = missing.slice(0, 12).join(", ");
2047
+ console.log(`missing lark-cli user auth scopes (${missing.length}): ${sample}${missing.length > 12 ? ", ..." : ""}`);
2048
+ process.exit(1);
2049
+ }
2050
+ console.log("ok");
2051
+ ')"
2052
+ local node_status=$?
2053
+ set -e
2054
+
2055
+ if [ "$node_status" -eq 0 ] && [ "$auth_check" = "ok" ]; then
2056
+ return 0
2057
+ fi
2058
+ [ -n "$auth_check" ] && agent_log "$auth_check"
2059
+ return 1
2060
+ }
2061
+
2062
+ normalize_lark_cli_auth_excludes() {
2063
+ EXCLUDES="$1" node -e '
2064
+ const excludes = String(process.env.EXCLUDES || "").split(",").map((item) => item.trim()).filter(Boolean);
2065
+ console.log([...new Set(excludes)].join(","));
2066
+ '
2067
+ }
2068
+
2069
+ auth_scope_tokens=()
2070
+ auth_scope_capture="false"
2071
+ auth_scope_header=""
2072
+
2073
+ auth_scope_token_exists() {
2074
+ local expected="$1"
2075
+ local token
2076
+ for token in ${auth_scope_tokens[@]+"${auth_scope_tokens[@]}"}; do
2077
+ if [ "$token" = "$expected" ]; then
2078
+ return 0
2079
+ fi
2080
+ done
2081
+ return 1
2082
+ }
2083
+
2084
+ add_auth_scope_tokens() {
2085
+ local text="$1"
2086
+ local token
2087
+
2088
+ text="${text//,/ }"
2089
+ text="${text//[/ }"
2090
+ text="${text//]/ }"
2091
+ text="${text//\"/ }"
2092
+ text="${text//\'/ }"
2093
+ text="${text//;/ }"
2094
+ for token in $text; do
2095
+ token="${token#-}"
2096
+ token="${token#*:}"
2097
+ token="${token#*: }"
2098
+ token="${token// /}"
2099
+ token="${token#"${token%%[![:space:]]*}"}"
2100
+ token="${token%"${token##*[![:space:]]}"}"
2101
+ if [[ "$token" =~ ^[A-Za-z][A-Za-z0-9_.-]*:[A-Za-z0-9_.:-]+$ ]] && ! auth_scope_token_exists "$token"; then
2102
+ auth_scope_tokens+=("$token")
2103
+ fi
2104
+ done
2105
+ }
2106
+
2107
+ line_has_auth_scope_token() {
2108
+ local text="$1"
2109
+ local token
2110
+
2111
+ text="${text//,/ }"
2112
+ text="${text//[/ }"
2113
+ text="${text//]/ }"
2114
+ text="${text//\"/ }"
2115
+ text="${text//\'/ }"
2116
+ text="${text//;/ }"
2117
+ for token in $text; do
2118
+ token="${token#-}"
2119
+ token="${token#*:}"
2120
+ token="${token#*: }"
2121
+ token="${token// /}"
2122
+ token="${token#"${token%%[![:space:]]*}"}"
2123
+ token="${token%"${token##*[![:space:]]}"}"
2124
+ if [[ "$token" =~ ^[A-Za-z][A-Za-z0-9_.-]*:[A-Za-z0-9_.:-]+$ ]]; then
2125
+ return 0
2126
+ fi
2127
+ done
2128
+ return 1
2129
+ }
2130
+
2131
+ flush_lark_cli_auth_scopes() {
2132
+ local total sample
2133
+
2134
+ if [ "$auth_scope_capture" != "true" ]; then
2135
+ return 0
2136
+ fi
2137
+
2138
+ total="${#auth_scope_tokens[@]}"
2139
+ if [ "$total" -eq 0 ]; then
2140
+ printf '%s\n' "$auth_scope_header"
2141
+ elif [ "$total" -eq 1 ]; then
2142
+ printf '%s%s,共计 1 个权限。\n' "$auth_scope_header" "${auth_scope_tokens[0]}"
2143
+ elif [ "$total" -eq 2 ]; then
2144
+ printf '%s%s, %s,共计 2 个权限。\n' "$auth_scope_header" "${auth_scope_tokens[0]}" "${auth_scope_tokens[1]}"
2145
+ else
2146
+ sample="${auth_scope_tokens[0]}, ${auth_scope_tokens[1]}"
2147
+ printf '%s%s 等共计 %s 个权限。\n' "$auth_scope_header" "$sample" "$total"
2148
+ fi
2149
+
2150
+ auth_scope_tokens=()
2151
+ auth_scope_capture="false"
2152
+ auth_scope_header=""
2153
+ }
2154
+
2155
+ print_lark_cli_auth_output_line() {
2156
+ local line="$1"
2157
+ local after_scopes
2158
+
2159
+ if [[ "$line" == *"本次请求 scopes:"* ]]; then
2160
+ flush_lark_cli_auth_scopes
2161
+ auth_scope_capture="true"
2162
+ auth_scope_tokens=()
2163
+ auth_scope_header="${line%%本次请求 scopes:*}本次请求 scopes: "
2164
+ after_scopes="${line#*本次请求 scopes:}"
2165
+ add_auth_scope_tokens "$after_scopes"
2166
+ return 0
2167
+ fi
2168
+
2169
+ if [ "$auth_scope_capture" = "true" ]; then
2170
+ if [ -z "${line//[[:space:]]/}" ]; then
2171
+ return 0
2172
+ fi
2173
+ if line_has_auth_scope_token "$line"; then
2174
+ add_auth_scope_tokens "$line"
2175
+ return 0
2176
+ fi
2177
+ flush_lark_cli_auth_scopes
2178
+ fi
2179
+
2180
+ printf '%s\n' "$line"
2181
+ }
2182
+
2183
+ run_lark_cli_auth_login_with_browser_open() {
2184
+ local opened_url=""
2185
+ local line=""
2186
+ local auth_url=""
2187
+
2188
+ set +e
2189
+ "$@" 2>&1 | {
2190
+ while IFS= read -r line; do
2191
+ print_lark_cli_auth_output_line "$line"
2192
+ if [ "${AAMP_AUTO_OPEN_AUTH_URL:-true}" != "false" ] \
2193
+ && [ -z "$opened_url" ] \
2194
+ && command -v open >/dev/null 2>&1 \
2195
+ && [[ "$line" =~ (https://[^[:space:]]+) ]]; then
2196
+ auth_url="${BASH_REMATCH[1]}"
2197
+ opened_url="$auth_url"
2198
+ agent_log "opening Feishu auth URL in browser"
2199
+ open "$auth_url" >/dev/null 2>&1 || agent_log "failed to open auth URL automatically; please open it manually"
2200
+ fi
2201
+ done
2202
+ flush_lark_cli_auth_scopes
2203
+ }
2204
+ local auth_status=${PIPESTATUS[0]}
2205
+ set -e
2206
+ return "$auth_status"
2207
+ }
2208
+
2209
+ run_lark_cli_auth_login() {
2210
+ local profile="$1"
2211
+ local auth_excludes="$2"
2212
+
2213
+ auth_excludes="$(normalize_lark_cli_auth_excludes "$auth_excludes")"
2214
+ if [ -z "$auth_excludes" ]; then
2215
+ run_lark_cli_auth_login_with_browser_open "$LARK_CLI_CMD" --profile "$profile" auth login --domain "$FEISHU_USER_AUTH_DOMAINS" --scope "$FEISHU_USER_AUTH_REQUIRED_SCOPES"
2216
+ else
2217
+ run_lark_cli_auth_login_with_browser_open "$LARK_CLI_CMD" --profile "$profile" auth login --domain "$FEISHU_USER_AUTH_DOMAINS" --scope "$FEISHU_USER_AUTH_REQUIRED_SCOPES" --exclude "$auth_excludes"
2218
+ fi
2219
+ }
2220
+
2221
+ ensure_lark_cli_profile() {
2222
+ ensure_lark_cli
2223
+ with_lark_cli_config_lock ensure_lark_cli_profile_locked "$@"
2224
+ }
2225
+
2226
+ ensure_lark_cli_profile_locked() {
2227
+ local app_id="$1"
2228
+ local app_secret="$2"
2229
+ local profile="$3"
2230
+ local auth_excludes
2231
+
2232
+ if "$LARK_CLI_CMD" profile list 2>/dev/null | grep -F "\"$profile\"" >/dev/null 2>&1; then
2233
+ agent_detail "lark-cli profile already exists: $profile"
2234
+ else
2235
+ [ -n "$app_secret" ] || agent_fail "missing app secret for creating lark-cli profile $profile"
2236
+ agent_log "creating lark-cli profile: $profile"
2237
+ printf '%s\n' "$app_secret" | "$LARK_CLI_CMD" profile add \
2238
+ --name "$profile" \
2239
+ --app-id "$app_id" \
2240
+ --app-secret-stdin
2241
+ fi
2242
+
2243
+ agent_detail "ensuring lark-cli user auth domains for profile: $profile"
2244
+ auth_excludes="$FEISHU_USER_AUTH_EXCLUDES"
2245
+ if lark_cli_user_auth_satisfied "$profile"; then
2246
+ agent_detail "lark-cli user auth already has required scopes for profile: $profile"
2247
+ else
2248
+ run_lark_cli_auth_login "$profile" "$auth_excludes"
2249
+ fi
2250
+ }
2251
+
2252
+ forget_current_bot_after_feishu_start_failure() {
2253
+ agent_log "Feishu bridge failed before ready; removing local bot config for $APP_ID"
2254
+ remove_bot_config "$APP_ID"
2255
+ clean_bot_instance_configs_preserve_mailbox "$APP_ID" "$LARK_CLI_PROFILE"
2256
+ agent_log "Removed local bot config and cleaned stale instance Feishu config while preserving mailbox. Re-run this script and choose 新建应用/选择其他应用 to recreate it."
2257
+ }
2258
+
2259
+ should_forget_bot_after_feishu_failure() {
2260
+ [ -n "$FEISHU_LOG" ] && [ -f "$FEISHU_LOG" ] || return 1
2261
+
2262
+ if grep -F 'does not contain a readable app secret' "$FEISHU_LOG" >/dev/null 2>&1; then
2263
+ return 0
2264
+ fi
2265
+ if grep -E 'code=99991672|应用尚未开通所需的应用身份权限|action_scope_required' "$FEISHU_LOG" >/dev/null 2>&1; then
2266
+ return 0
2267
+ fi
2268
+
2269
+ return 1
2270
+ }
2271
+
2272
+ forget_current_bot_after_feishu_failure_if_needed() {
2273
+ if should_forget_bot_after_feishu_failure; then
2274
+ forget_current_bot_after_feishu_start_failure
2275
+ else
2276
+ agent_log "Feishu bridge failed before ready; keeping local bot config for $APP_ID"
2277
+ fi
2278
+ }
2279
+
2280
+ select_existing_bot_or_create() {
2281
+ local app_ids=()
2282
+ local names=()
2283
+ local profiles=()
2284
+ local app_secrets=()
2285
+ local bot_labels=()
2286
+ local app_id name profile app_secret
2287
+ local index create_index selected
2288
+
2289
+ acquire_bot_selection_lock
2290
+ while IFS=$'\t' read -r app_id name profile app_secret; do
2291
+ [ -n "$app_id" ] || continue
2292
+ app_ids+=("$app_id")
2293
+ names+=("${name:-$app_id}")
2294
+ profiles+=("$profile")
2295
+ app_secrets+=("$app_secret")
2296
+ bot_labels+=("${name:-$app_id} ($app_id)")
2297
+ done < <(load_bot_configs)
2298
+
2299
+ if [ "${#app_ids[@]}" -eq 0 ]; then
2300
+ if has_saved_bot_configs; then
2301
+ agent_log "saved Feishu bot(s) are unavailable or already running; choose new app to continue."
2302
+ bot_labels+=("新建应用/选择其他应用")
2303
+ set +e
2304
+ select_bot_menu
2305
+ set -e
2306
+ release_bot_selection_lock
2307
+ register_feishu_app
2308
+ return 0
2309
+ fi
2310
+ release_bot_selection_lock
2311
+ register_feishu_app
2312
+ return 0
2313
+ fi
2314
+
2315
+ create_index=$((${#app_ids[@]} + 1))
2316
+ bot_labels+=("新建应用/选择其他应用")
2317
+
2318
+ set +e
2319
+ select_bot_menu
2320
+ selected=$?
2321
+ set -e
2322
+
2323
+ if [ "$selected" -eq $((create_index - 1)) ]; then
2324
+ release_bot_selection_lock
2325
+ register_feishu_app
2326
+ return 0
2327
+ fi
2328
+
2329
+ index="$selected"
2330
+ APP_ID="${app_ids[$index]}"
2331
+ BOT_NAME="${names[$index]}"
2332
+ LARK_CLI_PROFILE="${profiles[$index]}"
2333
+ APP_SECRET="${app_secrets[$index]:-}"
2334
+ reserve_selected_bot "$APP_ID" "$LARK_CLI_PROFILE" "$BOT_NAME"
2335
+ release_bot_selection_lock
2336
+ agent_detail "using Feishu bot: $BOT_NAME ($APP_ID, profile=$LARK_CLI_PROFILE)"
2337
+ agent_log "正在检查飞书授权..."
2338
+ ensure_lark_cli_profile "$APP_ID" "$APP_SECRET" "$LARK_CLI_PROFILE"
2339
+ }
2340
+
2341
+ register_feishu_app() {
2342
+ local workdir
2343
+ local register_script
2344
+ local register_log
2345
+ local register_result_file
2346
+ local register_status
2347
+ local default_name
2348
+ local bot_name
2349
+
2350
+ workdir="$(mktemp -d "${TMPDIR:-/tmp}/aamp-register-feishu-app.XXXXXX")"
2351
+ register_script="$workdir/register-app.mjs"
2352
+ if [ -n "$AAMP_RUN_LOG_DIR" ]; then
2353
+ mkdir -p "$AAMP_RUN_LOG_DIR"
2354
+ register_log="$(mktemp "$AAMP_RUN_LOG_DIR/feishu-register.XXXXXX")"
2355
+ else
2356
+ register_log="$workdir/register-app.log"
2357
+ fi
2358
+ chmod 600 "$register_log" 2>/dev/null || true
2359
+ register_result_file="$workdir/register-app-result.json"
2360
+ default_name="${AGENT} 飞书 CLI"
2361
+
2362
+ agent_detail "preparing Feishu app registration helper"
2363
+ npm_install_register_helper "$workdir"
2364
+
2365
+ cat >"$register_script" <<'NODE'
2366
+ import * as lark from '@larksuiteoapi/node-sdk';
2367
+ import { execFile } from 'node:child_process';
2368
+ import { createRequire } from 'node:module';
2369
+ import { writeSync } from 'node:fs';
2370
+
2371
+ const require = createRequire(import.meta.url);
2372
+ const sdkPackage = require('@larksuiteoapi/node-sdk/package.json');
2373
+
2374
+ function splitList(value) {
2375
+ return String(value || '')
2376
+ .split(',')
2377
+ .map((item) => item.trim())
2378
+ .filter(Boolean);
2379
+ }
2380
+
2381
+ const tenantScopes = splitList(process.env.FEISHU_APP_SCOPES_TENANT);
2382
+ const userScopes = splitList(process.env.FEISHU_APP_SCOPES_USER);
2383
+ const tenantEvents = splitList(process.env.FEISHU_APP_EVENTS_TENANT);
2384
+ const userEvents = splitList(process.env.FEISHU_APP_EVENTS_USER);
2385
+ const appName = process.env.FEISHU_APP_PRESET_NAME || '飞书 CLI';
2386
+
2387
+ function userLog(message) {
2388
+ writeSync(5, `${message}\n`);
2389
+ }
2390
+
2391
+ console.log(`[aamp-one-click] registerApp sdk=${sdkPackage.version}`);
2392
+ console.log(`[aamp-one-click] registerApp appPreset.name=${appName}`);
2393
+ console.log(`[aamp-one-click] registerApp addons.counts tenantScopes=${tenantScopes.length} userScopes=${userScopes.length} tenantEvents=${tenantEvents.length} userEvents=${userEvents.length}`);
2394
+
2395
+ const addons = {
2396
+ scopes: {
2397
+ tenant: tenantScopes,
2398
+ user: userScopes,
2399
+ },
2400
+ events: {
2401
+ items: {
2402
+ tenant: tenantEvents,
2403
+ user: userEvents,
2404
+ },
2405
+ },
2406
+ };
2407
+ console.log(`[aamp-one-click] registerApp addons.json=${JSON.stringify(addons)}`);
2408
+
2409
+ const result = await lark.registerApp({
2410
+ source: 'aamp-feishu-task-agent',
2411
+ appPreset: {
2412
+ name: appName,
2413
+ desc: 'AAMP Feishu bridge bot',
2414
+ },
2415
+ addons,
2416
+ onQRCodeReady(info) {
2417
+ const url = new URL(info.url);
2418
+ console.log(`[aamp-one-click] registerApp url.has_addons=${url.searchParams.has('addons') ? 'yes' : 'no'}`);
2419
+ console.log(`[aamp-one-click] registerApp url.has_name=${url.searchParams.has('name') ? 'yes' : 'no'}`);
2420
+ if (process.platform === 'darwin') {
2421
+ execFile('open', [info.url], (error) => {
2422
+ if (error) {
2423
+ userLog('[aamp-one-click] 未能自动打开浏览器,请打开以下链接完成飞书 Bot 授权:');
2424
+ userLog(info.url);
2425
+ userLog(`[aamp-one-click] 授权链接将在 ${info.expireIn} 秒后过期`);
2426
+ return;
2427
+ }
2428
+ userLog(`[aamp-one-click] 已打开浏览器,请完成飞书 Bot 授权(链接 ${info.expireIn} 秒内有效)`);
2429
+ });
2430
+ } else {
2431
+ userLog('[aamp-one-click] 请打开以下链接完成飞书 Bot 授权:');
2432
+ userLog(info.url);
2433
+ userLog(`[aamp-one-click] 授权链接将在 ${info.expireIn} 秒后过期`);
2434
+ }
2435
+ },
2436
+ onStatusChange(info) {
2437
+ if (info.status === 'polling') return;
2438
+ console.log(`[aamp-one-click] registerApp status: ${info.status}`);
2439
+ },
2440
+ });
2441
+
2442
+ async function fetchRegisteredAppName(appId, appSecret) {
2443
+ try {
2444
+ const client = new lark.Client({
2445
+ appId,
2446
+ appSecret,
2447
+ });
2448
+ const response = await client.application.application.get({
2449
+ path: { app_id: appId },
2450
+ params: { lang: 'zh_cn', user_id_type: 'open_id' },
2451
+ });
2452
+ const app = response?.data?.app;
2453
+ return app?.app_name || app?.i18n?.find?.((item) => item?.i18n_key === 'zh_cn')?.name || '';
2454
+ } catch (error) {
2455
+ console.log(`[aamp-one-click] failed to fetch registered app name; using preset name: ${error instanceof Error ? error.message : String(error)}`);
2456
+ return '';
2457
+ }
2458
+ }
2459
+
2460
+ const registeredAppName = await fetchRegisteredAppName(result.client_id, result.client_secret);
2461
+ const resultPayload = {
2462
+ app_id: result.client_id,
2463
+ app_secret: result.client_secret,
2464
+ app_name: registeredAppName || appName,
2465
+ };
2466
+ await import('node:fs/promises').then(({ writeFile }) => writeFile(process.env.AAMP_REGISTER_APP_RESULT_FILE, JSON.stringify(resultPayload)));
2467
+ console.log(`[aamp-one-click] Feishu app registration completed: ${result.client_id}`);
2468
+ console.log(`[aamp-one-click] Feishu app name: ${resultPayload.app_name}`);
2469
+ NODE
2470
+
2471
+ agent_log "正在授权飞书 Bot..."
2472
+ set +e
2473
+ AAMP_REGISTER_APP_RESULT_FILE="$register_result_file" \
2474
+ FEISHU_APP_SCOPES_TENANT="$FEISHU_APP_SCOPES_TENANT" \
2475
+ FEISHU_APP_SCOPES_USER="$FEISHU_APP_SCOPES_USER" \
2476
+ FEISHU_APP_EVENTS_TENANT="$FEISHU_APP_EVENTS_TENANT" \
2477
+ FEISHU_APP_EVENTS_USER="$FEISHU_APP_EVENTS_USER" \
2478
+ FEISHU_APP_PRESET_NAME="$default_name" \
2479
+ node "$register_script" 5>&1 >"$register_log" 2>&1
2480
+ register_status=$?
2481
+ set -e
2482
+ if [ "$register_status" -ne 0 ]; then
2483
+ agent_fail "飞书 Bot 授权失败,详细日志:$register_log"
2484
+ fi
2485
+ agent_detail "Feishu app registration log: $register_log"
2486
+
2487
+ [ -s "$register_result_file" ] || agent_fail "failed to get app credentials from Feishu app registration"
2488
+
2489
+ APP_ID="$(node -e 'const fs = require("fs"); const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(data.app_id || "")' "$register_result_file")"
2490
+ APP_SECRET="$(node -e 'const fs = require("fs"); const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(data.app_secret || "")' "$register_result_file")"
2491
+ bot_name="$(node -e 'const fs = require("fs"); const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(data.app_name || "")' "$register_result_file")"
2492
+ : >"$register_result_file"
2493
+ [ -n "$APP_ID" ] && [ -n "$APP_SECRET" ] || agent_fail "Feishu app registration returned incomplete credentials"
2494
+
2495
+ bot_name="${bot_name:-$default_name}"
2496
+ BOT_NAME="$bot_name"
2497
+ LARK_CLI_PROFILE="$(task_profile_name_for_app_id "$APP_ID")"
2498
+ ensure_lark_cli_profile "$APP_ID" "$APP_SECRET" "$LARK_CLI_PROFILE"
2499
+ if [ "$AAMP_TASK_INTERNAL" = "true" ]; then
2500
+ agent_detail "Feishu Bot 已授权:$BOT_NAME ($APP_ID)"
2501
+ return 0
2502
+ fi
2503
+ acquire_bot_selection_lock
2504
+ save_bot_config "$bot_name" "$APP_ID" "$LARK_CLI_PROFILE" "$APP_SECRET"
2505
+ reserve_selected_bot "$APP_ID" "$LARK_CLI_PROFILE" "$bot_name"
2506
+ release_bot_selection_lock
2507
+ agent_log "saved Feishu task profile config: $bot_name ($APP_ID, profile=$LARK_CLI_PROFILE)"
2508
+ }
2509
+
2510
+ resolve_feishu_bot_credentials() {
2511
+ select_existing_bot_or_create
2512
+ }
2513
+
2514
+ ensure_acpx() {
2515
+ if ! command -v acpx >/dev/null 2>&1; then
2516
+ agent_log "acpx not found; installing acpx"
2517
+ npm_install_global acpx --force
2518
+ fi
2519
+ acpx --version >/dev/null
2520
+ }
2521
+
2522
+ ensure_node_major_at_least() {
2523
+ local required="$1"
2524
+ local major
2525
+ major="$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || printf '0')"
2526
+ if [ "$major" -lt "$required" ]; then
2527
+ agent_fail "Codem requires Node.js >= $required, current version is $(node --version 2>/dev/null || printf 'unknown')"
2528
+ fi
2529
+ }
2530
+
2531
+ print_codem_login_notice() {
2532
+ cat >&2 <<NOTICE
2533
+
2534
+ ============================================================
2535
+ [aamp-one-click] CodeM needs Feishu device binding before the service can run.
2536
+ [aamp-one-click] Server: $CODEM_SERVER_URL
2537
+ [aamp-one-click] If a QR code appears, scan it in Feishu or send the shown /bind command to the CodeM bot.
2538
+ ============================================================
2539
+
2540
+ NOTICE
2541
+ }
2542
+
2543
+ print_codem_update_notice() {
2544
+ cat >&2 <<NOTICE
2545
+
2546
+ ============================================================
2547
+ [aamp-one-click] CodeM CLI appears to be out of date or using an old login endpoint.
2548
+ [aamp-one-click] Running CodeM update automatically, then retrying service start.
2549
+ [aamp-one-click] If this still fails, run these commands manually:
2550
+ [aamp-one-click] codem update -y
2551
+ [aamp-one-click] hash -r
2552
+ [aamp-one-click] codem service start
2553
+ ============================================================
2554
+
2555
+ NOTICE
2556
+ }
2557
+
2558
+ codem_service_output_indicates_update_available() {
2559
+ printf '%s' "$1" | grep -E '发现 CodeM CLI 新版本|CodeM CLI 新版本|codem update' >/dev/null 2>&1
2560
+ }
2561
+
2562
+ codem_service_output_indicates_login_required() {
2563
+ printf '%s' "$1" | grep -E 'CodeM 尚未登录|尚未登录|oauth/login/create.*404' >/dev/null 2>&1
2564
+ }
2565
+
2566
+ codem_output_indicates_provider_unauthorized() {
2567
+ printf '%s' "$1" | grep -Ei 'provider error: unauthorized|unauthorized.*check API key|check API key' >/dev/null 2>&1
2568
+ }
2569
+
2570
+ install_codem_cli() {
2571
+ command -v curl >/dev/null 2>&1 || {
2572
+ agent_log "curl not found; cannot install Codem CLI automatically"
2573
+ return 1
2574
+ }
2575
+
2576
+ ensure_node_major_at_least 20
2577
+ ensure_codem_local_bin_on_path
2578
+ agent_log "installing Codem CLI"
2579
+ printf '%s\n' "$CODEM_INSTALLER_CONFIRM" | bash <(curl -fsSL "$CODEM_INSTALLER_URL")
2580
+ ensure_codem_local_bin_on_path
2581
+ hash -r 2>/dev/null || true
2582
+ }
2583
+
2584
+ codem_config_migration_reason() {
2585
+ CODEM_CONFIG_FILE="$HOME/.codem/config.json" CODEM_SERVER_URL="$CODEM_SERVER_URL" node -e '
2586
+ const fs = require("fs");
2587
+ const file = process.env.CODEM_CONFIG_FILE;
2588
+ const expectedServer = process.env.CODEM_SERVER_URL;
2589
+ let config;
2590
+ try {
2591
+ config = JSON.parse(fs.readFileSync(file, "utf8"));
2592
+ } catch {
2593
+ process.exit(0);
2594
+ }
2595
+
2596
+ const reasons = [];
2597
+ if (config.serverUrl && config.serverUrl !== expectedServer) {
2598
+ reasons.push(`serverUrl=${config.serverUrl}`);
2599
+ }
2600
+ if (config.user && (!config.auth || config.auth.oauth == null)) {
2601
+ reasons.push("missing_feishu_oauth");
2602
+ }
2603
+ if (reasons.length > 0) {
2604
+ process.stdout.write(reasons.join(","));
2605
+ }
2606
+ '
2607
+ }
2608
+
2609
+ ensure_codem_breaking_change_compat() {
2610
+ local reason
2611
+ reason="$(codem_config_migration_reason)"
2612
+ [ -n "$reason" ] || return 0
2613
+
2614
+ agent_log "detected pre-migration CodeM config ($reason); refreshing CodeM installer and forcing Feishu OAuth login"
2615
+ install_codem_cli || agent_fail "failed to refresh CodeM CLI with installer: $CODEM_INSTALLER_URL"
2616
+ run_codem_login_force_once || agent_fail "codem login --force failed after CodeM service migration. Log: $CODEM_LOGIN_LOG"
2617
+ }
2618
+
2619
+ ensure_codem_local_bin_on_path() {
2620
+ path_prepend "$CODEM_LOCAL_BIN"
2621
+ }
2622
+
2623
+ is_cursor_agent_cli() {
2624
+ local candidate="$1"
2625
+ "$candidate" login --help 2>&1 | grep -Fq 'Authenticate with Cursor'
2626
+ }
2627
+
2628
+ find_cursor_agent_cli() {
2629
+ if command -v cursor-agent >/dev/null 2>&1; then
2630
+ command -v cursor-agent
2631
+ return 0
2632
+ fi
2633
+ if [ -x "$CURSOR_LOCAL_BIN/cursor-agent" ]; then
2634
+ printf '%s\n' "$CURSOR_LOCAL_BIN/cursor-agent"
2635
+ return 0
2636
+ fi
2637
+ if [ -x "$CURSOR_LOCAL_BIN/agent" ] && is_cursor_agent_cli "$CURSOR_LOCAL_BIN/agent"; then
2638
+ printf '%s\n' "$CURSOR_LOCAL_BIN/agent"
2639
+ return 0
2640
+ fi
2641
+ if command -v agent >/dev/null 2>&1; then
2642
+ local agent_bin
2643
+ agent_bin="$(command -v agent)"
2644
+ if is_cursor_agent_cli "$agent_bin"; then
2645
+ printf '%s\n' "$agent_bin"
2646
+ return 0
2647
+ fi
2648
+ fi
2649
+ return 1
2650
+ }
2651
+
2652
+ resolve_cursor_cli_for_acp() {
2653
+ command -v cursor
2654
+ }
2655
+
2656
+ ensure_cursor_acp_command() {
2657
+ local cursor_agent
2658
+ local cursor_wrapper
2659
+
2660
+ if resolve_cursor_cli_for_acp >/dev/null 2>&1; then
2661
+ return 0
2662
+ fi
2663
+
2664
+ cursor_agent="$(find_cursor_agent_cli)" || return 1
2665
+ mkdir -p "$NPM_GLOBAL_PREFIX/bin"
2666
+ cursor_wrapper="$NPM_GLOBAL_PREFIX/bin/cursor"
2667
+ agent_log "creating cursor command wrapper for ACP bridge: $cursor_wrapper -> $cursor_agent"
2668
+ {
2669
+ printf '#!/usr/bin/env bash\n'
2670
+ printf 'cursor_agent=%q\n' "$cursor_agent"
2671
+ printf 'exec "$cursor_agent" "$@"\n'
2672
+ } > "$cursor_wrapper"
2673
+ chmod +x "$cursor_wrapper" 2>/dev/null || true
2674
+ hash -r 2>/dev/null || true
2675
+ resolve_cursor_cli_for_acp >/dev/null 2>&1
2676
+ }
2677
+
2678
+ ensure_agent_cli() {
2679
+ if [ "$AGENT" = "codem" ]; then
2680
+ ensure_codem_local_bin_on_path
2681
+ fi
2682
+
2683
+ if [ "$AGENT" = "cursor" ]; then
2684
+ find_cursor_agent_cli >/dev/null 2>&1 || agent_fail "未检测到 cursor CLI。请先安装 Cursor CLI 后重新运行脚本。"
2685
+ ensure_cursor_acp_command || agent_fail "cursor ACP command is unavailable; check PATH for $NPM_GLOBAL_PREFIX/bin/cursor"
2686
+ return 0
2687
+ fi
2688
+
2689
+ if [ "$AGENT" = "codex" ]; then
2690
+ resolve_codex_cli_for_acp >/dev/null 2>&1 || agent_fail "未检测到 codex CLI。请先安装 Codex CLI 后重新运行脚本。"
2691
+ return 0
2692
+ fi
2693
+
2694
+ command -v "$AGENT" >/dev/null 2>&1 || agent_fail "未检测到 $AGENT CLI。请先安装后重新运行脚本。"
2695
+ }
2696
+
2697
+ clear_quarantine_path() {
2698
+ is_macos || return 0
2699
+ local target="$1"
2700
+ [ -n "$target" ] || return 0
2701
+ [ -e "$target" ] || [ -L "$target" ] || return 0
2702
+
2703
+ if xattr -dr com.apple.quarantine "$target" 2>/dev/null; then
2704
+ return 0
2705
+ fi
2706
+
2707
+ agent_log "need elevated permission to clear macOS quarantine for $target"
2708
+ if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
2709
+ sudo xattr -dr com.apple.quarantine "$target" 2>/dev/null && return 0
2710
+ fi
2711
+
2712
+ if command -v sudo >/dev/null 2>&1 && [ -t 0 ]; then
2713
+ agent_log "requesting sudo to clear macOS quarantine for $target"
2714
+ sudo xattr -dr com.apple.quarantine "$target" && return 0
2715
+ fi
2716
+
2717
+ agent_log "warning: failed to clear macOS quarantine for $target"
2718
+ agent_log "if macOS blocks this CLI, run: sudo xattr -dr com.apple.quarantine \"$target\""
2719
+ return 1
2720
+ }
2721
+
2722
+ clear_cli_quarantine() {
2723
+ local label="$1"
2724
+ local bin="$2"
2725
+ is_macos || return 0
2726
+ command -v xattr >/dev/null 2>&1 || return 0
2727
+ [ -n "$bin" ] || return 0
2728
+
2729
+ agent_detail "clearing macOS quarantine attributes for $label CLI"
2730
+ clear_quarantine_path "$bin" || true
2731
+
2732
+ local real
2733
+ real="$(python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$bin" 2>/dev/null || true)"
2734
+ if [ -n "$real" ] && [ "$real" != "$bin" ]; then
2735
+ clear_quarantine_path "$real" || true
2736
+ fi
2737
+
2738
+ case "$bin" in
2739
+ /Applications/*.app/*)
2740
+ local app_bundle
2741
+ app_bundle="${bin%%.app/*}.app"
2742
+ clear_quarantine_path "$app_bundle" || true
2743
+ ;;
2744
+ esac
2745
+
2746
+ case "$real" in
2747
+ /Applications/*.app/*)
2748
+ local app_bundle
2749
+ app_bundle="${real%%.app/*}.app"
2750
+ clear_quarantine_path "$app_bundle" || true
2751
+ ;;
2752
+ esac
2753
+ }
2754
+
2755
+ clear_codex_quarantine() {
2756
+ is_macos || return 0
2757
+
2758
+ local codex_bin
2759
+ codex_bin="$(command -v codex || true)"
2760
+ [ -n "$codex_bin" ] || return 0
2761
+
2762
+ clear_cli_quarantine "codex" "$codex_bin"
2763
+
2764
+ local npm_root
2765
+ npm_root="$("$NPM_BIN" root -g 2>/dev/null || true)"
2766
+ if [ -n "$npm_root" ]; then
2767
+ clear_quarantine_path "$npm_root/@openai/codex" || true
2768
+ fi
2769
+
2770
+ local npm_prefix
2771
+ npm_prefix="$("$NPM_BIN" prefix -g 2>/dev/null || true)"
2772
+ if [ -n "$npm_prefix" ]; then
2773
+ clear_quarantine_path "$npm_prefix/bin/codex" || true
2774
+ fi
2775
+ }
2776
+
2777
+ clear_cursor_quarantine() {
2778
+ local cursor_bin
2779
+ cursor_bin="$(find_cursor_agent_cli 2>/dev/null || true)"
2780
+ [ -n "$cursor_bin" ] || return 0
2781
+
2782
+ clear_cli_quarantine "cursor" "$cursor_bin"
2783
+ }
2784
+
2785
+ print_gatekeeper_help() {
2786
+ local label="$1"
2787
+ local bin="$2"
2788
+ local real="$3"
2789
+
2790
+ cat >&2 <<HELP
2791
+
2792
+ macOS blocked $label CLI while starting it.
2793
+ This is usually caused by Gatekeeper quarantine on the app bundle or CLI binary.
2794
+
2795
+ Run these commands, then retry this script:
2796
+ sudo xattr -dr com.apple.quarantine "$bin"
2797
+ HELP
2798
+
2799
+ if [ -n "$real" ] && [ "$real" != "$bin" ]; then
2800
+ printf ' sudo xattr -dr com.apple.quarantine "%s"\n' "$real" >&2
2801
+ fi
2802
+
2803
+ case "$bin" in
2804
+ /Applications/*.app/*)
2805
+ printf ' sudo xattr -dr com.apple.quarantine "%s"\n' "${bin%%.app/*}.app" >&2
2806
+ ;;
2807
+ esac
2808
+
2809
+ case "$real" in
2810
+ /Applications/*.app/*)
2811
+ printf ' sudo xattr -dr com.apple.quarantine "%s"\n' "${real%%.app/*}.app" >&2
2812
+ ;;
2813
+ esac
2814
+
2815
+ printf '\n' >&2
2816
+ }
2817
+
2818
+ print_codex_gatekeeper_help() {
2819
+ print_gatekeeper_help "codex" "$1" "$2"
2820
+ }
2821
+
2822
+ codex_cli_version() {
2823
+ local codex_bin="$1"
2824
+ "$codex_bin" --version 2>>"$ONE_CLICK_LOG" | head -n 1 | tr -d '\r'
2825
+ }
2826
+
2827
+ codex_cli_version_number() {
2828
+ local codex_bin="$1"
2829
+ codex_cli_version "$codex_bin" | sed -nE 's/.*([0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z]+)*).*/\1/p' | head -n 1
2830
+ }
2831
+
2832
+ resolve_latest_codex_cli_version() {
2833
+ [ -n "$NPM_BIN" ] || return 1
2834
+ sanitize_inherited_npm_exec_env
2835
+ "$NPM_BIN" view "$CODEX_NPM_PACKAGE@latest" version \
2836
+ --registry "$NPM_REGISTRY" \
2837
+ --cache "$NPM_CACHE_DIR" \
2838
+ --prefix "$NPM_GLOBAL_PREFIX" 2>>"$ONE_CLICK_LOG" | tail -n 1 | tr -d '[:space:]'
2839
+ }
2840
+
2841
+ codex_cli_update_available() {
2842
+ CODEX_CURRENT_VERSION="$1" CODEX_LATEST_VERSION="$2" node -e '
2843
+ function parse(value) {
2844
+ const match = String(value || "").match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
2845
+ if (!match) return null;
2846
+ return {
2847
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
2848
+ prerelease: match[4] ? match[4].split(".") : [],
2849
+ };
2850
+ }
2851
+ function compareIdentifiers(left, right) {
2852
+ const leftNumeric = /^\d+$/.test(left);
2853
+ const rightNumeric = /^\d+$/.test(right);
2854
+ if (leftNumeric && rightNumeric) return Number(left) - Number(right);
2855
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
2856
+ return left.localeCompare(right);
2857
+ }
2858
+ function compare(left, right) {
2859
+ for (let index = 0; index < left.core.length; index += 1) {
2860
+ if (left.core[index] !== right.core[index]) return left.core[index] - right.core[index];
2861
+ }
2862
+ if (left.prerelease.length === 0 || right.prerelease.length === 0) {
2863
+ if (left.prerelease.length === right.prerelease.length) return 0;
2864
+ return left.prerelease.length === 0 ? 1 : -1;
2865
+ }
2866
+ const count = Math.max(left.prerelease.length, right.prerelease.length);
2867
+ for (let index = 0; index < count; index += 1) {
2868
+ if (left.prerelease[index] === undefined) return -1;
2869
+ if (right.prerelease[index] === undefined) return 1;
2870
+ const result = compareIdentifiers(left.prerelease[index], right.prerelease[index]);
2871
+ if (result !== 0) return result;
2872
+ }
2873
+ return 0;
2874
+ }
2875
+ const current = parse(process.env.CODEX_CURRENT_VERSION);
2876
+ const latest = parse(process.env.CODEX_LATEST_VERSION);
2877
+ if (!current || !latest) process.exit(2);
2878
+ process.exit(compare(latest, current) > 0 ? 0 : 1);
2879
+ '
2880
+ }
2881
+
2882
+ acquire_codex_update_lock() {
2883
+ local attempt owner owner_pid
2884
+ mkdir -p "$(dirname "$CODEX_UPDATE_LOCK_DIR")"
2885
+ for attempt in $(seq 1 50); do
2886
+ if mkdir "$CODEX_UPDATE_LOCK_DIR" 2>/dev/null; then
2887
+ printf '%s\n' "$ONE_CLICK_RUN_ID" >"$CODEX_UPDATE_LOCK_DIR/owner" 2>/dev/null || true
2888
+ return 0
2889
+ fi
2890
+ owner="$(cat "$CODEX_UPDATE_LOCK_DIR/owner" 2>/dev/null || true)"
2891
+ owner_pid="${owner##*-}"
2892
+ if [ -n "$owner_pid" ] && [ "$owner_pid" != "$owner" ] && ! kill -0 "$owner_pid" 2>/dev/null; then
2893
+ rm -f "$CODEX_UPDATE_LOCK_DIR/owner" 2>/dev/null || true
2894
+ rmdir "$CODEX_UPDATE_LOCK_DIR" 2>/dev/null || true
2895
+ continue
2896
+ fi
2897
+ sleep 0.2
2898
+ done
2899
+ return 1
2900
+ }
2901
+
2902
+ codex_update_output_file() {
2903
+ if [ -n "${AAMP_RUN_LOG_DIR:-}" ]; then
2904
+ printf '%s\n' "$AAMP_RUN_LOG_DIR/codex-update.log"
2905
+ elif [ -n "${ONE_CLICK_LOG:-}" ]; then
2906
+ printf '%s\n' "${ONE_CLICK_LOG}.codex-update"
2907
+ else
2908
+ mktemp "${TMPDIR:-/tmp}/aamp-codex-update.XXXXXX"
2909
+ fi
2910
+ }
2911
+
2912
+ run_codex_cli_update() {
2913
+ local codex_bin="$1"
2914
+ local output_file="$2"
2915
+ local status
2916
+
2917
+ : >"$output_file"
2918
+ if acquire_codex_update_lock; then
2919
+ if "$codex_bin" update >"$output_file" 2>&1; then
2920
+ status=0
2921
+ else
2922
+ status=$?
2923
+ fi
2924
+ release_dir_lock "$CODEX_UPDATE_LOCK_DIR"
2925
+ else
2926
+ status=75
2927
+ printf '%s\n' '无法获取 Codex CLI 升级锁。' >"$output_file"
2928
+ fi
2929
+
2930
+ if [ -n "${ONE_CLICK_LOG:-}" ]; then
2931
+ cat "$output_file" >>"$ONE_CLICK_LOG" 2>/dev/null || true
2932
+ fi
2933
+ return "$status"
2934
+ }
2935
+
2936
+ report_codex_update_warning() {
2937
+ local reason="$1"
2938
+ local output_file="${2:-}"
2939
+
2940
+ agent_detail "warning: $reason"
2941
+ printf '\n🔴 %s\n' "$reason" >&2
2942
+ if [ -n "$output_file" ] && [ -s "$output_file" ]; then
2943
+ printf '%s\n' '--- Codex CLI 升级输出 ---' >&2
2944
+ if ! cat "$output_file" >&2; then
2945
+ printf '%s\n' '无法读取 Codex CLI 升级输出。' >&2
2946
+ fi
2947
+ printf '%s\n' '--- Codex CLI 升级输出结束 ---' >&2
2948
+ fi
2949
+ printf '%s\n\n' '已忽略本次升级失败,继续后续启动流程。' >&2
2950
+ }
2951
+
2952
+ confirm_codex_cli_update() {
2953
+ local answer tty_path
2954
+ tty_path="${AAMP_CODEX_UPDATE_TTY:-/dev/tty}"
2955
+ if ! exec 5<>"$tty_path"; then
2956
+ return 2
2957
+ fi
2958
+ printf '是否现在升级?[y/n] ' >&5
2959
+ if ! IFS= read -r answer <&5; then
2960
+ exec 5>&-
2961
+ return 2
2962
+ fi
2963
+ exec 5>&-
2964
+ case "$answer" in
2965
+ y|Y) return 0 ;;
2966
+ n|N) return 1 ;;
2967
+ *) return 1 ;;
2968
+ esac
2969
+ }
2970
+
2971
+ ensure_codex_cli_updated() {
2972
+ [ "$AGENT" = "codex" ] || return 0
2973
+ [ "$CODEX_AUTO_UPDATE" = "true" ] || return 0
2974
+
2975
+ local codex_bin version_before latest_version version_line comparison_status confirmation_status refreshed_bin version_after status update_log
2976
+ codex_bin="$(resolve_codex_cli_for_acp || true)"
2977
+ [ -n "$codex_bin" ] || return 0
2978
+ version_before="$(codex_cli_version_number "$codex_bin" || true)"
2979
+ latest_version="$(resolve_latest_codex_cli_version || true)"
2980
+ version_line="当前 Codex CLI 版本是:${version_before:-未知},最新版本是:${latest_version:-获取失败}"
2981
+ write_one_click_log "[aamp-one-click] $version_line"
2982
+ agent_detail "checking Codex CLI update: path=$codex_bin version=${version_before:-unknown}"
2983
+
2984
+ if codex_cli_update_available "$version_before" "$latest_version"; then
2985
+ comparison_status=0
2986
+ else
2987
+ comparison_status=$?
2988
+ fi
2989
+ if [ "$comparison_status" -eq 1 ]; then
2990
+ printf '%s\n' "$version_line"
2991
+ agent_detail "Codex CLI is already current: current=${version_before:-unknown} latest=${latest_version:-unknown}"
2992
+ return 0
2993
+ fi
2994
+ if [ "$comparison_status" -ne 0 ]; then
2995
+ agent_detail "warning: unable to compare Codex CLI versions; skipping update"
2996
+ return 0
2997
+ fi
2998
+
2999
+ printf '%s\n' "$version_line"
3000
+ printf '%s\n' '检测到 Codex CLI 有可用更新。不升级可能导致后续任务执行失败。'
3001
+ if confirm_codex_cli_update; then
3002
+ confirmation_status=0
3003
+ else
3004
+ confirmation_status=$?
3005
+ fi
3006
+ if [ "$confirmation_status" -eq 2 ]; then
3007
+ agent_log "无法读取 Codex CLI 升级确认,已跳过升级并继续使用当前版本。"
3008
+ return 0
3009
+ fi
3010
+ if [ "$confirmation_status" -ne 0 ]; then
3011
+ agent_log "已跳过 Codex CLI 升级,继续使用当前版本。"
3012
+ return 0
3013
+ fi
3014
+
3015
+ agent_log "正在更新 Codex CLI..."
3016
+ update_log="$(codex_update_output_file)"
3017
+ if run_codex_cli_update "$codex_bin" "$update_log"; then
3018
+ status=0
3019
+ else
3020
+ status=$?
3021
+ fi
3022
+ if [ "$status" -ne 0 ]; then
3023
+ report_codex_update_warning "Codex CLI 升级失败(状态码:${status})。" "$update_log"
3024
+ return 0
3025
+ fi
3026
+
3027
+ refreshed_bin="$(resolve_codex_cli_for_acp || true)"
3028
+ [ -n "$refreshed_bin" ] || refreshed_bin="$codex_bin"
3029
+ version_after="$(codex_cli_version_number "$refreshed_bin" || true)"
3030
+ agent_detail "Codex CLI update completed: path=$refreshed_bin version=${version_after:-unknown}"
3031
+ if codex_cli_update_available "$version_after" "$latest_version"; then
3032
+ report_codex_update_warning "Codex CLI 升级未生效(当前版本:${version_after:-未知},目标版本:${latest_version})。" "$update_log"
3033
+ return 0
3034
+ else
3035
+ comparison_status=$?
3036
+ fi
3037
+ if [ "$comparison_status" -ne 1 ]; then
3038
+ report_codex_update_warning "无法验证 Codex CLI 升级结果(当前版本:${version_after:-未知},目标版本:${latest_version})。" "$update_log"
3039
+ return 0
3040
+ fi
3041
+ }
3042
+
3043
+ run_codex_login_status() {
3044
+ local codex_bin
3045
+ codex_bin="$(resolve_codex_cli_for_acp || true)"
3046
+ [ -n "$codex_bin" ] || return 127
3047
+
3048
+ set +e
3049
+ "$codex_bin" login status >/dev/null 2>&1
3050
+ local status=$?
3051
+ set -e
3052
+ if [ "$status" -eq 137 ] && is_macos; then
3053
+ local codex_real
3054
+ codex_real="$(python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$codex_bin" 2>/dev/null || true)"
3055
+ print_codex_gatekeeper_help "$codex_bin" "$codex_real"
3056
+ agent_fail "codex CLI was killed by macOS security policy"
3057
+ fi
3058
+ return "$status"
3059
+ }
3060
+
3061
+ run_codex_login() {
3062
+ local codex_bin
3063
+ codex_bin="$(resolve_codex_cli_for_acp || true)"
3064
+ [ -n "$codex_bin" ] || return 127
3065
+
3066
+ set +e
3067
+ "$codex_bin" login
3068
+ local status=$?
3069
+ set -e
3070
+ if [ "$status" -eq 137 ] && is_macos; then
3071
+ local codex_real
3072
+ codex_real="$(python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$codex_bin" 2>/dev/null || true)"
3073
+ print_codex_gatekeeper_help "$codex_bin" "$codex_real"
3074
+ agent_fail "codex CLI was killed by macOS security policy"
3075
+ fi
3076
+ return "$status"
3077
+ }
3078
+
3079
+ print_cursor_gatekeeper_help() {
3080
+ local cursor_bin
3081
+ local cursor_real
3082
+ cursor_bin="$(find_cursor_agent_cli 2>/dev/null || true)"
3083
+ cursor_real="$(python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$cursor_bin" 2>/dev/null || true)"
3084
+ print_gatekeeper_help "cursor" "$cursor_bin" "$cursor_real"
3085
+ }
3086
+
3087
+ run_cursor_command() {
3088
+ local cursor_bin
3089
+ cursor_bin="$(find_cursor_agent_cli)" || return 127
3090
+ "$cursor_bin" "$@"
3091
+ }
3092
+
3093
+ run_cursor_login_status() {
3094
+ local output
3095
+ set +e
3096
+ output="$(run_cursor_command status 2>&1)"
3097
+ local status=$?
3098
+ set -e
3099
+ if [ "$status" -eq 137 ] && is_macos; then
3100
+ print_cursor_gatekeeper_help
3101
+ agent_fail "cursor CLI was killed by macOS security policy"
3102
+ fi
3103
+ if [ "$status" -ne 0 ]; then
3104
+ return "$status"
3105
+ fi
3106
+
3107
+ case "$output" in
3108
+ *"Not logged in"*|*"not logged in"*|*"Logged out"*|*"logged out"*)
3109
+ return 1
3110
+ ;;
3111
+ esac
3112
+
3113
+ return 0
3114
+ }
3115
+
3116
+ run_cursor_login() {
3117
+ set +e
3118
+ run_cursor_command login
3119
+ local status=$?
3120
+ set -e
3121
+ if [ "$status" -eq 137 ] && is_macos; then
3122
+ print_cursor_gatekeeper_help
3123
+ agent_fail "cursor CLI was killed by macOS security policy"
3124
+ fi
3125
+ return "$status"
3126
+ }
3127
+
3128
+ codem_login_output_indicates_bound() {
3129
+ [ -n "$CODEM_LOGIN_LOG" ] || return 1
3130
+ grep -E '绑定成功|已经绑定过设备|already bound|already.*bind|bound.*device' "$CODEM_LOGIN_LOG" >/dev/null 2>&1
3131
+ }
3132
+
3133
+ run_codem_service_start() {
3134
+ local output
3135
+ set +e
3136
+ output="$(codem service start 2>&1)"
3137
+ local status=$?
3138
+ set -e
3139
+ CODEM_SERVICE_START_OUTPUT="$output"
3140
+ if [ "$status" -eq 0 ]; then
3141
+ agent_log "codem service is running"
3142
+ return 0
3143
+ fi
3144
+
3145
+ agent_log "codem service start failed: $output"
3146
+ if codem_service_output_indicates_update_available "$output"; then
3147
+ print_codem_update_notice
3148
+ fi
3149
+ return "$status"
3150
+ }
3151
+
3152
+ run_codem_update_once() {
3153
+ if [ "$CODEM_AUTO_UPDATE_DONE" = "true" ]; then
3154
+ agent_log "codem update was already attempted in this run; not retrying update"
3155
+ return 1
3156
+ fi
3157
+
3158
+ CODEM_AUTO_UPDATE_DONE="true"
3159
+ agent_log "running codem update"
3160
+ set +e
3161
+ codem update -y
3162
+ local status=$?
3163
+ set -e
3164
+ hash -r 2>/dev/null || true
3165
+ ensure_codem_local_bin_on_path
3166
+ if [ "$status" -ne 0 ]; then
3167
+ agent_log "codem update failed with status $status"
3168
+ return "$status"
3169
+ fi
3170
+ agent_log "codem update completed"
3171
+ return 0
3172
+ }
3173
+
3174
+ run_codem_service_start_with_auto_update() {
3175
+ if run_codem_service_start; then
3176
+ return 0
3177
+ fi
3178
+
3179
+ if codem_service_output_indicates_update_available "$CODEM_SERVICE_START_OUTPUT"; then
3180
+ if run_codem_update_once; then
3181
+ run_codem_service_start && return 0
3182
+ fi
3183
+ fi
3184
+
3185
+ return 1
3186
+ }
3187
+
3188
+ run_codem_login() {
3189
+ CODEM_LOGIN_LOG="$(run_log_file "codem-login.log")"
3190
+ print_codem_login_notice
3191
+ set +e
3192
+ codem login --server "$CODEM_SERVER_URL" 2>&1 | tee "$CODEM_LOGIN_LOG"
3193
+ local status=${PIPESTATUS[0]}
3194
+ set -e
3195
+ return "$status"
3196
+ }
3197
+
3198
+ run_codem_login_force_once() {
3199
+ if [ "$CODEM_FORCE_LOGIN_DONE" = "true" ]; then
3200
+ agent_log "codem login --force was already attempted in this run; not retrying force login"
3201
+ return 1
3202
+ fi
3203
+
3204
+ CODEM_FORCE_LOGIN_DONE="true"
3205
+ CODEM_LOGIN_LOG="$(run_log_file "codem-login-force.log")"
3206
+ agent_log "codem still reports login is required after normal login; starting codem login --force"
3207
+ print_codem_login_notice
3208
+ set +e
3209
+ codem login --force --server "$CODEM_SERVER_URL" 2>&1 | tee "$CODEM_LOGIN_LOG"
3210
+ local status=${PIPESTATUS[0]}
3211
+ set -e
3212
+ return "$status"
3213
+ }
3214
+
3215
+ print_codem_provider_auth_notice() {
3216
+ cat >&2 <<NOTICE
3217
+
3218
+ ============================================================
3219
+ [aamp-one-click] CodeM provider authorization check failed.
3220
+ [aamp-one-click] CodeM service is running, but the selected model/provider returned unauthorized.
3221
+ [aamp-one-click] This is usually caused by an old CodeM installation, stale login, or an unavailable selected model.
3222
+ [aamp-one-click] The script will refresh CodeM and force login once, then retry the provider check.
3223
+ [aamp-one-click] If it still fails, run these commands manually:
3224
+ [aamp-one-click] curl -fsSL $CODEM_INSTALLER_URL | bash
3225
+ [aamp-one-click] codem login --force --server "$CODEM_SERVER_URL"
3226
+ [aamp-one-click] codem select-model
3227
+ [aamp-one-click] codem service start
3228
+ ============================================================
3229
+
3230
+ NOTICE
3231
+ }
3232
+
3233
+ run_codem_provider_preflight() {
3234
+ [ "$CODEM_PROVIDER_PREFLIGHT" = "false" ] && {
3235
+ agent_log "codem provider preflight disabled by CODEM_PROVIDER_PREFLIGHT=false"
3236
+ return 0
3237
+ }
3238
+
3239
+ local session
3240
+ local output_file
3241
+ local output
3242
+ local status
3243
+ local pid
3244
+ local waited
3245
+ local prompt
3246
+ session="aamp-codem-preflight-$(date +%s)-$$"
3247
+ output_file="$(run_log_file "codem-preflight.log")"
3248
+ agent_log "checking CodeM provider authorization with isolated session: $session"
3249
+ prompt="Use the bash tool to run: echo AAMP_CODEM_PREFLIGHT_TOOL_OK. After the tool result is available, reply exactly: AAMP_CODEM_PREFLIGHT_FINAL_OK"
3250
+
3251
+ set +e
3252
+ codem -p "$prompt" --sse --yolo --session "$session" >"$output_file" 2>&1 &
3253
+ pid=$!
3254
+ waited=0
3255
+ status=0
3256
+ while kill -0 "$pid" >/dev/null 2>&1; do
3257
+ if [ "$waited" -ge "$CODEM_PROVIDER_PREFLIGHT_TIMEOUT_SECONDS" ]; then
3258
+ kill "$pid" >/dev/null 2>&1 || true
3259
+ wait "$pid" >/dev/null 2>&1
3260
+ status=124
3261
+ break
3262
+ fi
3263
+ sleep 1
3264
+ waited=$((waited + 1))
3265
+ done
3266
+ if [ "$status" -ne 124 ]; then
3267
+ wait "$pid"
3268
+ status=$?
3269
+ fi
3270
+ set -e
3271
+
3272
+ output="$(cat "$output_file" 2>/dev/null || true)"
3273
+ rm -f "$output_file" 2>/dev/null || true
3274
+
3275
+ if [ "$status" -eq 0 ] && printf '%s' "$output" | grep -F 'AAMP_CODEM_PREFLIGHT_FINAL_OK' >/dev/null 2>&1; then
3276
+ agent_log "codem provider authorization check passed"
3277
+ return 0
3278
+ fi
3279
+
3280
+ if codem_output_indicates_provider_unauthorized "$output"; then
3281
+ agent_log "codem provider authorization check failed: unauthorized"
3282
+ return 2
3283
+ fi
3284
+
3285
+ if [ "$status" -eq 124 ]; then
3286
+ agent_log "codem provider authorization check timed out after ${CODEM_PROVIDER_PREFLIGHT_TIMEOUT_SECONDS}s; continuing"
3287
+ return 0
3288
+ fi
3289
+
3290
+ if [ "$status" -eq 0 ]; then
3291
+ agent_log "codem provider authorization check did not produce final marker; continuing. Output: $output"
3292
+ return 0
3293
+ fi
3294
+
3295
+ agent_log "codem provider authorization check returned status $status; continuing. Output: $output"
3296
+ return 0
3297
+ }
3298
+
3299
+ recover_codem_provider_auth_once() {
3300
+ if [ "$CODEM_PROVIDER_RECOVERY_DONE" = "true" ]; then
3301
+ agent_log "codem provider recovery was already attempted in this run; not retrying"
3302
+ return 1
3303
+ fi
3304
+
3305
+ CODEM_PROVIDER_RECOVERY_DONE="true"
3306
+ print_codem_provider_auth_notice
3307
+ install_codem_cli || agent_log "warning: failed to refresh CodeM CLI with installer: $CODEM_INSTALLER_URL"
3308
+ run_codem_login_force_once || agent_log "warning: codem login --force failed. Log: $CODEM_LOGIN_LOG"
3309
+ run_codem_service_start_with_auto_update || agent_log "warning: codem service start still failed after provider recovery"
3310
+ }
3311
+
3312
+ ensure_codem_provider_ready() {
3313
+ local status
3314
+ set +e
3315
+ run_codem_provider_preflight
3316
+ status=$?
3317
+ set -e
3318
+ if [ "$status" -eq 0 ]; then
3319
+ return 0
3320
+ fi
3321
+ if [ "$status" -ne 2 ]; then
3322
+ return "$status"
3323
+ fi
3324
+
3325
+ recover_codem_provider_auth_once || true
3326
+
3327
+ set +e
3328
+ run_codem_provider_preflight
3329
+ status=$?
3330
+ set -e
3331
+ if [ "$status" -eq 0 ]; then
3332
+ return 0
3333
+ fi
3334
+ if [ "$status" -eq 2 ]; then
3335
+ agent_fail "codem provider authorization is still failing; run 'codem select-model' or refresh/login CodeM manually, then restart this script"
3336
+ fi
3337
+ return "$status"
3338
+ }
3339
+
3340
+ ensure_agent_login() {
3341
+ case "$AGENT" in
3342
+ codex)
3343
+ clear_codex_quarantine
3344
+ if ! run_codex_login_status; then
3345
+ agent_log "codex CLI 未登录,正在启动登录流程。"
3346
+ run_codex_login || agent_fail "codex CLI 登录失败。请先执行 'codex login' 完成登录后重新运行脚本。"
3347
+ run_codex_login_status || agent_fail "codex CLI 仍未登录。请先执行 'codex login' 完成登录后重新运行脚本。"
3348
+ fi
3349
+ ;;
3350
+ cursor)
3351
+ clear_cursor_quarantine
3352
+ if run_cursor_login_status; then
3353
+ agent_detail "cursor CLI is already logged in"
3354
+ else
3355
+ agent_log "cursor CLI 未登录,正在启动登录流程。"
3356
+ run_cursor_login || agent_fail "cursor CLI 登录失败。请先执行 'cursor login' 或 'agent login' 完成登录后重新运行脚本。"
3357
+ run_cursor_login_status || agent_fail "cursor CLI 仍未登录。请先执行 'cursor login' 或 'agent login' 完成登录后重新运行脚本。"
3358
+ fi
3359
+ ;;
3360
+ codem)
3361
+ ensure_codem_local_bin_on_path
3362
+ codem --version >/dev/null
3363
+ ensure_codem_breaking_change_compat
3364
+ if run_codem_service_start_with_auto_update; then
3365
+ agent_log "codem service is ready; continuing"
3366
+ ensure_codem_provider_ready
3367
+ return 0
3368
+ fi
3369
+ if codem_service_output_indicates_update_available "$CODEM_SERVICE_START_OUTPUT"; then
3370
+ agent_fail "codem service start failed after attempting CodeM update; run 'codem update -y' and 'codem service start' manually to inspect the error"
3371
+ fi
3372
+
3373
+ agent_log "codem service is not ready; checking Codem device binding"
3374
+ if codem_service_output_indicates_login_required "$CODEM_SERVICE_START_OUTPUT"; then
3375
+ agent_log "codem service reports login is required; starting explicit codem login"
3376
+ fi
3377
+ if ! run_codem_login && ! codem_login_output_indicates_bound; then
3378
+ agent_fail "codem login failed. Log: $CODEM_LOGIN_LOG"
3379
+ fi
3380
+ if run_codem_service_start_with_auto_update; then
3381
+ agent_log "codem device is bound and service is running; continuing"
3382
+ ensure_codem_provider_ready
3383
+ return 0
3384
+ fi
3385
+ if codem_service_output_indicates_login_required "$CODEM_SERVICE_START_OUTPUT"; then
3386
+ run_codem_login_force_once || agent_fail "codem login --force failed. Log: $CODEM_LOGIN_LOG"
3387
+ fi
3388
+ run_codem_service_start_with_auto_update || {
3389
+ if codem_service_output_indicates_update_available "$CODEM_SERVICE_START_OUTPUT"; then
3390
+ agent_fail "codem service start failed after attempting CodeM update; run 'codem update -y' and 'codem service start' manually to inspect the error"
3391
+ fi
3392
+ agent_fail "codem service start failed after binding check; run 'codem service start' manually to inspect the error"
3393
+ }
3394
+ agent_log "codem device is bound and service is running; continuing"
3395
+ ensure_codem_provider_ready
3396
+ ;;
3397
+ claude|gemini)
3398
+ "$AGENT" --version >/dev/null
3399
+ ;;
3400
+ *)
3401
+ "$AGENT" --version >/dev/null 2>&1 || true
3402
+ ;;
3403
+ esac
3404
+ }
3405
+
3406
+ run_acp_bridge() {
3407
+ npx_package --package "$ACP_BRIDGE_PKG" aamp-acp-bridge "$@"
3408
+ }
3409
+
3410
+ run_cli_bridge() {
3411
+ npx_package --package "$CLI_BRIDGE_PKG" aamp-cli-bridge "$@"
3412
+ }
3413
+
3414
+ run_feishu_bridge() {
3415
+ npx_package --package "$FEISHU_BRIDGE_PKG" aamp-feishu-bridge "$@"
3416
+ }
3417
+
3418
+ uses_cli_bridge() {
3419
+ [ "$AGENT" = "codem" ]
3420
+ }
3421
+
3422
+ resolve_codex_cli_for_acp() {
3423
+ if command -v codex >/dev/null 2>&1; then
3424
+ command -v codex
3425
+ return 0
3426
+ fi
3427
+
3428
+ if is_macos && [ -x "$CODEX_APP_CLI" ]; then
3429
+ printf '%s\n' "$CODEX_APP_CLI"
3430
+ return 0
3431
+ fi
3432
+
3433
+ return 1
3434
+ }
3435
+
3436
+ build_acp_agent_command() {
3437
+ ACP_AGENT_COMMAND="$AGENT"
3438
+ if [ "$AGENT" != "codex" ]; then
3439
+ return 0
3440
+ fi
3441
+
3442
+ local codex_bin
3443
+ codex_bin="$(resolve_codex_cli_for_acp)" || agent_fail "codex CLI is unavailable after installation"
3444
+ ACP_AGENT_COMMAND="env CODEX_PATH=$codex_bin npx -y $CODEX_ACP_PKG"
3445
+ agent_detail "using fixed Codex ACP command: CODEX_PATH=$codex_bin $CODEX_ACP_PKG"
3446
+ }
3447
+
3448
+ validate_codex_acp_command() {
3449
+ [ "$AGENT" = "codex" ] || return 0
3450
+
3451
+ local output
3452
+ set +e
3453
+ output="$(acpx --approve-all --cwd "$PWD" --timeout 30 --agent "$ACP_AGENT_COMMAND" sessions ensure --name aamp-one-click-probe 2>&1)"
3454
+ local status=$?
3455
+ set -e
3456
+ if [ "$status" -ne 0 ]; then
3457
+ agent_log "Codex ACP command validation failed:"
3458
+ printf '%s\n' "$output" >&2
3459
+ agent_fail "codex ACP command is unavailable; check Codex.app or reinstall codex CLI"
3460
+ fi
3461
+ }
3462
+
3463
+ start_acp_bridge_and_capture_pairing_url() {
3464
+ ACP_LOG="$(run_log_file "acp-bridge.jsonl")"
3465
+ write_run_manifest
3466
+ agent_log "正在启动 $AGENT 本地桥接..."
3467
+ agent_detail "initializing ACP bridge; log: $ACP_LOG"
3468
+
3469
+ local init_payload
3470
+ local init_output
3471
+ init_payload="$(AAMP_HOST="$AAMP_HOST" AGENT="$AGENT" ACP_AGENT_COMMAND="$ACP_AGENT_COMMAND" node -e 'process.stdout.write(JSON.stringify({ aampHost: process.env.AAMP_HOST, agents: [{ name: process.env.AGENT, acpCommand: process.env.ACP_AGENT_COMMAND, createPairing: true }] }))')"
3472
+
3473
+ set +e
3474
+ init_output="$(printf '%s' "$init_payload" | run_acp_bridge init --json 2>&1)"
3475
+ local init_status=$?
3476
+ set -e
3477
+ printf '%s\n' "$init_output" | tee "$ACP_LOG" >/dev/null
3478
+ if [ "$init_status" -ne 0 ]; then
3479
+ agent_fail "ACP bridge init failed. Log: $ACP_LOG"
3480
+ fi
3481
+
3482
+ PAIRING_URL="$(printf '%s' "$init_output" | node -e 'let input = ""; process.stdin.on("data", chunk => input += chunk); process.stdin.on("end", () => { const start = input.indexOf("{"); const end = input.lastIndexOf("}"); if (start < 0 || end < start) return; const data = JSON.parse(input.slice(start, end + 1)); process.stdout.write(data.agents?.[0]?.pairing?.connectUrl || ""); });')"
3483
+ [ -n "$PAIRING_URL" ] || agent_fail "ACP bridge init did not return a pairing URL. Log: $ACP_LOG"
3484
+ AGENT_BRIDGE_EMAIL="$(printf '%s' "$init_output" | extract_agent_email_from_acp_init_output)"
3485
+ if [ -z "$AGENT_BRIDGE_EMAIL" ]; then
3486
+ AGENT_BRIDGE_EMAIL="$(extract_email_from_pairing_url "$PAIRING_URL")"
3487
+ fi
3488
+ write_run_manifest
3489
+ agent_detail "captured Pairing URL"
3490
+
3491
+ agent_detail "starting ACP bridge; log: $ACP_LOG"
3492
+ local command=(
3493
+ start
3494
+ --agent "$AGENT"
3495
+ )
3496
+ if [ "$DEBUG_MODE" = "true" ]; then
3497
+ command+=(--debug)
3498
+ fi
3499
+
3500
+ start_logged_bridge "$ACP_LOG" ACP_TAIL_PID append run_acp_bridge "${command[@]}"
3501
+ ACP_PID="$STARTED_BRIDGE_PID"
3502
+ update_selected_bot_reservation_pids
3503
+
3504
+ for _ in $(seq 1 90); do
3505
+ if ! kill -0 "$ACP_PID" 2>/dev/null; then
3506
+ agent_fail "ACP bridge exited before becoming ready. Log: $ACP_LOG"
3507
+ fi
3508
+ if grep -E 'Bridge running with|agent\(s\):' "$ACP_LOG" >/dev/null 2>&1; then
3509
+ agent_detail "ACP bridge is ready"
3510
+ return 0
3511
+ fi
3512
+ sleep 1
3513
+ done
3514
+
3515
+ agent_fail "timed out waiting for ACP bridge readiness. Log: $ACP_LOG"
3516
+ }
3517
+
3518
+ start_cli_bridge_and_capture_pairing_url() {
3519
+ CLI_LOG="$(run_log_file "cli-bridge.jsonl")"
3520
+ write_run_manifest
3521
+ agent_log "正在启动 $AGENT 本地桥接..."
3522
+ agent_detail "starting CLI bridge; log: $CLI_LOG"
3523
+ if [ "$DEBUG_MODE" = "true" ]; then
3524
+ export AAMP_CLI_BRIDGE_DEBUG_TASK=1
3525
+ export AAMP_CLI_BRIDGE_DEBUG_PROMPT=1
3526
+ export AAMP_CLI_BRIDGE_DEBUG_RESULT=1
3527
+ export AAMP_CLI_BRIDGE_DEBUG_CLI=1
3528
+ agent_log "CLI bridge debug logging enabled: raw prompt/result will be written to $CLI_LOG"
3529
+ fi
3530
+
3531
+ local command=(
3532
+ init
3533
+ --agent "$AGENT"
3534
+ --aamp-host "$AAMP_HOST"
3535
+ --connection-setup pairing-code
3536
+ )
3537
+ if [ "$DEBUG_MODE" = "true" ]; then
3538
+ command+=(--debug)
3539
+ fi
3540
+
3541
+ start_logged_bridge "$CLI_LOG" CLI_TAIL_PID write run_cli_bridge "${command[@]}"
3542
+ CLI_PID="$STARTED_BRIDGE_PID"
3543
+ update_selected_bot_reservation_pids
3544
+
3545
+ for _ in $(seq 1 90); do
3546
+ if ! kill -0 "$CLI_PID" 2>/dev/null; then
3547
+ agent_fail "CLI bridge exited before emitting Pairing URL. Log: $CLI_LOG"
3548
+ fi
3549
+ PAIRING_URL="$(grep -Eo 'aamp://connect[^[:space:]]+' "$CLI_LOG" | tail -1 || true)"
3550
+ if [ -n "$PAIRING_URL" ]; then
3551
+ AGENT_BRIDGE_EMAIL="$(extract_email_from_pairing_url "$PAIRING_URL")"
3552
+ write_run_manifest
3553
+ agent_detail "captured Pairing URL"
3554
+ return 0
3555
+ fi
3556
+ sleep 1
3557
+ done
3558
+
3559
+ agent_fail "timed out waiting for Pairing URL. Log: $CLI_LOG"
3560
+ }
3561
+
3562
+ start_feishu_task_bridge() {
3563
+ FEISHU_LOG="$(run_log_file "feishu-bridge.jsonl")"
3564
+ write_run_manifest
3565
+ agent_log "正在连接飞书任务..."
3566
+ agent_detail "starting Feishu bridge with task enabled; log: $FEISHU_LOG"
3567
+ agent_detail "Feishu bridge profile: app=$APP_ID lark-cli-profile=$LARK_CLI_PROFILE use-feishu-cli=yes"
3568
+ local command=(
3569
+ start
3570
+ --enable-task
3571
+ --aamp-host "$AAMP_HOST" \
3572
+ --agent "$AGENT" \
3573
+ --app-id "$APP_ID" \
3574
+ --use-feishu-cli \
3575
+ --feishu-cli-profile "$LARK_CLI_PROFILE" \
3576
+ --feishu-cli-bin "$LARK_CLI_CMD" \
3577
+ --pairing-url "$PAIRING_URL"
3578
+ )
3579
+
3580
+ if [ -n "$APP_SECRET" ]; then
3581
+ command+=(--app-secret "$APP_SECRET")
3582
+ fi
3583
+ if [ "$DEBUG_MODE" = "true" ]; then
3584
+ command+=(--debug)
3585
+ fi
3586
+
3587
+ start_logged_bridge "$FEISHU_LOG" FEISHU_TAIL_PID write run_feishu_bridge "${command[@]}"
3588
+ FEISHU_PID="$STARTED_BRIDGE_PID"
3589
+ update_selected_bot_reservation_pids
3590
+ maybe_mock_fail "feishu-bridge"
3591
+
3592
+ for _ in $(seq 1 90); do
3593
+ if ! kill -0 "$FEISHU_PID" 2>/dev/null; then
3594
+ agent_log "Feishu bridge exited before becoming ready. Log: $FEISHU_LOG"
3595
+ agent_log "last 80 lines from Feishu bridge log:"
3596
+ tail -80 "$FEISHU_LOG" >&2 || true
3597
+ forget_current_bot_after_feishu_failure_if_needed
3598
+ agent_fail "Feishu bridge exited before ready"
3599
+ fi
3600
+
3601
+ if [ -z "$FEISHU_BRIDGE_EMAIL" ]; then
3602
+ FEISHU_BRIDGE_EMAIL="$(extract_feishu_email_from_log "$FEISHU_LOG")"
3603
+ if [ -n "$FEISHU_BRIDGE_EMAIL" ]; then
3604
+ write_run_manifest
3605
+ fi
3606
+ fi
3607
+
3608
+ if grep -E 'Feishu bridge IM \+ Task is running for|bridge.task_runtime.running|\[feishu\] listener started|\[feishu ws\] connected' "$FEISHU_LOG" >/dev/null 2>&1; then
3609
+ if [ -z "$FEISHU_BRIDGE_EMAIL" ]; then
3610
+ FEISHU_BRIDGE_EMAIL="$(extract_feishu_email_from_log "$FEISHU_LOG")"
3611
+ fi
3612
+ write_run_manifest
3613
+ agent_detail "Feishu bridge is ready. Keep this terminal open. Press Ctrl+C to stop."
3614
+ agent_success
3615
+ return 0
3616
+ fi
3617
+
3618
+ sleep 1
3619
+ done
3620
+
3621
+ agent_log "timed out waiting for Feishu bridge readiness. Log: $FEISHU_LOG"
3622
+ agent_log "last 80 lines from Feishu bridge log:"
3623
+ tail -80 "$FEISHU_LOG" >&2 || true
3624
+ forget_current_bot_after_feishu_failure_if_needed
3625
+ agent_fail "timed out waiting for Feishu bridge readiness"
3626
+ }
3627
+
3628
+ emit_internal_result() {
3629
+ local payload="$1"
3630
+ printf '%s\n' "$payload" >&"$AAMP_TASK_INTERNAL_RESULT_FD"
3631
+ }
3632
+
3633
+ binding_json_field() {
3634
+ local path_value="$1"
3635
+ printf '%s' "${AAMP_TASK_INTERNAL_BINDING_JSON:-}" | AAMP_BINDING_PATH="$path_value" node -e '
3636
+ let input = "";
3637
+ process.stdin.setEncoding("utf8");
3638
+ process.stdin.on("data", (chunk) => { input += chunk; });
3639
+ process.stdin.on("end", () => {
3640
+ const data = JSON.parse(input || "{}");
3641
+ const value = String(process.env.AAMP_BINDING_PATH || "")
3642
+ .split(".")
3643
+ .filter(Boolean)
3644
+ .reduce((current, key) => current == null ? undefined : current[key], data);
3645
+ if (value !== undefined && value !== null) process.stdout.write(String(value));
3646
+ });
3647
+ '
3648
+ }
3649
+
3650
+ run_internal_discover_agents() {
3651
+ local agents=()
3652
+ if agent_cli_detected codex; then
3653
+ agents+=("codex")
3654
+ fi
3655
+ if agent_cli_detected cursor; then
3656
+ agents+=("cursor")
3657
+ fi
3658
+ local joined=""
3659
+ if [ "${#agents[@]}" -gt 0 ]; then
3660
+ joined="$(IFS=,; printf '%s' "${agents[*]}")"
3661
+ fi
3662
+ AAMP_DISCOVERED_AGENTS="$joined" node -e '
3663
+ const agents = String(process.env.AAMP_DISCOVERED_AGENTS || "").split(",").filter(Boolean);
3664
+ process.stdout.write(JSON.stringify({ agents }));
3665
+ ' >&"$AAMP_TASK_INTERNAL_RESULT_FD"
3666
+ }
3667
+
3668
+ prepare_internal_agent_environment() {
3669
+ [ -n "$AGENT" ] || agent_fail "internal Agent preparation requires --agent"
3670
+ validate_agent_name "$AGENT"
3671
+ source_lark_env
3672
+ ensure_agent_cli
3673
+ }
3674
+
3675
+ run_internal_register_binding() {
3676
+ [ -n "$AGENT" ] || agent_fail "internal Bot registration requires --agent"
3677
+ validate_agent_name "$AGENT"
3678
+ source_lark_env
3679
+ register_feishu_app
3680
+ emit_internal_result "{\"app_id\":\"$(json_escape "$APP_ID")\",\"app_secret\":\"$(json_escape "$APP_SECRET")\",\"display_name\":\"$(json_escape "$BOT_NAME")\",\"lark_cli_profile\":\"$(json_escape "$LARK_CLI_PROFILE")\"}"
3681
+ }
3682
+
3683
+ run_internal_prepare_agent() {
3684
+ prepare_internal_agent_environment
3685
+ ensure_codex_cli_updated
3686
+ ensure_agent_login
3687
+ maybe_mock_fail "agent-login"
3688
+ ensure_acpx
3689
+ build_acp_agent_command
3690
+ emit_internal_result "{\"agent_type\":\"$(json_escape "$AGENT")\",\"acp_command\":\"$(json_escape "$ACP_AGENT_COMMAND")\",\"lark_cli_config_dir\":\"$(json_escape "${LARKSUITE_CLI_CONFIG_DIR:-}")\"}"
3691
+ }
3692
+
3693
+ run_internal_ensure_profile() {
3694
+ AAMP_TASK_INTERNAL_BINDING_JSON=""
3695
+ IFS= read -r AAMP_TASK_INTERNAL_BINDING_JSON <&"$AAMP_TASK_INTERNAL_INPUT_FD" || true
3696
+ [ -n "$AAMP_TASK_INTERNAL_BINDING_JSON" ] || agent_fail "missing internal binding payload"
3697
+ APP_ID="$(binding_json_field bot.app_id)"
3698
+ APP_SECRET="$(binding_json_field bot.app_secret)"
3699
+ BOT_NAME="$(binding_json_field bot.display_name)"
3700
+ LARK_CLI_PROFILE="$(binding_json_field bot.lark_cli_profile)"
3701
+ [ -n "$APP_ID" ] && [ -n "$APP_SECRET" ] && [ -n "$LARK_CLI_PROFILE" ] || agent_fail "binding is missing Feishu credentials or profile"
3702
+ AAMP_TASK_INTERNAL_BINDING_JSON=""
3703
+ source_lark_env
3704
+ ensure_lark_cli_profile "$APP_ID" "$APP_SECRET" "$LARK_CLI_PROFILE"
3705
+ APP_SECRET=""
3706
+ emit_internal_result "{\"lark_cli_bin\":\"$(json_escape "$LARK_CLI_CMD")\",\"lark_cli_config_dir\":\"$(json_escape "${LARKSUITE_CLI_CONFIG_DIR:-}")\"}"
3707
+ }
3708
+
3709
+ run_internal_action() {
3710
+ AAMP_TASK_INTERNAL="true"
3711
+ ensure_node_toolchain
3712
+ case "$AAMP_TASK_ACTION" in
3713
+ __discover-agents)
3714
+ run_internal_discover_agents
3715
+ ;;
3716
+ __register-binding)
3717
+ run_internal_register_binding
3718
+ ;;
3719
+ __prepare-agent)
3720
+ run_internal_prepare_agent
3721
+ ;;
3722
+ __ensure-profile)
3723
+ run_internal_ensure_profile
3724
+ ;;
3725
+ *)
3726
+ agent_fail "unknown internal action: $AAMP_TASK_ACTION"
3727
+ ;;
3728
+ esac
3729
+ }
3730
+
3731
+ task_agent_controller_path() {
3732
+ printf '%s/bin/feishu-task-agent-controller.mjs\n' "$(task_agent_global_package_dir)"
3733
+ }
3734
+
3735
+ run_task_agent_controller() {
3736
+ local controller bootstrap_path install_command
3737
+ controller="$(task_agent_controller_path)"
3738
+ [ -r "$controller" ] || agent_fail "Task Agent controller is missing: $controller"
3739
+ bootstrap_path="$AAMP_TASK_COMMAND_PATH"
3740
+ [ -r "$bootstrap_path" ] || agent_fail "installed Task Agent command is missing: $bootstrap_path"
3741
+ install_command="npx -y --package $AAMP_TASK_AGENT_NAME@$AAMP_TASK_AGENT_CHANNEL feishu-task-agent install"
3742
+ export AAMP_TASK_BOOTSTRAP_PATH="$bootstrap_path"
3743
+ export AAMP_TASK_NPM_BIN="$NPM_BIN"
3744
+ export AAMP_TASK_NPX_BIN="$NPX_BIN"
3745
+ export AAMP_TASK_ACP_BRIDGE_PKG="$ACP_BRIDGE_PKG"
3746
+ export AAMP_TASK_FEISHU_BRIDGE_PKG="$FEISHU_BRIDGE_PKG"
3747
+ export AAMP_TASK_CODEX_ACP_PKG="$CODEX_ACP_PKG"
3748
+ export AAMP_TASK_AGENT_VERSION
3749
+ export AAMP_TASK_DEFAULT_AGENT="$AGENT"
3750
+ export AAMP_TASK_AAMP_HOST="$AAMP_HOST"
3751
+ export AAMP_TASK_DEBUG_MODE="$DEBUG_MODE"
3752
+ export AAMP_TASK_INSTALL_COMMAND="$install_command"
3753
+ export AAMP_TASK_NPM_REGISTRY="$NPM_REGISTRY"
3754
+ export AAMP_TASK_NPM_CACHE_DIR="$NPM_CACHE_DIR"
3755
+ export AAMP_TASK_NPM_GLOBAL_PREFIX="$NPM_GLOBAL_PREFIX"
3756
+ export AAMP_LARK_CLI_CONFIG_DIR
3757
+ trap - EXIT INT TERM HUP
3758
+ exec node "$controller" "$AAMP_TASK_ACTION"
3759
+ }
3760
+
3761
+ cleanup() {
3762
+ local status=$?
3763
+ trap - EXIT INT TERM HUP
3764
+ if [ -n "$FEISHU_TAIL_PID" ]; then
3765
+ kill "$FEISHU_TAIL_PID" 2>/dev/null || true
3766
+ fi
3767
+ if [ -n "$CLI_TAIL_PID" ]; then
3768
+ kill "$CLI_TAIL_PID" 2>/dev/null || true
3769
+ fi
3770
+ if [ -n "$ACP_TAIL_PID" ]; then
3771
+ kill "$ACP_TAIL_PID" 2>/dev/null || true
3772
+ fi
3773
+ if [ -n "$FEISHU_PID" ]; then
3774
+ force_kill_process_tree "$FEISHU_PID"
3775
+ fi
3776
+ if [ -n "$CLI_PID" ]; then
3777
+ force_kill_process_tree "$CLI_PID"
3778
+ fi
3779
+ if [ -n "$ACP_PID" ]; then
3780
+ force_kill_process_tree "$ACP_PID"
3781
+ fi
3782
+ release_selected_bot_reservation
3783
+ release_bot_selection_lock
3784
+ exit "$status"
3785
+ }
3786
+
3787
+ main() {
3788
+ if [ "$#" -eq 0 ] && { \
3789
+ [ "$AAMP_TASK_ENTRY" = "short" ] \
3790
+ || [ "${0:-}" = "$AAMP_TASK_COMMAND_PATH" ] \
3791
+ || [ "${0:-}" = "$NPM_GLOBAL_PREFIX/bin/$AAMP_TASK_COMMAND_NAME" ]; \
3792
+ }; then
3793
+ AAMP_TASK_DEFAULT_ACTION="help"
3794
+ else
3795
+ AAMP_TASK_DEFAULT_ACTION="install"
3796
+ fi
3797
+ parse_args "$@"
3798
+
3799
+ if [ "$AAMP_TASK_ACTION" = "help" ]; then
3800
+ usage
3801
+ return 0
3802
+ fi
3803
+
3804
+ case "$AAMP_TASK_ACTION" in
3805
+ __discover-agents|__register-binding|__prepare-agent|__ensure-profile)
3806
+ run_internal_action
3807
+ return 0
3808
+ ;;
3809
+ esac
3810
+
3811
+ trap cleanup EXIT INT TERM HUP
3812
+ init_log_run
3813
+ validate_mock_fail_stage
3814
+ # Do not clean globally by default: users may run one-click scripts in
3815
+ # multiple terminals. Cleanup on exit is limited to pids started by this run.
3816
+ cleanup_stale_one_click_processes
3817
+ ensure_node_toolchain
3818
+ maybe_mock_fail "node-toolchain"
3819
+ install_short_command || agent_fail "failed to install $AAMP_TASK_COMMAND_NAME"
3820
+ if [ "$AAMP_TASK_ACTION" = "update" ]; then
3821
+ run_task_agent_update_command
3822
+ return 0
3823
+ fi
3824
+ auto_update_short_command_if_needed normal
3825
+ adopt_newer_global_task_agent_if_available
3826
+ install_aamp_logs_bin "$AAMP_TASK_AGENT_VERSION" || agent_log "warning: aamp-logs command is unavailable for this run"
3827
+ adopt_newer_global_task_agent_if_available
3828
+ ensure_task_agent_global_install "$AAMP_TASK_AGENT_VERSION" || agent_fail "failed to install the complete Task Agent package"
3829
+ record_version_line
3830
+ write_task_update_cache "$AAMP_TASK_AGENT_VERSION"
3831
+ run_task_agent_controller
3832
+ }
3833
+
3834
+ main "$@"