@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.
- package/README.md +153 -0
- package/bin/aamp-logs.mjs +523 -0
- package/bin/feishu-task-agent-controller.mjs +1899 -0
- package/bin/runtime-network.mjs +326 -0
- package/bootstrap/aamp-feishu-task-agent-bootstrap.sh +3834 -0
- package/package.json +20 -0
|
@@ -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 "$@"
|