@konglx/rotom 2.21.0
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 +417 -0
- package/bin/mesh-master.sh +439 -0
- package/bin/rotom +29 -0
- package/bin/rotom-link.sh +136 -0
- package/bin/rotom-send-with-status +57 -0
- package/bin/rotom-up.sh +428 -0
- package/dist/cli/ask.js +62 -0
- package/dist/cli/common.js +321 -0
- package/dist/cli/config.js +65 -0
- package/dist/cli/directory.js +17 -0
- package/dist/cli/executor.js +58 -0
- package/dist/cli/fed.js +91 -0
- package/dist/cli/group.js +273 -0
- package/dist/cli/identity.js +62 -0
- package/dist/cli/init.js +268 -0
- package/dist/cli/issue.js +202 -0
- package/dist/cli/join.js +170 -0
- package/dist/cli/link.js +47 -0
- package/dist/cli/master.js +51 -0
- package/dist/cli/memory.js +307 -0
- package/dist/cli/note.js +68 -0
- package/dist/cli/repo.js +77 -0
- package/dist/cli/rotom.js +277 -0
- package/dist/cli/routes.js +118 -0
- package/dist/cli/run.js +45 -0
- package/dist/cli/schedule.js +237 -0
- package/dist/cli/skill.js +173 -0
- package/dist/cli/team.js +106 -0
- package/dist/executor/claude-code-hook.cjs +80 -0
- package/dist/executor/cli-executor.js +8 -0
- package/dist/executor/executors/claude-code.js +780 -0
- package/dist/executor/executors/codex.js +719 -0
- package/dist/executor/executors/hermes-cli.js +855 -0
- package/dist/executor/executors/openclaw.js +467 -0
- package/dist/executor/executors/pi.js +514 -0
- package/dist/executor/index.js +269 -0
- package/dist/executor/jsonrpc-transport.js +125 -0
- package/dist/executor/process-runner.js +101 -0
- package/dist/executor/reasoning-status.js +83 -0
- package/dist/executor/repo-cache.js +502 -0
- package/dist/executor/session-store.js +188 -0
- package/dist/executor/worker-chat.js +257 -0
- package/dist/executor/worker-connection.js +89 -0
- package/dist/executor/worker-issue.js +264 -0
- package/dist/executor/worker.js +877 -0
- package/dist/link/pending-requests.js +72 -0
- package/dist/link/server.js +233 -0
- package/dist/link/visibility-store.js +58 -0
- package/dist/master/api/agents.js +333 -0
- package/dist/master/api/artifacts.js +271 -0
- package/dist/master/api/domains.js +64 -0
- package/dist/master/api/groups.js +635 -0
- package/dist/master/api/guidance-templates.js +147 -0
- package/dist/master/api/index.js +89 -0
- package/dist/master/api/issues-patrol.js +172 -0
- package/dist/master/api/issues.js +663 -0
- package/dist/master/api/links-patrol.js +168 -0
- package/dist/master/api/links.js +114 -0
- package/dist/master/api/memory.js +259 -0
- package/dist/master/api/messages.js +157 -0
- package/dist/master/api/notes.js +77 -0
- package/dist/master/api/schedule-patterns.js +133 -0
- package/dist/master/api/schedules.js +272 -0
- package/dist/master/api/sessions.js +158 -0
- package/dist/master/api/share.js +269 -0
- package/dist/master/api/skills.js +190 -0
- package/dist/master/api/teams.js +122 -0
- package/dist/master/api/uploads.js +245 -0
- package/dist/master/auth.js +134 -0
- package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
- package/dist/master/dashboard/animations/calico-error.apng +0 -0
- package/dist/master/dashboard/animations/calico-happy.apng +0 -0
- package/dist/master/dashboard/animations/calico-notification.apng +0 -0
- package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
- package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
- package/dist/master/dashboard/animations/calico-waking.apng +0 -0
- package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
- package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
- package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
- package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
- package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
- package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
- package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
- package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
- package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
- package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
- package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
- package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
- package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
- package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
- package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
- package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
- package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
- package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
- package/dist/master/dashboard/debug-auth.html +197 -0
- package/dist/master/dashboard/favicon.ico +0 -0
- package/dist/master/dashboard/index.html +20 -0
- package/dist/master/dashboard/rotom-avatar.png +0 -0
- package/dist/master/db/agent-sessions.js +60 -0
- package/dist/master/db/agent-visibility.js +64 -0
- package/dist/master/db/agents.js +119 -0
- package/dist/master/db/ask-bridges.js +157 -0
- package/dist/master/db/build-update.js +59 -0
- package/dist/master/db/core.js +82 -0
- package/dist/master/db/domains.js +80 -0
- package/dist/master/db/groups.js +316 -0
- package/dist/master/db/guidance-templates.js +58 -0
- package/dist/master/db/index.js +12 -0
- package/dist/master/db/internal.js +45 -0
- package/dist/master/db/issues-patrol.js +81 -0
- package/dist/master/db/issues.js +373 -0
- package/dist/master/db/links.js +221 -0
- package/dist/master/db/master-node.js +43 -0
- package/dist/master/db/memory.js +272 -0
- package/dist/master/db/messages.js +210 -0
- package/dist/master/db/notes.js +55 -0
- package/dist/master/db/schedule-patterns.js +56 -0
- package/dist/master/db/schedules.js +135 -0
- package/dist/master/db/skills.js +144 -0
- package/dist/master/db/team.js +88 -0
- package/dist/master/db/types.js +10 -0
- package/dist/master/db.js +12 -0
- package/dist/master/embedded.js +133 -0
- package/dist/master/federation/client.js +283 -0
- package/dist/master/federation/identity.js +133 -0
- package/dist/master/federation/manager.js +267 -0
- package/dist/master/federation/publisher.js +87 -0
- package/dist/master/federation/self-publisher.js +69 -0
- package/dist/master/federation/server.js +487 -0
- package/dist/master/group-paths.js +208 -0
- package/dist/master/offline-queue.js +38 -0
- package/dist/master/opc-bootstrap.js +245 -0
- package/dist/master/patrol-terminal.js +275 -0
- package/dist/master/repo-scan.js +188 -0
- package/dist/master/router.js +214 -0
- package/dist/master/scheduler-handlers.js +510 -0
- package/dist/master/scheduler.js +201 -0
- package/dist/master/server.js +203 -0
- package/dist/master/services/link-collector.js +82 -0
- package/dist/master/services/link-patrol-bootstrap.js +50 -0
- package/dist/master/services/memory-extract-prompt.js +34 -0
- package/dist/master/services/patrol-bootstrap.js +63 -0
- package/dist/master/share-tokens.js +56 -0
- package/dist/master/terminal-hub.js +300 -0
- package/dist/master/uploads.js +108 -0
- package/dist/master/util/fs.js +100 -0
- package/dist/master/util/paths.js +50 -0
- package/dist/master/util/persona.js +10 -0
- package/dist/master/ws-hub/connection.js +928 -0
- package/dist/master/ws-hub/conversation.js +290 -0
- package/dist/master/ws-hub/directory.js +70 -0
- package/dist/master/ws-hub/dispatch-enrich.js +34 -0
- package/dist/master/ws-hub/hub.js +136 -0
- package/dist/master/ws-hub/index.js +9 -0
- package/dist/master/ws-hub/internal.js +35 -0
- package/dist/master/ws-hub/routing.js +295 -0
- package/dist/master/ws-hub/sessions.js +130 -0
- package/dist/master/ws-hub.js +11 -0
- package/dist/shared/agent-profile.js +44 -0
- package/dist/shared/constants.js +55 -0
- package/dist/shared/dedup.js +33 -0
- package/dist/shared/group-context.js +62 -0
- package/dist/shared/json-codec.js +33 -0
- package/dist/shared/logger.js +136 -0
- package/dist/shared/mention.js +22 -0
- package/dist/shared/network.js +40 -0
- package/dist/shared/parse.js +18 -0
- package/dist/shared/prompt-composer.js +171 -0
- package/dist/shared/protocol/client-messages.js +8 -0
- package/dist/shared/protocol/enums.js +6 -0
- package/dist/shared/protocol/federation.js +62 -0
- package/dist/shared/protocol/guards.js +87 -0
- package/dist/shared/protocol/server-messages.js +8 -0
- package/dist/shared/protocol/types.js +8 -0
- package/dist/shared/protocol.js +19 -0
- package/dist/shared/readonly-allowlist.js +122 -0
- package/dist/shared/rotom-cli-prompt.js +23 -0
- package/dist/shared/skill-context.js +19 -0
- package/dist/shared/skill-md.js +43 -0
- package/dist/shared/slash-commands.js +50 -0
- package/dist/shared/time.js +80 -0
- package/dist/shared/title.js +46 -0
- package/dist/shared/url-extractor.js +99 -0
- package/migrations/001-schema.sql +942 -0
- package/package.json +68 -0
- package/scripts/fix-node-pty-perms.mjs +46 -0
- package/skill/rotom-a2a-communicate/SKILL.md +257 -0
- package/skill/rotom-bus-host/SKILL.md +78 -0
- package/skill/rotom-bus-host/scripts/poll-replies.sh +148 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Digital Employee Mesh — Master 管理脚本
|
|
4
|
+
#
|
|
5
|
+
# 用法: mesh-master <command> [options]
|
|
6
|
+
# 命令: start | stop | restart | status | install-service | uninstall-service
|
|
7
|
+
# 选项: --port/-p <port> --host <addr> --data/-d <dir> --daemon
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
13
|
+
SERVER_JS="$SCRIPT_DIR/dist/master/server.js"
|
|
14
|
+
OPENCLAW_DIR="$HOME/.openclaw"
|
|
15
|
+
PID_FILE="$OPENCLAW_DIR/mesh-master.pid"
|
|
16
|
+
SVC="com.openclaw.mesh-master"
|
|
17
|
+
|
|
18
|
+
PORT="${MESH_MASTER_PORT:-28800}"
|
|
19
|
+
HOST="${MESH_MASTER_HOST:-0.0.0.0}"
|
|
20
|
+
DATA="${MESH_MASTER_DATA:-$HOME/.rotom}"
|
|
21
|
+
DAEMON=false
|
|
22
|
+
DEV=false
|
|
23
|
+
|
|
24
|
+
# ── 工具函数 ──────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
detect_os() { [[ "$OSTYPE" == darwin* ]] && echo macos || echo linux; }
|
|
27
|
+
|
|
28
|
+
# 查找占用端口的 PID(lsof 在 macOS SIP 下可能看不到 launchd 子进程)
|
|
29
|
+
find_port_pid() {
|
|
30
|
+
if command -v lsof &>/dev/null; then
|
|
31
|
+
lsof -ti :"$PORT" -sTCP:LISTEN 2>/dev/null || true
|
|
32
|
+
elif command -v ss &>/dev/null; then
|
|
33
|
+
ss -tlnp "sport = :$PORT" 2>/dev/null | grep -oP 'pid=\K[0-9]+' | head -1 || true
|
|
34
|
+
elif command -v fuser &>/dev/null; then
|
|
35
|
+
fuser "$PORT/tcp" 2>/dev/null | tr -d ' ' || true
|
|
36
|
+
else
|
|
37
|
+
echo ""
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# 通过 launchctl 获取服务 PID(macOS)
|
|
42
|
+
find_service_pid() {
|
|
43
|
+
if [ "$(detect_os)" = "macos" ]; then
|
|
44
|
+
launchctl list "$SVC" 2>/dev/null | awk '{print $1}' | grep -E '^[0-9]+$' || true
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# 检测端口是否被占用:lsof → TCP 探测 → curl health
|
|
49
|
+
is_port_in_use() {
|
|
50
|
+
# 1. lsof(快,但 macOS SIP 下可能看不到 launchd 子进程)
|
|
51
|
+
[ -n "$(find_port_pid)" ] && return 0
|
|
52
|
+
# 2. TCP 连接探测(不受 SIP 限制)
|
|
53
|
+
python3 -c "
|
|
54
|
+
import socket, sys
|
|
55
|
+
s = socket.socket()
|
|
56
|
+
s.settimeout(1)
|
|
57
|
+
try:
|
|
58
|
+
s.connect(('127.0.0.1', $PORT))
|
|
59
|
+
s.close()
|
|
60
|
+
except:
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
" 2>/dev/null && return 0
|
|
63
|
+
# 3. curl health 端点
|
|
64
|
+
curl -sf --connect-timeout 2 "http://127.0.0.1:$PORT/health" >/dev/null 2>&1 && return 0
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# 轮询等待端口就绪(默认最多 10 秒)
|
|
69
|
+
wait_for_port() {
|
|
70
|
+
local max_wait=${1:-10} i=0
|
|
71
|
+
while [ $i -lt $max_wait ]; do
|
|
72
|
+
is_port_in_use && return 0
|
|
73
|
+
sleep 1
|
|
74
|
+
i=$((i+1))
|
|
75
|
+
done
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# 轮询等待端口释放(默认最多 5 秒)
|
|
80
|
+
wait_for_port_free() {
|
|
81
|
+
local max_wait=${1:-5} i=0
|
|
82
|
+
while [ $i -lt $max_wait ]; do
|
|
83
|
+
is_port_in_use || return 0
|
|
84
|
+
sleep 1
|
|
85
|
+
i=$((i+1))
|
|
86
|
+
done
|
|
87
|
+
return 1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# 检查系统服务是否已安装(plist/service 文件存在)
|
|
91
|
+
is_service_installed() {
|
|
92
|
+
if [ "$(detect_os)" = "macos" ]; then
|
|
93
|
+
[ -f "$HOME/Library/LaunchAgents/$SVC.plist" ]
|
|
94
|
+
else
|
|
95
|
+
[ -f "$HOME/.config/systemd/user/mesh-master.service" ]
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# 停止系统服务(不删除 plist 文件)
|
|
100
|
+
# macOS: bootout 替代废弃的 stop+unload,彻底停止并阻止 KeepAlive 重启
|
|
101
|
+
stop_service() {
|
|
102
|
+
if [ "$(detect_os)" = "macos" ]; then
|
|
103
|
+
local domain="gui/$(id -u)"
|
|
104
|
+
launchctl bootout "$domain/$SVC" 2>/dev/null || true
|
|
105
|
+
else
|
|
106
|
+
systemctl --user stop mesh-master 2>/dev/null || true
|
|
107
|
+
fi
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# 启动系统服务
|
|
111
|
+
# macOS: bootstrap + kickstart 替代废弃的 load,确保进程实际启动
|
|
112
|
+
start_service() {
|
|
113
|
+
if [ "$(detect_os)" = "macos" ]; then
|
|
114
|
+
local domain="gui/$(id -u)"
|
|
115
|
+
local plist="$HOME/Library/LaunchAgents/$SVC.plist"
|
|
116
|
+
# bootstrap 注册服务(如果已注册则忽略错误)
|
|
117
|
+
launchctl bootstrap "$domain" "$plist" 2>/dev/null || true
|
|
118
|
+
# kickstart -k 强制(重新)启动,解决 bootstrap 不触发 RunAtLoad 的已知问题
|
|
119
|
+
launchctl kickstart -k "$domain/$SVC" 2>/dev/null || true
|
|
120
|
+
else
|
|
121
|
+
systemctl --user start mesh-master 2>/dev/null || true
|
|
122
|
+
fi
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fix_path() {
|
|
126
|
+
[ -d "/opt/homebrew/bin" ] && export PATH="/opt/homebrew/bin:$PATH"
|
|
127
|
+
[ -d "/usr/local/bin" ] && export PATH="/usr/local/bin:$PATH"
|
|
128
|
+
[ -d "$HOME/.nodejs/bin" ] && export PATH="$HOME/.nodejs/bin:$PATH"
|
|
129
|
+
[ -s "$HOME/.nvm/nvm.sh" ] && { export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"; }
|
|
130
|
+
[ -d "$HOME/.fnm" ] && { export PATH="$HOME/.fnm:$PATH"; eval "$(fnm env 2>/dev/null)" || true; }
|
|
131
|
+
[ -d "$HOME/.volta" ] && export PATH="$HOME/.volta/bin:$PATH"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
detect_pkg() {
|
|
135
|
+
command -v pnpm &>/dev/null && echo pnpm && return
|
|
136
|
+
command -v npm &>/dev/null && echo npm && return
|
|
137
|
+
command -v yarn &>/dev/null && echo yarn && return
|
|
138
|
+
echo ""
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ensure_built() {
|
|
142
|
+
local need=false
|
|
143
|
+
[ ! -f "$SERVER_JS" ] && need=true
|
|
144
|
+
# 选择哪些源文件参与“是否需要重建”的判断
|
|
145
|
+
# dev: 只看 src/ 下的后端 .ts;prod: 同时看 packages/ 下的前端
|
|
146
|
+
local watch_dirs="$SCRIPT_DIR/src"
|
|
147
|
+
[ "$DEV" = false ] && watch_dirs="$watch_dirs $SCRIPT_DIR/packages"
|
|
148
|
+
[ "$need" = false ] && {
|
|
149
|
+
local newer
|
|
150
|
+
# shellcheck disable=SC2086
|
|
151
|
+
newer=$(find $watch_dirs \( -name '*.ts' -o -name '*.tsx' -o -name '*.html' -o -name '*.css' \) -newer "$SERVER_JS" -print -quit 2>/dev/null)
|
|
152
|
+
[ -n "$newer" ] && need=true
|
|
153
|
+
}
|
|
154
|
+
if [ "$need" = true ]; then
|
|
155
|
+
local target="build:master"
|
|
156
|
+
[ "$DEV" = true ] && target="build"
|
|
157
|
+
echo "[mesh-master] 构建中 ($target)..."
|
|
158
|
+
local pkg; pkg=$(detect_pkg)
|
|
159
|
+
case "$pkg" in
|
|
160
|
+
pnpm) (cd "$SCRIPT_DIR" && pnpm "$target") ;;
|
|
161
|
+
npm) (cd "$SCRIPT_DIR" && npm run "$target") ;;
|
|
162
|
+
yarn) (cd "$SCRIPT_DIR" && yarn "$target") ;;
|
|
163
|
+
*) echo "[mesh-master] 未找到包管理器"; return 1 ;;
|
|
164
|
+
esac
|
|
165
|
+
fi
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# 杀掉进程(PID 文件 → launchctl 查询 → 端口查找)
|
|
169
|
+
kill_process() {
|
|
170
|
+
local pid
|
|
171
|
+
# 1. PID 文件
|
|
172
|
+
pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
|
173
|
+
# 2. launchctl list(macOS,SIP 下 lsof 看不到 launchd 子进程)
|
|
174
|
+
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
|
|
175
|
+
pid=$(find_service_pid)
|
|
176
|
+
fi
|
|
177
|
+
# 3. 端口查找
|
|
178
|
+
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
|
|
179
|
+
pid=$(find_port_pid)
|
|
180
|
+
fi
|
|
181
|
+
if [ -z "$pid" ]; then
|
|
182
|
+
return 1
|
|
183
|
+
fi
|
|
184
|
+
echo "[mesh-master] 停止中 (PID $pid)..."
|
|
185
|
+
kill "$pid" 2>/dev/null || true
|
|
186
|
+
local i=0; while kill -0 "$pid" 2>/dev/null && [ $i -lt 50 ]; do sleep 0.1; i=$((i+1)); done
|
|
187
|
+
kill -0 "$pid" 2>/dev/null && kill -9 "$pid" 2>/dev/null || true
|
|
188
|
+
rm -f "$PID_FILE"
|
|
189
|
+
return 0
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# ── 命令 ──────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
do_run() {
|
|
195
|
+
fix_path || true
|
|
196
|
+
ensure_built || true
|
|
197
|
+
mkdir -p "$OPENCLAW_DIR" "$DATA"
|
|
198
|
+
exec node "$SERVER_JS" --port "$PORT" --host "$HOST" --data "$DATA"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
do_start() {
|
|
202
|
+
if is_port_in_use; then
|
|
203
|
+
echo "[mesh-master] 端口 $PORT 已被占用"; return 1
|
|
204
|
+
fi
|
|
205
|
+
ensure_built
|
|
206
|
+
mkdir -p "$OPENCLAW_DIR" "$DATA"
|
|
207
|
+
if [ "$DAEMON" = true ]; then
|
|
208
|
+
nohup node "$SERVER_JS" --port "$PORT" --host "$HOST" --data "$DATA" >/dev/null 2>&1 &
|
|
209
|
+
echo "$!" > "$PID_FILE"
|
|
210
|
+
sleep 1
|
|
211
|
+
if kill -0 "$!" 2>/dev/null; then
|
|
212
|
+
echo "[mesh-master] 启动成功 (PID $!, port $PORT)"
|
|
213
|
+
else
|
|
214
|
+
rm -f "$PID_FILE"; echo "[mesh-master] 启动失败,查看 ~/.rotom/logs/ 下的日志文件"; return 1
|
|
215
|
+
fi
|
|
216
|
+
else
|
|
217
|
+
echo "$$" > "$PID_FILE"
|
|
218
|
+
trap 'rm -f "$PID_FILE"' EXIT INT TERM
|
|
219
|
+
exec node "$SERVER_JS" --port "$PORT" --host "$HOST" --data "$DATA"
|
|
220
|
+
fi
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
do_stop() {
|
|
224
|
+
local was_running=false
|
|
225
|
+
|
|
226
|
+
# 如果有系统服务,先 bootout(彻底停止 + 阻止 KeepAlive 重启)
|
|
227
|
+
if is_service_installed; then
|
|
228
|
+
echo "[mesh-master] 检测到系统服务,停止服务..."
|
|
229
|
+
stop_service
|
|
230
|
+
was_running=true
|
|
231
|
+
sleep 1
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
# 再杀残留进程(手动启动的、或 bootout 未杀干净的)
|
|
235
|
+
if kill_process 2>/dev/null; then
|
|
236
|
+
was_running=true
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
if [ "$was_running" = true ]; then
|
|
240
|
+
wait_for_port_free 5 || true
|
|
241
|
+
echo "[mesh-master] 已停止"
|
|
242
|
+
elif is_port_in_use; then
|
|
243
|
+
echo "[mesh-master] 警告: 端口 $PORT 被未知进程占用"
|
|
244
|
+
return 1
|
|
245
|
+
else
|
|
246
|
+
echo "[mesh-master] 未在运行"
|
|
247
|
+
fi
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
do_restart() {
|
|
251
|
+
do_stop
|
|
252
|
+
sleep 1
|
|
253
|
+
# 如果有系统服务,用系统服务启动
|
|
254
|
+
if is_service_installed; then
|
|
255
|
+
echo "[mesh-master] 通过系统服务启动..."
|
|
256
|
+
start_service
|
|
257
|
+
if wait_for_port 10; then
|
|
258
|
+
echo "[mesh-master] 重启成功"
|
|
259
|
+
else
|
|
260
|
+
echo "[mesh-master] 重启失败,查看 ~/.rotom/logs/ 下的日志文件"; return 1
|
|
261
|
+
fi
|
|
262
|
+
else
|
|
263
|
+
do_start
|
|
264
|
+
fi
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
do_status() {
|
|
268
|
+
if is_port_in_use; then
|
|
269
|
+
echo "[mesh-master] 运行中 (port $PORT)"
|
|
270
|
+
is_service_installed && echo " 模式: 系统服务" || echo " 模式: 手动"
|
|
271
|
+
curl -sf "http://127.0.0.1:$PORT/health" 2>/dev/null | python3 -m json.tool 2>/dev/null || true
|
|
272
|
+
else
|
|
273
|
+
echo "[mesh-master] 未运行"; return 1
|
|
274
|
+
fi
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# ── 系统服务 ──────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
do_install_service() {
|
|
280
|
+
# 先停掉现有进程和旧服务
|
|
281
|
+
if is_service_installed || is_port_in_use; then
|
|
282
|
+
echo "[mesh-master] 停止现有服务/进程..."
|
|
283
|
+
do_stop
|
|
284
|
+
sleep 1
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
ensure_built
|
|
288
|
+
mkdir -p "$OPENCLAW_DIR" "$DATA"
|
|
289
|
+
local self="$SCRIPT_DIR/bin/mesh-master.sh"
|
|
290
|
+
|
|
291
|
+
if [ "$(detect_os)" = "macos" ]; then
|
|
292
|
+
local plist="$HOME/Library/LaunchAgents/$SVC.plist"
|
|
293
|
+
local domain="gui/$(id -u)"
|
|
294
|
+
mkdir -p "$HOME/Library/LaunchAgents"
|
|
295
|
+
|
|
296
|
+
local data_args=""
|
|
297
|
+
if [ -n "$DATA" ]; then
|
|
298
|
+
data_args=" <string>--data</string><string>${DATA}</string>"
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
cat > "$plist" << PLIST
|
|
302
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
303
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
304
|
+
<plist version="1.0">
|
|
305
|
+
<dict>
|
|
306
|
+
<key>Label</key><string>${SVC}</string>
|
|
307
|
+
<key>ProgramArguments</key>
|
|
308
|
+
<array>
|
|
309
|
+
<string>${self}</string><string>_run</string>
|
|
310
|
+
<string>--port</string><string>${PORT}</string>
|
|
311
|
+
<string>--host</string><string>${HOST}</string>
|
|
312
|
+
${data_args}
|
|
313
|
+
</array>
|
|
314
|
+
<key>EnvironmentVariables</key>
|
|
315
|
+
<dict>
|
|
316
|
+
<key>PATH</key><string>${PATH}</string>
|
|
317
|
+
<key>HOME</key><string>${HOME}</string>
|
|
318
|
+
<key>LANG</key><string>${LANG:-en_US.UTF-8}</string>
|
|
319
|
+
</dict>
|
|
320
|
+
<key>RunAtLoad</key><true/>
|
|
321
|
+
<key>KeepAlive</key><true/>
|
|
322
|
+
<key>StandardOutPath</key><string>/dev/null</string>
|
|
323
|
+
<key>StandardErrorPath</key><string>/dev/null</string>
|
|
324
|
+
<key>WorkingDirectory</key><string>${SCRIPT_DIR}</string>
|
|
325
|
+
</dict>
|
|
326
|
+
</plist>
|
|
327
|
+
PLIST
|
|
328
|
+
# 现代 API: bootstrap 注册 + kickstart 强制启动
|
|
329
|
+
if ! launchctl bootstrap "$domain" "$plist" 2>&1; then
|
|
330
|
+
echo "[mesh-master] bootstrap 失败"; return 1
|
|
331
|
+
fi
|
|
332
|
+
launchctl kickstart -k "$domain/$SVC" 2>/dev/null || true
|
|
333
|
+
else
|
|
334
|
+
local svc_file="$HOME/.config/systemd/user/mesh-master.service"
|
|
335
|
+
mkdir -p "$(dirname "$svc_file")"
|
|
336
|
+
|
|
337
|
+
local data_flag=""
|
|
338
|
+
[ -n "$DATA" ] && data_flag=" --data ${DATA}"
|
|
339
|
+
|
|
340
|
+
cat > "$svc_file" << SERVICE
|
|
341
|
+
[Unit]
|
|
342
|
+
Description=Digital Employee Mesh Master
|
|
343
|
+
After=network.target
|
|
344
|
+
[Service]
|
|
345
|
+
Type=simple
|
|
346
|
+
ExecStart=${self} _run --port ${PORT} --host ${HOST}${data_flag}
|
|
347
|
+
Restart=always
|
|
348
|
+
RestartSec=5
|
|
349
|
+
Environment=PATH=${PATH}
|
|
350
|
+
Environment=HOME=${HOME}
|
|
351
|
+
Environment=LANG=${LANG:-en_US.UTF-8}
|
|
352
|
+
StandardOutput=null
|
|
353
|
+
StandardError=null
|
|
354
|
+
WorkingDirectory=${SCRIPT_DIR}
|
|
355
|
+
[Install]
|
|
356
|
+
WantedBy=default.target
|
|
357
|
+
SERVICE
|
|
358
|
+
systemctl --user daemon-reload
|
|
359
|
+
systemctl --user enable mesh-master
|
|
360
|
+
systemctl --user start mesh-master
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
if wait_for_port 10; then
|
|
364
|
+
echo "[mesh-master] 系统服务已安装并启动 ✅"
|
|
365
|
+
echo " 升级: git pull → $0 restart"
|
|
366
|
+
echo " 卸载: $0 uninstall-service"
|
|
367
|
+
echo " 日志: JS logger → ~/.rotom/logs/mesh-master-YYYY-MM-DD.log"
|
|
368
|
+
else
|
|
369
|
+
echo "[mesh-master] 启动失败,查看 ~/.rotom/logs/ 下的日志文件"; return 1
|
|
370
|
+
fi
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
do_uninstall_service() {
|
|
374
|
+
if [ "$(detect_os)" = "macos" ]; then
|
|
375
|
+
local domain="gui/$(id -u)"
|
|
376
|
+
# bootout 停止并注销服务
|
|
377
|
+
launchctl bootout "$domain/$SVC" 2>/dev/null || true
|
|
378
|
+
rm -f "$HOME/Library/LaunchAgents/$SVC.plist"
|
|
379
|
+
else
|
|
380
|
+
systemctl --user stop mesh-master 2>/dev/null || true
|
|
381
|
+
systemctl --user disable mesh-master 2>/dev/null || true
|
|
382
|
+
rm -f "$HOME/.config/systemd/user/mesh-master.service"
|
|
383
|
+
systemctl --user daemon-reload
|
|
384
|
+
fi
|
|
385
|
+
echo "[mesh-master] 系统服务已卸载 ✅"
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# ── 帮助 ──────────────────────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
do_help() {
|
|
391
|
+
cat <<EOF
|
|
392
|
+
Digital Employee Mesh — Master 管理脚本
|
|
393
|
+
|
|
394
|
+
用法: mesh-master <command> [options]
|
|
395
|
+
|
|
396
|
+
命令:
|
|
397
|
+
start 启动 (默认前台, --daemon 后台)
|
|
398
|
+
stop 停止(自动识别手动/系统服务)
|
|
399
|
+
restart 重启(自动识别手动/系统服务)
|
|
400
|
+
status 查看状态
|
|
401
|
+
install-service 安装系统服务(开机自启 + 守护进程 + 升级自动编译)
|
|
402
|
+
uninstall-service 卸载系统服务
|
|
403
|
+
|
|
404
|
+
选项:
|
|
405
|
+
--port, -p <port> 端口 (默认: ${PORT})
|
|
406
|
+
--host <addr> 地址 (默认: ${HOST})
|
|
407
|
+
--data, -d <dir> 数据目录 (默认: ~/Library/Application Support/a2a-gateway/mesh-data)
|
|
408
|
+
--daemon 后台运行
|
|
409
|
+
--dev 开发模式:只编译后端,跳过 vite 前端构建
|
|
410
|
+
前端用 \`pnpm dashboard:dev\` 单独跑(vite dev :3000)
|
|
411
|
+
|
|
412
|
+
示例:
|
|
413
|
+
mesh-master start --daemon # 后台启动
|
|
414
|
+
mesh-master start --dev # 开发模式(不 build vite)
|
|
415
|
+
mesh-master install-service # 安装为系统服务
|
|
416
|
+
mesh-master restart # 重启(升级后用)
|
|
417
|
+
mesh-master status # 查看状态
|
|
418
|
+
EOF
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# ── 入口 ──────────────────────────────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
CMD="${1:-help}"; shift 2>/dev/null || true
|
|
424
|
+
|
|
425
|
+
while [[ $# -gt 0 ]]; do
|
|
426
|
+
case "$1" in
|
|
427
|
+
--port|-p) PORT="$2"; shift 2 ;; --host) HOST="$2"; shift 2 ;;
|
|
428
|
+
--data|-d) DATA="$2"; shift 2 ;; --daemon) DAEMON=true; shift ;;
|
|
429
|
+
--dev) DEV=true; shift ;;
|
|
430
|
+
--help) do_help; exit 0 ;; *) echo "未知选项: $1"; do_help; exit 1 ;;
|
|
431
|
+
esac
|
|
432
|
+
done
|
|
433
|
+
|
|
434
|
+
case "$CMD" in
|
|
435
|
+
start) do_start ;; stop) do_stop ;; restart) do_restart ;;
|
|
436
|
+
status) do_status ;; _run) do_run ;;
|
|
437
|
+
install-service) do_install_service ;; uninstall-service) do_uninstall_service ;;
|
|
438
|
+
help|-h|--help) do_help ;; *) echo "未知命令: $CMD"; do_help; exit 1 ;;
|
|
439
|
+
esac
|
package/bin/rotom
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rotom CLI launcher.
|
|
3
|
+
# Prefers the compiled JS; falls back to running the TS source through tsx
|
|
4
|
+
# (so contributors don't need to rebuild on every change).
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
# Resolve the real path of this script, following symlinks (so global installs
|
|
8
|
+
# via `ln -s ... /usr/local/bin/rotom` still locate the repo).
|
|
9
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
10
|
+
while [ -L "$SOURCE" ]; do
|
|
11
|
+
TARGET="$(readlink "$SOURCE")"
|
|
12
|
+
case "$TARGET" in
|
|
13
|
+
/*) SOURCE="$TARGET" ;;
|
|
14
|
+
*) SOURCE="$(cd "$(dirname "$SOURCE")" && pwd)/$TARGET" ;;
|
|
15
|
+
esac
|
|
16
|
+
done
|
|
17
|
+
HERE="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
18
|
+
ROOT="$(cd "$HERE/.." && pwd)"
|
|
19
|
+
DIST="$ROOT/dist/cli/rotom.js"
|
|
20
|
+
SRC="$ROOT/src/cli/rotom.ts"
|
|
21
|
+
|
|
22
|
+
if [ -f "$DIST" ]; then
|
|
23
|
+
exec node "$DIST" "$@"
|
|
24
|
+
elif [ -f "$SRC" ]; then
|
|
25
|
+
exec node --import tsx "$SRC" "$@"
|
|
26
|
+
else
|
|
27
|
+
echo "rotom: cannot find dist/cli/rotom.js or src/cli/rotom.ts (looked in $ROOT)" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# rotom-link —— 轻量 federation link daemon 启停脚本。
|
|
4
|
+
#
|
|
5
|
+
# 用法: rotom-link <command> [options]
|
|
6
|
+
# 命令: start | stop | restart | status | logs
|
|
7
|
+
# 选项: --port/-p <port> 默认 28900
|
|
8
|
+
#
|
|
9
|
+
# 运行时目录: ~/.rotom/{run,logs}
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
15
|
+
LINK_JS="$SCRIPT_DIR/dist/link/server.js"
|
|
16
|
+
|
|
17
|
+
ROTOM_HOME="${ROTOM_HOME:-$HOME/.rotom}"
|
|
18
|
+
RUN_DIR="$ROTOM_HOME/run"
|
|
19
|
+
LOG_DIR="$ROTOM_HOME/logs"
|
|
20
|
+
LINK_PID="$RUN_DIR/link.pid"
|
|
21
|
+
LINK_LOG="$LOG_DIR/link.log"
|
|
22
|
+
LINK_CONFIG="$ROTOM_HOME/link.json"
|
|
23
|
+
|
|
24
|
+
PORT="${ROTOM_LINK_PORT:-28900}"
|
|
25
|
+
|
|
26
|
+
mkdir -p "$RUN_DIR" "$LOG_DIR"
|
|
27
|
+
|
|
28
|
+
is_pid_alive() {
|
|
29
|
+
local pid_file="$1"
|
|
30
|
+
[ -f "$pid_file" ] || return 1
|
|
31
|
+
local pid; pid=$(cat "$pid_file" 2>/dev/null || true)
|
|
32
|
+
[ -z "$pid" ] && return 1
|
|
33
|
+
kill -0 "$pid" 2>/dev/null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
stop_pid_file() {
|
|
37
|
+
local pid_file="$1" label="$2"
|
|
38
|
+
if ! is_pid_alive "$pid_file"; then
|
|
39
|
+
rm -f "$pid_file"
|
|
40
|
+
return 1
|
|
41
|
+
fi
|
|
42
|
+
local pid; pid=$(cat "$pid_file")
|
|
43
|
+
echo "[rotom-link] 停止 $label (PID $pid)..."
|
|
44
|
+
kill "$pid" 2>/dev/null || true
|
|
45
|
+
local i=0
|
|
46
|
+
while kill -0 "$pid" 2>/dev/null && [ $i -lt 50 ]; do
|
|
47
|
+
sleep 0.1; i=$((i+1))
|
|
48
|
+
done
|
|
49
|
+
kill -0 "$pid" 2>/dev/null && kill -9 "$pid" 2>/dev/null || true
|
|
50
|
+
rm -f "$pid_file"
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
wait_link_ready() {
|
|
55
|
+
local max_wait=${1:-10} i=0
|
|
56
|
+
while [ $i -lt $max_wait ]; do
|
|
57
|
+
if curl -sf --connect-timeout 1 "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
|
58
|
+
return 0
|
|
59
|
+
fi
|
|
60
|
+
sleep 1; i=$((i+1))
|
|
61
|
+
done
|
|
62
|
+
return 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
do_start() {
|
|
66
|
+
if is_pid_alive "$LINK_PID"; then
|
|
67
|
+
echo "[rotom-link] 已在运行 (PID $(cat "$LINK_PID"))"
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
if [ ! -f "$LINK_JS" ]; then
|
|
71
|
+
echo "[rotom-link] ❌ $LINK_JS 不存在,请先 \`pnpm build\`"
|
|
72
|
+
return 1
|
|
73
|
+
fi
|
|
74
|
+
if [ ! -f "$LINK_CONFIG" ]; then
|
|
75
|
+
echo "[rotom-link] ❌ $LINK_CONFIG 不存在,请先 \`rotom link join <coordEndpoint> --hostname <name>\`"
|
|
76
|
+
return 1
|
|
77
|
+
fi
|
|
78
|
+
# 端口预检
|
|
79
|
+
if command -v lsof >/dev/null 2>&1; then
|
|
80
|
+
local occupier
|
|
81
|
+
occupier=$(lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t 2>/dev/null | head -n1 || true)
|
|
82
|
+
if [ -n "$occupier" ]; then
|
|
83
|
+
local occupier_cmd
|
|
84
|
+
occupier_cmd=$(ps -p "$occupier" -o command= 2>/dev/null | head -c 120 || echo "(unknown)")
|
|
85
|
+
echo "[rotom-link] ❌ 端口 $PORT 已被占用 (PID $occupier: $occupier_cmd)"
|
|
86
|
+
return 1
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
echo "[rotom-link] 启动 link daemon (port=$PORT)..."
|
|
90
|
+
nohup node "$LINK_JS" --port "$PORT" >> "$LINK_LOG" 2>&1 &
|
|
91
|
+
echo "$!" > "$LINK_PID"
|
|
92
|
+
if ! wait_link_ready 10; then
|
|
93
|
+
echo "[rotom-link] ❌ 启动失败,查看 $LINK_LOG"
|
|
94
|
+
stop_pid_file "$LINK_PID" link || true
|
|
95
|
+
return 1
|
|
96
|
+
fi
|
|
97
|
+
echo "[rotom-link] ✅ 就绪 (PID $(cat "$LINK_PID"), http://127.0.0.1:$PORT)"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
do_stop() {
|
|
101
|
+
stop_pid_file "$LINK_PID" link || echo "[rotom-link] 未在运行"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
do_status() {
|
|
105
|
+
if is_pid_alive "$LINK_PID"; then
|
|
106
|
+
local pid; pid=$(cat "$LINK_PID")
|
|
107
|
+
echo "[rotom-link] running (PID $pid, port=$PORT)"
|
|
108
|
+
if curl -sf --connect-timeout 1 "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
|
109
|
+
curl -s "http://127.0.0.1:$PORT/health"
|
|
110
|
+
echo
|
|
111
|
+
fi
|
|
112
|
+
else
|
|
113
|
+
echo "[rotom-link] stopped"
|
|
114
|
+
fi
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
do_logs() {
|
|
118
|
+
if [ -f "$LINK_LOG" ]; then
|
|
119
|
+
tail -n 200 "$LINK_LOG"
|
|
120
|
+
else
|
|
121
|
+
echo "[rotom-link] no log at $LINK_LOG"
|
|
122
|
+
fi
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
cmd="${1:-}"; shift || true
|
|
126
|
+
case "$cmd" in
|
|
127
|
+
start) do_start ;;
|
|
128
|
+
stop) do_stop ;;
|
|
129
|
+
restart) do_stop; do_start ;;
|
|
130
|
+
status) do_status ;;
|
|
131
|
+
logs) do_logs ;;
|
|
132
|
+
*)
|
|
133
|
+
echo "usage: rotom-link <start|stop|restart|status|logs> [--port N]"
|
|
134
|
+
exit 1
|
|
135
|
+
;;
|
|
136
|
+
esac
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# rotom-send-with-status - 发送消息并解释投递状态
|
|
3
|
+
#
|
|
4
|
+
# 用法:rotom-send-with-status <groupId> <target> <message>
|
|
5
|
+
# 示例:rotom-send-with-status cda34ffc-c8e9-428b-b9da-2bec7c6039d1 小寿 "@小寿 帮我看看这个问题"
|
|
6
|
+
|
|
7
|
+
GROUP_ID=$1
|
|
8
|
+
TARGET=$2
|
|
9
|
+
MESSAGE=$3
|
|
10
|
+
|
|
11
|
+
if [ -z "$GROUP_ID" ] || [ -z "$TARGET" ] || [ -z "$MESSAGE" ]; then
|
|
12
|
+
echo "用法: rotom-send-with-status <groupId> <target> <message>" >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# 调用 rotom 并捕获输出
|
|
17
|
+
RESULT=$(rotom group send "$GROUP_ID" "$TARGET" "$MESSAGE" 2>&1)
|
|
18
|
+
EXIT_CODE=$?
|
|
19
|
+
|
|
20
|
+
if [ $EXIT_CODE -ne 0 ]; then
|
|
21
|
+
echo "❌ 命令执行失败: $RESULT" >&2
|
|
22
|
+
exit $EXIT_CODE
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# 检查是否有 jq 命令
|
|
26
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
27
|
+
# 没有 jq,直接返回原始 JSON
|
|
28
|
+
echo "$RESULT"
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# 解析 JSON
|
|
33
|
+
DELIVERED=$(echo "$RESULT" | jq -r '.delivered')
|
|
34
|
+
QUEUED=$(echo "$RESULT" | jq -r '.queued')
|
|
35
|
+
ERROR=$(echo "$RESULT" | jq -r '.error // empty')
|
|
36
|
+
REQUEST_ID=$(echo "$RESULT" | jq -r '.requestId // empty')
|
|
37
|
+
|
|
38
|
+
# 生成人类可读的输出
|
|
39
|
+
echo "$RESULT" # 原始 JSON(供程序处理)
|
|
40
|
+
echo "" # 空行分隔
|
|
41
|
+
|
|
42
|
+
# 解释性输出(供人类阅读)
|
|
43
|
+
echo "📤 → $TARGET: $MESSAGE"
|
|
44
|
+
|
|
45
|
+
if [ "$DELIVERED" = "true" ]; then
|
|
46
|
+
echo "✅ 消息已实时送达(对方在线,会立即处理)"
|
|
47
|
+
elif [ "$QUEUED" = "true" ]; then
|
|
48
|
+
echo "⏳ $TARGET 当前离线,消息已缓存,上线后会收到"
|
|
49
|
+
else
|
|
50
|
+
if [ -n "$ERROR" ]; then
|
|
51
|
+
echo "❌ 消息投递失败:$ERROR"
|
|
52
|
+
else
|
|
53
|
+
echo "❓ 未知投递状态(请联系管理员检查)"
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
exit 0
|