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
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env bash
set -Eeuo pipefail
readonly BRIDGE_USER="${JWEBOT_RUNTIME_USER:-jwebot}"
BRIDGE_HOME="${JWEBOT_RUNTIME_HOME:-$(getent passwd "${BRIDGE_USER}" | cut -d: -f6)}"
readonly BRIDGE_HOME
BRIDGE_UID=$(id -u "${BRIDGE_USER}")
readonly BRIDGE_UID
readonly DISPLAY_NAME=":1"
readonly XAUTHORITY_FILE="${BRIDGE_HOME}/.Xauthority"
readonly RUNTIME_DIR="/run/user/${BRIDGE_UID}"
readonly STATE_DIR="${BRIDGE_HOME}/.local/state/wechat-gui-bridge"
readonly PENDING_TEXT="${STATE_DIR}/pending.txt"
readonly PENDING_TOKEN="${STATE_DIR}/pending.token"
readonly PENDING_TTL=120
readonly EXPECTED_WIDTH=1440
readonly EXPECTED_HEIGHT=873
readonly DRAFT_CLICK_X=900
readonly DRAFT_CLICK_Y=780
readonly SEND_CLICK_X=1390
readonly SEND_CLICK_Y=842
require_root() {
if [[ ${EUID} -ne 0 ]]; then
printf 'error: run as root\n' >&2
exit 1
fi
}
as_bridge_user() {
/usr/sbin/runuser -u "${BRIDGE_USER}" -- env \
DISPLAY="${DISPLAY_NAME}" \
XAUTHORITY="${XAUTHORITY_FILE}" \
XDG_RUNTIME_DIR="${RUNTIME_DIR}" \
DBUS_SESSION_BUS_ADDRESS="unix:path=${RUNTIME_DIR}/bus" \
"$@"
}
ensure_state_dir() {
install -d -o "${BRIDGE_USER}" -g "${BRIDGE_USER}" -m 700 "${STATE_DIR}"
}
window_id() {
local wid name
while IFS= read -r wid; do
[[ -n ${wid} ]] || continue
name=$(as_bridge_user xdotool getwindowname "${wid}" 2>/dev/null || true)
if [[ ${name} == "Weixin" ]]; then
printf '%s\n' "${wid}"
return 0
fi
done < <(as_bridge_user xdotool search --onlyvisible --class wechat || true)
printf 'error: visible main WeChat window not found\n' >&2
return 1
}
window_geometry() {
local wid=$1
as_bridge_user xdotool getwindowgeometry --shell "${wid}"
}
ensure_maximized_window() {
local wid=$1 geometry width height
geometry=$(window_geometry "${wid}")
eval "${geometry}"
width=${WIDTH:?}
height=${HEIGHT:?}
if (( width == EXPECTED_WIDTH && height == EXPECTED_HEIGHT )); then
return
fi
as_bridge_user wmctrl -ir "${wid}" -b add,maximized_vert,maximized_horz
sleep 0.25
geometry=$(window_geometry "${wid}")
eval "${geometry}"
width=${WIDTH:?}
height=${HEIGHT:?}
if (( width != EXPECTED_WIDTH || height != EXPECTED_HEIGHT )); then
printf 'error: WeChat maximize failed; expected %sx%s, got %sx%s\n' \
"${EXPECTED_WIDTH}" "${EXPECTED_HEIGHT}" "${width}" "${height}" >&2
return 1
fi
}
capture_window() {
local destination=${1:-"${STATE_DIR}/latest.png"}
local wid
wid=$(window_id)
ensure_maximized_window "${wid}"
as_bridge_user scrot --overwrite --window "${wid}" "${destination}"
chown "${BRIDGE_USER}:${BRIDGE_USER}" "${destination}"
chmod 600 "${destination}"
printf '%s\n' "${destination}"
}
status_command() {
local wid name geometry
wid=$(window_id)
ensure_maximized_window "${wid}"
name=$(as_bridge_user xdotool getwindowname "${wid}")
geometry=$(window_geometry "${wid}" | tr '\n' ' ')
printf '{"ok":true,"display":"%s","window_id":"%s","window_name":"%s","geometry":"%s","wechat_pid":"%s"}\n' \
"${DISPLAY_NAME}" "${wid}" "${name//\"/}" "${geometry//\"/}" "$(pgrep -u "${BRIDGE_USER}" -x wechat | head -n 1)"
}
ocr_command() {
ensure_state_dir
local image="${STATE_DIR}/latest.png"
local output="${STATE_DIR}/latest-ocr"
capture_window "${image}" >/dev/null
as_bridge_user tesseract "${image}" "${output}" -l chi_sim+eng --psm 11 >/dev/null 2>&1
local lines chars
lines=$(grep -cve '^[[:space:]]*$' "${output}.txt" || true)
chars=$(wc -m < "${output}.txt")
printf '{"ok":true,"image":"%s","text":"%s","lines":%s,"characters":%s}\n' \
"${image}" "${output}.txt" "${lines}" "${chars}"
}
ocr_text_command() {
ocr_command >/dev/null
cat "${STATE_DIR}/latest-ocr.txt"
}
draft_command() {
ensure_state_dir
local text=${1:-}
if [[ -z ${text} ]]; then
printf 'error: draft text is empty\n' >&2
exit 2
fi
if (( ${#text} > 1000 )); then
printf 'error: draft exceeds 1000 characters\n' >&2
exit 2
fi
printf '%s' "${text}" > "${PENDING_TEXT}"
chown "${BRIDGE_USER}:${BRIDGE_USER}" "${PENDING_TEXT}"
chmod 600 "${PENDING_TEXT}"
local token wid
token=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')
printf '%s\n' "${token}" > "${PENDING_TOKEN}"
chown "${BRIDGE_USER}:${BRIDGE_USER}" "${PENDING_TOKEN}"
chmod 600 "${PENDING_TOKEN}"
wid=$(window_id)
ensure_maximized_window "${wid}"
# xclip backgrounds itself and otherwise keeps a service caller's captured
# stdout/stderr pipes open, making a completed draft look like a timeout.
as_bridge_user xclip -selection clipboard -in < "${PENDING_TEXT}" >/dev/null 2>&1
as_bridge_user xdotool windowactivate --sync "${wid}"
as_bridge_user xdotool mousemove --window "${wid}" \
"${DRAFT_CLICK_X}" "${DRAFT_CLICK_Y}" click 1
as_bridge_user xdotool key --clearmodifiers ctrl+a BackSpace ctrl+v
printf '{"ok":true,"state":"drafted","confirmation_token":"%s","expires_in_seconds":%s}\n' \
"${token}" "${PENDING_TTL}"
}
send_command() {
local supplied=${1:-}
if [[ -z ${supplied} || ! -s ${PENDING_TOKEN} || ! -s ${PENDING_TEXT} ]]; then
printf 'error: no armed draft\n' >&2
exit 3
fi
local expected created now age
expected=$(tr -d '\r\n' < "${PENDING_TOKEN}")
created=$(stat -c %Y "${PENDING_TOKEN}")
now=$(date +%s)
age=$((now - created))
if [[ ${supplied} != "${expected}" ]]; then
printf 'error: confirmation token mismatch\n' >&2
exit 3
fi
if (( age < 0 || age > PENDING_TTL )); then
printf 'error: confirmation token expired\n' >&2
exit 3
fi
local wid
wid=$(window_id)
ensure_maximized_window "${wid}"
as_bridge_user xdotool windowactivate --sync "${wid}"
as_bridge_user xdotool mousemove --window "${wid}" \
"${SEND_CLICK_X}" "${SEND_CLICK_Y}" click 1
rm -f -- "${PENDING_TOKEN}" "${PENDING_TEXT}"
printf '{"ok":true,"state":"sent"}\n'
}
clear_command() {
local wid
wid=$(window_id)
ensure_maximized_window "${wid}"
as_bridge_user xdotool windowactivate --sync "${wid}"
as_bridge_user xdotool mousemove --window "${wid}" \
"${DRAFT_CLICK_X}" "${DRAFT_CLICK_Y}" click 1
as_bridge_user xdotool key --clearmodifiers ctrl+a BackSpace
rm -f -- "${PENDING_TOKEN}" "${PENDING_TEXT}"
printf '{"ok":true,"state":"cleared"}\n'
}
usage() {
printf 'usage: %s {status|capture|ocr|ocr-text|draft TEXT|send TOKEN|clear}\n' "$0" >&2
exit 64
}
require_root
case ${1:-} in
status) status_command ;;
capture) ensure_state_dir; capture_window ;;
ocr) ocr_command ;;
ocr-text) ocr_text_command ;;
draft) shift; draft_command "$*" ;;
send) shift; send_command "${1:-}" ;;
clear) clear_command ;;
*) usage ;;
esac