release: public alpha 0.6.0 baseline

This commit is contained in:
JWeBot Contributors
2026-10-15 03:58:31 +08:00
commit d4242ae07c
238 changed files with 37724 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
$session = "xfce";
$geometry = "1440x900";
$depth = "24";
$localhost = "yes";
$SecurityTypes = "None";
$AcceptSetDesktopSize = "no";
+6
View File
@@ -0,0 +1,6 @@
JWEBOT_RUNTIME_USER=jwebot
JWEBOT_RUNTIME_HOME=/home/jwebot
WECHAT_QUEUE_ENABLED=0
WECHAT_ROUTER_ENABLED=1
WECHAT_MONITOR_MODE=damage
WECHAT_OCR_PREWARM=1
@@ -0,0 +1,6 @@
JWEBOT_RUNTIME_USER=jwebot
JWEBOT_RUNTIME_HOME=/home/jwebot
WECHAT_QUEUE_ENABLED=1
WECHAT_ROUTER_ENABLED=1
WECHAT_MONITOR_MODE=damage
WECHAT_OCR_PREWARM=1
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env bash
set -Eeuo pipefail
action=${1:-}
source_dir=${2:-}
target_user=${3:-jwebot}
profile_id=${4:-default}
runtime_root=${JWEBOT_RUNTIME_ROOT:-/opt/jwebot}
runtime_python="${runtime_root}/venv/bin/python"
usage() {
cat >&2 <<'EOF'
usage:
bootstrap-jwebot.sh prepare SOURCE_DIR [TARGET_USER] [PROFILE]
bootstrap-jwebot.sh verify SOURCE_DIR [TARGET_USER] [PROFILE]
bootstrap-jwebot.sh deploy SOURCE_DIR [TARGET_USER] [PROFILE]
EOF
exit 64
}
[[ -n "${action}" && -n "${source_dir}" ]] || usage
[[ "${target_user}" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || {
echo "[JWeBot] error: invalid target user" >&2
exit 1
}
[[ "${profile_id}" =~ ^[a-z][a-z0-9-]{0,47}$ ]] || {
echo "[JWeBot] error: invalid profile id" >&2
exit 1
}
[[ "${EUID}" -eq 0 ]] || {
echo "[JWeBot] error: bootstrap must run as root" >&2
exit 1
}
[[ -d "${source_dir}" ]] || {
echo "[JWeBot] error: source directory is unavailable" >&2
exit 1
}
progress() {
local step=$1
local total=$2
local label=$3
local width=20
local filled=$((step * width / total))
local empty=$((width - filled))
local bar padding
printf -v bar '%*s' "${filled}" ''
printf -v padding '%*s' "${empty}" ''
printf '[JWeBot] [%s%s] %3d%% %s\n' \
"${bar// /#}" "${padding// /-}" "$((step * 100 / total))" "${label}"
}
account_home() {
getent passwd "${target_user}" | cut -d: -f6
}
as_runtime_user() {
local home
home=$(account_home)
/usr/sbin/runuser -u "${target_user}" -- /usr/bin/env \
HOME="${home}" \
JWEBOT_CONFIG_ROOT="${home}/.config/jwebot" \
JWEBOT_CONFIG_STATE_ROOT="${home}/.local/state/jwebot" \
"$@"
}
prepare() {
progress 1 6 "operating-system packages"
command -v apt-get >/dev/null 2>&1 || {
echo "[JWeBot] error: automatic prepare currently supports apt-based systems" >&2
exit 1
}
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y \
python3 \
python3-pip \
python3-venv \
libx11-6 \
libxdamage1 \
scrot \
tesseract-ocr \
tesseract-ocr-chi-sim \
wmctrl \
xclip \
xdotool
progress 2 6 "runtime account"
if ! id "${target_user}" >/dev/null 2>&1; then
useradd --create-home --shell /bin/bash "${target_user}"
fi
target_home=$(account_home)
[[ -n "${target_home}" && -d "${target_home}" ]]
progress 3 6 "python environment"
install -d -o root -g root -m 0755 "${runtime_root}"
if [[ ! -x "${runtime_python}" ]]; then
python3 -m venv "${runtime_root}/venv"
fi
"${runtime_python}" -m pip install --upgrade pip
"${runtime_python}" -m pip install -r "${source_dir}/requirements-runtime.txt"
"${runtime_python}" -m pip install \
"${source_dir}/packages/jwebot-config" \
"${source_dir}/packages/jwebot-sdk" \
"${source_dir}/packages/jwebot-official-extensions" \
"${source_dir}/packages/jwebot-kernel" \
"${source_dir}/packages/jwebot-extension-host"
progress 4 6 "configuration profile"
if [[ ! -L "${target_home}/.config/jwebot/current" ]]; then
as_runtime_user "${runtime_python}" -m jwebot_config \
init "${profile_id}" --activate
else
as_runtime_user "${runtime_python}" -m jwebot_config \
validate "${profile_id}"
fi
progress 5 6 "private state permissions"
install -d -o "${target_user}" -g "${target_user}" -m 0700 \
"${target_home}/.config/jwebot" \
"${target_home}/.local/state/jwebot" \
"${target_home}/.local/state/jwebot-edge" \
"${target_home}/.local/state/wechat-adaptive-ocr"
progress 6 6 "prepare ready"
cat <<EOF
Edit and activate:
${target_home}/.config/jwebot/profiles/${profile_id}/
Then install and sign in to desktop WeChat on display :1 before running:
$0 verify ${source_dir} ${target_user} ${profile_id}
EOF
}
verify() {
progress 1 5 "runtime account"
id "${target_user}" >/dev/null
target_home=$(account_home)
[[ -d "${target_home}" ]]
progress 2 5 "runtime dependencies"
for command in xdotool scrot xclip wmctrl tesseract; do
command -v "${command}" >/dev/null
done
"${runtime_python}" -c \
'import cv2,numpy,PIL,rapidocr,jwebot_config,jwebot_kernel'
progress 3 5 "configuration profile"
as_runtime_user "${runtime_python}" -m jwebot_config validate "${profile_id}"
as_runtime_user "${runtime_python}" -m jwebot_config activate "${profile_id}"
progress 4 5 "repository gate"
PATH="${runtime_root}/venv/bin:${PATH}" \
"${source_dir}/tools/check-config.sh"
PATH="${runtime_root}/venv/bin:${PATH}" \
"${source_dir}/tools/check-secrets.py"
progress 5 5 "desktop prerequisites"
/usr/sbin/runuser -u "${target_user}" -- /usr/bin/env \
HOME="${target_home}" \
DISPLAY=:1 \
XAUTHORITY="${target_home}/.Xauthority" \
xdotool search --onlyvisible --class wechat >/dev/null
echo "[JWeBot] verify ready"
}
deploy() {
verify
progress 1 2 "coordinated deployment"
JWEBOT_RUNTIME_PYTHON="${runtime_python}" \
"${source_dir}/deploy/scripts/deploy-jwebot-production.sh" \
"${source_dir}" "${target_user}"
progress 2 2 "running"
}
case "${action}" in
prepare) prepare ;;
verify) verify ;;
deploy) deploy ;;
*) usage ;;
esac
+628
View File
@@ -0,0 +1,628 @@
#!/usr/bin/env bash
set -Eeuo pipefail
source_dir=${1:?usage: deploy-jwebot-production.sh SOURCE_DIR [TARGET_USER]}
target_user=${2:-jwebot}
[[ "${target_user}" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || {
echo "[JWeBot] error: invalid target user" >&2
exit 1
}
target_uid=$(id -u "${target_user}")
target_gid=$(id -g "${target_user}")
target_home=$(getent passwd "${target_user}" | cut -d: -f6)
user_runtime="/run/user/${target_uid}"
runtime_python=${JWEBOT_RUNTIME_PYTHON:-/opt/jwebot/venv/bin/python}
require_wechat_ready=${REQUIRE_WECHAT_READY:-1}
wechat_ready_timeout_seconds=${WECHAT_READY_TIMEOUT_SECONDS:-30}
health_settle_seconds=${HEALTH_SETTLE_SECONDS:-8}
backup_root=${JWEBOT_PRODUCTION_BACKUP_ROOT:-/var/backups/jwebot-production}
timestamp=$(date +%Y%m%d-%H%M%S)
backup_dir="${backup_root}/${timestamp}-$$"
preflight_rendered_dir=""
target_runtime="${target_home}/.local/lib/wechat-health-watch"
target_user_units="${target_home}/.config/systemd/user"
target_user_config="${target_home}/.config/jwebot"
target_edge_state="${target_home}/.local/state/jwebot-edge"
target_adaptive_state="${target_home}/.local/state/wechat-adaptive-ocr"
root_ocr=/usr/local/sbin/wechat-local-ocr
root_bridge=/usr/local/sbin/wechat-gui-bridge
root_recorder_unit=/etc/systemd/system/wechat-local-ocr-recorder.service
root_responder_unit=/etc/systemd/system/wechat-local-ocr-responder.service
root_defaults=/etc/default/wechat-local-ocr
system_state_file="${backup_dir}/metadata/system-state.tsv"
user_state_file="${backup_dir}/metadata/user-state.tsv"
armed=0
success=0
system_units=(
wechat-local-ocr-recorder.service
wechat-local-ocr-responder.service
)
user_units=(
jwebot-edge.service
wechat-ocr-adaptive-recorder.service
wechat-health-watch.timer
jwebot-edge-memory-sync.timer
wechat-health-watch.service
jwebot-edge-memory-sync.service
)
jwebot_log() {
printf '[JWeBot] %s\n' "$*"
}
jwebot_progress() {
local step=$1
local total=$2
local label=$3
local width=20
local filled=$((step * width / total))
local empty=$((width - filled))
local bar
printf -v bar '%*s' "${filled}" ''
bar=${bar// /#}
printf -v padding '%*s' "${empty}" ''
padding=${padding// /-}
printf '[JWeBot] [%s%s] %3d%% %s\n' \
"${bar}" "${padding}" "$((step * 100 / total))" "${label}"
}
if [[ "${EUID}" -ne 0 ]]; then
echo "[JWeBot] error: production deploy must run as root" >&2
exit 1
fi
if [[ -z "${target_home}" || ! -d "${target_home}" ]]; then
echo "[JWeBot] error: target user home is unavailable" >&2
exit 1
fi
jwebot_log "production deploy starting version=$(cat "${source_dir}/VERSION")"
jwebot_progress 1 8 "preflight"
user_systemctl() {
/usr/sbin/runuser -u "${target_user}" -- /usr/bin/env \
HOME="${target_home}" \
XDG_RUNTIME_DIR="${user_runtime}" \
DBUS_SESSION_BUS_ADDRESS="unix:path=${user_runtime}/bus" \
systemctl --user "$@"
}
save_system_state() {
: >"${system_state_file}"
local unit enabled active
for unit in "${system_units[@]}"; do
enabled=$(systemctl is-enabled "${unit}" 2>/dev/null || true)
active=$(systemctl is-active "${unit}" 2>/dev/null || true)
printf '%s\t%s\t%s\n' "${unit}" "${enabled:-unknown}" "${active:-unknown}" \
>>"${system_state_file}"
done
}
save_user_state() {
: >"${user_state_file}"
local unit enabled active
for unit in "${user_units[@]}"; do
enabled=$(user_systemctl is-enabled "${unit}" 2>/dev/null || true)
active=$(user_systemctl is-active "${unit}" 2>/dev/null || true)
printf '%s\t%s\t%s\n' "${unit}" "${enabled:-unknown}" "${active:-unknown}" \
>>"${user_state_file}"
done
}
state_field() {
local file="$1"
local unit="$2"
local field="$3"
awk -F '\t' -v unit="${unit}" -v field="${field}" \
'$1 == unit { print $field; exit }' "${file}"
}
restore_enable_state() {
local scope="$1"
local file="$2"
shift 2
local unit enabled
for unit in "$@"; do
enabled=$(state_field "${file}" "${unit}" 2 2>/dev/null || true)
case "${enabled}" in
enabled)
if [[ "${scope}" == "user" ]]; then
user_systemctl unmask "${unit}" >/dev/null 2>&1 || true
user_systemctl enable "${unit}" >/dev/null 2>&1 || true
else
systemctl unmask "${unit}" >/dev/null 2>&1 || true
systemctl enable "${unit}" >/dev/null 2>&1 || true
fi
;;
enabled-runtime)
if [[ "${scope}" == "user" ]]; then
user_systemctl unmask "${unit}" >/dev/null 2>&1 || true
user_systemctl enable --runtime "${unit}" >/dev/null 2>&1 || true
else
systemctl unmask "${unit}" >/dev/null 2>&1 || true
systemctl enable --runtime "${unit}" >/dev/null 2>&1 || true
fi
;;
disabled)
if [[ "${scope}" == "user" ]]; then
user_systemctl unmask "${unit}" >/dev/null 2>&1 || true
user_systemctl disable "${unit}" >/dev/null 2>&1 || true
else
systemctl unmask "${unit}" >/dev/null 2>&1 || true
systemctl disable "${unit}" >/dev/null 2>&1 || true
fi
;;
masked)
if [[ "${scope}" == "user" ]]; then
user_systemctl mask "${unit}" >/dev/null 2>&1 || true
else
systemctl mask "${unit}" >/dev/null 2>&1 || true
fi
;;
masked-runtime)
if [[ "${scope}" == "user" ]]; then
user_systemctl mask --runtime "${unit}" >/dev/null 2>&1 || true
else
systemctl mask --runtime "${unit}" >/dev/null 2>&1 || true
fi
;;
esac
done
}
restore_active_state() {
local scope="$1"
local file="$2"
shift 2
local unit active
for unit in "$@"; do
active=$(state_field "${file}" "${unit}" 3 2>/dev/null || true)
case "${active}" in
active|activating|reloading)
if [[ "${scope}" == "user" ]]; then
user_systemctl start "${unit}" >/dev/null 2>&1 || true
else
systemctl start "${unit}" >/dev/null 2>&1 || true
fi
;;
esac
done
}
backup_file() {
local source="$1"
local name="$2"
if [[ -e "${source}" || -L "${source}" ]]; then
cp -a "${source}" "${backup_dir}/root/${name}"
: >"${backup_dir}/root/${name}.present"
fi
}
restore_file() {
local target="$1"
local name="$2"
if [[ -f "${backup_dir}/root/${name}.present" ]]; then
rm -rf "${target}"
cp -a "${backup_dir}/root/${name}" "${target}"
else
rm -rf "${target}"
fi
}
copy_directory_backup() {
local source="$1"
local name="$2"
if [[ -d "${source}" ]]; then
cp -a "${source}" "${backup_dir}/user/${name}"
: >"${backup_dir}/user/${name}.present"
fi
}
restore_directory_backup() {
local target="$1"
local name="$2"
rm -rf "${target}"
if [[ -f "${backup_dir}/user/${name}.present" ]]; then
cp -a "${backup_dir}/user/${name}" "${target}"
fi
}
queue_count() {
local directory="$1"
if [[ ! -d "${directory}" ]]; then
printf '0\n'
return
fi
find "${directory}" -maxdepth 1 -type f -print | wc -l
}
assert_queues_empty() {
local adaptive legacy
adaptive=$(queue_count "${target_adaptive_state}/queue")
legacy=$(queue_count /var/lib/wechat-local-ocr/queue)
if [[ "${adaptive}" -ne 0 || "${legacy}" -ne 0 ]]; then
echo "[JWeBot] error: OCR queues must be empty before deployment" >&2
return 1
fi
}
wechat_geometry() {
local identifiers identifier title geometry width height
local class_identifiers name_identifiers
local best_width=0
local best_height=0
local display_environment=(
HOME="${target_home}"
DISPLAY=:1
XAUTHORITY="${target_home}/.Xauthority"
XDG_RUNTIME_DIR="${user_runtime}"
DBUS_SESSION_BUS_ADDRESS="unix:path=${user_runtime}/bus"
)
class_identifiers=$(
/usr/sbin/runuser -u "${target_user}" -- \
/usr/bin/env "${display_environment[@]}" \
/usr/bin/xdotool search --onlyvisible --class wechat \
2>/dev/null || true
)
name_identifiers=$(
/usr/sbin/runuser -u "${target_user}" -- \
/usr/bin/env "${display_environment[@]}" \
/usr/bin/xdotool search --onlyvisible --name '^Weixin$' \
2>/dev/null || true
)
identifiers=$(
printf '%s\n%s\n' "${class_identifiers}" "${name_identifiers}" \
| awk 'NF && !seen[$0]++'
)
for identifier in ${identifiers}; do
title=$(
/usr/sbin/runuser -u "${target_user}" -- \
/usr/bin/env "${display_environment[@]}" \
/usr/bin/xdotool getwindowname "${identifier}" \
2>/dev/null || true
)
[[ "${title}" == "Weixin" ]] || continue
geometry=$(
/usr/sbin/runuser -u "${target_user}" -- \
/usr/bin/env "${display_environment[@]}" \
/usr/bin/xdotool getwindowgeometry --shell "${identifier}" \
2>/dev/null || true
)
width=$(sed -n 's/^WIDTH=//p' <<<"${geometry}" | head -n 1)
height=$(sed -n 's/^HEIGHT=//p' <<<"${geometry}" | head -n 1)
[[ "${width}" =~ ^[0-9]+$ && "${height}" =~ ^[0-9]+$ ]] || continue
if (( width * height > best_width * best_height )); then
best_width=${width}
best_height=${height}
fi
done
if (( best_width > 0 && best_height > 0 )); then
printf 'WIDTH=%s\nHEIGHT=%s\n' "${best_width}" "${best_height}"
fi
}
assert_wechat_ready() {
local geometry="" width="" height=""
local attempt=0
jwebot_log "WeChat readiness starting timeout=${wechat_ready_timeout_seconds}s"
while (( attempt < wechat_ready_timeout_seconds )); do
geometry=$(wechat_geometry)
width=$(sed -n 's/^WIDTH=//p' <<<"${geometry}" | head -n 1)
height=$(sed -n 's/^HEIGHT=//p' <<<"${geometry}" | head -n 1)
if [[ "${width}" =~ ^[0-9]+$ && "${height}" =~ ^[0-9]+$ ]] \
&& (( width >= 700 && height >= 500 ))
then
jwebot_log "WeChat ready geometry=${width}x${height}"
return 0
fi
attempt=$((attempt + 1))
if (( attempt < wechat_ready_timeout_seconds )); then
sleep 1
fi
done
if [[ "${width}" =~ ^[0-9]+$ && "${height}" =~ ^[0-9]+$ ]]; then
echo "[JWeBot] error: WeChat is not ready (${width}x${height})" >&2
else
echo "[JWeBot] error: WeChat window geometry is unavailable" >&2
fi
return 1
}
verify_edge_health() {
local response
response=$(
printf '%s\n' '{"op":"health"}' \
| /usr/sbin/runuser -u "${target_user}" -- /usr/bin/env \
HOME="${target_home}" \
"${runtime_python}" "${target_runtime}/jwebot_edge.py" respond
)
printf '%s' "${response}" \
| "${runtime_python}" -c \
'import json,sys; value=json.load(sys.stdin); raise SystemExit(0 if value.get("status") == "ok" else 1)'
}
render_root_configuration() {
local output_dir="$1"
sed \
-e "s/^User=.*/User=${target_user}/" \
-e "s/^Group=.*/Group=${target_user}/" \
-e "s#/opt/jwebot/venv/bin/python#${runtime_python}#g" \
"${source_dir}/deploy/systemd/system/wechat-local-ocr-recorder.service" \
>"${output_dir}/wechat-local-ocr-recorder.service"
sed \
-e "s#/opt/jwebot/venv/bin/python#${runtime_python}#g" \
"${source_dir}/deploy/systemd/system/wechat-local-ocr-responder.service" \
>"${output_dir}/wechat-local-ocr-responder.service"
sed \
-e "s#^JWEBOT_RUNTIME_USER=.*#JWEBOT_RUNTIME_USER=${target_user}#" \
-e "s#^JWEBOT_RUNTIME_HOME=.*#JWEBOT_RUNTIME_HOME=${target_home}#" \
"${source_dir}/deploy/defaults/wechat-local-ocr.production.default" \
>"${output_dir}/wechat-local-ocr.default"
}
rollback() {
local status=$?
trap - EXIT HUP INT TERM
if [[ -n "${preflight_rendered_dir}" ]]; then
rm -rf "${preflight_rendered_dir}"
fi
if [[ "${armed}" -eq 1 && "${success}" -eq 0 ]]; then
jwebot_log "rollback starting backup=${backup_dir}"
set +e
user_systemctl stop \
wechat-ocr-adaptive-recorder.service \
jwebot-edge.service \
wechat-health-watch.timer \
jwebot-edge-memory-sync.timer \
>/dev/null 2>&1
systemctl disable --now \
wechat-local-ocr-recorder.service \
wechat-local-ocr-responder.service \
>/dev/null 2>&1
restore_file "${root_ocr}" wechat-local-ocr
restore_file "${root_bridge}" wechat-gui-bridge
restore_file "${root_recorder_unit}" wechat-local-ocr-recorder.service
restore_file "${root_responder_unit}" wechat-local-ocr-responder.service
restore_file "${root_defaults}" wechat-local-ocr.default
restore_directory_backup "${target_runtime}" runtime
restore_directory_backup "${target_user_units}" systemd-user
restore_directory_backup "${target_user_config}" jwebot-config
if [[ -f "${backup_dir}/state/edge.sqlite3" ]]; then
install -d -o "${target_uid}" -g "${target_gid}" -m 0700 \
"${target_edge_state}"
rm -f \
"${target_edge_state}/edge.sqlite3" \
"${target_edge_state}/edge.sqlite3-wal" \
"${target_edge_state}/edge.sqlite3-shm"
install -o "${target_uid}" -g "${target_gid}" -m 0600 \
"${backup_dir}/state/edge.sqlite3" \
"${target_edge_state}/edge.sqlite3"
fi
systemctl daemon-reload
user_systemctl daemon-reload
restore_enable_state system "${system_state_file}" "${system_units[@]}"
restore_enable_state user "${user_state_file}" "${user_units[@]}"
restore_active_state user "${user_state_file}" "${user_units[@]}"
restore_active_state system "${system_state_file}" "${system_units[@]}"
systemctl reset-failed
user_systemctl reset-failed
echo "[JWeBot] rollback ready backup=${backup_dir}" >&2
fi
exit "${status}"
}
trap rollback EXIT HUP INT TERM
jwebot_log "preflight starting"
test -x "${runtime_python}"
for path in \
"${source_dir}/wechat_local_ocr.py" \
"${source_dir}/wechat-gui-bridge" \
"${source_dir}/deploy/scripts/install-wechat-health-watch.sh" \
"${source_dir}/deploy/systemd/system/wechat-local-ocr-recorder.service" \
"${source_dir}/deploy/systemd/system/wechat-local-ocr-responder.service" \
"${source_dir}/deploy/defaults/wechat-local-ocr.production.default"
do
test -f "${path}"
done
bash -n "${source_dir}/wechat-gui-bridge"
sh -n "${source_dir}/deploy/scripts/install-wechat-health-watch.sh"
"${runtime_python}" -m py_compile "${source_dir}/wechat_local_ocr.py"
preflight_rendered_dir=$(mktemp -d)
render_root_configuration "${preflight_rendered_dir}"
systemd-analyze verify \
"${preflight_rendered_dir}/wechat-local-ocr-recorder.service" \
"${preflight_rendered_dir}/wechat-local-ocr-responder.service"
rm -rf "${preflight_rendered_dir}"
preflight_rendered_dir=""
assert_queues_empty
if [[ "${require_wechat_ready}" -eq 1 ]]; then
assert_wechat_ready
fi
jwebot_log "preflight ready"
jwebot_progress 2 8 "backup"
jwebot_log "backup starting path=${backup_dir}"
install -d -o root -g root -m 0700 \
"${backup_root}" \
"${backup_dir}" \
"${backup_dir}/root" \
"${backup_dir}/user" \
"${backup_dir}/state" \
"${backup_dir}/metadata"
save_system_state
save_user_state
backup_file "${root_ocr}" wechat-local-ocr
backup_file "${root_bridge}" wechat-gui-bridge
backup_file "${root_recorder_unit}" wechat-local-ocr-recorder.service
backup_file "${root_responder_unit}" wechat-local-ocr-responder.service
backup_file "${root_defaults}" wechat-local-ocr.default
copy_directory_backup "${target_runtime}" runtime
copy_directory_backup "${target_user_units}" systemd-user
copy_directory_backup "${target_user_config}" jwebot-config
jwebot_log "backup ready path=${backup_dir}"
armed=1
jwebot_progress 3 8 "stop services"
jwebot_log "cutover starting"
user_systemctl stop \
wechat-ocr-adaptive-recorder.service \
jwebot-edge.service \
wechat-health-watch.timer \
jwebot-edge-memory-sync.timer \
wechat-health-watch.service \
jwebot-edge-memory-sync.service \
>/dev/null 2>&1 || true
systemctl stop \
wechat-local-ocr-responder.service \
wechat-local-ocr-recorder.service \
>/dev/null 2>&1 || true
assert_queues_empty
jwebot_log "services stopped queues=empty"
if [[ -f "${target_edge_state}/edge.sqlite3" ]]; then
"${runtime_python}" - \
"${target_edge_state}/edge.sqlite3" \
"${backup_dir}/state/edge.sqlite3" <<'PY'
import sqlite3
import sys
source = sqlite3.connect(f"file:{sys.argv[1]}?mode=ro", uri=True)
target = sqlite3.connect(sys.argv[2])
try:
if source.execute("PRAGMA quick_check").fetchone()[0] != "ok":
raise SystemExit("source SQLite quick_check failed")
source.backup(target)
if target.execute("PRAGMA quick_check").fetchone()[0] != "ok":
raise SystemExit("backup SQLite quick_check failed")
target.commit()
finally:
target.close()
source.close()
PY
fi
jwebot_log "database backup ready"
jwebot_progress 4 8 "install root runtime"
jwebot_log "runtime install starting"
rendered_recorder_unit="${backup_dir}/metadata/wechat-local-ocr-recorder.service"
rendered_responder_unit="${backup_dir}/metadata/wechat-local-ocr-responder.service"
rendered_defaults="${backup_dir}/metadata/wechat-local-ocr.default"
render_root_configuration "${backup_dir}/metadata"
install -o root -g root -m 0755 \
"${source_dir}/wechat_local_ocr.py" "${root_ocr}"
install -o root -g root -m 0755 \
"${source_dir}/wechat-gui-bridge" "${root_bridge}"
install -o root -g root -m 0644 \
"${rendered_recorder_unit}" \
"${root_recorder_unit}"
install -o root -g root -m 0644 \
"${rendered_responder_unit}" \
"${root_responder_unit}"
install -o root -g root -m 0644 \
"${rendered_defaults}" \
"${root_defaults}"
systemctl daemon-reload
systemctl disable --now \
wechat-local-ocr-recorder.service \
wechat-local-ocr-responder.service
jwebot_log "runtime install ready"
jwebot_progress 5 8 "install user runtime"
jwebot_log "user runtime install starting"
(
cd "${target_home}"
/usr/sbin/runuser -u "${target_user}" -- /usr/bin/env \
HOME="${target_home}" \
XDG_RUNTIME_DIR="${user_runtime}" \
DBUS_SESSION_BUS_ADDRESS="unix:path=${user_runtime}/bus" \
START_TIMER=1 \
HEALTH_SETTLE_SECONDS="${health_settle_seconds}" \
JWEBOT_RUNTIME_PYTHON="${runtime_python}" \
/bin/sh "${source_dir}/deploy/scripts/install-wechat-health-watch.sh" \
"${source_dir}"
)
jwebot_log "user runtime install ready"
jwebot_progress 6 8 "verify services"
systemctl disable --now \
wechat-local-ocr-recorder.service \
wechat-local-ocr-responder.service
test "$(
systemctl is-enabled wechat-local-ocr-recorder.service 2>/dev/null || true
)" = "disabled"
test "$(
systemctl is-enabled wechat-local-ocr-responder.service 2>/dev/null || true
)" = "disabled"
if systemctl is-active --quiet wechat-local-ocr-recorder.service \
|| systemctl is-active --quiet wechat-local-ocr-responder.service
then
echo "[JWeBot] error: legacy OCR unexpectedly remained active" >&2
exit 1
fi
user_systemctl is-enabled jwebot-edge.service \
| grep -qx enabled
user_systemctl is-enabled wechat-ocr-adaptive-recorder.service \
| grep -qx enabled
user_systemctl is-active --quiet jwebot-edge.service
jwebot_log "edge active"
user_systemctl is-active --quiet wechat-ocr-adaptive-recorder.service
jwebot_log "adaptive active"
verify_edge_health
jwebot_log "edge health ready"
restarts_before=$(
user_systemctl show wechat-ocr-adaptive-recorder.service \
-p NRestarts --value
)
sleep "${health_settle_seconds}"
user_systemctl is-active --quiet wechat-ocr-adaptive-recorder.service
restarts_after=$(
user_systemctl show wechat-ocr-adaptive-recorder.service \
-p NRestarts --value
)
test "${restarts_before}" = "${restarts_after}"
jwebot_log "adaptive stability ready restarts=${restarts_after}"
assert_queues_empty
jwebot_log "queues ready depth=0"
if [[ "${require_wechat_ready}" -eq 1 ]]; then
assert_wechat_ready
fi
cmp -s "${source_dir}/wechat_local_ocr.py" "${root_ocr}"
cmp -s "${source_dir}/wechat_local_ocr.py" \
"${target_runtime}/wechat_local_ocr.py"
if [[ -f "${target_edge_state}/edge.sqlite3" ]]; then
db_check=$(
/usr/sbin/runuser -u "${target_user}" -- \
/usr/bin/env HOME="${target_home}" \
"${runtime_python}" -c \
'import sqlite3; print(sqlite3.connect("file:'"${target_edge_state}"'/edge.sqlite3?mode=ro", uri=True).execute("pragma quick_check").fetchone()[0])'
)
test "${db_check}" = "ok"
fi
jwebot_log "database ready quick_check=${db_check:-absent}"
jwebot_progress 7 8 "verify backup"
(
cd "${backup_dir}"
find . -type f ! -name SHA256SUMS -print0 \
| LC_ALL=C sort -z \
| xargs -0 sha256sum >SHA256SUMS
sha256sum -c SHA256SUMS >metadata/sha256-verification.txt
)
jwebot_log "backup verification ready"
success=1
armed=0
trap - EXIT HUP INT TERM
jwebot_log "deployment ready version=$(cat "${source_dir}/VERSION")"
jwebot_log "backup active path=${backup_dir}"
jwebot_log "legacy OCR inactive"
jwebot_log "edge active health=ok"
jwebot_log "adaptive active restarts=${restarts_after}"
jwebot_log "database ready quick_check=${db_check:-absent}"
jwebot_progress 8 8 "deployment ready"
@@ -0,0 +1,81 @@
#!/bin/sh
set -eu
config=/www/dk_project/openclaw/config/openclaw.json
workspace=/www/dk_project/openclaw/config/workspace-wechat-responder
agent_state=/www/dk_project/openclaw/config/agents/wechat-responder
timestamp=$(date +%Y%m%d-%H%M%S)
backup="${config}.bak.wechat-responder-${timestamp}"
success=0
rollback() {
status=$?
if [ "$success" -eq 0 ]; then
cp -a "$backup" "$config"
rm -rf "$workspace" "$agent_state"
fi
exit "$status"
}
trap rollback EXIT HUP INT TERM
test -f "$config"
test ! -e "$workspace"
test ! -e "$agent_state"
test -f /tmp/wechat-responder-AGENTS.md
test -f /tmp/wechat-responder-SOUL.md
test -f /tmp/wechat-responder-IDENTITY.md
test -f /tmp/wechat-responder-USER.md
cp -a "$config" "$backup"
timeout 45 docker exec openclaw-gateway \
node /app/dist/index.js agents add wechat-responder \
--workspace /home/node/.openclaw/workspace-wechat-responder \
--model deepseek/deepseek-flash \
--non-interactive \
--json
install -d -o 1000 -g 1000 -m 0700 "$workspace"
install -o 1000 -g 1000 -m 0644 \
/tmp/wechat-responder-AGENTS.md \
"$workspace/AGENTS.md"
install -o 1000 -g 1000 -m 0644 \
/tmp/wechat-responder-SOUL.md \
"$workspace/SOUL.md"
install -o 1000 -g 1000 -m 0644 \
/tmp/wechat-responder-IDENTITY.md \
"$workspace/IDENTITY.md"
install -o 1000 -g 1000 -m 0644 \
/tmp/wechat-responder-USER.md \
"$workspace/USER.md"
rm -f "$workspace/BOOTSTRAP.md"
temporary="${config}.wechat-responder.tmp"
jq '
.agents.entries["wechat-responder"].name = "WeChat Tool-Free Chat"
| .agents.entries["wechat-responder"].thinkingDefault = "off"
| del(.agents.entries["wechat-responder"].timeoutSeconds)
| .agents.entries["wechat-responder"].skills = []
| .agents.entries["wechat-responder"].tools = {
"profile": "minimal",
"allow": ["session_status"],
"codeMode": false
}
' "$config" > "$temporary"
chown --reference="$config" "$temporary"
chmod --reference="$config" "$temporary"
mv "$temporary" "$config"
sleep 3
test "$(jq -r '.agents.entries["wechat-responder"].tools.profile' "$config")" = minimal
test "$(jq -r '.agents.entries["wechat-responder"].tools.allow | join(",")' "$config")" = session_status
test "$(jq -r '.agents.entries["wechat-responder"].tools.codeMode' "$config")" = false
test "$(jq -r '.agents.entries["wechat-responder"].skills | length' "$config")" -eq 0
test "$(jq -r '.agents.entries["wechat-responder"] | has("timeoutSeconds")' "$config")" = false
test ! -e "$workspace/BOOTSTRAP.md"
success=1
trap - EXIT HUP INT TERM
echo "backup=$backup"
jq -c '.agents.entries["wechat-responder"]' "$config"
@@ -0,0 +1,68 @@
#!/bin/sh
set -eu
config=/www/dk_project/openclaw/config/openclaw.json
workspace=/www/dk_project/openclaw/config/workspace-wechat-responder
timestamp=$(date +%Y%m%d-%H%M%S)
backup="${config}.bak.wechat-responder-finalize-${timestamp}"
success=0
rollback() {
status=$?
if [ "$success" -eq 0 ]; then
cp -a "$backup" "$config"
fi
exit "$status"
}
trap rollback EXIT HUP INT TERM
test "$(jq -r '.agents.entries | has("wechat-responder")' "$config")" = true
test -d "$workspace"
cp -a "$config" "$backup"
install -o 1000 -g 1000 -m 0644 \
/tmp/wechat-responder-AGENTS.md \
"$workspace/AGENTS.md"
install -o 1000 -g 1000 -m 0644 \
/tmp/wechat-responder-SOUL.md \
"$workspace/SOUL.md"
install -o 1000 -g 1000 -m 0644 \
/tmp/wechat-responder-IDENTITY.md \
"$workspace/IDENTITY.md"
install -o 1000 -g 1000 -m 0644 \
/tmp/wechat-responder-USER.md \
"$workspace/USER.md"
rm -f "$workspace/BOOTSTRAP.md"
temporary="${config}.wechat-responder.tmp"
jq '
.agents.entries["wechat-responder"].name = "WeChat Tool-Free Chat"
| .agents.entries["wechat-responder"].thinkingDefault = "off"
| del(.agents.entries["wechat-responder"].timeoutSeconds)
| .agents.entries["wechat-responder"].skills = []
| .agents.entries["wechat-responder"].tools = {
"profile": "minimal",
"allow": ["session_status"],
"codeMode": false
}
' "$config" > "$temporary"
chown --reference="$config" "$temporary"
chmod --reference="$config" "$temporary"
mv "$temporary" "$config"
sleep 3
test "$(jq -r '.agents.entries | ."wechat-responder" | .tools.profile' "$config")" = minimal
test "$(jq -r '.agents.entries | ."wechat-responder" | .tools.allow | join(",")' "$config")" = session_status
test "$(jq -r '.agents.entries | ."wechat-responder" | .tools.codeMode' "$config")" = false
test "$(jq -r '.agents.entries | ."wechat-responder" | .skills | length' "$config")" -eq 0
test "$(jq -r '.agents.entries | ."wechat-responder" | has("timeoutSeconds")' "$config")" = false
test ! -e "$workspace/BOOTSTRAP.md"
test "$(wc -c < "$workspace/AGENTS.md")" -lt 2000
success=1
trap - EXIT HUP INT TERM
echo "backup=$backup"
jq -c '.agents.entries | ."wechat-responder"' "$config"
find "$workspace" -maxdepth 1 -type f -printf "%f %s bytes\n" | sort
+446
View File
@@ -0,0 +1,446 @@
#!/bin/sh
set -eu
source_dir=${1:?usage: install-wechat-health-watch.sh SOURCE_DIR}
runtime_source_dir="$source_dir"
user_unit_source_dir="$source_dir/deploy/systemd/user"
config_source_dir="$source_dir/packages/jwebot-config/src/jwebot_config"
sdk_source_dir="$source_dir/packages/jwebot-sdk/src/jwebot_sdk"
kernel_source_dir="$source_dir/packages/jwebot-kernel/src/jwebot_kernel"
official_source_dir="$source_dir/packages/jwebot-official-extensions/src/jwebot_official_extensions"
official_adapter_source_dir="$official_source_dir/adapters"
target_dir="$HOME/.local/lib/wechat-health-watch"
unit_dir="$HOME/.config/systemd/user"
config_dir="$HOME/.config/jwebot"
environment_file="$config_dir/current/generated/runtime.env"
adaptive_state_file="$HOME/.local/state/wechat-adaptive-ocr/recorder-state.json"
runtime_python=${JWEBOT_RUNTIME_PYTHON:-/opt/jwebot/venv/bin/python}
health_settle_seconds=${HEALTH_SETTLE_SECONDS:-5}
edge_health_timeout_seconds=${EDGE_HEALTH_TIMEOUT_SECONDS:-45}
adaptive_ready_timeout_seconds=${ADAPTIVE_READY_TIMEOUT_SECONDS:-90}
start_timer=${START_TIMER:-1}
ocr_script_name=wechat_local_ocr.py
script_name=wechat_health_watch.py
adaptive_script_name=wechat_adaptive_recorder.py
adaptive_bridge_name=wechat_adaptive_gui_bridge.py
edge_script_name=jwebot_edge.py
edge_delivery_name=jwebot_edge_delivery.py
service_name=wechat-health-watch.service
timer_name=wechat-health-watch.timer
adaptive_service_name=wechat-ocr-adaptive-recorder.service
edge_service_name=jwebot-edge.service
edge_memory_service_name=jwebot-edge-memory-sync.service
edge_memory_timer_name=jwebot-edge-memory-sync.timer
backup_dir="$target_dir/.backup-$(date +%Y%m%d-%H%M%S)-$$"
unit_state_file="$backup_dir/systemd-user-state.tsv"
adaptive_ready_marker="$backup_dir/adaptive-start.marker"
installed=0
managed_units="
$edge_service_name
$adaptive_service_name
$timer_name
$edge_memory_timer_name
$service_name
$edge_memory_service_name
"
jwebot_log() {
printf '[JWeBot] %s\n' "$*"
}
unit_enabled_state() {
systemctl --user is-enabled "$1" 2>/dev/null || true
}
unit_active_state() {
systemctl --user is-active "$1" 2>/dev/null || true
}
save_unit_states() {
: >"$unit_state_file"
for unit in $managed_units; do
printf '%s\t%s\t%s\n' \
"$unit" \
"$(unit_enabled_state "$unit")" \
"$(unit_active_state "$unit")" \
>>"$unit_state_file"
done
}
recorded_unit_field() {
unit=$1
field=$2
awk -F ' ' -v unit="$unit" -v field="$field" \
'$1 == unit { print $field; exit }' "$unit_state_file"
}
restore_unit_states() {
systemctl --user daemon-reload >/dev/null 2>&1 || true
for unit in $managed_units; do
systemctl --user stop "$unit" >/dev/null 2>&1 || true
done
for unit in $managed_units; do
enabled=$(recorded_unit_field "$unit" 2>/dev/null || true)
case "$enabled" in
enabled)
systemctl --user unmask "$unit" >/dev/null 2>&1 || true
systemctl --user enable "$unit" >/dev/null 2>&1 || true
;;
enabled-runtime)
systemctl --user unmask "$unit" >/dev/null 2>&1 || true
systemctl --user enable --runtime "$unit" >/dev/null 2>&1 || true
;;
disabled)
systemctl --user unmask "$unit" >/dev/null 2>&1 || true
systemctl --user disable "$unit" >/dev/null 2>&1 || true
;;
masked)
systemctl --user mask "$unit" >/dev/null 2>&1 || true
;;
masked-runtime)
systemctl --user mask --runtime "$unit" >/dev/null 2>&1 || true
;;
esac
done
for unit in $managed_units; do
active=$(recorded_unit_field "$unit" 3 2>/dev/null || true)
case "$active" in
active|activating|reloading)
systemctl --user start "$unit" >/dev/null 2>&1 || true
;;
esac
done
systemctl --user reset-failed >/dev/null 2>&1 || true
}
restore_file() {
name=$1
target=$2
mode=$3
if [ -f "$backup_dir/$name" ]; then
install -m "$mode" "$backup_dir/$name" "$target"
else
rm -f "$target"
fi
}
wait_for_edge_health() {
attempt=0
while [ "$attempt" -lt "$edge_health_timeout_seconds" ]; do
if systemctl --user is-failed --quiet "$edge_service_name"; then
echo "edge service failed before becoming healthy" >&2
return 1
fi
if edge_health=$(
printf '%s\n' '{"op":"health"}' \
| "$runtime_python" "$target_dir/$edge_script_name" respond \
2>/dev/null
); then
if printf '%s' "$edge_health" \
| "$runtime_python" -c \
'import json,sys; value=json.load(sys.stdin); raise SystemExit(0 if value.get("status") == "ok" else 1)'
then
return 0
fi
fi
attempt=$((attempt + 1))
sleep 1
done
echo "edge health did not become ready within ${edge_health_timeout_seconds}s" >&2
return 1
}
wait_for_adaptive_ready() {
attempt=0
while [ "$attempt" -lt "$adaptive_ready_timeout_seconds" ]; do
if systemctl --user is-failed --quiet "$adaptive_service_name"; then
echo "[JWeBot] error: adaptive service failed before becoming ready" >&2
return 1
fi
if [ -f "$adaptive_state_file" ] \
&& [ "$adaptive_state_file" -nt "$adaptive_ready_marker" ] \
&& "$runtime_python" - "$adaptive_state_file" "$adaptive_started_epoch" <<'PY'
import json
import sys
from datetime import datetime
from pathlib import Path
try:
value = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
captured_at = datetime.fromisoformat(str(value["last_capture_at"]))
ready = (
value.get("initialized") is True
and isinstance(value.get("message_signature_scheme"), str)
and bool(value["message_signature_scheme"])
and isinstance(value.get("visible_signatures"), list)
and captured_at.timestamp() >= int(sys.argv[2])
)
except (KeyError, OSError, TypeError, ValueError, json.JSONDecodeError):
ready = False
raise SystemExit(0 if ready else 1)
PY
then
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
echo "[JWeBot] error: adaptive OCR did not become ready within ${adaptive_ready_timeout_seconds}s" >&2
return 1
}
rollback() {
status=$?
trap - EXIT HUP INT TERM
if [ "$installed" -eq 1 ]; then
jwebot_log "user runtime rollback starting backup=$backup_dir"
systemctl --user stop \
"$adaptive_service_name" \
"$edge_service_name" \
"$timer_name" \
"$edge_memory_timer_name" \
>/dev/null 2>&1 || true
restore_file "$ocr_script_name" "$target_dir/$ocr_script_name" 0700
restore_file "$script_name" "$target_dir/$script_name" 0700
restore_file "$adaptive_script_name" "$target_dir/$adaptive_script_name" 0700
restore_file "$adaptive_bridge_name" "$target_dir/$adaptive_bridge_name" 0700
restore_file "$edge_script_name" "$target_dir/$edge_script_name" 0700
restore_file "$edge_delivery_name" "$target_dir/$edge_delivery_name" 0700
for package_name in \
jwebot_config \
jwebot_sdk \
jwebot_kernel \
jwebot_official_extensions
do
rm -rf "$target_dir/$package_name"
if [ -d "$backup_dir/$package_name" ]; then
cp -a "$backup_dir/$package_name" "$target_dir/$package_name"
fi
done
restore_file "$service_name" "$unit_dir/$service_name" 0644
restore_file "$timer_name" "$unit_dir/$timer_name" 0644
restore_file "$adaptive_service_name" "$unit_dir/$adaptive_service_name" 0644
restore_file "$edge_service_name" "$unit_dir/$edge_service_name" 0644
restore_file "$edge_memory_service_name" "$unit_dir/$edge_memory_service_name" 0644
restore_file "$edge_memory_timer_name" "$unit_dir/$edge_memory_timer_name" 0644
restore_file environment "$environment_file" 0600
restore_unit_states
jwebot_log "user runtime rollback ready backup=$backup_dir"
fi
exit "$status"
}
trap rollback EXIT HUP INT TERM
jwebot_log "user runtime preflight starting"
test -x "$runtime_python"
for path in \
"$runtime_source_dir/$ocr_script_name" \
"$runtime_source_dir/$script_name" \
"$runtime_source_dir/$adaptive_script_name" \
"$runtime_source_dir/$adaptive_bridge_name" \
"$runtime_source_dir/$edge_script_name" \
"$runtime_source_dir/$edge_delivery_name" \
"$user_unit_source_dir/$service_name" \
"$user_unit_source_dir/$timer_name" \
"$user_unit_source_dir/$adaptive_service_name" \
"$user_unit_source_dir/$edge_service_name" \
"$user_unit_source_dir/$edge_memory_service_name" \
"$user_unit_source_dir/$edge_memory_timer_name"
do
test -f "$path"
done
for directory in \
"$config_source_dir" \
"$sdk_source_dir" \
"$kernel_source_dir" \
"$official_source_dir" \
"$official_adapter_source_dir"
do
test -d "$directory"
find "$directory" -maxdepth 1 -type f -name '*.py' | grep -q .
done
test -f "$environment_file"
if systemctl is-active --quiet wechat-local-ocr-recorder.service \
|| systemctl is-active --quiet wechat-local-ocr-responder.service
then
echo "[JWeBot] error: legacy system OCR is active; use root deployer" >&2
exit 1
fi
jwebot_log "user runtime preflight ready"
jwebot_log "user runtime backup starting path=$backup_dir"
install -d -m 0700 "$target_dir" "$unit_dir" "$config_dir" "$backup_dir"
save_unit_states
for name in \
"$ocr_script_name" \
"$script_name" \
"$adaptive_script_name" \
"$adaptive_bridge_name" \
"$edge_script_name" \
"$edge_delivery_name"
do
if [ -f "$target_dir/$name" ]; then
cp -a "$target_dir/$name" "$backup_dir/$name"
fi
done
for package_name in \
jwebot_config \
jwebot_sdk \
jwebot_kernel \
jwebot_official_extensions
do
if [ -d "$target_dir/$package_name" ]; then
cp -a "$target_dir/$package_name" "$backup_dir/$package_name"
fi
done
for name in \
"$service_name" \
"$timer_name" \
"$adaptive_service_name" \
"$edge_service_name" \
"$edge_memory_service_name" \
"$edge_memory_timer_name"
do
if [ -f "$unit_dir/$name" ]; then
cp -a "$unit_dir/$name" "$backup_dir/$name"
fi
done
if [ -f "$environment_file" ]; then
cp -a "$environment_file" "$backup_dir/environment"
fi
jwebot_log "user runtime backup ready path=$backup_dir"
installed=1
jwebot_log "user services stopping"
systemctl --user stop \
"$adaptive_service_name" \
"$edge_service_name" \
"$timer_name" \
"$edge_memory_timer_name" \
"$service_name" \
"$edge_memory_service_name" \
>/dev/null 2>&1 || true
jwebot_log "user runtime files install starting"
install -m 0700 "$runtime_source_dir/$ocr_script_name" "$target_dir/$ocr_script_name"
install -m 0700 "$runtime_source_dir/$script_name" "$target_dir/$script_name"
install -m 0700 "$runtime_source_dir/$adaptive_script_name" "$target_dir/$adaptive_script_name"
install -m 0700 "$runtime_source_dir/$adaptive_bridge_name" "$target_dir/$adaptive_bridge_name"
install -m 0700 "$runtime_source_dir/$edge_script_name" "$target_dir/$edge_script_name"
install -m 0700 "$runtime_source_dir/$edge_delivery_name" "$target_dir/$edge_delivery_name"
rm -rf \
"$target_dir/jwebot_config" \
"$target_dir/jwebot_sdk" \
"$target_dir/jwebot_kernel" \
"$target_dir/jwebot_official_extensions"
install -d -m 0700 \
"$target_dir/jwebot_config" \
"$target_dir/jwebot_sdk" \
"$target_dir/jwebot_kernel" \
"$target_dir/jwebot_official_extensions" \
"$target_dir/jwebot_official_extensions/adapters"
for path in "$config_source_dir"/*.py; do
install -m 0600 "$path" "$target_dir/jwebot_config/$(basename "$path")"
done
for path in "$sdk_source_dir"/*.py; do
install -m 0600 "$path" "$target_dir/jwebot_sdk/$(basename "$path")"
done
for path in "$kernel_source_dir"/*.py; do
install -m 0600 "$path" "$target_dir/jwebot_kernel/$(basename "$path")"
done
for path in "$official_source_dir"/*.py; do
install -m 0600 "$path" "$target_dir/jwebot_official_extensions/$(basename "$path")"
done
for path in "$official_adapter_source_dir"/*.py; do
install -m 0600 "$path" \
"$target_dir/jwebot_official_extensions/adapters/$(basename "$path")"
done
for name in \
"$service_name" \
"$timer_name" \
"$adaptive_service_name" \
"$edge_service_name" \
"$edge_memory_service_name" \
"$edge_memory_timer_name"
do
rendered_unit="$backup_dir/rendered-$name"
sed "s#/opt/jwebot/venv/bin/python#$runtime_python#g" \
"$user_unit_source_dir/$name" >"$rendered_unit"
install -m 0644 "$rendered_unit" "$unit_dir/$name"
done
for name in \
"$ocr_script_name" \
"$script_name" \
"$adaptive_script_name" \
"$adaptive_bridge_name" \
"$edge_script_name" \
"$edge_delivery_name"
do
"$runtime_python" -m py_compile "$target_dir/$name"
done
"$runtime_python" -m compileall -q \
"$target_dir/jwebot_config" \
"$target_dir/jwebot_sdk" \
"$target_dir/jwebot_kernel" \
"$target_dir/jwebot_official_extensions"
systemd-analyze --user verify \
"$unit_dir/$service_name" \
"$unit_dir/$timer_name" \
"$unit_dir/$adaptive_service_name" \
"$unit_dir/$edge_service_name" \
"$unit_dir/$edge_memory_service_name" \
"$unit_dir/$edge_memory_timer_name"
jwebot_log "user runtime files install ready"
systemctl --user daemon-reload
systemctl --user enable \
"$timer_name" \
"$edge_service_name" \
"$edge_memory_timer_name" \
"$adaptive_service_name"
jwebot_log "edge starting"
systemctl --user start "$edge_service_name"
systemctl --user is-active --quiet "$edge_service_name"
wait_for_edge_health
jwebot_log "edge active health=ok"
: >"$adaptive_ready_marker"
adaptive_started_epoch=$(date +%s)
jwebot_log "adaptive starting"
systemctl --user start "$adaptive_service_name"
systemctl --user is-active --quiet "$adaptive_service_name"
jwebot_log "adaptive active"
restarts_before=$(
systemctl --user show "$adaptive_service_name" -p NRestarts --value
)
wait_for_adaptive_ready
jwebot_log "adaptive ready baseline=captured"
sleep "$health_settle_seconds"
systemctl --user is-active --quiet "$adaptive_service_name"
restarts_after=$(
systemctl --user show "$adaptive_service_name" -p NRestarts --value
)
test "$restarts_before" = "$restarts_after"
jwebot_log "adaptive stability ready restarts=$restarts_after"
if [ "$start_timer" -eq 1 ]; then
systemctl --user start "$timer_name" "$edge_memory_timer_name"
systemctl --user is-active --quiet "$timer_name"
systemctl --user is-active --quiet "$edge_memory_timer_name"
jwebot_log "timers active"
else
jwebot_log "timers inactive requested=false"
fi
installed=0
trap - EXIT HUP INT TERM
jwebot_log "user runtime ready"
jwebot_log "backup active path=$backup_dir"
jwebot_log "edge active health=ok"
jwebot_log "adaptive active restarts=$restarts_after"
+151
View File
@@ -0,0 +1,151 @@
#!/bin/sh
set -eu
source_script=/tmp/wechat-local-ocr.py
source_bridge=/tmp/wechat-gui-bridge
source_recorder_unit=/tmp/wechat-local-ocr-recorder.service
source_responder_unit=/tmp/wechat-local-ocr-responder.service
source_defaults=/tmp/wechat-local-ocr.default
runtime_user=${JWEBOT_RUNTIME_USER:-jwebot}
target_script=/usr/local/sbin/wechat-local-ocr
target_bridge=/usr/local/sbin/wechat-gui-bridge
target_recorder_unit=/etc/systemd/system/wechat-local-ocr-recorder.service
target_responder_unit=/etc/systemd/system/wechat-local-ocr-responder.service
target_defaults=/etc/default/wechat-local-ocr
venv_python=${JWEBOT_RUNTIME_PYTHON:-/opt/jwebot/venv/bin/python}
timestamp=$(date +%Y%m%d-%H%M%S)
backup_dir="/root/wechat-local-ocr-backup-${timestamp}"
enable_legacy_ocr=${ENABLE_LEGACY_OCR:-0}
success=0
recorder_enabled=$(systemctl is-enabled wechat-local-ocr-recorder.service 2>/dev/null || true)
recorder_active=$(systemctl is-active wechat-local-ocr-recorder.service 2>/dev/null || true)
responder_enabled=$(systemctl is-enabled wechat-local-ocr-responder.service 2>/dev/null || true)
responder_active=$(systemctl is-active wechat-local-ocr-responder.service 2>/dev/null || true)
restore_file() {
name=$1
target=$2
if [ -f "${backup_dir}/${name}" ]; then
cp -a "${backup_dir}/${name}" "$target"
else
rm -f "$target"
fi
}
restore_service_state() {
systemctl disable --now \
wechat-local-ocr-recorder.service \
wechat-local-ocr-responder.service \
>/dev/null 2>&1 || true
case "$recorder_enabled" in
enabled) systemctl enable wechat-local-ocr-recorder.service >/dev/null 2>&1 || true ;;
masked) systemctl mask wechat-local-ocr-recorder.service >/dev/null 2>&1 || true ;;
esac
case "$responder_enabled" in
enabled) systemctl enable wechat-local-ocr-responder.service >/dev/null 2>&1 || true ;;
masked) systemctl mask wechat-local-ocr-responder.service >/dev/null 2>&1 || true ;;
esac
case "$recorder_active" in
active|activating|reloading)
systemctl start wechat-local-ocr-recorder.service >/dev/null 2>&1 || true
;;
esac
case "$responder_active" in
active|activating|reloading)
systemctl start wechat-local-ocr-responder.service >/dev/null 2>&1 || true
;;
esac
}
rollback() {
status=$?
if [ "$success" -eq 0 ]; then
systemctl disable --now wechat-local-ocr-recorder.service >/dev/null 2>&1 || true
systemctl disable --now wechat-local-ocr-responder.service >/dev/null 2>&1 || true
restore_file wechat-local-ocr "$target_script"
restore_file wechat-gui-bridge "$target_bridge"
restore_file wechat-local-ocr-recorder.service "$target_recorder_unit"
restore_file wechat-local-ocr-responder.service "$target_responder_unit"
restore_file wechat-local-ocr.default "$target_defaults"
systemctl daemon-reload || true
restore_service_state
fi
exit "$status"
}
trap rollback EXIT HUP INT TERM
test -x "$venv_python"
test -f "$source_script"
test -f "$source_bridge"
test -f "$source_recorder_unit"
test -f "$source_responder_unit"
test -f "$source_defaults"
install -d -o root -g root -m 0700 "$backup_dir"
for pair in \
"wechat-local-ocr:$target_script" \
"wechat-gui-bridge:$target_bridge" \
"wechat-local-ocr-recorder.service:$target_recorder_unit" \
"wechat-local-ocr-responder.service:$target_responder_unit" \
"wechat-local-ocr.default:$target_defaults"
do
name=${pair%%:*}
target=${pair#*:}
if [ -f "$target" ]; then
cp -a "$target" "${backup_dir}/${name}"
fi
done
rendered_recorder_unit="${backup_dir}/wechat-local-ocr-recorder.rendered.service"
rendered_responder_unit="${backup_dir}/wechat-local-ocr-responder.rendered.service"
sed \
-e "s/^User=.*/User=$runtime_user/" \
-e "s/^Group=.*/Group=$runtime_user/" \
-e "s#/opt/jwebot/venv/bin/python#$venv_python#g" \
"$source_recorder_unit" >"$rendered_recorder_unit"
sed \
-e "s#/opt/jwebot/venv/bin/python#$venv_python#g" \
"$source_responder_unit" >"$rendered_responder_unit"
install -o root -g root -m 0755 "$source_script" "$target_script"
install -o root -g root -m 0755 "$source_bridge" "$target_bridge"
install -o root -g root -m 0644 "$rendered_recorder_unit" "$target_recorder_unit"
install -o root -g root -m 0644 "$rendered_responder_unit" "$target_responder_unit"
install -o root -g root -m 0644 "$source_defaults" "$target_defaults"
install -d -o "$runtime_user" -g "$runtime_user" -m 0700 \
/var/lib/wechat-local-ocr \
/var/lib/wechat-local-ocr/history \
/var/lib/wechat-local-ocr/queue \
/var/lib/wechat-local-ocr/failed
chown -R "$runtime_user:$runtime_user" /var/lib/wechat-local-ocr
touch /var/lib/wechat-local-ocr/gui-interaction.lock
chown "$runtime_user:$runtime_user" /var/lib/wechat-local-ocr/gui-interaction.lock
chmod 0600 /var/lib/wechat-local-ocr/gui-interaction.lock
"$venv_python" -m py_compile "$target_script"
bash -n "$target_bridge"
systemd-analyze verify "$target_recorder_unit" "$target_responder_unit"
systemctl daemon-reload
systemctl disable --now \
wechat-local-ocr-recorder.service \
wechat-local-ocr-responder.service \
>/dev/null 2>&1 || true
if [ "$enable_legacy_ocr" -eq 1 ]; then
systemctl enable wechat-local-ocr-recorder.service
systemctl start wechat-local-ocr-recorder.service
systemctl is-active --quiet wechat-local-ocr-recorder.service
fi
success=1
trap - EXIT HUP INT TERM
echo "backup_dir=$backup_dir"
sha256sum "$target_script" "$target_bridge" "$target_recorder_unit" "$target_responder_unit" "$target_defaults"
if [ "$enable_legacy_ocr" -eq 1 ]; then
systemctl status wechat-local-ocr-recorder.service --no-pager -l | sed -n '1,28p'
else
echo "legacy_ocr=disabled"
fi
+55
View File
@@ -0,0 +1,55 @@
#!/bin/sh
set -eu
defaults=/etc/default/wechat-local-ocr
backup=$(mktemp /root/wechat-local-ocr-default.XXXXXX)
old_service=wechat-openclaw-bridge.service
recorder_service=wechat-local-ocr-recorder.service
responder_service=wechat-local-ocr-responder.service
success=0
cp -a "$defaults" "$backup"
rollback() {
status=$?
if [ "$success" -eq 0 ]; then
systemctl disable --now "$responder_service" >/dev/null 2>&1 || true
cp -a "$backup" "$defaults"
systemctl restart "$recorder_service" >/dev/null 2>&1 || true
systemctl enable --now "$old_service" >/dev/null 2>&1 || true
fi
rm -f "$backup"
exit "$status"
}
trap rollback EXIT HUP INT TERM
test "$(find /var/lib/wechat-local-ocr/queue -maxdepth 1 -type f | wc -l)" -eq 0
test "$(find /var/lib/wechat-local-ocr/failed -maxdepth 1 -type f | wc -l)" -eq 0
systemctl is-active --quiet "$recorder_service"
systemctl is-active --quiet "$old_service"
printf '%s\n' \
'WECHAT_QUEUE_ENABLED=1' \
'WECHAT_ROUTER_ENABLED=1' \
> "$defaults"
chmod 0644 "$defaults"
systemctl disable --now "$old_service"
systemctl restart "$recorder_service"
sleep 2
systemctl is-active --quiet "$recorder_service"
systemctl enable --now "$responder_service"
sleep 2
systemctl is-active --quiet "$responder_service"
test "$(systemctl is-active "$old_service")" = inactive
test "$(find /var/lib/wechat-local-ocr/queue -maxdepth 1 -type f | wc -l)" -eq 0
success=1
trap - EXIT HUP INT TERM
rm -f "$backup"
systemctl is-active "$recorder_service"
systemctl is-active "$responder_service"
systemctl is-active "$old_service" || true
@@ -0,0 +1,69 @@
#!/bin/sh
set -eu
source_script=/tmp/wechat-local-ocr.py
source_bridge=/tmp/wechat-gui-bridge
runtime_user=${JWEBOT_RUNTIME_USER:-jwebot}
runtime_python=${JWEBOT_RUNTIME_PYTHON:-/opt/jwebot/venv/bin/python}
target_script=/usr/local/sbin/wechat-local-ocr
target_bridge=/usr/local/sbin/wechat-gui-bridge
recorder_service=wechat-local-ocr-recorder.service
responder_service=wechat-local-ocr-responder.service
backup_script=$(mktemp /root/wechat-local-ocr-script.XXXXXX)
backup_bridge=$(mktemp /root/wechat-gui-bridge.XXXXXX)
success=0
recorder_active=$(systemctl is-active "$recorder_service" 2>/dev/null || true)
responder_active=$(systemctl is-active "$responder_service" 2>/dev/null || true)
cp -a "$target_script" "$backup_script"
cp -a "$target_bridge" "$backup_bridge"
restore_service_state() {
systemctl stop "$responder_service" "$recorder_service" >/dev/null 2>&1 || true
case "$recorder_active" in
active|activating|reloading)
systemctl start "$recorder_service" >/dev/null 2>&1 || true
;;
esac
case "$responder_active" in
active|activating|reloading)
systemctl start "$responder_service" >/dev/null 2>&1 || true
;;
esac
}
rollback() {
status=$?
if [ "$success" -eq 0 ]; then
cp -a "$backup_script" "$target_script"
cp -a "$backup_bridge" "$target_bridge"
restore_service_state
fi
rm -f "$backup_script" "$backup_bridge"
exit "$status"
}
trap rollback EXIT HUP INT TERM
test -f "$source_script"
test -f "$source_bridge"
test "$(find /var/lib/wechat-local-ocr/queue -maxdepth 1 -type f | wc -l)" -eq 0
touch /var/lib/wechat-local-ocr/gui-interaction.lock
chown "$runtime_user:$runtime_user" /var/lib/wechat-local-ocr/gui-interaction.lock
chmod 0600 /var/lib/wechat-local-ocr/gui-interaction.lock
systemctl stop "$responder_service" "$recorder_service" >/dev/null 2>&1 || true
install -o root -g root -m 0755 "$source_script" "$target_script"
install -o root -g root -m 0755 "$source_bridge" "$target_bridge"
"$runtime_python" -m py_compile "$target_script"
bash -n "$target_bridge"
restore_service_state
test "$(find /var/lib/wechat-local-ocr/queue -maxdepth 1 -type f | wc -l)" -eq 0
success=1
trap - EXIT HUP INT TERM
rm -f "$backup_script" "$backup_bridge"
sha256sum "$target_script" "$target_bridge"
printf 'recorder_state=%s\n' "$(systemctl is-active "$recorder_service" 2>/dev/null || true)"
printf 'responder_state=%s\n' "$(systemctl is-active "$responder_service" 2>/dev/null || true)"
@@ -0,0 +1,55 @@
[Unit]
Description=JWeBot isolated extension host for %i
After=local-fs.target
Requires=jwebot-extension-host@%i.socket
ConditionPathExists=/etc/jwebot-extension-host/allowed_signers
[Service]
Type=exec
User=jwebot-ext
Group=jwebot-ext
Environment=PYTHONDONTWRITEBYTECODE=1
ExecStart=/opt/jwebot-extension-host/venv/bin/python -I -m jwebot_extension_host --release-dir /var/lib/jwebot-extensions/%i/current --allowed-signers /etc/jwebot-extension-host/allowed_signers
WorkingDirectory=/
StandardInput=socket
StandardOutput=socket
StandardError=journal
Restart=no
TimeoutStartSec=12s
RuntimeMaxSec=12s
UMask=0077
NoNewPrivileges=true
CapabilityBoundingSet=
AmbientCapabilities=
PrivateTmp=true
PrivateDevices=true
PrivateNetwork=true
ProtectSystem=strict
ProtectHome=true
ProtectClock=true
ProtectControlGroups=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
ProcSubset=pid
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@clock @debug @module @mount @obsolete @privileged @raw-io @reboot @swap
MemoryMax=128M
CPUQuota=50%
TasksMax=16
LimitNOFILE=64
LimitNPROC=16
LimitCORE=0
ReadOnlyPaths=/var/lib/jwebot-extensions/%i
InaccessiblePaths=/home /root /srv /mnt /media
@@ -0,0 +1,15 @@
[Unit]
Description=JWeBot extension host socket for %i
[Socket]
ListenStream=/run/jwebot-extension-host/%i.sock
SocketUser=jwebot
SocketGroup=jwebot
SocketMode=0600
DirectoryMode=0700
RemoveOnStop=true
Service=jwebot-extension-host@%i.service
[Install]
WantedBy=sockets.target
@@ -0,0 +1,21 @@
[Unit]
Description=Local OCR recorder for one fixed WeChat group
After=tigervncserver@:1.service
Requires=tigervncserver@:1.service
[Service]
Type=simple
User=jwebot
Group=jwebot
EnvironmentFile=-/etc/default/wechat-local-ocr
ExecStart=/opt/jwebot/venv/bin/python /usr/local/sbin/wechat-local-ocr record
Restart=always
RestartSec=3
Nice=10
UMask=0077
MemoryMax=768M
CPUQuota=150%
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,17 @@
[Unit]
Description=JWeBot responder for local WeChat OCR events
After=wechat-local-ocr-recorder.service openclaw-tunnel.service
Requires=wechat-local-ocr-recorder.service openclaw-tunnel.service
[Service]
Type=simple
EnvironmentFile=-/etc/default/wechat-local-ocr
ExecStart=/opt/jwebot/venv/bin/python /usr/local/sbin/wechat-local-ocr respond
Restart=always
RestartSec=3
UMask=0077
MemoryMax=256M
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,12 @@
[Unit]
Description=Project JWeBot edge memory to cloud workspace
After=jwebot-edge.service
Wants=jwebot-edge.service
[Service]
Type=oneshot
EnvironmentFile=-%h/.config/jwebot/current/generated/runtime.env
ExecStart=/opt/jwebot/venv/bin/python %h/.local/lib/wechat-health-watch/jwebot_edge.py sync-memory
NoNewPrivileges=true
PrivateTmp=true
UMask=0077
@@ -0,0 +1,11 @@
[Unit]
Description=Synchronize JWeBot edge memory every three days
[Timer]
OnBootSec=15min
OnUnitActiveSec=3d
Persistent=true
Unit=jwebot-edge-memory-sync.service
[Install]
WantedBy=timers.target
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=JWeBot local edge router and event ledger
After=default.target
[Service]
Type=simple
EnvironmentFile=-%h/.config/jwebot/current/generated/runtime.env
ExecStart=/opt/jwebot/venv/bin/python %h/.local/lib/wechat-health-watch/jwebot_edge.py serve
Restart=always
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
UMask=0077
MemoryMax=256M
CPUQuota=100%
[Install]
WantedBy=default.target
@@ -0,0 +1,11 @@
[Unit]
Description=WeChat GUI automation health monitor
After=default.target
[Service]
Type=oneshot
EnvironmentFile=-%h/.config/jwebot/current/generated/runtime.env
ExecStart=%h/.local/lib/wechat-health-watch/wechat_health_watch.py
NoNewPrivileges=true
PrivateTmp=true
UMask=0077
@@ -0,0 +1,11 @@
[Unit]
Description=Run WeChat GUI automation health monitor every minute
[Timer]
OnBootSec=45s
OnUnitActiveSec=60s
Persistent=true
Unit=wechat-health-watch.service
[Install]
WantedBy=timers.target
@@ -0,0 +1,21 @@
[Unit]
Description=Adaptive local WeChat OCR recorder and responder
After=default.target jwebot-edge.service
Wants=jwebot-edge.service
StartLimitIntervalSec=300
StartLimitBurst=6
[Service]
Type=simple
EnvironmentFile=-%h/.config/jwebot/current/generated/runtime.env
ExecStart=/opt/jwebot/venv/bin/python %h/.local/lib/wechat-health-watch/wechat_adaptive_recorder.py
Restart=on-failure
RestartSec=10
NoNewPrivileges=true
PrivateTmp=true
UMask=0077
MemoryMax=768M
CPUQuota=250%
[Install]
WantedBy=default.target