From b5dd09bb7e5ff20c42ba926320a7d2ab787316b5 Mon Sep 17 00:00:00 2001 From: "Grok Bot (local)" Date: Sun, 18 Oct 2026 23:32:17 +0800 Subject: [PATCH 1/7] Migrate to plain Selkies 2.0 base and replace WeFlow with Alpha 0.2.0 --- .dockerignore | 4 +- .env.example | 9 +- .github/workflows/image.yml | 6 +- AGENTS.md | 21 +-- Dockerfile | 73 +++++---- THIRD_PARTY_NOTICES.md | 22 +-- compose.yaml | 10 +- licenses/WeFlow.CC-BY-NC-SA-4.0.txt | 141 ------------------ licenses/wechat-selkies.MIT.txt | 21 --- root/defaults/alpha-api-proxy.conf | 20 +++ root/defaults/autostart | 4 +- root/defaults/menu.xml | 2 +- .../rules.d/49-weflow-container.rules | 6 - .../dependencies.d/init-selkies-config | 0 .../s6-rc.d/init-weharbor-profile/run | 39 +++++ .../s6-rc.d/init-weharbor-profile/type | 1 + .../s6-rc.d/init-weharbor-profile/up | 1 + .../dependencies.d/init-weharbor-profile | 0 .../s6-rc.d/svc-de/dependencies.d/svc-polkitd | 1 - .../dependencies.d/init-weharbor-profile | 0 .../svc-polkitd/dependencies.d/svc-dbus | 1 - root/etc/s6-overlay/s6-rc.d/svc-polkitd/run | 7 - root/etc/s6-overlay/s6-rc.d/svc-polkitd/type | 1 - .../user/contents.d/init-weharbor-profile | 0 .../s6-rc.d/user/contents.d/svc-polkitd | 1 - .../alpha-autostart.sh} | 12 +- root/scripts/alpha/alpha-start.sh | 13 ++ root/scripts/alpha/keyring-start.sh | 33 ++++ root/scripts/app-switcher/app-switcher.py | 22 +-- root/scripts/notifications/app_control.py | 5 +- .../browser-notification-bridge.py | 106 ++++++------- .../notifications/notification_config.py | 4 +- root/scripts/notifications/window_control.py | 11 +- root/scripts/wechat/wechat-start.sh | 7 + root/scripts/weflow/weflow-start.sh | 12 -- root/scripts/weharbor/desktop-start.sh | 23 +++ root/scripts/weharbor/healthcheck.py | 25 ++-- root/scripts/weharbor/install-web.py | 9 +- root/scripts/weharbor/window-layout.py | 18 +-- root/usr/share/weharbor/pwa/weharbor-apps.js | 14 +- root/usr/share/weharbor/pwa/weharbor-sw.js | 2 +- .../share/weharbor/pwa/weharbor.webmanifest | 2 +- scripts/check.py | 4 +- scripts/prepare-assets.py | 2 +- scripts/smoke-test.sh | 19 +-- scripts/verify-assets.py | 48 ++---- skills/{weflow-api => alpha-api}/LICENSE | 0 skills/{weflow-api => alpha-api}/SKILL.md | 44 +++--- skills/alpha-api/agents/openai.yaml | 4 + .../references/http-api.md | 12 +- .../scripts/alpha_api.py} | 22 +-- skills/weflow-api/agents/openai.yaml | 4 - tests/test_app_control.py | 6 +- tests/test_assets_and_init.py | 14 +- tests/test_healthcheck.py | 4 +- tests/test_skill_client.py | 18 +-- tests/test_window_control.py | 8 +- tests/test_window_layout.py | 14 +- versions.lock.json | 19 ++- 59 files changed, 447 insertions(+), 504 deletions(-) delete mode 100644 licenses/WeFlow.CC-BY-NC-SA-4.0.txt delete mode 100644 licenses/wechat-selkies.MIT.txt create mode 100644 root/defaults/alpha-api-proxy.conf delete mode 100644 root/etc/polkit-1/rules.d/49-weflow-container.rules create mode 100644 root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/dependencies.d/init-selkies-config create mode 100644 root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/run create mode 100644 root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/type create mode 100644 root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/up create mode 100644 root/etc/s6-overlay/s6-rc.d/svc-de/dependencies.d/init-weharbor-profile delete mode 100644 root/etc/s6-overlay/s6-rc.d/svc-de/dependencies.d/svc-polkitd create mode 100644 root/etc/s6-overlay/s6-rc.d/svc-nginx/dependencies.d/init-weharbor-profile delete mode 100644 root/etc/s6-overlay/s6-rc.d/svc-polkitd/dependencies.d/svc-dbus delete mode 100644 root/etc/s6-overlay/s6-rc.d/svc-polkitd/run delete mode 100644 root/etc/s6-overlay/s6-rc.d/svc-polkitd/type create mode 100644 root/etc/s6-overlay/s6-rc.d/user/contents.d/init-weharbor-profile delete mode 100644 root/etc/s6-overlay/s6-rc.d/user/contents.d/svc-polkitd rename root/scripts/{weflow/weflow-autostart.sh => alpha/alpha-autostart.sh} (55%) create mode 100644 root/scripts/alpha/alpha-start.sh create mode 100644 root/scripts/alpha/keyring-start.sh create mode 100644 root/scripts/wechat/wechat-start.sh delete mode 100644 root/scripts/weflow/weflow-start.sh create mode 100644 root/scripts/weharbor/desktop-start.sh rename skills/{weflow-api => alpha-api}/LICENSE (100%) rename skills/{weflow-api => alpha-api}/SKILL.md (65%) create mode 100644 skills/alpha-api/agents/openai.yaml rename skills/{weflow-api => alpha-api}/references/http-api.md (83%) rename skills/{weflow-api/scripts/weflow_api.py => alpha-api/scripts/alpha_api.py} (93%) delete mode 100644 skills/weflow-api/agents/openai.yaml diff --git a/.dockerignore b/.dockerignore index 00b481c..f852460 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,8 +3,6 @@ !versions.lock.json !LICENSE !THIRD_PARTY_NOTICES.md -!licenses/ -!licenses/** !scripts/ scripts/* !scripts/verify-assets.py @@ -12,5 +10,5 @@ scripts/* !root/** !downloads/ downloads/* -!downloads/weflow.tar.gz +!downloads/alpha.deb !downloads/wechat.deb diff --git a/.env.example b/.env.example index f95a5ef..89f4d4a 100644 --- a/.env.example +++ b/.env.example @@ -13,17 +13,12 @@ BIND_ADDRESS=127.0.0.1 HTTP_PORT=3000 HTTPS_PORT=3001 API_BIND_ADDRESS=127.0.0.1 -WEFLOW_API_PORT=5031 +API_PORT=5031 AUTO_START_WECHAT=true -AUTO_START_WEFLOW=true +AUTO_START_ALPHA=true ENABLE_APP_SWITCHER=true APP_SWITCHER_MODE=web ENABLE_WINDOW_DEFAULTS=true ENABLE_BROWSER_NOTIFICATIONS=true SELKIES_ENABLE_BINARY_CLIPBOARD=true -ENABLE_WECHAT_AUTO_LOGIN=true -AUTO_LOGIN_DELAY=3 -ENABLE_WECHAT_NIGHTLY_RESTART=false -WECHAT_NIGHTLY_STOP_TIME=23:30 -WECHAT_NIGHTLY_START_TIME=01:30 diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index a440e44..eac952f 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -7,8 +7,8 @@ on: branches: [main] workflow_dispatch: inputs: - weflow_download_url: - description: Trusted HTTPS URL for the original WeFlow archive + alpha_download_url: + description: Trusted HTTPS URL for the original Alpha deb type: string required: false publish: @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 env: - WEFLOW_DOWNLOAD_URL: ${{ inputs.weflow_download_url || vars.WEFLOW_DOWNLOAD_URL }} + ALPHA_DOWNLOAD_URL: ${{ inputs.alpha_download_url || vars.ALPHA_DOWNLOAD_URL }} WECHAT_DOWNLOAD_URL: ${{ vars.WECHAT_DOWNLOAD_URL }} PROJECT_SOURCE: ${{ github.server_url }}/${{ github.repository }} steps: diff --git a/AGENTS.md b/AGENTS.md index efa783a..8cc1358 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,18 +1,21 @@ # WeHarbor contributor notes -- Keep upstream WeChat and WeFlow application files unmodified. -- Reuse the pinned wechat-selkies minimal image; keep only our integration - files under root/. Avoid copying the complete upstream repository. +- Keep upstream WeChat and Alpha application files unmodified (install the + pinned debs with dpkg/apt; never patch /opt/wechat or /opt/Alpha). +- Build on the pinned plain LinuxServer.io Selkies base (debiantrixie digest + in versions.lock.json). Keep only our integration files under root/. - versions.lock.json is the source of component versions and checksums. -- Only linux/amd64 is supported by the current WeFlow archive. +- Only linux/amd64 is supported (Alpha and WeChat are pinned amd64 debs). - All runtime state belongs under /config; never check data/, .env, package archives, credentials, or user databases into Git. -- Preserve browser authentication and the WeFlow API's own token authentication. -- Changes to initialization must preserve existing profiles and user settings. +- Preserve browser authentication and Alpha's own bearer-token API auth. + Alpha binds 127.0.0.1 only; nginx republishes it on ALPHA_API_PROXY_PORT + (5032) and rejects browser Origin headers. +- Changes to initialization must preserve existing profiles and user settings + (see init-weharbor-profile for the legacy WeFlow/wechat-selkies migration). - Run python3 scripts/check.py; for image changes, build and run scripts/smoke-test.sh IMAGE with an empty, temporary profile. -- Keep the public WeFlow skill in skills/weflow-api self-contained and portable. - Credentials come from the user's environment or local token file. Verify - client behavior and installation when changing the skill or its scripts. +- Keep the public skill in skills/alpha-api self-contained and portable. + Credentials come from the user's environment or local token file. - MIT covers this repository's integration code. Vendor components retain their own licenses; see THIRD_PARTY_NOTICES.md. diff --git a/Dockerfile b/Dockerfile index 74507f3..71f4a12 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,6 @@ # syntax=docker/dockerfile:1 -ARG BASE_IMAGE=ghcr.io/nickrunning/wechat-selkies:0.0.14-minimal@sha256:32b9094316b0bd267dc7d1778116a91a6a349fce20bb8dd652dbb9b38e0ef6ee -FROM ${BASE_IMAGE} AS weflow-extracted -ARG TARGETARCH -COPY versions.lock.json /tmp/versions.lock.json -COPY scripts/verify-assets.py /tmp/verify-assets.py -COPY downloads/weflow.tar.gz /tmp/weflow.tar.gz -RUN test "${TARGETARCH:-amd64}" = amd64 \ - && python3 /tmp/verify-assets.py weflow \ - --lock /tmp/versions.lock.json --asset /tmp/weflow.tar.gz \ - --extract /opt/weflow - +# Plain LinuxServer.io Selkies base (Debian trixie, Selkies 2.0.0, X11/Openbox). +ARG BASE_IMAGE=ghcr.io/linuxserver/baseimage-selkies:debiantrixie@sha256:5b3f10459933740554081efbca9c8d5e006faf5116b54ab40b7bfad7c8111849 FROM ${BASE_IMAGE} AS integration-files COPY root/ /integration/ # A checkout copied from a private home can inherit restrictive directory modes. @@ -18,62 +9,78 @@ RUN find /integration -type d -exec chmod 0755 {} + \ && find /integration -type f -exec chmod 0644 {} + FROM ${BASE_IMAGE} +ARG TARGETARCH ARG PROJECT_VERSION=0.1.0 ARG PROJECT_SOURCE ARG PROJECT_REVISION ARG APT_MIRROR LABEL org.opencontainers.image.title="WeHarbor" \ - org.opencontainers.image.description="Self-hosted WeChat desktop, WeFlow APIs and message notifications in one container" \ + org.opencontainers.image.description="Self-hosted WeChat desktop, Alpha data APIs and message notifications in one container" \ org.opencontainers.image.version="${PROJECT_VERSION}" \ org.opencontainers.image.source="${PROJECT_SOURCE}" \ org.opencontainers.image.revision="${PROJECT_REVISION}" -RUN if [ -n "${APT_MIRROR}" ]; then \ - sed -i "s#http://archive.ubuntu.com/ubuntu/#${APT_MIRROR%/}/#g" /etc/apt/sources.list; \ +# Desktop integration packages that wechat-selkies used to provide, plus the +# Secret Service Alpha needs for Electron safeStorage. +RUN test "${TARGETARCH:-amd64}" = amd64 \ + && sed -i -e '/^deb-src /d' \ + -e 's#http://security.debian.org/debian-security/*#http://deb.debian.org/debian-security#' \ + /etc/apt/sources.list \ + && if [ -n "${APT_MIRROR}" ]; then \ + sed -i "s#http://deb.debian.org/#${APT_MIRROR%/}/#g" /etc/apt/sources.list; \ fi \ - && apt-get update -o Dir::Etc::sourcelist="sources.list" -o Dir::Etc::sourceparts="-" \ - && apt-get install -y --no-install-recommends pkexec \ + && echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-weharbor-retries \ + && apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + dbus-x11 gnome-keyring libsecret-1-0 inotify-tools stalonetray \ + python3-dbus python3-xlib \ + libatomic1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 \ + libxcb-render-util0 libxcb-shape0 libxcb-xinerama0 libxcb-xkb1 \ + libxkbcommon-x11-0 libnss3 libgbm1 libasound2t64 \ && rm -rf /var/lib/apt/lists/* COPY versions.lock.json /usr/share/weharbor/versions.lock.json COPY scripts/verify-assets.py /tmp/verify-assets.py COPY downloads/wechat.deb /tmp/wechat.deb -RUN python3 /tmp/verify-assets.py wechat \ +COPY downloads/alpha.deb /tmp/alpha.deb +RUN apt-get update \ + && python3 /tmp/verify-assets.py wechat \ --lock /usr/share/weharbor/versions.lock.json --asset /tmp/wechat.deb --install \ - && rm /tmp/wechat.deb /tmp/verify-assets.py - -COPY --from=weflow-extracted /opt/weflow /opt/weflow -RUN chmod -R a+rX /opt/weflow \ - && ln -s /opt/weflow/weflow /usr/local/bin/weflow \ - && cp /opt/weflow/resources/icon.png /usr/share/pixmaps/weflow.png + && python3 /tmp/verify-assets.py alpha \ + --lock /usr/share/weharbor/versions.lock.json --asset /tmp/alpha.deb --install \ + && rm -rf /var/lib/apt/lists/* /tmp/wechat.deb /tmp/alpha.deb /tmp/verify-assets.py \ + && icon="$(find /usr/share/icons/hicolor -name alpha.png | sort -V | tail -1)" \ + && cp "$icon" /usr/share/pixmaps/alpha.png \ + && cp /usr/share/icons/hicolor/128x128/apps/wechat.png /usr/share/selkies/www/icon.png COPY --from=integration-files /integration/ / COPY LICENSE THIRD_PARTY_NOTICES.md /usr/share/doc/weharbor/ -COPY licenses/ /usr/share/doc/weharbor/licenses/ RUN test -f /defaults/default.conf \ && chmod -R a+rX /usr/share/doc/weharbor \ - && sed -i '\|location SUBFOLDERfiles {|i\ include /defaults/notification-proxy.conf;' /defaults/default.conf \ + && sed -i '\|location SUBFOLDERapi {|i\ include /defaults/notification-proxy.conf;' /defaults/default.conf \ + && grep -q notification-proxy.conf /defaults/default.conf \ + && cat /defaults/alpha-api-proxy.conf >> /defaults/default.conf \ && sed -i 's###' \ /usr/share/selkies/selkies-dashboard/index.html \ - && if [ -d /usr/share/selkies/web ]; then \ - cp /usr/share/selkies/selkies-dashboard/browser-notifications.js \ - /usr/share/selkies/web/browser-notifications.js; \ - fi \ && python3 /scripts/weharbor/install-web.py \ + && sed -i '//,/<\/dock>/s/no<\/noStrut>/yes<\/noStrut>/' /etc/xdg/openbox/rc.xml \ && ln -sfn /scripts/weharbor/wechat-launch.py /usr/bin/wechat \ && chmod 0755 \ /etc/s6-overlay/s6-rc.d/svc-app-switcher/run \ - /etc/s6-overlay/s6-rc.d/svc-polkitd/run \ + /etc/s6-overlay/s6-rc.d/init-weharbor-profile/run \ /scripts/app-switcher/app-switcher.py \ /scripts/notifications/*.sh /scripts/notifications/*.py \ - /scripts/weflow/*.sh /scripts/weharbor/* \ + /scripts/alpha/*.sh /scripts/wechat/*.sh /scripts/weharbor/* \ /usr/local/bin/stalonetray /usr/local/bin/weharbor-health ENV TITLE="WeHarbor|微港" \ SELKIES_UI_TITLE="WeHarbor|微港" \ + TZ="Asia/Shanghai" \ + LC_ALL="zh_CN.UTF-8" \ AUTO_START_WECHAT="true" \ - AUTO_START_QQ="false" \ - AUTO_START_WEFLOW="true" \ + AUTO_START_ALPHA="true" \ + ALPHA_API_PORT="5031" \ + ALPHA_API_PROXY_PORT="5032" \ ENABLE_APP_SWITCHER="true" \ APP_SWITCHER_MODE="web" \ ENABLE_WINDOW_DEFAULTS="true" \ diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 82c23b9..726609b 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -5,20 +5,14 @@ It does not relicense vendor applications, base-image packages or native librari | Component | Source / origin | License and handling | | --- | --- | --- | -| wechat-selkies | https://github.com/nickrunning/wechat-selkies | MIT; pinned minimal base image. The original notice is in licenses/wechat-selkies.MIT.txt. | -| LinuxServer.io / Selkies | Dependencies provided by the pinned base image | Their upstream licenses and installed package notices remain applicable. | -| Official Linux WeChat | https://linux.weixin.qq.com/ | Proprietary vendor distribution. The original deb is mirrored in the component-assets Release with its pinned SHA256; its application and package files remain unchanged. Verify redistribution terms before publishing a combined image. | -| WeFlow | https://github.com/hicccc77/WeFlow | The original 6.3.2 archive is mirrored in the component-assets Release with its pinned SHA256. Published source carries CC BY-NC-SA 4.0. A reference license is retained in licenses/WeFlow.CC-BY-NC-SA-4.0.txt. Exact 6.3.2 binary/native redistribution terms remain unverified. | -| Electron / Chromium / WeFlow native libraries | Included in the original WeFlow archive | Original files and embedded notices are retained unchanged under /opt/weflow. | +| LinuxServer.io baseimage-selkies | https://github.com/linuxserver/docker-baseimage-selkies | Pinned `debiantrixie` image digest. GPL-3.0 (LinuxServer.io); bundled Debian packages keep their own notices. | +| Selkies 2.0.0 | https://github.com/selkies-project/selkies | Provided by the base image. Mozilla Public License 2.0. | +| Official Linux WeChat | https://linux.weixin.qq.com/ | Proprietary vendor distribution. The original deb is mirrored in the component-assets Release with its pinned SHA256; it is installed unmodified with dpkg. Verify redistribution terms before publishing a combined image. | +| Alpha 0.2.0 | https://github.com/xunchahaha/alpha-releases | Closed-source, unsigned binary release; the deb control file declares `License: unknown`. Installed unmodified from the pinned SHA256. Redistribution terms are unverified: do not publish a combined image until they are clarified. Bundled Electron/Chromium/ffmpeg notices remain under /opt/Alpha. | -The WeFlow reference license was retained from a previously provided upstream -distribution. It documents the published source license and is not evidence that -every component of the 6.3.2 binary has the same license. - -WeHarbor does not unpack/rebuild app.asar, patch database libraries or claim -ownership of upstream applications. The ASAR checksum is verified before packaging. -Image notices are installed at /usr/share/doc/weharbor; original application -notices remain in their vendor locations. +WeHarbor does not unpack, patch or rebuild vendor packages. Image notices are +installed at /usr/share/doc/weharbor; original application notices remain in +their vendor locations. WeHarbor is an independent community integration. Its name does not imply -endorsement by Tencent, WeFlow, LinuxServer.io or the Selkies maintainers. +endorsement by Tencent, the Alpha authors, LinuxServer.io or the Selkies maintainers. diff --git a/compose.yaml b/compose.yaml index fb46eda..f95ad16 100644 --- a/compose.yaml +++ b/compose.yaml @@ -10,7 +10,8 @@ services: ports: - "${BIND_ADDRESS:-127.0.0.1}:${HTTP_PORT:-3000}:3000" - "${BIND_ADDRESS:-127.0.0.1}:${HTTPS_PORT:-3001}:3001" - - "${API_BIND_ADDRESS:-127.0.0.1}:${WEFLOW_API_PORT:-5031}:${WEFLOW_API_PORT:-5031}" + # Alpha binds 127.0.0.1:5031 inside the container; nginx republishes it on 5032. + - "${API_BIND_ADDRESS:-127.0.0.1}:${API_PORT:-5031}:5032" cap_add: - SYS_PTRACE environment: @@ -21,15 +22,10 @@ services: CUSTOM_USER: ${CUSTOM_USER:-weharbor} PASSWORD: ${PASSWORD:?Run scripts/init.sh or set PASSWORD in .env} AUTO_START_WECHAT: ${AUTO_START_WECHAT:-true} - AUTO_START_WEFLOW: ${AUTO_START_WEFLOW:-true} + AUTO_START_ALPHA: ${AUTO_START_ALPHA:-true} AUTO_START_QQ: "false" ENABLE_APP_SWITCHER: ${ENABLE_APP_SWITCHER:-true} APP_SWITCHER_MODE: ${APP_SWITCHER_MODE:-web} ENABLE_WINDOW_DEFAULTS: ${ENABLE_WINDOW_DEFAULTS:-true} ENABLE_BROWSER_NOTIFICATIONS: ${ENABLE_BROWSER_NOTIFICATIONS:-true} SELKIES_ENABLE_BINARY_CLIPBOARD: ${SELKIES_ENABLE_BINARY_CLIPBOARD:-true} - ENABLE_WECHAT_AUTO_LOGIN: ${ENABLE_WECHAT_AUTO_LOGIN:-true} - AUTO_LOGIN_DELAY: ${AUTO_LOGIN_DELAY:-3} - ENABLE_WECHAT_NIGHTLY_RESTART: ${ENABLE_WECHAT_NIGHTLY_RESTART:-false} - WECHAT_NIGHTLY_STOP_TIME: ${WECHAT_NIGHTLY_STOP_TIME:-23:30} - WECHAT_NIGHTLY_START_TIME: ${WECHAT_NIGHTLY_START_TIME:-01:30} diff --git a/licenses/WeFlow.CC-BY-NC-SA-4.0.txt b/licenses/WeFlow.CC-BY-NC-SA-4.0.txt deleted file mode 100644 index c956c6d..0000000 --- a/licenses/WeFlow.CC-BY-NC-SA-4.0.txt +++ /dev/null @@ -1,141 +0,0 @@ -Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International - -By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. - -Section 1 – Definitions. - -a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. - -b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. - -c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. - -d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. - -e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. - -f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. - -g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. - -h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. - -i. NonCommercial means not intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. - -j. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. - -k. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. - -l. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. - -Section 2 – Scope. - -a. License grant. - -1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: -A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and -B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. - -2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. - -3. Term. The term of this Public License is specified in Section 6(a). - -4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. - -5. Downstream recipients. -A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. -B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter's License You apply. -C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. - -6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). - -b. Other rights. - -1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. - -2. Patent and trademark rights are not licensed under this Public License. - -3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used for other than NonCommercial purposes. - -Section 3 – License Conditions. - -Your exercise of the Licensed Rights is expressly made subject to the following conditions. - -a. Attribution. - -1. If You Share the Licensed Material (including in modified form), You must: -A. retain the following if it is supplied by the Licensor with the Licensed Material: -i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); -ii. a copyright notice; -iii. a notice that refers to this Public License; -iv. a notice that refers to the disclaimer of warranties; -v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; -B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and -C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. - -2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. - -3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. - -4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. - -b. ShareAlike. - -In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. - -1. The Adapter's License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. - -2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. - -3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. - -Section 4 – Sui Generis Database Rights. - -Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: - -a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; - -b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and - -c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. - -For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights and Sui Generis Database Rights apply to Your use of the Licensed Material. - -Section 5 – Disclaimer of Warranties and Limitation of Liability. - -a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. - -b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. - -c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. - -Section 6 – Term and Termination. - -a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. - -b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: - -1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or -2. upon express reinstatement by the Licensor. - -For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. - -c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. - -d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. - -Section 7 – Other Terms and Conditions. - -a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. - -b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. - -Section 8 – Interpretation. - -a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. - -b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. - -c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. - -d. Nothing in this Public License constitutes or may be interpreted as a limitation upon or waiver of any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. \ No newline at end of file diff --git a/licenses/wechat-selkies.MIT.txt b/licenses/wechat-selkies.MIT.txt deleted file mode 100644 index aa9fb03..0000000 --- a/licenses/wechat-selkies.MIT.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Nick007 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/root/defaults/alpha-api-proxy.conf b/root/defaults/alpha-api-proxy.conf new file mode 100644 index 0000000..bbba00d --- /dev/null +++ b/root/defaults/alpha-api-proxy.conf @@ -0,0 +1,20 @@ +server { + listen ALPHA_API_PROXY_PORT; + listen [::]:ALPHA_API_PROXY_PORT; + location / { + if ($http_origin) { + return 403; + } + proxy_pass http://127.0.0.1:ALPHA_API_PORT; + proxy_set_header Host 127.0.0.1:ALPHA_API_PORT; + proxy_set_header Connection ""; + proxy_http_version 1.1; + proxy_buffering off; + proxy_cache off; + proxy_request_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 5s; + client_max_body_size 16m; + } +} diff --git a/root/defaults/autostart b/root/defaults/autostart index bc535d6..5ccea82 100644 --- a/root/defaults/autostart +++ b/root/defaults/autostart @@ -1,3 +1,3 @@ /scripts/notifications/notification-autostart.sh -/scripts/start.sh & -/scripts/weflow/weflow-autostart.sh +/scripts/weharbor/desktop-start.sh & +/scripts/alpha/alpha-autostart.sh diff --git a/root/defaults/menu.xml b/root/defaults/menu.xml index 16b1db8..13079e0 100644 --- a/root/defaults/menu.xml +++ b/root/defaults/menu.xml @@ -2,7 +2,7 @@ /scripts/wechat/wechat-start.sh -/scripts/weflow/weflow-start.sh +/scripts/alpha/alpha-start.sh /usr/bin/xterm diff --git a/root/etc/polkit-1/rules.d/49-weflow-container.rules b/root/etc/polkit-1/rules.d/49-weflow-container.rules deleted file mode 100644 index e99f2b8..0000000 --- a/root/etc/polkit-1/rules.d/49-weflow-container.rules +++ /dev/null @@ -1,6 +0,0 @@ -polkit.addRule(function(action, subject) { - if (action.id == "org.freedesktop.policykit.exec" && - subject.user == "abc") { - return polkit.Result.YES; - } -}); diff --git a/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/dependencies.d/init-selkies-config b/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/dependencies.d/init-selkies-config new file mode 100644 index 0000000..e69de29 diff --git a/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/run b/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/run new file mode 100644 index 0000000..65b74a3 --- /dev/null +++ b/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/run @@ -0,0 +1,39 @@ +#!/usr/bin/with-contenv bash +# Migrate desktop profiles created by the former wechat-selkies/WeFlow image. +# init-selkies-config copies /defaults only when a file is missing, so legacy +# autostart and menu entries would otherwise keep calling removed scripts. +conf="${HOME:-/config}/.config/openbox" +stamp="$(date +%Y%m%d%H%M%S)" +for name in autostart menu.xml; do + file="${conf}/${name}" + if [ -f "${file}" ] && grep -qE '/scripts/(start\.sh|weflow/)' "${file}"; then + cp -p "${file}" "${file}.legacy-${stamp}" + cp "/defaults/${name}" "${file}" + chown abc:abc "${file}" "${file}.legacy-${stamp}" + echo "[weharbor] migrated legacy ${file}" + fi +done +# Settings moved out of the WeFlow-specific directory. +old="${HOME:-/config}/.config/weflow" +new="${HOME:-/config}/.config/weharbor" +if [ -d "${old}" ]; then + mkdir -p "${new}" + for name in browser-notifications.json app-switcher-position app-switcher-settings.json; do + if [ -f "${old}/${name}" ] && [ ! -e "${new}/${name}" ]; then + cp -p "${old}/${name}" "${new}/${name}" + fi + done + chown -R abc:abc "${new}" +fi + +# Alpha's HTTP API only binds 127.0.0.1 and rejects foreign Host/Origin +# headers. nginx re-publishes it on ALPHA_API_PROXY_PORT; Alpha's own bearer +# token still authenticates every request except /health. +api_port="${ALPHA_API_PORT:-5031}" +proxy_port="${ALPHA_API_PROXY_PORT:-5032}" +if ! [[ "${api_port}" =~ ^[0-9]+$ && "${proxy_port}" =~ ^[0-9]+$ ]] || [ "${api_port}" = "${proxy_port}" ]; then + echo "[weharbor] invalid ALPHA_API_PORT/ALPHA_API_PROXY_PORT" >&2 + exit 1 +fi +sed -i -e "s/ALPHA_API_PROXY_PORT/${proxy_port}/g" -e "s/ALPHA_API_PORT/${api_port}/g" \ + /etc/nginx/sites-available/default diff --git a/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/type b/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/type new file mode 100644 index 0000000..bdd22a1 --- /dev/null +++ b/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/type @@ -0,0 +1 @@ +oneshot diff --git a/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/up b/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/up new file mode 100644 index 0000000..45cdb8b --- /dev/null +++ b/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-weharbor-profile/run diff --git a/root/etc/s6-overlay/s6-rc.d/svc-de/dependencies.d/init-weharbor-profile b/root/etc/s6-overlay/s6-rc.d/svc-de/dependencies.d/init-weharbor-profile new file mode 100644 index 0000000..e69de29 diff --git a/root/etc/s6-overlay/s6-rc.d/svc-de/dependencies.d/svc-polkitd b/root/etc/s6-overlay/s6-rc.d/svc-de/dependencies.d/svc-polkitd deleted file mode 100644 index 8b13789..0000000 --- a/root/etc/s6-overlay/s6-rc.d/svc-de/dependencies.d/svc-polkitd +++ /dev/null @@ -1 +0,0 @@ - diff --git a/root/etc/s6-overlay/s6-rc.d/svc-nginx/dependencies.d/init-weharbor-profile b/root/etc/s6-overlay/s6-rc.d/svc-nginx/dependencies.d/init-weharbor-profile new file mode 100644 index 0000000..e69de29 diff --git a/root/etc/s6-overlay/s6-rc.d/svc-polkitd/dependencies.d/svc-dbus b/root/etc/s6-overlay/s6-rc.d/svc-polkitd/dependencies.d/svc-dbus deleted file mode 100644 index 8b13789..0000000 --- a/root/etc/s6-overlay/s6-rc.d/svc-polkitd/dependencies.d/svc-dbus +++ /dev/null @@ -1 +0,0 @@ - diff --git a/root/etc/s6-overlay/s6-rc.d/svc-polkitd/run b/root/etc/s6-overlay/s6-rc.d/svc-polkitd/run deleted file mode 100644 index d372325..0000000 --- a/root/etc/s6-overlay/s6-rc.d/svc-polkitd/run +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/with-contenv bash - -until [ -S /run/dbus/system_bus_socket ]; do - sleep 0.25 -done - -exec s6-setuidgid polkitd /usr/lib/polkit-1/polkitd --no-debug diff --git a/root/etc/s6-overlay/s6-rc.d/svc-polkitd/type b/root/etc/s6-overlay/s6-rc.d/svc-polkitd/type deleted file mode 100644 index 5883cff..0000000 --- a/root/etc/s6-overlay/s6-rc.d/svc-polkitd/type +++ /dev/null @@ -1 +0,0 @@ -longrun diff --git a/root/etc/s6-overlay/s6-rc.d/user/contents.d/init-weharbor-profile b/root/etc/s6-overlay/s6-rc.d/user/contents.d/init-weharbor-profile new file mode 100644 index 0000000..e69de29 diff --git a/root/etc/s6-overlay/s6-rc.d/user/contents.d/svc-polkitd b/root/etc/s6-overlay/s6-rc.d/user/contents.d/svc-polkitd deleted file mode 100644 index 8b13789..0000000 --- a/root/etc/s6-overlay/s6-rc.d/user/contents.d/svc-polkitd +++ /dev/null @@ -1 +0,0 @@ - diff --git a/root/scripts/weflow/weflow-autostart.sh b/root/scripts/alpha/alpha-autostart.sh similarity index 55% rename from root/scripts/weflow/weflow-autostart.sh rename to root/scripts/alpha/alpha-autostart.sh index ab29a10..bcefa82 100644 --- a/root/scripts/weflow/weflow-autostart.sh +++ b/root/scripts/alpha/alpha-autostart.sh @@ -3,19 +3,19 @@ /scripts/notifications/notification-autostart.sh /scripts/weharbor/window-autostart.sh -if [ "${AUTO_START_WEFLOW:-true}" = "true" ]; then - # WeChat in this image stores its profile at /config/xwechat_files, while - # WeFlow's Linux auto-detection checks ~/Documents/xwechat_files. Keep a - # compatibility link without replacing an existing user-managed path. +if [ "${AUTO_START_ALPHA:-true}" = "true" ]; then + # WeChat stores its profile at /config/xwechat_files; Alpha's Linux + # discovery also checks ~/Documents/xwechat_files. Keep a compatibility + # link without replacing an existing user-managed path. mkdir -p /config/Documents if [ ! -e /config/Documents/xwechat_files ] \ && [ ! -L /config/Documents/xwechat_files ]; then ln -s ../xwechat_files /config/Documents/xwechat_files fi - # Let Openbox and WeChat create their first windows before opening WeFlow. + # Let Openbox and WeChat create their first windows before opening Alpha. ( sleep 3 - /scripts/weflow/weflow-start.sh + /scripts/alpha/alpha-start.sh ) >/dev/null 2>&1 & fi diff --git a/root/scripts/alpha/alpha-start.sh b/root/scripts/alpha/alpha-start.sh new file mode 100644 index 0000000..4ca6697 --- /dev/null +++ b/root/scripts/alpha/alpha-start.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Start Alpha once. Alpha keeps its HTTP API token in Electron safeStorage, +# which on Linux needs a Secret Service; plain-text storage is refused. +unset APPDIR APPIMAGE ELECTRON_RUN_AS_NODE ELECTRON_NO_ATTACH_CONSOLE + +if pgrep -f '^/opt/Alpha/alpha( |$)' >/dev/null 2>&1; then + exit 0 +fi + +/scripts/alpha/keyring-start.sh || echo "WeHarbor: Secret Service unavailable" >&2 +mkdir -p /config/.local/log +nohup /opt/Alpha/alpha --no-sandbox --password-store=gnome-libsecret "$@" \ + >>/config/.local/log/alpha.log 2>&1 & diff --git a/root/scripts/alpha/keyring-start.sh b/root/scripts/alpha/keyring-start.sh new file mode 100644 index 0000000..bfc9a60 --- /dev/null +++ b/root/scripts/alpha/keyring-start.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Provide an unlocked Secret Service (gnome-keyring) on the desktop session bus. +# The collection lives unencrypted in /config/.local/share/keyrings: secrets +# such as Alpha's API token are protected by the volume boundary only. +if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then + exit 1 +fi +if dbus-send --session --print-reply --dest=org.freedesktop.DBus / \ + org.freedesktop.DBus.NameHasOwner string:org.freedesktop.secrets 2>/dev/null \ + | grep -q 'boolean true'; then + exit 0 +fi +keyrings="${HOME}/.local/share/keyrings" +mkdir -p "${keyrings}" +chmod 700 "${HOME}/.local/share/keyrings" +# Without a default collection libsecret pops up a "new keyring password" +# dialog. Seed an unencrypted (plaintext-format) collection once instead. +if [ -z "$(find "${keyrings}" -maxdepth 1 -name '*.keyring' -print -quit)" ]; then + umask 077 + printf '[keyring]\ndisplay-name=weharbor\nctime=0\nmtime=0\nlock-on-idle=false\nlock-after=false\n' \ + >"${keyrings}/weharbor.keyring" + printf 'weharbor' >"${keyrings}/default" +fi +gnome-keyring-daemon --replace --daemonize --components=secrets >/dev/null 2>&1 +for _ in $(seq 1 20); do + if dbus-send --session --print-reply --dest=org.freedesktop.DBus / \ + org.freedesktop.DBus.NameHasOwner string:org.freedesktop.secrets 2>/dev/null \ + | grep -q 'boolean true'; then + exit 0 + fi + sleep 0.25 +done +exit 1 diff --git a/root/scripts/app-switcher/app-switcher.py b/root/scripts/app-switcher/app-switcher.py index 03b7536..43b2be6 100644 --- a/root/scripts/app-switcher/app-switcher.py +++ b/root/scripts/app-switcher/app-switcher.py @@ -1,5 +1,5 @@ #!/lsiopy/bin/python3 -"""Tiny X11 task switcher for the WeChat + WeFlow Selkies desktop. +"""Tiny X11 task switcher for the WeChat + Alpha Selkies desktop. This intentionally uses only python-xlib, which is already present in the upstream image. It is a draggable floating window, not a desktop panel. @@ -42,9 +42,9 @@ PANEL_OPACITY = 0.92 POLL_SECONDS = 0.5 RAISE_INTERVAL_SECONDS = 1.0 -POSITION_FILE = "/config/.config/weflow/app-switcher-position" +POSITION_FILE = "/config/.config/weharbor/app-switcher-position" SWITCHER_SETTINGS_FILE = ( - "/config/.config/weflow/app-switcher-settings.json" + "/config/.config/weharbor/app-switcher-settings.json" ) APPS = ( @@ -57,12 +57,12 @@ "command": ["/scripts/wechat/wechat-start.sh"], }, { - "label": "WeFlow", - "classes": {"weflow"}, - "primary_classes": {"weflow"}, - "process_names": {"weflow"}, - "executable_prefixes": ("/opt/weflow/",), - "command": ["/scripts/weflow/weflow-start.sh"], + "label": "Alpha", + "classes": {"alpha-desktop", "alpha"}, + "primary_classes": {"alpha-desktop"}, + "process_names": {"alpha"}, + "executable_prefixes": ("/opt/Alpha/",), + "command": ["/scripts/alpha/alpha-start.sh"], }, ) @@ -1271,7 +1271,7 @@ def paint_notification_menu(self, force=False): rows = ( ("All", settings["enabled"]), ("WeChat", settings["apps"]["wechat"]), - ("WeFlow", settings["apps"]["weflow"]), + ("Alpha", settings["apps"]["alpha"]), ) self.notification_menu_buffer.fill_rectangle( self.panel_gc, @@ -1439,7 +1439,7 @@ def toggle_notification_setting(self, row): elif row == 1: settings["apps"]["wechat"] = not settings["apps"]["wechat"] elif row == 2: - settings["apps"]["weflow"] = not settings["apps"]["weflow"] + settings["apps"]["alpha"] = not settings["apps"]["alpha"] save_settings(settings) self.notification_menu_last_interaction = time.monotonic() self.last_visual_state = None diff --git a/root/scripts/notifications/app_control.py b/root/scripts/notifications/app_control.py index fc4e994..efa27f6 100644 --- a/root/scripts/notifications/app_control.py +++ b/root/scripts/notifications/app_control.py @@ -10,8 +10,9 @@ APPS = { 'wechat': {'executable': '/opt/wechat/wechat', 'prefix': '/opt/wechat/', 'command': ['/usr/bin/wechat']}, - 'weflow': {'executable': '/opt/weflow/weflow', 'prefix': '/opt/weflow/', - 'command': ['/opt/weflow/weflow', '--no-sandbox']}, + 'alpha': {'executable': '/opt/Alpha/alpha', 'prefix': '/opt/Alpha/', + 'command': ['/opt/Alpha/alpha', '--no-sandbox', + '--password-store=gnome-libsecret']}, } diff --git a/root/scripts/notifications/browser-notification-bridge.py b/root/scripts/notifications/browser-notification-bridge.py index 269e723..73349be 100644 --- a/root/scripts/notifications/browser-notification-bridge.py +++ b/root/scripts/notifications/browser-notification-bridge.py @@ -28,10 +28,12 @@ OBJECT_PATH = "/org/freedesktop/Notifications" LISTEN_HOST = "127.0.0.1" LISTEN_PORT = 8765 -SUPPORTED_APPS = {"wechat", "weflow"} -WEFLOW_CONFIG_PATH = Path("/config/.config/weflow/WeFlow-config.json") -WEFLOW_PUSH_RECONNECT_SECONDS = 3 -WEFLOW_PUSH_EVENT_TTL_SECONDS = 15 * 60 +SUPPORTED_APPS = {"wechat", "alpha"} +ALPHA_TOKEN_PATH = Path( + os.environ.get("ALPHA_API_TOKEN_FILE", "/config/.config/weharbor/alpha-api-token") +) +ALPHA_PUSH_RECONNECT_SECONDS = 3 +ALPHA_PUSH_EVENT_TTL_SECONDS = 15 * 60 def env_enabled(name, default=True): @@ -62,8 +64,8 @@ def classify_app(app_name, summary, hints): str(summary or ""), ) ).lower() - if "weflow" in identity: - return "weflow" + if "alpha" in identity: + return "alpha" if any(value in identity for value in ("wechat", "weixin", "微信")): return "wechat" return None @@ -74,7 +76,7 @@ def __init__(self): self.loop = None self.clients = set() self.browser_status = {} - self.weflow_push_status = { + self.alpha_push_status = { "stage": "starting", "lastEvent": None, "lastError": None, @@ -114,7 +116,7 @@ async def health(self, _request): "status": "ok", "clients": len(self.clients), "browser_status": list(self.browser_status.values()), - "weflow_push": self.weflow_push_status, + "alpha_push": self.alpha_push_status, } ) @@ -189,28 +191,26 @@ async def events(self, request): return response -def load_weflow_push_config(): - try: - config = json.loads(WEFLOW_CONFIG_PATH.read_text(encoding="utf-8")) - except (FileNotFoundError, OSError, json.JSONDecodeError): - return None - if not isinstance(config, dict): +def load_alpha_push_config(): + """Alpha encrypts its API token with safeStorage, so the bridge cannot read + it from Alpha's settings. The user opts in by saving the token copied from + Alpha's settings page to ALPHA_API_TOKEN_FILE (or ALPHA_API_TOKEN).""" + token = os.environ.get("ALPHA_API_TOKEN", "").strip() + if not token: + try: + token = ALPHA_TOKEN_PATH.read_text(encoding="utf-8").strip() + except (FileNotFoundError, OSError): + return None + if not token: return None - - token = str(config.get("httpApiToken", "")).strip() - if not token or token.startswith(("safe:", "lock:")): - return { - "enabled": config.get("messagePushEnabled") is True, - "error": "WeFlow API token is unavailable to the bridge", - } try: - port = int(config.get("httpApiPort", 5031)) - except (TypeError, ValueError): + port = int(os.environ.get("ALPHA_API_PORT", "5031")) + except ValueError: port = 5031 if not 1 <= port <= 65535: port = 5031 return { - "enabled": config.get("messagePushEnabled") is True, + "enabled": True, "url": f"http://127.0.0.1:{port}/api/v1/push/messages", "token": token, } @@ -231,7 +231,7 @@ def __init__(self): def add(self, payload): now = time.monotonic() - cutoff = now - WEFLOW_PUSH_EVENT_TTL_SECONDS + cutoff = now - ALPHA_PUSH_EVENT_TTL_SECONDS while self.events: _, created_at = next(iter(self.events.items())) if created_at >= cutoff: @@ -249,8 +249,8 @@ def add(self, payload): return True -async def consume_weflow_push(relay): - """Forward WeFlow's structured message SSE directly to browser clients.""" +async def consume_alpha_push(relay): + """Forward Alpha's structured message SSE directly to browser clients.""" seen = RecentPushEvents() last_event_id = "" process_started_at = int(time.time()) @@ -258,31 +258,31 @@ async def consume_weflow_push(relay): async with ClientSession(timeout=timeout) as session: while True: - config = load_weflow_push_config() + config = load_alpha_push_config() if not config: - relay.weflow_push_status = { + relay.alpha_push_status = { "stage": "waiting-for-config", "lastEvent": None, "lastError": None, } - await asyncio.sleep(WEFLOW_PUSH_RECONNECT_SECONDS) + await asyncio.sleep(ALPHA_PUSH_RECONNECT_SECONDS) continue if config.get("error"): - relay.weflow_push_status = { + relay.alpha_push_status = { "stage": "configuration-error", "lastEvent": None, "lastError": config["error"], } - LOGGER.warning("WeFlow push: %s", config["error"]) + LOGGER.warning("Alpha push: %s", config["error"]) await asyncio.sleep(10) continue if not config["enabled"]: - relay.weflow_push_status = { + relay.alpha_push_status = { "stage": "disabled", "lastEvent": None, - "lastError": "messagePushEnabled is false", + "lastError": "Alpha push is disabled", } - await asyncio.sleep(WEFLOW_PUSH_RECONNECT_SECONDS) + await asyncio.sleep(ALPHA_PUSH_RECONNECT_SECONDS) continue headers = { @@ -293,30 +293,30 @@ async def consume_weflow_push(relay): headers["Last-Event-ID"] = last_event_id try: - relay.weflow_push_status = { + relay.alpha_push_status = { "stage": "connecting", - "lastEvent": relay.weflow_push_status.get("lastEvent"), + "lastEvent": relay.alpha_push_status.get("lastEvent"), "lastError": None, } async with session.get(config["url"], headers=headers) as response: if response.status != 200: error = f"HTTP {response.status}" - relay.weflow_push_status = { + relay.alpha_push_status = { "stage": "error", - "lastEvent": relay.weflow_push_status.get( + "lastEvent": relay.alpha_push_status.get( "lastEvent" ), "lastError": error, } - LOGGER.warning("WeFlow push connection failed: %s", error) + LOGGER.warning("Alpha push connection failed: %s", error) await response.read() - await asyncio.sleep(WEFLOW_PUSH_RECONNECT_SECONDS) + await asyncio.sleep(ALPHA_PUSH_RECONNECT_SECONDS) continue - LOGGER.info("connected to WeFlow message push stream") - relay.weflow_push_status = { + LOGGER.info("connected to Alpha message push stream") + relay.alpha_push_status = { "stage": "connected", - "lastEvent": relay.weflow_push_status.get("lastEvent"), + "lastEvent": relay.alpha_push_status.get("lastEvent"), "lastError": None, } event_name = "" @@ -349,7 +349,7 @@ async def consume_weflow_push(relay): payload = json.loads("\n".join(data_lines)) except (json.JSONDecodeError, TypeError): LOGGER.warning( - "ignored malformed WeFlow push event %r", + "ignored malformed Alpha push event %r", event_name, ) else: @@ -368,7 +368,7 @@ async def consume_weflow_push(relay): ) if stale_replay: LOGGER.info( - "skipped stale WeFlow replay event=%r", + "skipped stale Alpha replay event=%r", event_name, ) elif seen.add(payload): @@ -400,7 +400,7 @@ async def consume_weflow_push(relay): "body": body, } ) - relay.weflow_push_status = { + relay.alpha_push_status = { "stage": ( "event-forwarded" if enabled @@ -413,7 +413,7 @@ async def consume_weflow_push(relay): "lastError": None, } LOGGER.info( - "WeFlow push event=%r " + "Alpha push event=%r " "browser_forwarded=%s", event_name, enabled, @@ -422,16 +422,16 @@ async def consume_weflow_push(relay): event_id = "" data_lines = [] except (ClientError, asyncio.TimeoutError, OSError) as error: - relay.weflow_push_status = { + relay.alpha_push_status = { "stage": "error", - "lastEvent": relay.weflow_push_status.get("lastEvent"), + "lastEvent": relay.alpha_push_status.get("lastEvent"), "lastError": type(error).__name__, } LOGGER.warning( - "WeFlow push stream disconnected: %s", + "Alpha push stream disconnected: %s", type(error).__name__, ) - await asyncio.sleep(WEFLOW_PUSH_RECONNECT_SECONDS) + await asyncio.sleep(ALPHA_PUSH_RECONNECT_SECONDS) class FreedesktopNotificationMonitor: @@ -521,7 +521,7 @@ async def serve_http(relay, ready): await runner.setup() site = web.TCPSite(runner, LISTEN_HOST, LISTEN_PORT) await site.start() - push_task = asyncio.create_task(consume_weflow_push(relay)) + push_task = asyncio.create_task(consume_alpha_push(relay)) ready.set() LOGGER.info( "notification bridge listening on http://%s:%s", diff --git a/root/scripts/notifications/notification_config.py b/root/scripts/notifications/notification_config.py index 8df1979..1061be2 100644 --- a/root/scripts/notifications/notification_config.py +++ b/root/scripts/notifications/notification_config.py @@ -7,12 +7,12 @@ from pathlib import Path -CONFIG_PATH = Path("/config/.config/weflow/browser-notifications.json") +CONFIG_PATH = Path("/config/.config/weharbor/browser-notifications.json") DEFAULT_SETTINGS = { "enabled": True, "apps": { "wechat": True, - "weflow": True, + "alpha": True, }, } diff --git a/root/scripts/notifications/window_control.py b/root/scripts/notifications/window_control.py index 1b31f7f..816e68b 100644 --- a/root/scripts/notifications/window_control.py +++ b/root/scripts/notifications/window_control.py @@ -3,6 +3,9 @@ import threading +PRIMARY_CLASS = {'wechat': 'wechat', 'alpha': 'alpha-desktop'} + + class WindowController: def __init__(self, processes): self.processes = processes @@ -43,11 +46,11 @@ def values(self, window, name): return value.value if value is not None else [] def snapshot(self): - owners = {pid: app for app in ('wechat', 'weflow') for pid in self.processes(app)} + owners = {pid: app for app in ('wechat', 'alpha') for pid in self.processes(app)} active = self.values(self.root, '_NET_ACTIVE_WINDOW') active_id = int(active[0]) if len(active) else None clients = self.values(self.root, '_NET_CLIENT_LIST') - result = {'wechat': [], 'weflow': []} + result = {'wechat': [], 'alpha': []} for wid in clients: try: window = self.connection.create_resource_object('window', int(wid)) @@ -65,13 +68,13 @@ def snapshot(self): continue title = self.values(window, '_NET_WM_NAME') title = bytes(title).decode('utf-8', errors='replace').strip(chr(0)) if len(title) else '' - title = title or str(window.get_wm_name() or '') or ('微信' if app == 'wechat' else 'WeFlow') + title = title or str(window.get_wm_name() or '') or ('微信' if app == 'wechat' else 'Alpha') classes = {str(value).lower() for value in (window.get_wm_class() or ())} states = self.values(window, '_NET_WM_STATE') result[app].append({'id': int(wid), 'title': title[:200], 'active': int(wid) == active_id, 'minimized': self.atom('_NET_WM_STATE_HIDDEN') in states, - 'primary': app in classes}) + 'primary': PRIMARY_CLASS[app] in classes}) if int(wid) == active_id: self.remembered[app] = int(wid) except (self.error.XError, OSError, ValueError, TypeError, UnicodeError): diff --git a/root/scripts/wechat/wechat-start.sh b/root/scripts/wechat/wechat-start.sh new file mode 100644 index 0000000..20f3515 --- /dev/null +++ b/root/scripts/wechat/wechat-start.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Start WeChat once; the X11 app switcher focuses an existing window instead. +if pgrep -x wechat >/dev/null 2>&1; then + exit 0 +fi +mkdir -p /config/.local/log +nohup /usr/bin/wechat >>/config/.local/log/wechat.log 2>&1 & diff --git a/root/scripts/weflow/weflow-start.sh b/root/scripts/weflow/weflow-start.sh deleted file mode 100644 index 7bb04e0..0000000 --- a/root/scripts/weflow/weflow-start.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Clear Electron and legacy AppImage variables inherited from other launchers. -unset APPDIR APPIMAGE ELECTRON_RUN_AS_NODE ELECTRON_NO_ATTACH_CONSOLE - -if pgrep -x weflow >/dev/null 2>&1; then - # Starting Electron again can create a second process tree. Existing windows - # are focused by the X11 app switcher, so never launch another copy here. - exit 0 -fi - -nohup /opt/weflow/weflow --no-sandbox "$@" >/config/weflow.log 2>&1 & diff --git a/root/scripts/weharbor/desktop-start.sh b/root/scripts/weharbor/desktop-start.sh new file mode 100644 index 0000000..2727cec --- /dev/null +++ b/root/scripts/weharbor/desktop-start.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# WeHarbor desktop session start-up for the plain LinuxServer.io Selkies base. +# Runs as abc from the Openbox autostart file. + +# A stale PID file from a previous container run stops dbus from starting. +rm -f /run/dbus/pid 2>/dev/null || true + +# Let docked tray windows overlap application windows instead of reserving space. +rc="${HOME}/.config/openbox/rc.xml" +mkdir -p "$(dirname "${rc}")" +[ -f "${rc}" ] || cp /etc/xdg/openbox/rc.xml "${rc}" +if grep -A20 '' "${rc}" | grep -q 'no'; then + sed -i '//,/<\/dock>/s/no<\/noStrut>/yes<\/noStrut>/' "${rc}" + openbox --reconfigure 2>/dev/null || true +fi + +if ! pgrep -u "$(id -u)" -x stalonetray >/dev/null 2>&1; then + nohup /usr/local/bin/stalonetray >/dev/null 2>&1 & +fi + +if [ "${AUTO_START_WECHAT:-true}" = "true" ]; then + /scripts/wechat/wechat-start.sh +fi diff --git a/root/scripts/weharbor/healthcheck.py b/root/scripts/weharbor/healthcheck.py index 5ac62d4..af59719 100644 --- a/root/scripts/weharbor/healthcheck.py +++ b/root/scripts/weharbor/healthcheck.py @@ -6,7 +6,6 @@ import subprocess import sys import urllib.request -from pathlib import Path def enabled(name): @@ -24,8 +23,8 @@ def get(url, authenticated=False): def main(): get('http://127.0.0.1:3000/', authenticated=True) - if enabled('AUTO_START_WEFLOW'): - subprocess.run(['pgrep', '-x', 'weflow'], check=True, stdout=subprocess.DEVNULL) + if enabled('AUTO_START_ALPHA'): + subprocess.run(['pgrep', '-x', 'alpha'], check=True, stdout=subprocess.DEVNULL) if enabled('AUTO_START_WECHAT'): subprocess.run(['pgrep', '-x', 'wechat'], check=True, stdout=subprocess.DEVNULL) if enabled('ENABLE_APP_SWITCHER') and os.environ.get('APP_SWITCHER_MODE', 'web') == 'desktop': @@ -36,20 +35,22 @@ def main(): raise ValueError('Application/notification bridge is not healthy') if enabled('ENABLE_APP_SWITCHER'): apps = json.loads(get('http://127.0.0.1:8765/apps')) - if not all(name in apps for name in ('wechat', 'weflow')): + if not all(name in apps for name in ('wechat', 'alpha')): raise ValueError('Application controls are not healthy') if os.environ.get('APP_SWITCHER_MODE', 'web') != 'desktop': script = get('http://127.0.0.1:3000/weharbor-apps.js', authenticated=True) if b'weharbor-apps-button' not in script: raise ValueError('Web application controls are not available') - # The API is optional on first launch. If explicitly enabled, check it too. - config_path = Path('/config/.config/weflow/WeFlow-config.json') - if enabled('AUTO_START_WEFLOW') and config_path.exists(): - config = json.loads(config_path.read_text()) - if config.get('httpApiEnabled'): - port = int(config.get('httpApiPort', 5031)) - if json.loads(get(f'http://127.0.0.1:{port}/health')).get('status') != 'ok': - raise ValueError('WeFlow API is not healthy') + # Alpha's API is optional on first launch. /health needs no token, so + # check it whenever the listener is up (the user enabled it in Alpha). + if enabled('AUTO_START_ALPHA'): + port = int(os.environ.get('ALPHA_API_PORT', '5031')) + try: + body = get(f'http://127.0.0.1:{port}/health') + except OSError: + body = None + if body is not None and not body.strip(): + raise ValueError('Alpha API is not healthy') print('WeHarbor: healthy') diff --git a/root/scripts/weharbor/install-web.py b/root/scripts/weharbor/install-web.py index 8fdc3a0..2ec0ae8 100644 --- a/root/scripts/weharbor/install-web.py +++ b/root/scripts/weharbor/install-web.py @@ -62,9 +62,12 @@ def install(selkies, assets, nginx): block = (nginx.parent / 'weharbor-pwa.conf').read_text().rstrip() + '\n' config = re.sub(r'location = SUBFOLDERweharbor\.webmanifest \{.*?' r'location = SUBFOLDERweharbor-pwa\.js \{.*?\}\n?', '', config, flags=re.S) - if ' location SUBFOLDERfiles {' not in config: - raise RuntimeError('Unsupported nginx template: no files location') - config = config.replace(' location SUBFOLDERfiles {', block + ' location SUBFOLDERfiles {') + # Selkies 1.x templates had a files location; Selkies 2.x keeps api. + anchor = next((a for a in (' location SUBFOLDERfiles {', ' location SUBFOLDERapi {') + if a in config), None) + if anchor is None: + raise RuntimeError('Unsupported nginx template: no files/api location') + config = config.replace(anchor, block + anchor) nginx.write_text(config) print(f'WeHarbor web integration installed in {installed} dashboards') diff --git a/root/scripts/weharbor/window-layout.py b/root/scripts/weharbor/window-layout.py index afc504e..0d08870 100644 --- a/root/scripts/weharbor/window-layout.py +++ b/root/scripts/weharbor/window-layout.py @@ -30,7 +30,7 @@ def tag(name): groups = root.findall(tag('applications')) group = groups[-1] if groups else ET.SubElement(root, tag('applications')) # Replace only our marked block. Keep unrelated rules, bindings and - # desktop preferences, including user-defined WeChat/WeFlow rules. + # desktop preferences, including user-defined WeChat/Alpha rules. for existing in groups: managed = False for rule in list(existing): @@ -46,11 +46,11 @@ def tag(name): wechat = ET.SubElement(group, tag('application'), {'class': 'wechat'}) for name, value in [('focus', 'yes'), ('maximized', 'no')]: ET.SubElement(wechat, tag(name)).text = value - # Restrict to the main WeFlow window; leave settings and dialogs usable. - weflow = ET.SubElement(group, tag('application'), - {'class': 'weflow', 'title': 'WeFlow', 'type': 'normal'}) - ET.SubElement(weflow, tag('focus')).text = 'no' - ET.SubElement(weflow, tag('iconic')).text = 'yes' + # Restrict to the main Alpha window; leave settings and dialogs usable. + alpha = ET.SubElement(group, tag('application'), + {'class': 'alpha-desktop', 'title': 'Alpha', 'type': 'normal'}) + ET.SubElement(alpha, tag('focus')).text = 'no' + ET.SubElement(alpha, tag('iconic')).text = 'yes' group.append(ET.Comment(RULE_END)) data = ET.tostring(root, encoding='utf-8', xml_declaration=True) temporary = config.with_suffix('.xml.weharbor-pending') @@ -147,14 +147,14 @@ def tick(self): classes = tuple((x or '').lower() for x in (window.get_wm_class() or ())) state = self.states.setdefault(wid, {'seen': now, 'minimized': False, 'focused': False, 'maximized': False}) - if 'weflow' in classes: + if 'alpha-desktop' in classes: title = self.property(window, '_NET_WM_NAME') name = title.value.decode('utf-8', errors='replace') if title else window.get_wm_name() - if name == 'WeFlow' and not state['minimized']: + if name == 'Alpha' and not state['minimized']: self.send(window, 'WM_CHANGE_STATE', [Xutil.IconicState, 0, 0, 0, 0]) state['minimized'] = True self.focus_at = now + 0.3 - print('WeFlow initial window minimized', flush=True) + print('Alpha initial window minimized', flush=True) elif 'wechat' in classes: wechat.append(window) if not state['focused']: diff --git a/root/usr/share/weharbor/pwa/weharbor-apps.js b/root/usr/share/weharbor/pwa/weharbor-apps.js index ba1bc4f..39412a7 100644 --- a/root/usr/share/weharbor/pwa/weharbor-apps.js +++ b/root/usr/share/weharbor/pwa/weharbor-apps.js @@ -61,14 +61,14 @@ 微信…
微信窗口
-
-
-
WeFlow 窗口
+
+
+
Alpha 窗口

点击应用切换;再次点击可轮换窗口。重启前请保存未发送的内容。

-
+

`; const feedback = panel.querySelector('.app-feedback'); let busy = false, refreshing = false, timer = null, drag = null, suppressClick = false; @@ -99,7 +99,7 @@ return result; }; const render = () => { - for (const app of ['wechat', 'weflow']) { + for (const app of ['wechat', 'alpha']) { const state = status[app] || {}; panel.querySelector('[data-status="' + app + '"]').textContent = state.restarting ? '处理中' : state.active ? '当前应用' : state.running ? '运行中' : '点击启动'; @@ -159,7 +159,7 @@ const button = event.target.closest('[data-action]'); if (!button || busy || button.disabled) return; const app = button.dataset.app, action = button.dataset.action; - const name = app === 'wechat' ? '微信' : 'WeFlow'; + const name = app === 'wechat' ? '微信' : 'Alpha'; busy = true; render(); feedback.textContent = name + (action === 'restart' ? '正在重启,请稍候…' : '正在切换…'); try { diff --git a/root/usr/share/weharbor/pwa/weharbor-sw.js b/root/usr/share/weharbor/pwa/weharbor-sw.js index fcd9d8f..316e0ae 100644 --- a/root/usr/share/weharbor/pwa/weharbor-sw.js +++ b/root/usr/share/weharbor/pwa/weharbor-sw.js @@ -13,7 +13,7 @@ self.addEventListener("fetch", (event) => { 'p{line-height:1.7;color:#bccddd}a{display:inline-block;padding:12px 18px;' + 'background:#56dfbb;color:#101c2d;border-radius:12px;text-decoration:none}' + '

WeHarbor|微港

无法连接桌面。' + - '请检查网络和服务器,恢复连接后重试。微信与 WeFlow 需要在线使用。

' + + '请检查网络和服务器,恢复连接后重试。微信与 Alpha 需要在线使用。

' + '重新连接
', {status: 503, headers: {"Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store"}} ))); diff --git a/root/usr/share/weharbor/pwa/weharbor.webmanifest b/root/usr/share/weharbor/pwa/weharbor.webmanifest index 4796960..a243df5 100644 --- a/root/usr/share/weharbor/pwa/weharbor.webmanifest +++ b/root/usr/share/weharbor/pwa/weharbor.webmanifest @@ -2,7 +2,7 @@ "id": "./", "name": "WeHarbor|微港", "short_name": "WeHarbor", - "description": "微信桌面与 WeFlow 数据服务", + "description": "微信桌面与 Alpha 数据服务", "lang": "zh-CN", "start_url": "./", "scope": "./", diff --git a/scripts/check.py b/scripts/check.py index 70d2106..5e91c98 100644 --- a/scripts/check.py +++ b/scripts/check.py @@ -15,9 +15,9 @@ def main(): assert lock['schema'] == 1 and lock['platform'] == 'linux/amd64' assert re.fullmatch(r'\d+\.\d+\.\d+', lock['project_version']) assert re.search(r'@sha256:[0-9a-f]{64}$', lock['base_image']) - for name in ['wechat', 'weflow']: + for name in ['wechat', 'alpha']: assert re.fullmatch(r'[0-9a-f]{64}', lock[name]['sha256']) - assert re.fullmatch(r'[0-9a-f]{64}', lock['weflow']['asar_sha256']) + assert lock['alpha']['url'].startswith('https://') dockerfile = (ROOT / 'Dockerfile').read_text() assert f'ARG BASE_IMAGE={lock["base_image"]}\n' in dockerfile assert f'ARG PROJECT_VERSION={lock["project_version"]}\n' in dockerfile diff --git a/scripts/prepare-assets.py b/scripts/prepare-assets.py index ab271d7..a2cdc3a 100644 --- a/scripts/prepare-assets.py +++ b/scripts/prepare-assets.py @@ -53,7 +53,7 @@ def main(): args = parser.parse_args() lock = json.loads((ROOT / 'versions.lock.json').read_text()) # Both default URLs point to the pinned, unmodified Release assets. - for component in ['weflow', 'wechat']: + for component in ['alpha', 'wechat']: prepare(component, lock[component], ROOT / 'downloads', args.check_only) diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index dafd366..a2de95c 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -73,8 +73,8 @@ with urllib.request.urlopen(urllib.request.Request(url + 'weharbor-sw.js', heade print('Browser authentication, notifications and PWA resources passed') with urllib.request.urlopen(urllib.request.Request(url + 'notifications/apps', headers=headers), timeout=5) as response: apps = json.load(response) - assert apps['wechat']['running'] and apps['weflow']['running'], apps -for app in ['wechat', 'weflow']: + assert apps['wechat']['running'] and apps['alpha']['running'], apps +for app in ['wechat', 'alpha']: restart_url = url + f'notifications/apps/{app}/restart' for extra in [{}, {'Content-Type': 'application/json', 'X-WeHarbor-Action': 'restart', 'Origin': 'https://evil.example'}]: @@ -93,7 +93,7 @@ print('Authenticated application restarts and cross-origin protection passed') def app_status(): with urllib.request.urlopen(urllib.request.Request(url + 'notifications/apps', headers=headers), timeout=5) as response: return json.load(response) -for app in ['weflow', 'wechat']: +for app in ['alpha', 'wechat']: for attempt in range(60): if app_status()[app]['windows']: break @@ -111,7 +111,7 @@ for app in ['weflow', 'wechat']: raise SystemExit('Switch accepted cross-origin request') with urllib.request.urlopen(urllib.request.Request(switch_url, data=b'{}', headers=switch_headers), timeout=10) as response: assert json.load(response)['windowId'] - other = 'wechat' if app == 'weflow' else 'weflow' + other = 'wechat' if app == 'alpha' else 'alpha' for attempt in range(30): state = app_status() if state[app]['active'] and all(w['minimized'] for w in state[other]['windows']): @@ -134,8 +134,9 @@ from pathlib import Path lock = json.loads(Path('/usr/share/weharbor/versions.lock.json').read_text()) version = subprocess.check_output(['dpkg-query', '-W', '-f=${Version}', 'wechat'], text=True) assert version == lock['wechat']['version'], version -asar = Path('/opt/weflow/resources/app.asar') -assert hashlib.sha256(asar.read_bytes()).hexdigest() == lock['weflow']['asar_sha256'] +alpha = subprocess.check_output(['dpkg-query', '-W', '-f=${Version}', 'alpha'], text=True) +assert alpha == lock['alpha']['version'], alpha +subprocess.run(['dpkg', '--verify', 'alpha'], check=True) assert Path('/config/Documents/xwechat_files').is_symlink() subprocess.run(['pgrep', '-f', '^python3 /scripts/weharbor/window-layout.py$'], check=True, stdout=subprocess.DEVNULL) @@ -155,7 +156,7 @@ assert clients, 'WeChat did not start through the DPI compatibility launcher' setting = Path('/config/.config/weharbor/wechat-dpi') setting.parent.mkdir(parents=True, exist_ok=True) setting.write_text('144\n') -print('Pinned versions, original ASAR and data-path compatibility passed') +print('Pinned versions, unmodified packages and data-path compatibility passed') PY docker restart "$container" >/dev/null ready=false @@ -175,8 +176,8 @@ assert config.read_text().count('WeHarbor window defaults: begin') == 1 rules = ET.parse(config).findall('.//{*}application') wechat = [r for r in rules if r.get('class') == 'wechat'][-1] assert wechat.find('{*}maximized').text == 'no' -weflow = [r for r in rules if r.get('class') == 'weflow' and r.get('title') == 'WeFlow'][-1] -assert weflow.find('{*}iconic').text == 'yes' +alpha = [r for r in rules if r.get('class') == 'alpha-desktop' and r.get('title') == 'Alpha'][-1] +assert alpha.find('{*}iconic').text == 'yes' assert Path('/config/.config/weharbor/wechat-dpi').read_text().strip() == '144' clients = [] for executable in Path('/proc').glob('[0-9]*/exe'): diff --git a/scripts/verify-assets.py b/scripts/verify-assets.py index 55dd7e1..624b2ab 100644 --- a/scripts/verify-assets.py +++ b/scripts/verify-assets.py @@ -1,9 +1,8 @@ #!/usr/bin/env python3 -"""Verify pinned vendor packages; keep the WeFlow ASAR byte-for-byte intact.""" +"""Verify pinned vendor packages before they are installed unmodified.""" import argparse import hashlib import json -import struct import subprocess from pathlib import Path @@ -21,45 +20,28 @@ def verify(path, expected): raise ValueError(f'SHA256 mismatch: {Path(path).name}; refusing this package') -def asar_version(path): - with Path(path).open('rb') as stream: - prefix = stream.read(16) - header_size = struct.unpack_from(' means the actual installed folder, not a literal shell path. ~~~bash -python3 /scripts/weflow_api.py health -python3 /scripts/weflow_api.py sessions --keyword 项目 --limit 50 -python3 /scripts/weflow_api.py messages --talker example@chatroom --limit 100 -python3 /scripts/weflow_api.py contacts --keyword Alice -python3 /scripts/weflow_api.py group-members --chatroom-id example@chatroom --counts -python3 /scripts/weflow_api.py sns-timeline --limit 20 -python3 /scripts/weflow_api.py sse --duration 30 --max-events 20 +python3 /scripts/alpha_api.py health +python3 /scripts/alpha_api.py sessions --keyword 项目 --limit 50 +python3 /scripts/alpha_api.py messages --talker example@chatroom --limit 100 +python3 /scripts/alpha_api.py contacts --keyword Alice +python3 /scripts/alpha_api.py group-members --chatroom-id example@chatroom --counts +python3 /scripts/alpha_api.py sns-timeline --limit 20 +python3 /scripts/alpha_api.py sse --duration 30 --max-events 20 ~~~ 1. Check health; status must be ok. Health alone does not prove database readiness. @@ -83,6 +83,16 @@ upload messages or attachments to external OCR services. HTTP 401/403 means the API token must be checked locally. HTTP 404 can indicate an unsupported endpoint/version or an incorrect base URL. Connection failure means checking address, service, port mapping and networking. Never automatically -disable authentication or change persistent WeFlow settings to repair a query. +disable authentication or change persistent Alpha settings to repair a query. The helper prints JSON, or one JSON object per SSE event, and exits nonzero on failures. It never prints server error bodies or request credentials. + +## Alpha compatibility note + +This client was written for the WeFlow HTTP API. Alpha 0.2.0 exposes the same +route names (/health, /api/v1/sessions, /api/v1/messages, /api/v1/contacts, +/api/v1/group-members, /api/v1/sns/*, /api/v1/media/*, /api/v1/push/messages, +plus /mcp), bearer or access_token authentication and port 5031, but query +parameters and response fields have not yet been verified against a live Alpha +instance. Report mismatches instead of guessing. In WeHarbor the host-side URL +is the published proxy port (default http://127.0.0.1:5031 -> container 5032). diff --git a/skills/alpha-api/agents/openai.yaml b/skills/alpha-api/agents/openai.yaml new file mode 100644 index 0000000..5c07fc2 --- /dev/null +++ b/skills/alpha-api/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Alpha API" + short_description: "Query WeChat history and events through Alpha" + default_prompt: "Use $alpha-api to find the requested WeChat session and summarize its relevant messages." diff --git a/skills/weflow-api/references/http-api.md b/skills/alpha-api/references/http-api.md similarity index 83% rename from skills/weflow-api/references/http-api.md rename to skills/alpha-api/references/http-api.md index b88dbfe..e16a844 100644 --- a/skills/weflow-api/references/http-api.md +++ b/skills/alpha-api/references/http-api.md @@ -1,7 +1,7 @@ # API notes -Endpoint definitions: [upstream HTTP API documentation](https://github.com/hicccc77/WeFlow/blob/837697ba26605152f216bd5ffc5e9d61aa9f2907/docs/HTTP-API.md). -WeHarbor pins WeFlow 6.3.2; upstream documentation can include newer optional +Endpoint definitions: [upstream HTTP API documentation](https://github.com/hicccc77/Alpha/blob/837697ba26605152f216bd5ffc5e9d61aa9f2907/docs/HTTP-API.md). +WeHarbor pins Alpha 6.3.2; upstream documentation can include newer optional features. A 404 should be reported as unsupported, not silently replaced by another operation. The helper uses GET and Bearer authentication, including SSE. @@ -26,7 +26,7 @@ For messages and timeline filters, use YYYYMMDD or Unix timestamps. The helper normalizes ten-digit Unix seconds to milliseconds for compatibility with older message-filter builds, and preserves thirteen-digit milliseconds. Returned createTime and SSE timestamp are normally seconds. Date-only filters use the -WeFlow host's timezone; date-only end includes that whole day. Confirm the user's +Alpha host's timezone; date-only end includes that whole day. Confirm the user's timezone before computing a window (WeHarbor's default is Asia/Shanghai). If an expected timestamp query is empty, retry the surrounding YYYYMMDD window, @@ -35,8 +35,8 @@ Do not summarize the broader window without that filtering. Dates must be valid; other time string formats are rejected by the helper. ~~~bash -python3 /scripts/weflow_api.py messages --talker example@chatroom --start 20261001 --end 20261007 --limit 100 --offset 0 -python3 /scripts/weflow_api.py chatlab-messages --session-id example@chatroom --since 1790812800 --limit 100 +python3 /scripts/alpha_api.py messages --talker example@chatroom --start 20261001 --end 20261007 --limit 100 --offset 0 +python3 /scripts/alpha_api.py chatlab-messages --session-id example@chatroom --since 1790812800 --limit 100 ~~~ Follow hasMore for ordinary messages. ChatLab uses sync.hasMore, nextOffset and @@ -46,7 +46,7 @@ results. There is no unlimited automatic history crawl. ## Media and streaming -messages --media asks WeFlow to export media files, which can write its local +messages --media asks Alpha to export media files, which can write its local media cache. Only enable this when attachments are needed. Take the relative path following /api/v1/media/ from a returned mediaUrl; pass it to media with --output. The helper sends authentication only to the configured API base, diff --git a/skills/weflow-api/scripts/weflow_api.py b/skills/alpha-api/scripts/alpha_api.py similarity index 93% rename from skills/weflow-api/scripts/weflow_api.py rename to skills/alpha-api/scripts/alpha_api.py index 7d48c37..4dd0b5d 100644 --- a/skills/weflow-api/scripts/weflow_api.py +++ b/skills/alpha-api/scripts/alpha_api.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Dependency-free CLI for a configured WeFlow HTTP API.""" +"""Dependency-free CLI for a configured Alpha HTTP API.""" from __future__ import annotations import argparse @@ -67,7 +67,7 @@ def __init__(self, base, token=None, timeout=30): def open(self, path, params=None, *, auth=True, stream=False, last_event_id=None, timeout=None): if auth and not self.token: - raise APIError('Set WEFLOW_API_TOKEN or WEFLOW_API_TOKEN_FILE locally') + raise APIError('Set ALPHA_API_TOKEN or ALPHA_API_TOKEN_FILE locally') url = self.base + path query = {key: value for key, value in (params or {}).items() if value is not None} if query: @@ -83,8 +83,8 @@ def open(self, path, params=None, *, auth=True, stream=False, last_event_id=None except urllib.error.HTTPError as error: error.close() advice = {401: 'Check the API token locally', 403: 'Check API access permissions', - 404: 'Check the API base and whether this WeFlow version supports the endpoint'} - raise APIError(f'HTTP {error.code}. ' + advice.get(error.code, 'API request failed; inspect WeFlow locally')) from None + 404: 'Check the API base and whether this Alpha version supports the endpoint'} + raise APIError(f'HTTP {error.code}. ' + advice.get(error.code, 'API request failed; inspect Alpha locally')) from None except (urllib.error.URLError, OSError, ValueError): raise APIError('Cannot connect to the configured API; check address, service and network') from None @@ -95,17 +95,17 @@ def get(self, path, params=None, *, auth=True): except (ValueError, UnicodeError): raise APIError('API returned invalid JSON') from None if isinstance(result, dict) and result.get('success') is False: - raise APIError('API reported failure; inspect account/database configuration in WeFlow') + raise APIError('API reported failure; inspect account/database configuration in Alpha') return result def read_token(): - token = os.environ.get('WEFLOW_API_TOKEN', '').strip() - if not token and os.environ.get('WEFLOW_API_TOKEN_FILE'): + token = os.environ.get('ALPHA_API_TOKEN', '').strip() + if not token and os.environ.get('ALPHA_API_TOKEN_FILE'): try: - token = Path(os.environ['WEFLOW_API_TOKEN_FILE']).read_text().strip() + token = Path(os.environ['ALPHA_API_TOKEN_FILE']).read_text().strip() except (OSError, UnicodeError): - raise APIError('Cannot read WEFLOW_API_TOKEN_FILE') from None + raise APIError('Cannot read ALPHA_API_TOKEN_FILE') from None if '\r' in token or '\n' in token: raise APIError('API token must be a single line') return token @@ -155,7 +155,7 @@ def sse_events(response, duration, max_events): def parser(): root = argparse.ArgumentParser(description=__doc__) - root.add_argument('--base', default=os.environ.get('WEFLOW_API_BASE', 'http://127.0.0.1:5031')) + root.add_argument('--base', default=os.environ.get('ALPHA_API_BASE', 'http://127.0.0.1:5031')) root.add_argument('--timeout', type=positive, default=30) sub = root.add_subparsers(dest='command', required=True) sub.add_parser('health') @@ -209,7 +209,7 @@ def run(args): if command == 'health': result = client.get('/health', auth=False) if not isinstance(result, dict) or result.get('status') != 'ok': - raise APIError('WeFlow health is not ok') + raise APIError('Alpha health is not ok') elif command == 'sse': try: with client.open('/api/v1/push/messages', stream=True, diff --git a/skills/weflow-api/agents/openai.yaml b/skills/weflow-api/agents/openai.yaml deleted file mode 100644 index 4206c35..0000000 --- a/skills/weflow-api/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "WeFlow API" - short_description: "Query WeChat history and events through WeFlow" - default_prompt: "Use $weflow-api to find the requested WeChat session and summarize its relevant messages." diff --git a/tests/test_app_control.py b/tests/test_app_control.py index 0f79fe2..065229e 100644 --- a/tests/test_app_control.py +++ b/tests/test_app_control.py @@ -30,9 +30,9 @@ def test_switch_stopped_app_launches_and_releases_lock(self): with patch.object(control.os, 'getuid', return_value=1000), \ patch.object(manager, 'processes', return_value={}), \ patch.object(manager, 'launch') as launch: - manager.switch('weflow') - launch.assert_called_once_with('weflow') - self.assertFalse(manager.locks['weflow'].locked()) + manager.switch('alpha') + launch.assert_called_once_with('alpha') + self.assertFalse(manager.locks['alpha'].locked()) def test_restart_requires_same_origin_json_custom_header(self): headers = {'X-WeHarbor-Action': 'restart', 'Origin': 'https://desktop.example:7555'} diff --git a/tests/test_assets_and_init.py b/tests/test_assets_and_init.py index 911cd90..1db6de1 100644 --- a/tests/test_assets_and_init.py +++ b/tests/test_assets_and_init.py @@ -29,13 +29,13 @@ def setUp(self): def test_verified_cache_works_offline(self): (self.directory / self.config['filename']).write_bytes(self.data) with patch.object(assets.urllib.request, 'urlopen', side_effect=AssertionError('must stay offline')): - assets.prepare('weflow', self.config, self.directory) + assets.prepare('alpha', self.config, self.directory) def test_corrupted_cache_is_rejected(self): destination = self.directory / self.config['filename'] destination.write_bytes(b'corrupt') with self.assertRaisesRegex(ValueError, 'SHA256 mismatch'): - assets.prepare('weflow', self.config, self.directory) + assets.prepare('alpha', self.config, self.directory) self.assertEqual(destination.read_bytes(), b'corrupt') def test_wrong_download_is_never_cached(self): @@ -43,27 +43,27 @@ def test_wrong_download_is_never_cached(self): response.geturl = lambda: self.config['url'] with patch.dict(os.environ, {}, clear=True), patch.object(assets.urllib.request, 'urlopen', return_value=response): with self.assertRaisesRegex(ValueError, 'SHA256 mismatch'): - assets.prepare('weflow', self.config, self.directory) + assets.prepare('alpha', self.config, self.directory) self.assertEqual(list(self.directory.iterdir()), []) def test_valid_download_is_cached(self): response = BytesIO(self.data) response.geturl = lambda: self.config['url'] with patch.dict(os.environ, {}, clear=True), patch.object(assets.urllib.request, 'urlopen', return_value=response): - assets.prepare('weflow', self.config, self.directory) + assets.prepare('alpha', self.config, self.directory) self.assertEqual((self.directory / self.config['filename']).read_bytes(), self.data) def test_missing_source_fails_with_instructions(self): self.config['url'] = None with patch.dict(os.environ, {}, clear=True): - with self.assertRaisesRegex(ValueError, 'WEFLOW_DOWNLOAD_URL'): - assets.prepare('weflow', self.config, self.directory) + with self.assertRaisesRegex(ValueError, 'ALPHA_DOWNLOAD_URL'): + assets.prepare('alpha', self.config, self.directory) def test_http_source_is_rejected(self): self.config['url'] = 'http://example.invalid/package.tar.gz' with patch.dict(os.environ, {}, clear=True): with self.assertRaisesRegex(ValueError, 'HTTPS'): - assets.prepare('weflow', self.config, self.directory) + assets.prepare('alpha', self.config, self.directory) class InitializationTests(unittest.TestCase): diff --git a/tests/test_healthcheck.py b/tests/test_healthcheck.py index 712a272..78280cc 100644 --- a/tests/test_healthcheck.py +++ b/tests/test_healthcheck.py @@ -22,9 +22,9 @@ def get(url, authenticated=False): if url.endswith('/health'): return b'{"status":"ok"}' if url.endswith('/apps'): - return b'{"wechat":{},"weflow":{}}' + return b'{"wechat":{},"alpha":{}}' return b'' - with patch.dict(os.environ, {'AUTO_START_WECHAT': 'false', 'AUTO_START_WEFLOW': 'false', + with patch.dict(os.environ, {'AUTO_START_WECHAT': 'false', 'AUTO_START_ALPHA': 'false', 'ENABLE_APP_SWITCHER': enabled, 'APP_SWITCHER_MODE': mode}, clear=True), \ patch.object(health, 'get', side_effect=get) as mock_get, \ patch.object(health.subprocess, 'run'): diff --git a/tests/test_skill_client.py b/tests/test_skill_client.py index c2a4974..ef36f69 100644 --- a/tests/test_skill_client.py +++ b/tests/test_skill_client.py @@ -13,8 +13,8 @@ from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] -SKILL = ROOT / 'skills/weflow-api' -spec = importlib.util.spec_from_file_location('skill_client', SKILL / 'scripts/weflow_api.py') +SKILL = ROOT / 'skills/alpha-api' +spec = importlib.util.spec_from_file_location('skill_client', SKILL / 'scripts/alpha_api.py') client = importlib.util.module_from_spec(spec) spec.loader.exec_module(client) @@ -81,18 +81,18 @@ def close_server(self): self.server.server_close() def cli(self, *args, token=True, extra_env=None): - env = {'WEFLOW_API_BASE': self.base} + env = {'ALPHA_API_BASE': self.base} if token: - env['WEFLOW_API_TOKEN'] = self.server.test_token + env['ALPHA_API_TOKEN'] = self.server.test_token env.update(extra_env or {}) stdout, stderr = io.StringIO(), io.StringIO() - with patch.dict(os.environ, env, clear=True), patch('sys.argv', ['weflow_api.py', *args]), contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + with patch.dict(os.environ, env, clear=True), patch('sys.argv', ['alpha_api.py', *args]), contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): code = client.main() self.assertNotIn(self.server.test_token, stdout.getvalue() + stderr.getvalue()) return code, stdout.getvalue(), stderr.getvalue() def test_health_does_not_read_or_send_credentials(self): - code, out, _ = self.cli('health', extra_env={'WEFLOW_API_TOKEN_FILE': '/nonexistent'}) + code, out, _ = self.cli('health', extra_env={'ALPHA_API_TOKEN_FILE': '/nonexistent'}) self.assertEqual(code, 0) self.assertEqual(json.loads(out)['status'], 'ok') self.assertIsNone(self.server.requests[0][2]) @@ -100,7 +100,7 @@ def test_health_does_not_read_or_send_credentials(self): def test_missing_token_never_makes_authenticated_request(self): code, _, error = self.cli('sessions', token=False) self.assertEqual(code, 1) - self.assertIn('WEFLOW_API_TOKEN', error) + self.assertIn('ALPHA_API_TOKEN', error) self.assertEqual(self.server.requests, []) def test_query_encoding_time_units_pagination_and_bearer_header(self): @@ -130,9 +130,9 @@ def test_token_file_and_invalid_multiline_token(self): with tempfile.TemporaryDirectory() as directory: token_file = Path(directory) / 'token' token_file.write_text(self.server.test_token + '\n') - self.assertEqual(self.cli('sessions', token=False, extra_env={'WEFLOW_API_TOKEN_FILE': str(token_file)})[0], 0) + self.assertEqual(self.cli('sessions', token=False, extra_env={'ALPHA_API_TOKEN_FILE': str(token_file)})[0], 0) count = len(self.server.requests) - self.assertEqual(self.cli('sessions', extra_env={'WEFLOW_API_TOKEN': 'first\nsecond'})[0], 1) + self.assertEqual(self.cli('sessions', extra_env={'ALPHA_API_TOKEN': 'first\nsecond'})[0], 1) self.assertEqual(len(self.server.requests), count) def test_errors_do_not_echo_private_server_payloads(self): diff --git a/tests/test_window_control.py b/tests/test_window_control.py index 5d3a8ae..5582a61 100644 --- a/tests/test_window_control.py +++ b/tests/test_window_control.py @@ -23,7 +23,7 @@ def desktop(self, snapshot, remembered=None): def windows(self, active=11): return {'wechat': [{'id': 11, 'active': active == 11, 'minimized': False}, {'id': 12, 'active': active == 12, 'minimized': False}], - 'weflow': [{'id': 21, 'active': active == 21, 'minimized': False}, + 'alpha': [{'id': 21, 'active': active == 21, 'minimized': False}, {'id': 22, 'active': False, 'minimized': True}]} def test_active_app_cycles_and_only_hides_other_app(self): @@ -56,7 +56,7 @@ def test_explicit_window_does_not_cycle_or_maximize(self): self.assertEqual(desktop.send.call_args.args[1], '_NET_ACTIVE_WINDOW') def test_no_window_does_not_hide_other_app(self): - desktop = self.desktop({'wechat': [], 'weflow': self.windows()['weflow']}) + desktop = self.desktop({'wechat': [], 'alpha': self.windows()['alpha']}) self.assertIsNone(desktop.activate('wechat')) desktop.send.assert_not_called() @@ -69,7 +69,7 @@ def test_snapshot_requires_owned_vendor_pid_and_excludes_desktop_surfaces(self): (33, 101, ['_NET_WM_WINDOW_TYPE_DOCK']), (44, 201, [])]: window = Mock() window.get_geometry.return_value = types.SimpleNamespace(width=600, height=400) - window.get_wm_class.return_value = ('wechat',) if pid == 101 else ('weflow',) + window.get_wm_class.return_value = ('wechat',) if pid == 101 else ('alpha-desktop',) window.get_wm_name.return_value = 'Window' windows[wid] = window properties[wid] = {'_NET_WM_PID': [pid], '_NET_WM_WINDOW_TYPE': kind, @@ -84,5 +84,5 @@ def test_snapshot_requires_owned_vendor_pid_and_excludes_desktop_surfaces(self): lambda app: {101: ('1', True)} if app == 'wechat' else {201: ('2', True)}, {}) result = desktop.snapshot() self.assertEqual([w['id'] for w in result['wechat']], [11]) - self.assertEqual([w['id'] for w in result['weflow']], [44]) + self.assertEqual([w['id'] for w in result['alpha']], [44]) self.assertTrue(result['wechat'][0]['active']) diff --git a/tests/test_window_layout.py b/tests/test_window_layout.py index 272ec62..f6948c1 100644 --- a/tests/test_window_layout.py +++ b/tests/test_window_layout.py @@ -117,9 +117,9 @@ def test_closed_main_and_restarted_process_are_recognized(self): self.assertEqual(self.layout.maximize.call_count, 2) self.layout.maximize.assert_called_with(main) - def test_weflow_minimized_once_and_its_dialog_is_untouched(self): - self.windows[1] = Window(1, app='weflow', title='WeFlow') - self.windows[2] = Window(2, app='weflow', title='Settings') + def test_alpha_minimized_once_and_its_dialog_is_untouched(self): + self.windows[1] = Window(1, app='alpha-desktop', title='Alpha') + self.windows[2] = Window(2, app='alpha-desktop', title='Settings') self.tick(0) self.tick(2) self.assertEqual(self.layout.send.call_count, 1) @@ -133,7 +133,7 @@ def test_openbox_migration_keeps_user_rules_and_is_repeatable(self): '' 'yes' 'no' - '' + '' 'above') config.chmod(0o640) original = ET.parse(config).getroot() @@ -150,8 +150,8 @@ def test_openbox_migration_keeps_user_rules_and_is_repeatable(self): self.assertIsNotNone(root.find('.//{*}keybind')) self.assertIn(b'user preference', first) self.assertEqual(config.stat().st_mode & 0o777, 0o640) - wechat, weflow = rules[-2:] + wechat, alpha = rules[-2:] self.assertEqual(wechat.get('class'), 'wechat') self.assertEqual(wechat.find('{*}maximized').text, 'no') - self.assertEqual(weflow.attrib, {'class': 'weflow', 'title': 'WeFlow', 'type': 'normal'}) - self.assertEqual(weflow.find('{*}iconic').text, 'yes') + self.assertEqual(alpha.attrib, {'class': 'alpha-desktop', 'title': 'Alpha', 'type': 'normal'}) + self.assertEqual(alpha.find('{*}iconic').text, 'yes') diff --git a/versions.lock.json b/versions.lock.json index ebf1cef..902f1dc 100644 --- a/versions.lock.json +++ b/versions.lock.json @@ -2,7 +2,7 @@ "schema": 1, "project_version": "0.1.0", "platform": "linux/amd64", - "base_image": "ghcr.io/nickrunning/wechat-selkies:0.0.14-minimal@sha256:32b9094316b0bd267dc7d1778116a91a6a349fce20bb8dd652dbb9b38e0ef6ee", + "base_image": "ghcr.io/linuxserver/baseimage-selkies:debiantrixie@sha256:5b3f10459933740554081efbca9c8d5e006faf5116b54ab40b7bfad7c8111849", "wechat": { "version": "4.1.13.23", "filename": "wechat.deb", @@ -10,12 +10,15 @@ "url": "https://github.com/fliaping/weharbor/releases/download/component-assets/WeChatLinux_4.1.13.23_amd64.deb", "sha256": "b7d0f8d53e9f648bc2c77a6096a04100d008f2d9f0d3988a2a4859b5992aca0a" }, - "weflow": { - "version": "6.3.2", - "filename": "weflow.tar.gz", - "original_filename": "WeFlow-6.3.2-Setup.tar.gz", - "url": "https://github.com/fliaping/weharbor/releases/download/component-assets/WeFlow-6.3.2-Setup.tar.gz", - "sha256": "7cd7b930725200951190eb7357b3e8b4494290a7ad68ce1e541a667fc1c65c70", - "asar_sha256": "7e1b81c18e1ad18185e778e0603c1ade145f33ba2bcb668f12f2f025cb6d1ecf" + "selkies": { + "version": "2.0.0", + "note": "bundled by the LinuxServer.io baseimage-selkies debiantrixie image" + }, + "alpha": { + "version": "0.2.0", + "filename": "alpha.deb", + "original_filename": "Alpha-0.2.0-x64.deb", + "url": "https://github.com/xunchahaha/alpha-releases/releases/download/v0.2.0/Alpha-0.2.0-x64.deb", + "sha256": "9d79a56b88981c496818d93600698d11f8e21d759381c66c3fec3a10b38980d6" } } From 9d190f7d95a163d59ea35bef498d27529834f62f Mon Sep 17 00:00:00 2001 From: "Grok Bot (local)" Date: Sun, 18 Oct 2026 23:40:04 +0800 Subject: [PATCH 2/7] Document Alpha and the plain Selkies 2.0 base --- README.md | 163 ++++++++++++-------------------- docs/README.en.md | 116 ++++++++--------------- docs/api.md | 55 +++++------ docs/first-run.en.md | 172 +++++----------------------------- docs/first-run.md | 210 +++++++++--------------------------------- docs/install-skill.md | 36 ++++---- docs/operations.md | 42 ++++++--- docs/pwa.md | 2 +- 8 files changed, 238 insertions(+), 558 deletions(-) diff --git a/README.md b/README.md index 777483e..5fa9e92 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ 把微信桌面、聊天数据 API 和实时消息通知装进一个容器。 -WeHarbor 是面向个人自托管的微信服务集成项目。它基于 -wechat-selkies 的精简镜像,把官方 Linux 微信和 WeFlow 放在同一个 -浏览器桌面里,提供窗口切换、浏览器通知,以及 WeFlow 的 HTTP API / SSE +WeHarbor 是面向个人自托管的微信服务集成项目。它直接基于 LinuxServer.io 的 +Selkies 2.0 基础镜像(Debian trixie),把官方 Linux 微信和 Alpha 放在同一个 +浏览器桌面里,提供窗口切换、浏览器通知,以及 Alpha 的 HTTP API / MCP / SSE 消息推送。用户数据保存在一个 /config 卷中。 [English](docs/README.en.md) · [首次使用教程](docs/first-run.md) · [API 接入](docs/api.md) · @@ -14,55 +14,34 @@ wechat-selkies 的精简镜像,把官方 Linux 微信和 WeFlow 放在同一 ## 当前状态 -首版源码与本地构建版本为 0.1.0,源码仓库为 -[fliaping/weharbor](https://github.com/fliaping/weharbor)。预构建镜像已发布到 -ghcr.io/fliaping/weharbor:edge,可直接匿名拉取。edge 跟随 main;固定提交标签 -ghcr.io/fliaping/weharbor:sha-06cb008 已通过首次启动与重启测试。 -当前组合:WeChat 4.1.13.23、WeFlow 6.3.2、wechat-selkies 0.0.14-minimal。 -目前仅支持 linux/amd64;不提供未经验证的 ARM64 镜像。 +源码版本 0.1.0,仓库为 [fliaping/weharbor](https://github.com/fliaping/weharbor)。 +当前组合:WeChat 4.1.13.23、Alpha 0.2.0、Selkies 2.0.0 +(ghcr.io/linuxserver/baseimage-selkies:debiantrixie,按 digest 锁定)。 +目前仅支持 linux/amd64。 -微信和 WeFlow 的锁定版本原版安装包保存在本仓库的 component-assets Release。 -源码构建会自动下载并验证 SHA256,无需自行寻找安装包或配置下载地址。 -也可通过 WECHAT_DOWNLOAD_URL / WEFLOW_DOWNLOAD_URL 覆盖地址,校验值保持固定。 +> 注意:从 WeFlow 切换到 Alpha 的版本尚未发布预构建镜像,请本地构建。 +> Alpha 是闭源、未签名的二进制发行版,许可证未声明;其密钥助手内置 +> 2026-11-15 的有效期,需要随上游更新。详见 [组件许可](THIRD_PARTY_NOTICES.md)。 + +微信安装包从本仓库的 component-assets Release 下载,Alpha 安装包从 +xunchahaha/alpha-releases 的 v0.2.0 Release 下载;构建时都会校验锁定的 SHA256。 +也可通过 WECHAT_DOWNLOAD_URL / ALPHA_DOWNLOAD_URL 覆盖地址,校验值保持固定。 ## 能力 -- 在浏览器里使用官方 Linux 微信与 WeFlow,支持上游的中文输入和剪贴板。 -- 网页贴边应用条,可拖动、切换微信和 WeFlow、选择窗口、重启应用及管理通知。 -- WeFlow 默认最小化,微信默认获得焦点;登录后只自动最大化微信主窗口一次。 - 后续打开的聊天、设置等子窗口保持自己的尺寸,手动还原主窗口后不会被再次最大化。 +- 在浏览器里使用官方 Linux 微信与 Alpha(Selkies 2.0,WebSocket 传输,支持剪贴板)。 +- 网页贴边应用条,可拖动、切换微信和 Alpha、选择窗口、重启应用及管理通知。 +- Alpha 默认最小化,微信默认获得焦点;登录后只自动最大化微信主窗口一次。 - 将聊天推送与桌面通知转发为浏览器原生通知。 - 支持 PWA 安装,以独立窗口从电脑桌面或手机主屏幕打开。 -- 通过 WeFlow 查询会话、消息、联系人与群成员,并订阅 SSE 消息事件。 -- 单容器、单数据卷;保留原版 WeFlow ASAR 和原生组件。 - -首次使用仍需要手机扫码登录微信,并在 WeFlow 中完成账号与数据库配置。 -这里提供的数据 API 不承诺自动发送微信消息或实现机器人协议。 - -## 使用预构建镜像 - -需要 Linux amd64 主机、Docker Engine 和 Compose v2。先获取配置并初始化: - -~~~bash -git clone https://github.com/fliaping/weharbor.git -cd weharbor -./scripts/init.sh -~~~ - -把 .env 中的 WEHARBOR_IMAGE 设置为 ghcr.io/fliaping/weharbor:edge,然后启动: - -~~~bash -docker compose pull -docker compose up -d -~~~ +- 通过 Alpha 查询会话、消息、联系人与群成员,订阅 SSE 消息事件,或接入 MCP。 +- 单容器、单数据卷;微信和 Alpha 均以原版 deb 安装,不修改应用文件。 -打开 https://localhost:3001,使用 .env 中生成的网页登录凭据。 -首次登录、数据库与 API 配置见 [首次使用教程](docs/first-run.md)。 +首次使用仍需要手机扫码登录微信,并在 Alpha 向导中完成账号与密钥配置。 ## 本地构建与启动 -需要 Linux amd64 主机、Docker Engine、Compose v2 / Buildx 和 Python 3.10+。 -获取源码后,在仓库目录执行: +需要 Linux amd64 主机、Docker Engine、Compose v2 / Buildx 和 Python 3.10+: ~~~bash git clone https://github.com/fliaping/weharbor.git @@ -72,70 +51,48 @@ cd weharbor docker compose up -d ~~~ -也可运行 ./scripts/quickstart.sh,自动准备安装包、初始化、构建缺失的 -本地镜像并启动。配置预构建镜像地址后,同一入口会拉取镜像再启动。 - -build.sh 会从本仓库的 component-assets Release 下载锁定版本的两个安装包。 -也可提前把 WeFlow-6.3.2-Setup.tar.gz 放到 downloads/weflow.tar.gz, -把 WeChatLinux_4.1.13.23_amd64.deb 放到 downloads/wechat.deb。 -Release 中仅保存校验通过的原版安装包;下载到其他版本时构建会明确失败。 +build.sh 会下载并校验两个锁定版本的安装包。离线构建可提前把 +Alpha-0.2.0-x64.deb 放到 downloads/alpha.deb,把 +WeChatLinux_4.1.13.23_amd64.deb 放到 downloads/wechat.deb。 打开 https://localhost:3001,使用 .env 中的 CUSTOM_USER 和 PASSWORD 登录。 首次 HTTPS 使用自签名证书。init.sh 会生成随机网页登录密码,并自动设置 宿主用户 UID/GID;已有 .env 不会被覆盖。不要把 .env 提交到 Git。 默认只绑定本机地址。需要从其他机器访问时,在 .env 设置 -BIND_ADDRESS=0.0.0.0,再运行 docker compose up -d。 -通过正式域名访问时,用支持 WebSocket / SSE 的 HTTPS 反向代理转发桌面端口。 - -使用可信 HTTPS 域名打开后,可点击“安装 WeHarbor”安装为桌面应用。 -iPhone / iPad 使用 Safari 的“添加到主屏幕”。局域网 HTTP IP 与不可信的 -自签名证书可能无法安装;详见 [PWA 安装说明](docs/pwa.md)。 - -已有部署切换到预构建镜像时,把 .env 的 WEHARBOR_IMAGE 改成上面的镜像地址,然后: - -~~~bash -docker compose pull -docker compose up -d -~~~ +BIND_ADDRESS=0.0.0.0,或使用 Tailscale Serve / 支持 WebSocket 的 HTTPS 反向代理 +转发 3001 端口。不建议直接暴露到公网。 ## 首次设置 -第一次使用请按 [完整教程](docs/first-run.md) 操作,每一步都有完成标志和排障说明。 +按 [完整教程](docs/first-run.md) 操作: -1. 先扫码登录微信,等待本地账号与聊天数据库生成。 -2. 在 WeFlow 向导中自动检测数据库目录,通常为 - /config/Documents/xwechat_files(兼容链接指向 /config/xwechat_files)。 -3. 获取密钥前退出微信登录、关闭自动登录;点击“自动获取密钥”, - 等 WeFlow 准备完成后再在手机上确认登录。Linux 流程会重启微信。 -4. 完成配置,在 WeFlow 中打开一个会话并确认可以读到消息。图片密钥可另行配置。 -5. 启用 HTTP API,使用 0.0.0.0 和 .env 中的 WEFLOW_API_PORT(默认 5031), - 生成 API Token;先查会话和消息验证读取,再安装 AI skill。 +1. 扫码登录微信,等待本地账号与聊天数据库生成。 +2. 在 Alpha 首次设置向导中勾选“我已了解”,点击“连接微信”,选择账号与数据目录 + (/config/Documents/xwechat_files,兼容链接指向 /config/xwechat_files)。 +3. 按向导获取数据库密钥(可选获取图片密钥),完成设置后确认能看到聊天记录。 +4. 在 Alpha 设置中启用 HTTP API(端口 5031),复制访问 Token; + 需要实时通知时同时开启主动推送。 -需要实时通知时再开启 WeFlow 的主动推送,并在浏览器桌面点击“启用消息通知”。 -容器健康、API health 正常和 skill 安装成功,均不代表账号数据库已经可读。 +Alpha 的 API 只监听容器内 127.0.0.1:5031;容器内 nginx 在 5032 端口转发, +Compose 默认映射到宿主机 127.0.0.1:${API_PORT:-5031}。所有请求(/health 除外) +仍需 Alpha 的 Bearer Token;带浏览器 Origin 头的请求会被拒绝。 -浏览器通知需要 HTTPS 或 localhost,并且桌面页面保持打开。 -WeFlow 的 API Token 与网页登录密码分开管理。API 默认仅映射到宿主机回环地址。 +浏览器消息通知需要把 Alpha Token 保存到 /config/.config/weharbor/alpha-api-token +(权限 600),或设置 ALPHA_API_TOKEN 环境变量,因为 Alpha 把 Token 加密保存在 +密钥环中,通知桥无法直接读取。 ## 接入 AI 工具 -仓库提供可安装的 weflow-api skill,附带无需 pip 依赖的 Python 调用脚本。 -把下面的 URL 交给支持 Agent Skills、能执行命令的 AI 工具,并说 -“请安装这个 skill,并帮我检查连接配置”: - -https://raw.githubusercontent.com/fliaping/weharbor/main/docs/install-skill.md - -也可在 AI 项目目录直接执行: +仓库提供 alpha-api skill,附带无需 pip 依赖的 Python 调用脚本: ~~~bash -npx --yes skills add https://github.com/fliaping/weharbor --skill weflow-api --yes +npx --yes skills add https://github.com/fliaping/weharbor --skill alpha-api --yes ~~~ -使用者自行在 AI 执行环境配置 WEFLOW_API_BASE 与 WEFLOW_API_TOKEN, -或通过 WEFLOW_API_TOKEN_FILE 读取本地凭据文件。skill 支持聊天查询、群聊摘要、 -联系人、群成员和限时 SSE 订阅;Token 不需要提交到仓库或发送到聊天中。 -详细安装、联网与版本兼容说明见 [安装页](docs/install-skill.md)。 +在 AI 执行环境配置 ALPHA_API_BASE 与 ALPHA_API_TOKEN(或 ALPHA_API_TOKEN_FILE)。 +客户端最初为 WeFlow API 编写;Alpha 的路由名称一致,但参数与返回字段仍待在真实 +实例上核对。详见 [安装页](docs/install-skill.md)。 ## 配置 @@ -143,20 +100,23 @@ npx --yes skills add https://github.com/fliaping/weharbor --skill weflow-api --y | --- | --- | --- | | WEHARBOR_IMAGE | weharbor:0.1.0 | 镜像地址与版本 | | DATA_DIR | ./data | 持久化数据目录 | -| CUSTOM_USER / PASSWORD | 自动生成 | 浏览器桌面登录 | +| CUSTOM_USER / PASSWORD | 自动生成 | 浏览器桌面登录(nginx Basic Auth) | | BIND_ADDRESS | 127.0.0.1 | 桌面端口绑定地址 | | HTTP_PORT / HTTPS_PORT | 3000 / 3001 | 桌面入口 | -| API_BIND_ADDRESS | 127.0.0.1 | API 绑定地址 | -| WEFLOW_API_PORT | 5031 | 须与 WeFlow 设置一致 | +| API_BIND_ADDRESS / API_PORT | 127.0.0.1 / 5031 | 宿主机上的 Alpha API 地址(映射到容器 5032) | +| AUTO_START_WECHAT / AUTO_START_ALPHA | true | 自动启动应用 | | PUID / PGID | init.sh 检测 | 持久化文件所属用户 | | ENABLE_APP_SWITCHER | true | 应用切换入口 | | APP_SWITCHER_MODE | web | 网页贴边应用条;desktop 恢复旧桌面切换器 | -| ENABLE_WINDOW_DEFAULTS | true | 初始窗口布局;关闭后保留自定义窗口行为 | +| ENABLE_WINDOW_DEFAULTS | true | 初始窗口布局 | | ENABLE_BROWSER_NOTIFICATIONS | true | 浏览器通知 | GPU 加速可选:宿主机存在 /dev/dri 时运行 docker compose -f compose.yaml -f compose.gpu.yaml up -d。 -普通部署不需要 GPU、宿主网络或特权容器;SYS_PTRACE 用于 WeFlow 的进程密钥助手。 +SYS_PTRACE 供 Alpha 读取微信进程内存以获取数据库密钥。 + +与 wechat-selkies 时期相比,微信自动点击登录与夜间定时重启已移除; +容器内暂未预装中文输入法。 ## 开发与贡献 @@ -167,20 +127,13 @@ python3 scripts/check.py ~~~ 版本、安装包 SHA256 与基础镜像 digest 集中在 versions.lock.json。 -构建过程不修改 WeFlow 应用文件。root/ 只保存本项目的集成层;上游桌面与微信 -启动脚本继续从基础镜像复用。 - -GitHub Actions 在 main 推送、PR、版本标签和手动触发时构建并测试镜像。 -main 的测试通过后发布 GHCR 的 edge / sha 标签,版本标签发布固定版本 / latest。 -安装包默认从固定 Release 下载并校验,无需配置额外 Actions 变量。 -部署与发布说明见 docs/operations.md。 +root/ 只保存本项目的集成层(启动脚本、窗口布局、通知桥、API 代理、旧配置迁移)。 ## 许可与致谢 -WeHarbor 的集成代码使用 MIT 许可证。微信、WeFlow、Selkies 及其他组件保留 -各自许可;本仓库的 MIT 不会改变这些组件的使用和分发条件。 -WeFlow 已公开的源码许可含非商业限制,所用 6.3.2 二进制及原生组件的授权 -还需在公开镜像发布前核实。详见 THIRD_PARTY_NOTICES.md。 +WeHarbor 的集成代码使用 MIT 许可证。微信、Alpha、Selkies 及其他组件保留 +各自许可;Alpha 的分发条款未明确,公开发布组合镜像前需先核实。 +详见 THIRD_PARTY_NOTICES.md。 -感谢 wechat-selkies、LinuxServer.io、Selkies 和 WeFlow 的维护者。 -WeHarbor 是独立的社区集成项目。 +感谢 LinuxServer.io、Selkies、wechat-selkies 与 WeFlow 项目的维护者, +以及 Alpha 的作者。WeHarbor 是独立的社区集成项目。 diff --git a/docs/README.en.md b/docs/README.en.md index 1e53897..61dd5ec 100644 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -1,105 +1,65 @@ # WeHarbor A self-hosted WeChat desktop, local data API and message notifications in one -container. WeHarbor combines the pinned minimal wechat-selkies image, official -Linux WeChat and an unmodified WeFlow distribution. +container. WeHarbor builds directly on the LinuxServer.io Selkies 2.0 base image +(Debian trixie) and installs the official Linux WeChat and Alpha, unmodified. -Version 0.1.0 currently includes WeChat 4.1.13.23 and WeFlow 6.3.2, on linux/amd64. -Source: [fliaping/weharbor](https://github.com/fliaping/weharbor). -The prebuilt image is available for anonymous pulls as ghcr.io/fliaping/weharbor:edge. -The edge tag follows main; ghcr.io/fliaping/weharbor:sha-06cb008 pins the first -build that passed fresh-profile and restart tests. +Version 0.1.0 includes WeChat 4.1.13.23, Alpha 0.2.0 and Selkies 2.0.0 +(`ghcr.io/linuxserver/baseimage-selkies:debiantrixie`, pinned by digest), on +linux/amd64 only. Source: [fliaping/weharbor](https://github.com/fliaping/weharbor). -## Use the prebuilt image +> The Alpha-based build has no published prebuilt image yet; build locally. +> Alpha is a closed-source, unsigned binary with no declared license, and its +> key helper embeds an expiry (2026-11-15) that requires upstream updates. -Requirements: Docker Engine, Compose v2 and a Linux amd64 host. +## Build and start + +Requirements: Docker Engine with Compose v2 / Buildx, Python 3.10+, amd64 host. ~~~bash git clone https://github.com/fliaping/weharbor.git cd weharbor ./scripts/init.sh -~~~ - -Set WEHARBOR_IMAGE=ghcr.io/fliaping/weharbor:edge in .env, then run: - -~~~bash -docker compose pull +./scripts/build.sh docker compose up -d ~~~ -Open https://localhost:3001 using the generated credentials in .env. -Follow the [first-run guide](first-run.en.md) to configure WeChat and WeFlow. +The pinned WeChat deb (component-assets Release) and Alpha deb +(xunchahaha/alpha-releases v0.2.0) are downloaded and SHA256-verified. +WECHAT_DOWNLOAD_URL / ALPHA_DOWNLOAD_URL can override the addresses. -## Build and start +Open https://localhost:3001 with CUSTOM_USER / PASSWORD from .env, then follow +the [first-run guide](first-run.en.md): log in to WeChat, complete Alpha's setup +wizard, and enable Alpha's HTTP API (port 5031). -Requirements: Docker Engine with Compose v2 / Buildx, Python 3.10+ and an amd64 host. -The pinned WeFlow 6.3.2 archive and WeChat deb are downloaded automatically from -this repository's component-assets Release. Every package must match the pinned -SHA256. The Release contains only original installers and their checksums, with -no user profiles or credentials. WEFLOW_DOWNLOAD_URL / WECHAT_DOWNLOAD_URL can -override the download addresses while retaining checksum verification. +Alpha listens on 127.0.0.1:5031 inside the container and rejects foreign Host +and Origin headers. An in-container nginx proxy on 5032 forwards to it, and +Compose publishes that on host `${API_BIND_ADDRESS}:${API_PORT}` (default +127.0.0.1:5031). Alpha's bearer token is still required for every route except +/health. For browser notifications, save the token to +/config/.config/weharbor/alpha-api-token. -~~~bash -git clone https://github.com/fliaping/weharbor.git -cd weharbor -./scripts/init.sh -./scripts/build.sh -docker compose up -d -~~~ +Alpha starts minimized and WeChat receives focus; only the WeChat main window +is maximized once after login. PWA installation is supported over trusted HTTPS +(see [pwa.md](pwa.md)). All state persists in ./data mounted at /config. -Open https://localhost:3001 with the browser credentials stored in .env. - -WeFlow initially opens minimized and WeChat receives focus. After login, only -the identified WeChat main window is maximized once. Later child windows keep -their own size; manually restoring the main window is respected. Set -ENABLE_WINDOW_DEFAULTS=false to disable the helper. - -PWA installation is supported: open the desktop over HTTPS with a trusted -certificate and click “安装 WeHarbor” when Chrome/Edge offers installation. -On iPhone/iPad, use Safari's Share → Add to Home Screen. Plain HTTP on a LAN IP -and untrusted certificates may prevent installation. Installed apps still need -the server and network; chat data and desktop frames are never cached by the -Service Worker. See [installation details](pwa.md). -init.sh generates a random password and detects your UID/GID without overwriting -existing settings. The default bind address is localhost. Set BIND_ADDRESS=0.0.0.0 -to permit access from other machines while retaining browser authentication. - -Follow the [first-run guide](first-run.en.md) after opening the desktop. -First log in to create local data, then configure the database in WeFlow. -Linux key acquisition restarts WeChat: disable automatic login, log out, -start acquisition, and confirm login only after WeFlow is ready. -The usual database path is /config/Documents/xwechat_files. -Verify readable messages in WeFlow before enabling the API or installing a skill. -Enable WeFlow's HTTP API on 0.0.0.0:5031, generate an API token, and enable its -message push feature. Browser notifications require HTTPS or localhost and an -open desktop page. API ports are published to host loopback by default. - -All profiles, databases and settings persist in ./data, mounted at /config. -Keep this directory and .env out of Git. Stop the container before backing it up. - -The image workflow builds and tests main pushes, PRs, version tags and manual -runs. Successful main builds publish GHCR edge / sha tags; matching version tags -publish the version / latest. No additional download variables are required. -PRs never publish, and only the exact image that passed the smoke test is pushed. +Compared with the former wechat-selkies/WeFlow image, WeChat auto-login clicking +and nightly restarts were removed, and no Chinese IME is preinstalled. +Existing profiles are migrated automatically (see [operations](operations.md)). [First-run guide](first-run.en.md) · [API usage](api.md) · [Operations](operations.md) · -[AI skill installation](install-skill.md) · -[Third-party notices](../THIRD_PARTY_NOTICES.md) +[AI skill installation](install-skill.md) · [Third-party notices](../THIRD_PARTY_NOTICES.md) -Install the bundled weflow-api Agent Skill from your AI project directory: +Install the bundled alpha-api Agent Skill from your AI project directory: ~~~bash -npx --yes skills add https://github.com/fliaping/weharbor --skill weflow-api --yes +npx --yes skills add https://github.com/fliaping/weharbor --skill alpha-api --yes ~~~ -Give your AI this installation URL: -https://raw.githubusercontent.com/fliaping/weharbor/main/docs/install-skill.md -The client needs Python 3.10+, a reachable WEFLOW_API_BASE and a locally configured -WEFLOW_API_TOKEN (or WEFLOW_API_TOKEN_FILE). No pip dependencies are required. -Installation does not configure or disclose account credentials. +Configure ALPHA_API_BASE and ALPHA_API_TOKEN (or ALPHA_API_TOKEN_FILE) locally. +The client was written for WeFlow's API; Alpha uses the same route names, but +parameters and response fields are not yet verified against a live instance. WeHarbor integration code is MIT licensed. Vendor components keep their own -licenses. WeFlow's published source license includes a noncommercial condition; -the exact 6.3.2 binary and native component redistribution terms must still be -confirmed before publishing a combined image. The repository's MIT license does -not apply to every component inside the image. +licenses; Alpha's redistribution terms are unclear, so do not publish a combined +image until they are confirmed. diff --git a/docs/api.md b/docs/api.md index a506d54..f52f3a4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,53 +1,48 @@ # API 接入 -AI 工具可安装仓库自带的 [weflow-api skill](install-skill.md), +AI 工具可安装仓库自带的 [alpha-api skill](install-skill.md), 通过 Python 客户端执行下面的查询,并使用本地环境变量管理凭据。 -API 由原版 WeFlow 提供。先在设置中启用 HTTP API,设置监听地址为 0.0.0.0、 -端口为 Compose 配置的 WEFLOW_API_PORT,并生成 Token。 -Compose 只负责端口映射,不会覆盖 WeFlow 的持久化设置。 -首次接入前,先按 [首次使用教程](first-run.md) 完成数据库密钥配置并在 -WeFlow 界面确认消息可读。服务已启动时需先关闭,再编辑监听地址和端口。 - -下面的例子从环境变量读取 Token;请在本地设置 WEFLOW_API_TOKEN。 -不要把实际 Token 放入文档、源码或公开日志。 +API 由原版 Alpha 0.2.0 提供,需先在 Alpha 设置中启用 HTTP API(端口 5031)并生成 +Token。Alpha 只监听容器内的 127.0.0.1,并拒绝 Host 不是 `127.0.0.1:5031` / +`localhost:5031` 或带 Origin 头的请求。WeHarbor 在容器内用 nginx 监听 +`ALPHA_API_PROXY_PORT`(默认 5032)转发并改写 Host,同时拒绝带 Origin 的请求; +Compose 把它映射到宿主机 `${API_BIND_ADDRESS}:${API_PORT}`(默认 127.0.0.1:5031)。 +首次接入前,先按 [首次使用教程](first-run.md) 完成 Alpha 向导并确认消息可读。 ~~~bash # 健康检查不需要 Token。 curl --fail http://127.0.0.1:5031/health -# 查询会话。 -curl --fail -H "Authorization: Bearer $WEFLOW_API_TOKEN" \ - http://127.0.0.1:5031/api/v1/sessions - -# 查询联系人。 -curl --fail -H "Authorization: Bearer $WEFLOW_API_TOKEN" \ - http://127.0.0.1:5031/api/v1/contacts +curl --fail -H "Authorization: Bearer $ALPHA_API_TOKEN" http://127.0.0.1:5031/api/v1/sessions +curl --fail -H "Authorization: Bearer $ALPHA_API_TOKEN" http://127.0.0.1:5031/api/v1/contacts -# 订阅实时消息,使用请求头传递 Token。 -curl --no-buffer \ - -H "Authorization: Bearer $WEFLOW_API_TOKEN" \ +# 实时消息 SSE(需在 Alpha 中开启主动推送)。 +curl --no-buffer -H "Authorization: Bearer $ALPHA_API_TOKEN" \ http://127.0.0.1:5031/api/v1/push/messages ~~~ -API 也接受 access_token 查询参数;界面生成的 SSE 地址可能包含它。 -使用这类地址时,反向代理访问日志应避免记录原始查询串。 -消息推送还需要在 WeFlow 设置中开启主动推送。 +API 也接受 `access_token` 查询参数;反向代理日志应避免记录原始查询串。 | 接口 | 用途 | | --- | --- | -| GET /health | API 存活检查 | +| GET /health, /api/v1/health | 存活检查(无需 Token) | +| GET /api/v1/accounts | 账号 | | GET /api/v1/sessions | 会话列表 | +| GET /api/v1/sessions/{id}/messages | 会话消息(ChatLab 分页) | | GET /api/v1/messages | 按条件查询消息 | +| GET /api/v1/search | 搜索 | | GET /api/v1/contacts | 联系人 | | GET /api/v1/group-members | 群成员 | +| GET /api/v1/sns/timeline 等 | 朋友圈 | +| GET /api/v1/media/... | 媒体 | +| GET /api/v1/exports, /api/v1/exports/formats | 导出 | | GET /api/v1/push/messages | 实时消息 SSE | +| POST /mcp | MCP(Streamable HTTP);也可用 /opt/Alpha/resources/bin/alpha-mcp-stdio | +| POST /api/v1/messages/send-text | 发送文本消息(Alpha 能力,谨慎授权) | -详细参数和响应结构以所用 WeFlow 版本为准。当前集成侧重本地聊天数据读取、 -查询与事件订阅。客户端在线并不代表账号数据库已经配置成功。 -health 只检查服务存活。继续带 Token 查询 sessions,再使用返回会话的 -username 作为 messages 的 talker 查询少量消息,才完成读取验证。 +路由列表来自 Alpha 0.2.0 安装包的静态分析,参数与响应结构尚未在真实实例上核对。 +health 只检查服务存活;带 Token 查询 sessions 并读取少量消息才算完成验证。 -其他容器接入时,建议让它们与 WeHarbor 使用同一个自定义 Docker 网络, -通过服务名 weharbor:5031 访问;外部调用仍需使用 WeFlow Token。 -桌面反向代理需要转发 WebSocket,并关闭 /notifications/ 路径的响应缓冲。 +其他容器接入时,可与 WeHarbor 使用同一 Docker 网络,通过 `weharbor:5032` 访问; +请求仍需 Alpha Token,且不能带 Origin 头。 diff --git a/docs/first-run.en.md b/docs/first-run.en.md index d37bdad..585e767 100644 --- a/docs/first-run.en.md +++ b/docs/first-run.en.md @@ -1,147 +1,25 @@ -# First run: from WeChat login to your first readable message - -This guide targets WeHarbor 0.1.0 with Linux WeChat 4.1.13.23 and WeFlow 6.3.2. -Other versions may have different labels or key acquisition steps. -Complete [build and startup](README.en.md#build-and-start) before continuing. - -WeFlow reads your local account's chat database using its database key. -WeChat login, database access and API authentication are separate steps. -Installing the Agent Skill does not configure the account or acquire its key. - -| Stage | Evidence of success | -| --- | --- | -| Desktop | Both application windows are available in the browser | -| WeChat login | Conversations appear and local account data exists | -| Database key | WeFlow reports success and selects the matching account | -| Database connection | A conversation opens in WeFlow with readable messages | -| API | Health works, then authenticated sessions and messages work | -| AI | The AI's execution environment reaches the API and reads a few messages | - -## Open the desktop and log in once - -Open `https://localhost:3001` on the Docker host and log in using `CUSTOM_USER` -and `PASSWORD` from your local `.env`. Use your configured HTTPS port if different. -The initial certificate is self-signed. For a remote host, an SSH tunnel keeps -the default loopback binding usable: - -~~~bash -ssh -N -L 3001:127.0.0.1:3001 user@your-docker-host -~~~ - -Open the same URL on the computer running that tunnel. Use the small web dock -at the right edge to select WeChat. Drag it to either edge to reposition it. -Expand an application's window list to select a specific window. -Scan its QR code with your phone, confirm login, -and wait for conversations and recent messages to appear. Open a conversation -to confirm text is available. This first login creates the local account data. - -WeFlow reads what this Linux instance has stored locally. It does not -automatically obtain the phone's complete history. Compare missing history -with what the Linux WeChat client itself can display. - -## Select the database directory - -WeFlow's first-run wizard has Welcome, Database Directory, Cache Directory, -Decryption Key, Image Key and Security steps. The current UI labels are -“欢迎”, “数据库目录”, “缓存目录”, “解密密钥”, “图片密钥” and “安全防护”. - -Choose “自动检测” in the database step. The usual root is -`/config/Documents/xwechat_files`, a compatibility link to -`/config/xwechat_files`. If detection fails, browse to the actual root and -compare it with WeChat's storage location. Use container paths, not the -host's `./data` path. An account subdirectory must contain `db_storage`. - -Leave the cache directory empty for the default. Custom caches should also -be under `/config` so they survive container replacement. - -## Acquire the database key, then confirm login - -**On Linux, expect another login. Wait for WeFlow to prepare before confirming it.** -Version 6.3.2 stops and relaunches WeChat, then uses its bundled helper to -capture the database key during login. - -1. Disable WeChat's automatic login and log out, leaving it at the login screen. - If your `.env` has `ENABLE_WECHAT_AUTO_LOGIN=true`, temporarily set it to - `false` and run `docker compose up -d` before acquisition. The startup - script and WeChat's own automatic login option are separate controls. -2. In “解密密钥”, select “自动获取密钥” and confirm its instructions. - WeChat closing and reopening is part of this version's preparation. -3. Wait for WeFlow to indicate it is ready for login, or waiting for the key - after authorization. Complete an authorization dialog if one appears. - WeHarbor includes `SYS_PTRACE`, polkit and container authorization rules. -4. Switch to WeChat, scan or select login, and confirm on your phone. -5. Wait for “密钥获取成功”. Verify that the key field is populated and the - selected account matches the one you just logged in to. - -If WeFlow cannot launch WeChat, open it from the desktop and use -“我已看到登录窗口,继续”. Still wait for preparation before confirming login. -If it times out or you logged in too early, log out and repeat this sequence. -Keep the database key in WeFlow's local configuration; it is not an API token -and does not need to be sent to an AI or committed to GitHub. - -## Finish configuration and verify messages - -Image XOR / AES keys decode images and are separate from the database key. -The image step tries to calculate them from local caches. If no suitable -images are cached, open a few images in WeChat, wait for downloads, and use -“重新计算”. “内存扫描” is another option while WeChat is running. - -The first-run wizard can continue without image keys. Verify text first and -configure image keys later if needed. The optional application lock has its -own password, separate from the desktop password and API token. - -Select “完成配置”. WeFlow tests and opens the database. Open a conversation -and compare a few text messages with WeChat. You can also use -Settings → “数据库连接” → “测试连接”. Container health alone does not verify this. - -## Enable and test the API - -In Settings → “API 服务”, turn off a running HTTP API before editing its -address or port. Set the address to `0.0.0.0`, set the port to your `.env` -`WEFLOW_API_PORT` (default `5031`), generate an Access Token with -“随机生成”, and enable the HTTP API. Its status should be “运行中”. - -Listening on all container interfaces allows Docker forwarding. The default -Compose configuration still publishes the API only to the host loopback. -Changing `.env` does not update WeFlow's persisted API settings. Enable -“主动推送” only when you need notifications or SSE, and check its session filters. - -Use the [API examples](api.md) to check health, query a few authenticated -sessions, then pass a returned session's `username` as the messages `talker`. -The sessions and messages responses should have `success: true` and contain -the data you verified in WeFlow. `/health` only checks that the server is alive. - -## Connect the AI skill - -Follow the [skill installation page](install-skill.md). Configure -`WEFLOW_API_BASE` and `WEFLOW_API_TOKEN`, or `WEFLOW_API_TOKEN_FILE`, in -the AI's execution environment. Its `localhost` is not necessarily your -Docker host. Another container on the same Docker network can use -`http://weharbor:5031`; remote tools need an appropriate network or tunnel. - -Ask the AI to check health, list a few sessions, and read a few messages -from a conversation you select. Configure summaries or SSE after this works. -Do not paste the database key or actual API token into the AI conversation. - -## Troubleshooting - -| Symptom | Check first | -| --- | --- | -| Healthy container but no messages | Wizard completion, account selection and database connection | -| No account / missing `db_storage` | Initial WeChat login and the container's database root path | -| Key acquisition times out | Automatic or premature login; log out and repeat the preparation sequence | -| Authorization / Hook fails | Compose retains `SYS_PTRACE`; polkit is running; inspect a redacted error | -| Connection fails with a key | Directory, key and selected account belong together | -| Text works but images fail | Downloaded local media and separate image keys | -| Missing history | Whether Linux WeChat itself has those messages | -| API connection refused | API running, `0.0.0.0` inside the container, matching port settings | -| API returns 401 | Correct API token rather than desktop password or database key | -| No SSE events | Push enabled, matching filters, WeChat online and actual new messages | -| Skill installed but cannot read | AI network access and credentials in the execution environment | - -Retain the same `/config` volume after restarting. Valid account settings and -keys persist, although WeChat may ask you to confirm login again. Reacquire a -key when needed for an account change or invalid key, not on every restart. -Back up the complete profile before upgrades; see [operations](operations.md). -When filing an issue, share versions, the failing stage and a redacted error, -not complete logs, databases, account configuration or screenshots of keys. +# First run: from WeChat login to the first message + +Applies to WeHarbor 0.1.0 with WeChat 4.1.13.23, Alpha 0.2.0 and Selkies 2.0. +Alpha wizard screens after the first step have not been verified in detail. + +[中文](first-run.md) + +1. **Open the desktop** at `https://localhost:3001` with `CUSTOM_USER`/`PASSWORD` + from `.env`. WeChat and Alpha start automatically (Alpha minimized). +2. **Log in to WeChat** by scanning the QR code (or pressing 登录 for a saved + account) and confirming on the phone. Automatic login clicking was removed. +3. **Run Alpha's setup wizard**: tick "我已了解", click "连接微信", pick the + account and data directory `/config/Documents/xwechat_files`, let Alpha + acquire the database key (WeChat must stay logged in; the container has + SYS_PTRACE), optionally the image key, then confirm messages are visible. +4. **Enable the HTTP API** in Alpha's settings (port 5031) and copy the token. + Host port 5031 maps to the in-container nginx proxy on 5032, which forwards + to Alpha on 127.0.0.1:5031 and rejects browser Origin headers. + For browser notifications, save the token to + `/config/.config/weharbor/alpha-api-token` (mode 600). +5. **Connect an AI tool** with the alpha-api skill (see install-skill.md). + +Alpha stores its API token with Electron safeStorage; the container runs a +password-less gnome-keyring, so the token is protected by the data volume's +access control. Alpha 0.2.0's key helper embeds an expiry of 2026-11-15. diff --git a/docs/first-run.md b/docs/first-run.md index 3930491..bfac71f 100644 --- a/docs/first-run.md +++ b/docs/first-run.md @@ -1,201 +1,81 @@ # 首次使用:从登录微信到读取第一条消息 -本页适用于 WeHarbor 0.1.0 的 Linux 组合:WeChat 4.1.13.23、WeFlow 6.3.2。 -界面名称和获取密钥的顺序按此版本说明;其他版本可能不同。 +本页适用于 WeHarbor 0.1.0 的组合:WeChat 4.1.13.23、Alpha 0.2.0、Selkies 2.0。 +Alpha 向导第一步之后的界面文字尚未在本项目中逐一核对,以实际界面为准。 先按 [README](../README.md#本地构建与启动) 完成构建和启动,再从这里继续。 [English](first-run.en.md) -WeFlow 使用本机微信账号的数据库密钥读取该账号的本地聊天数据库。 -手机扫码登录、数据库解密、API 鉴权是三个独立步骤;安装 skill 不会替你完成前两步。 +Alpha 通过读取本机微信进程内存获取数据库密钥,再只读打开该账号的本地聊天数据库。 +手机扫码登录、获取密钥、API 鉴权是三个独立步骤;安装 skill 不会替你完成前两步。 | 阶段 | 完成标志 | | --- | --- | -| 打开桌面 | 能在浏览器中看到微信和 WeFlow 窗口 | +| 打开桌面 | 能在浏览器中看到微信和 Alpha 窗口 | | 登录微信 | 微信会话列表出现,本地账号数据已生成 | -| 获取数据库密钥 | WeFlow 显示获取成功,并选中了对应账号 | -| 连接数据库 | WeFlow 完成向导,能打开一个会话并看到消息 | +| 连接微信 | Alpha 向导识别到账号与数据目录并取得数据库密钥 | +| 读取数据 | Alpha 完成向导,能打开一个会话并看到消息 | | 开启 API | health 正常,带 Token 能查询会话和消息 | | 接入 AI | AI 所在环境能访问 API,并成功读取少量消息 | ## 1. 打开浏览器桌面 -在 Docker 主机上打开 `https://localhost:3001`,使用本地 `.env` 中的 -`CUSTOM_USER` 和 `PASSWORD` 登录。若修改了 `HTTPS_PORT`,使用实际端口。 -首次使用自签名证书时,核对访问地址后按浏览器提示继续。 +在 Docker 主机上打开 `https://localhost:3001`,使用 `.env` 中的 +`CUSTOM_USER` 和 `PASSWORD` 登录。首次使用自签名证书时,核对地址后继续。 +在其他电脑访问可使用 SSH 隧道(`ssh -N -L 3001:127.0.0.1:3001 user@host`) +或 Tailscale Serve(`tailscale serve --bg --https=443 https+insecure://127.0.0.1:3001`,仅 tailnet 可见)。 -默认桌面端口只绑定宿主机回环地址。在另一台电脑访问时,可以使用 SSH 隧道: +微信和 Alpha 会自动启动,Alpha 初始最小化。点击网页右侧的小条展开应用管理切换窗口。 +容器 healthy 只代表程序和桌面服务正常,不代表已经能读取聊天记录。 -~~~bash -ssh -N -L 3001:127.0.0.1:3001 user@your-docker-host -~~~ - -然后在这台电脑打开同一个 `https://localhost:3001`。正式域名和反向代理部署 -见 [README](../README.md#本地构建与启动)。 - -微信和 WeFlow 会自动启动。点击网页右侧的小条展开应用管理,在两者间切换。 -小条可拖到左右边缘,位置会记在当前浏览器;窗口列表可直接选择子窗口。 -此时容器显示 healthy 仅代表程序和桌面服务正常,并不代表已经能读取聊天记录。 - -## 2. 首次登录微信,让本地数据库生成 +## 2. 登录微信 -1. 切换到微信,用手机扫码,并在手机上确认登录。 +1. 切换到微信,用手机扫码(已有账号时点击“登录”),在手机上确认。 2. 等待会话列表和近期消息加载,打开一个熟悉的会话确认有文字消息。 -3. 保留这个账号的数据,继续 WeFlow 配置。 - -WeFlow 能读取的是这个 Linux 微信实例已经保存在本地的数据。手机上有的 -全部历史记录不会因此自动出现在容器里;某个会话缺少历史时,先检查微信 -客户端本身是否已有这些消息,再检查 WeFlow。 - -## 3. 在 WeFlow 选择数据库目录 - -首次启动进入配置向导,依次是“欢迎”“数据库目录”“缓存目录” -“解密密钥”“图片密钥”“安全防护”。 - -在“数据库目录”点击“自动检测”。本项目的通常路径是: - -~~~text -/config/Documents/xwechat_files -~~~ - -这是指向 `/config/xwechat_files` 的兼容链接。自动检测失败时,点击“浏览” -选择实际的 `xwechat_files` 根目录;也可在微信的存储位置设置中核对。 -不要填写宿主机上的 `./data` 路径,WeFlow 看到的是容器内路径。 - -根目录下应有账号子目录,账号子目录内有 `db_storage`。向导能继续到下一步 -说明已识别可用账号数据;如果提示缺少 `db_storage`,先返回微信完成登录, -并确认没有选错目录。 - -“缓存目录”可以留空使用默认值。自定义时也应放在 `/config` 下, -这样缓存能随数据卷保留。 - -## 4. 获取数据库密钥:先准备,再确认登录 - -**Linux 首次自动获取密钥通常需要再登录一次。不要在 WeFlow 准备完成前确认登录。** - -WeFlow 6.3.2 的 Linux 流程会结束当前微信进程、重新启动微信, -再通过自带 helper 等待登录过程中出现的数据库密钥。操作顺序如下: -1. 在微信中关闭“自动登录”,然后退出登录,保持在未登录状态。 - 若部署配置中 `ENABLE_WECHAT_AUTO_LOGIN=true`,首次获取前建议临时改为 - `false` 并执行 `docker compose up -d`。部署脚本的自动登录和微信自身 - 的自动登录选项分别控制,均应避免提前登录。 -2. 回到 WeFlow 的“解密密钥”,点击“自动获取密钥”,阅读确认框后开始。 - 微信窗口关闭并重新出现是此版本的正常准备过程。 -3. 等待 WeFlow 提示准备就绪、允许登录,或显示 - “授权通过后请在手机上确认登录微信,正在等待密钥回调”。 - 如果出现管理员授权提示,先完成授权。WeHarbor 已集成 - `SYS_PTRACE`、polkit 服务和容器内授权规则。 -4. 切换到微信,再扫码或点击登录,并在手机上确认。 -5. 回到 WeFlow,等待“密钥获取成功”。密钥字段应填写完成, - 并确认所选微信账号与刚登录的账号一致;有多个账号时尤其需要核对。 +Alpha 能读取的是这个 Linux 微信实例已保存在本地的数据,手机上的全部历史不会自动出现。 +本版本已移除旧镜像的“自动点击登录”,容器重启后可能需要在手机上再次确认。 -如果提示“未能自动启动微信”,从桌面打开微信,看到登录窗口后点击 -WeFlow 的“我已看到登录窗口,继续”,仍然等待准备完成再确认登录。 -如果超时或提前登录了,退出微信登录,重新执行这一步。 +## 3. 完成 Alpha 首次设置向导 -数据库密钥是账号凭据,留在 WeFlow 的本地配置中即可。无需抄到终端、 -发送给 AI,或提交到 GitHub。已有该账号有效密钥时,向导也支持手动填写。 +向导分为“连接微信”“获取图片密钥”“完成”三步: -## 5. 图片密钥与完成配置 +1. 在“连接你的微信”页勾选“我已了解,开始连接本地数据”,点击“连接微信”。 +2. 选择账号与数据目录。本项目的路径为 `/config/Documents/xwechat_files` + (兼容链接,指向 `/config/xwechat_files`)。 +3. 按提示获取数据库密钥。微信需保持登录;Alpha 的密钥助手以同一用户读取微信 + 进程内存,Compose 已添加 SYS_PTRACE。 +4. “获取图片密钥”可跳过,需要查看聊天图片时再配置。 +5. 完成后打开一个会话,确认能看到消息。 -数据库密钥用于聊天数据库;图片 XOR / AES 密钥用于解码图片,二者不同。 +Alpha 的 HTTP API Token 由 Electron safeStorage 加密保存。容器在桌面会话中自动启动 +无密码的 gnome-keyring(`/config/.local/share/keyrings`),因此不会弹出密钥环密码框; +Token 的安全性依赖数据目录的访问控制。 -进入“图片密钥”时,WeFlow 会尝试从本地缓存自动计算。如果暂时没有图片 -缓存,可先在微信中打开几张图片,等待下载后回到 WeFlow 点击“重新计算”。 -仍失败时,可以尝试“内存扫描”,微信需要保持运行。 +## 4. 开启 HTTP API -首次向导允许在尚未得到图片密钥时继续配置。先完成文字消息读取即可; -图片不可读时再补充图片密钥,不要因此重新获取已经有效的数据库密钥。 - -“安全防护”中的应用锁按需配置。它的密码与浏览器桌面密码、API Token 都不同。 -点击“完成配置”;此时 WeFlow 会测试并打开数据库。成功后进入首页, -打开一个已有会话,对照微信确认几条文字消息。也可在“设置 → 数据库连接” -中点击“测试连接”复查。 - -**以能看到自己的消息作为数据库配置成功的标准。** - -## 6. 开启并验证 HTTP API - -在 WeFlow 的“设置 → API 服务”找到“HTTP API 服务”。如果服务已启动,先关闭开关, -因为运行时监听地址和端口不可编辑。然后设置: - -| 设置 | 值 | -| --- | --- | -| 监听地址 | `0.0.0.0`,用于接收 Docker 端口转发和同网络容器请求 | -| 服务端口 | 与 `.env` 的 `WEFLOW_API_PORT` 一致,默认 `5031` | -| Access Token | 点击“随机生成”,将凭据保存在自己的环境或 secret 管理器 | -| HTTP API 服务 | 开启,状态应为“运行中” | -| 主动推送 | 需要实时通知或 SSE 时再开启,并检查会话过滤设置 | - -容器内监听 `0.0.0.0` 与宿主机对外公开是两回事:默认 Compose 仍然只 -将 API 映射到宿主机 `127.0.0.1`。`.env` 的端口设置不会自动修改 WeFlow -界面中的持久化配置。 - -在 Docker 主机的 Bash 终端执行下列检查。使用实际端口替换 `5031`。 +在 Alpha 设置中启用 HTTP API,端口保持 5031,生成并复制访问 Token。 +需要实时消息时同时开启主动推送。验证: ~~~bash -# 这一步只检查 API 服务是否存活,不检查数据库和鉴权。 curl --fail http://127.0.0.1:5031/health - -# 在本地输入 Token,输入内容不会回显或写入该命令的历史记录。 -read -r -s -p 'WeFlow API Token: ' WEFLOW_API_TOKEN -printf '\n' -export WEFLOW_API_TOKEN - -curl --fail --get \ - -H "Authorization: Bearer $WEFLOW_API_TOKEN" \ - --data-urlencode 'limit=5' \ - http://127.0.0.1:5031/api/v1/sessions -~~~ - -从 `sessions` 数组中选一个会话的 `username`,它是消息查询的 `talker`, -不是群昵称或显示名。用返回的真实值替换下面的占位符: - -~~~bash -curl --fail --get \ - -H "Authorization: Bearer $WEFLOW_API_TOKEN" \ - --data-urlencode 'talker=REPLACE_WITH_SESSION_USERNAME' \ - --data-urlencode 'limit=5' \ - http://127.0.0.1:5031/api/v1/messages +curl --fail -H "Authorization: Bearer $ALPHA_API_TOKEN" http://127.0.0.1:5031/api/v1/sessions ~~~ -会话和消息接口应返回 `success: true`,并能查到刚在 WeFlow 中确认的消息。 -只有 `/health` 返回 `status: ok` 还不足以证明可以读取聊天记录。 - -## 7. 安装 skill,让 AI 读取少量消息 +宿主机的 5031 实际映射到容器内 nginx 的 5032 端口,再转发到 Alpha 的 +127.0.0.1:5031。浏览器通知需要把 Token 写入 +`/config/.config/weharbor/alpha-api-token`(权限 600)。 -把 [skill 安装页 URL](https://raw.githubusercontent.com/fliaping/weharbor/main/docs/install-skill.md) -交给支持技能和命令执行的 AI 工具,或者按 [安装说明](install-skill.md) 执行安装命令。 +## 5. 接入 AI -在 **AI 执行环境** 中配置 `WEFLOW_API_BASE` 和 `WEFLOW_API_TOKEN`, -也可用 `WEFLOW_API_TOKEN_FILE` 指向本地凭据文件。默认地址 -`http://127.0.0.1:5031` 只有在 AI 与 API 位于同一网络命名空间,或已建好 -隧道时才适用;同一 Docker 网络的其他容器用 `http://weharbor:5031`。 - -先让 AI 检查 health,查询少量会话,再读取指定会话的少量消息。 -可以说:“找到我指定的项目群,读取最近几条文字消息,确认接入成功。” -确认正确后再做按时间查询、群聊摘要或限时 SSE 订阅。 -真实数据库密钥与 Token 不需要发到 AI 聊天窗口。 +按 [安装页](install-skill.md) 安装 alpha-api skill,并在 AI 执行环境配置 +ALPHA_API_BASE / ALPHA_API_TOKEN。先查少量 sessions,再查一个会话的消息。 ## 常见问题 -| 现象 | 优先检查 | -| --- | --- | -| 容器 healthy,但 WeFlow 没有消息 | 是否完成账号、数据库密钥与连接测试;health 不检查账号数据是否可读 | -| 找不到账号或缺少 `db_storage` | 微信是否首次登录完成;是否选了容器内正确的根目录 | -| 自动获取时微信突然关闭 | 6.3.2 Linux 的准备过程会重启微信;等准备完成再登录 | -| 获取密钥超时 | 是否提前确认登录或仍开启自动登录;退出登录后按第 4 步重试 | -| 管理员授权 / Hook 失败 | 是否用仓库 Compose 保留了 `SYS_PTRACE`;polkit 是否运行;记录已脱敏错误后排查 | -| 密钥存在但连接失败 | 账号、数据库目录和密钥是否对应;换账号后不能沿用旧账号密钥 | -| 文字正常,图片打不开 | 先下载几张图片,再获取图片密钥;检查本地媒体是否存在 | -| 历史记录不完整 | 对照 Linux 微信里实际可见的历史;WeFlow 不会自动拉取手机全部记录 | -| API 连接失败或拒绝连接 | API 是否“运行中”;容器内是否监听 `0.0.0.0`;端口是否与 Compose 一致 | -| API 返回 401 | 使用 API Token,而不是网页登录密码或数据库密钥;核对 Token 是否更新 | -| SSE 没事件 | 主动推送与过滤规则;微信是否在线;观察期间是否真的有新消息 | -| skill 已安装却无法读取 | AI 的网络与凭据是否配置;远程环境的 `localhost` 不指向用户的 Docker 主机 | - -重启后使用同一个 `/config` 卷,账号配置和数据会保留,无需每次重新获取密钥。 -微信可能仍需要重新确认登录;账号变更或密钥失效时再按对应步骤处理。 -升级前备份完整数据,见 [维护与迁移](operations.md)。提交 issue 时仅提供版本、 -失败阶段和脱敏错误,不附完整 `weflow.log`、微信数据库、账号配置或密钥截图。 +- 出现“Choose password for new keyring”弹窗:说明密钥环未正确初始化, + 重启 Alpha(应用条中的重启按钮)后应消失。 +- API 返回 403 `HTTP_API_HOST_DENIED` / `HTTP_API_ORIGIN_DENIED`:不要绕过容器内 + 代理直接访问,也不要从浏览器页面发起请求。 +- 密钥获取失败:确认微信已登录;Alpha 的密钥助手内置有效期 + (0.2.0 为 2026-11-15),过期后需升级 Alpha。 diff --git a/docs/install-skill.md b/docs/install-skill.md index 06672ac..dc55ae9 100644 --- a/docs/install-skill.md +++ b/docs/install-skill.md @@ -1,7 +1,7 @@ -# 安装 WeFlow API skill +# 安装 Alpha API skill 把本页 URL 交给支持 Agent Skills、能执行命令的 AI 工具,并说: -“请安装这个 WeFlow API skill,并帮我检查连接配置。” +“请安装这个 Alpha API skill,并帮我检查连接配置。” 本页公开地址: https://raw.githubusercontent.com/fliaping/weharbor/main/docs/install-skill.md @@ -9,9 +9,9 @@ https://raw.githubusercontent.com/fliaping/weharbor/main/docs/install-skill.md ## 给 AI 安装者的步骤 用户要求通过本页安装时,请读取 -[SKILL.md](https://raw.githubusercontent.com/fliaping/weharbor/main/skills/weflow-api/SKILL.md), +[SKILL.md](https://raw.githubusercontent.com/fliaping/weharbor/main/skills/alpha-api/SKILL.md), 再安装整个 skill 目录。技能源码位于 -https://github.com/fliaping/weharbor/tree/main/skills/weflow-api 。 +https://github.com/fliaping/weharbor/tree/main/skills/alpha-api 。 只保存 SKILL.md 会缺失调用脚本。 1. 在用户选择的项目目录执行以下命令;安装器会识别可用的 AI 工具。 @@ -19,20 +19,20 @@ https://github.com/fliaping/weharbor/tree/main/skills/weflow-api 。 默认安装到项目;用户要求全局安装时再加 --global。 ~~~bash - npx --yes skills add https://github.com/fliaping/weharbor --skill weflow-api --yes + npx --yes skills add https://github.com/fliaping/weharbor --skill alpha-api --yes ~~~ -2. 如果没有 Node.js,克隆仓库,再把 skills/weflow-api 整个目录复制到当前 +2. 如果没有 Node.js,克隆仓库,再把 skills/alpha-api 整个目录复制到当前 AI 工具支持的 skills 目录。检查已有同名 skill,保留用户定制内容。 3. 找到安装后的目录,运行: ~~~bash - python3 /scripts/weflow_api.py --help - python3 /scripts/weflow_api.py health + python3 /scripts/alpha_api.py --help + python3 /scripts/alpha_api.py health ~~~ -4. 使用者在本地配置 WEFLOW_API_BASE 和 WEFLOW_API_TOKEN,或用 - WEFLOW_API_TOKEN_FILE 指向本地凭据文件。通过工具自己的环境变量或 +4. 使用者在本地配置 ALPHA_API_BASE 和 ALPHA_API_TOKEN,或用 + ALPHA_API_TOKEN_FILE 指向本地凭据文件。通过工具自己的环境变量或 secret 设置注入;不要要求用户把真实 Token 发到聊天中。 5. 读取 SKILL.md 后先检查 health,再查询少量 sessions 验证鉴权。 缺少地址或凭据时,报告尚未完成连接配置,不能声称已经可读取微信数据。 @@ -42,20 +42,20 @@ Python 3.10+ 即可调用 API,无需 pip 依赖。某些工具需要新会话 ## 连接自己的微信 -先在 WeFlow 中配置微信账号、数据库密钥并启用 HTTP API;实时订阅还需开启 -主动推送。API Token 与 WeHarbor 的浏览器登录密码不同。 -第一次安装 WeHarbor 时,请先完成 [首次使用教程](first-run.md), -在 WeFlow 内确认消息可读。Linux 获取密钥需等待准备完成后再确认微信登录; -skill 不会替用户扫码登录、获取数据库密钥或自动配置 WeFlow。 +先在 Alpha 首次设置向导中连接微信账号、获取数据库密钥,再在 Alpha 设置中启用 +HTTP API;实时订阅还需开启主动推送。API Token 与 WeHarbor 的浏览器登录密码不同。 +第一次安装 WeHarbor 时,请先完成 [首次使用教程](first-run.md),在 Alpha 内确认消息可读。 +skill 不会替用户扫码登录、获取数据库密钥或配置 Alpha。客户端最初为 WeFlow API +编写,Alpha 的路由名称一致,但参数与返回字段尚待核对,遇到不一致请如实报告。 默认 API 地址为 http://127.0.0.1:5031。这个地址指 AI 执行环境自身:远程 AI 无法通过它连接使用者电脑。需要通过可信网络、VPN 或 SSH 隧道到达实际 -WeFlow 服务,并保留鉴权。WeHarbor 同网络的其他容器可使用 http://weharbor:5031。 +Alpha 服务,并保留鉴权。WeHarbor 同网络的其他容器可使用 http://weharbor:5032(容器内 API 代理端口)。 无需公开自己的服务地址或凭据到这个 GitHub 仓库。 安装完成后,可以说:“找到项目群,汇总今天的讨论与待办”,或 “订阅接下来 30 秒的新消息”。skill 支持会话、消息、联系人、群成员、 ChatLab、可用版本的朋友圈接口、媒体下载和有时间限制的 SSE 订阅。 -更新时重新执行安装命令,或使用 npx skills update weflow-api。 -源代码使用本仓库 MIT 许可,安装不会改变微信和 WeFlow 的组件许可。 +更新时重新执行安装命令,或使用 npx skills update alpha-api。 +源代码使用本仓库 MIT 许可,安装不会改变微信和 Alpha 的组件许可。 diff --git a/docs/operations.md b/docs/operations.md index f617990..a1a508e 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -12,7 +12,7 @@ docker compose logs --tail=100 ~~~ Docker 健康检查覆盖浏览器桌面、启用的客户端、窗口切换器与通知桥接器。 -首次未配置 WeFlow API 时不会因此判定故障;启用后会额外检查 API 健康。 +Alpha 的 HTTP API 未启用时不会因此判定故障;启用后 /health 会被额外检查。 未登录微信或未设置数据库密钥时,容器仍可健康运行,等待用户完成配置。 首次登录、数据库密钥获取顺序及常见读取问题见 [首次使用教程](first-run.md)。 @@ -45,17 +45,25 @@ docker compose exec weharbor weharbor-health ## 从旧集成迁移 -1. 记录旧容器的镜像、数据挂载源路径、用户 UID/GID 和 WeFlow API 端口。 +1. 记录旧容器的镜像、数据挂载源路径、用户 UID/GID 和 API 端口。 2. 停止旧容器,备份完整数据目录和部署配置。 3. 在 WeHarbor 的 .env 中把 DATA_DIR 指向该数据目录,设置原 UID/GID 和端口。 -4. 启动新容器,核对微信登录、WeFlow 数据读取与浏览器通知。 +4. 启动新容器,核对微信登录、Alpha 数据读取与浏览器通知。 -同一个微信数据目录不能同时由两个容器使用。迁移保留现有 WeFlow 配置, -不会自动修改其 API Token、监听地址或推送开关。 +同一个微信数据目录不能同时由两个容器使用。 + +### 从 WeFlow / wechat-selkies 版本升级 + +新镜像首次启动时,init-weharbor-profile 会把引用旧脚本的 +/config/.config/openbox/autostart 与 menu.xml 替换为新默认值(原文件保留为 +*.legacy-<时间戳>),并把应用条与通知设置从 .config/weflow 复制到 .config/weharbor。 +微信登录数据保持不变;WeFlow 的配置与 Token 不会迁移,需要在 Alpha 中重新完成 +首次设置并生成新的 API Token。.env 中 WEFLOW_API_PORT 改为 API_PORT, +AUTO_START_WEFLOW 改为 AUTO_START_ALPHA;自动登录与夜间重启相关变量已不再使用。 ## 初始窗口布局 -WeFlow 主窗口默认最小化,微信先获得焦点。登录后只最大化当前微信进程的 +Alpha 主窗口默认最小化,微信先获得焦点。登录后只最大化当前微信进程的 一个主窗口一次;聊天、图片、设置等后续子窗口不自动最大化。用户手动还原 主窗口后,助手不会强制再次最大化。微信重启或重新创建主窗口后会重新识别。 @@ -73,7 +81,7 @@ WeHarbor window defaults: begin / end 之间的规则,重新加载 Openbox。 选择主窗口或子窗口。切换会最小化另一应用的窗口,不改变所选窗口的大小。 旁边的 ↻ 按钮重启对应应用,通知开关也统一放在此面板。 重启前保存未发送的内容;重启不会清除登录数据、数据库或配置, -WeFlow 重启时 API 会短暂不可用。此入口沿用网页登录认证,通知关闭后仍可使用。 +Alpha 重启时 API 会短暂不可用。此入口沿用网页登录认证,通知关闭后仍可使用。 正常退出超时后才强制结束残留进程;同一应用的重启请求不会并行执行。 默认 APP_SWITCHER_MODE=web,不再显示旧桌面悬浮条。设为 desktop 并重建 @@ -95,11 +103,11 @@ WeFlow 重启时 API 会短暂不可用。此入口沿用网页登录认证, ## 源码构建 versions.lock.json 记录准确版本、平台、基础镜像 digest 和安装包校验值。 -微信 deb 和 WeFlow tar.gz 默认从本仓库的 component-assets Release 下载, -避免官网地址滚动更新版本。Release 附件仅包含原版安装包及校验清单, +微信 deb 默认从本仓库的 component-assets Release 下载,Alpha deb 从 +xunchahaha/alpha-releases 的 v0.2.0 Release 下载,避免地址滚动更新版本。Release 附件仅包含原版安装包及校验清单, 不包含部署配置、账号目录或密钥。 -离线构建可提前把两个安装包分别放在 downloads/wechat.deb 和 downloads/weflow.tar.gz。 -WECHAT_DOWNLOAD_URL / WEFLOW_DOWNLOAD_URL 可覆盖下载地址,但仍须通过锁定校验值。 +离线构建可提前把两个安装包分别放在 downloads/wechat.deb 和 downloads/alpha.deb。 +WECHAT_DOWNLOAD_URL / ALPHA_DOWNLOAD_URL 可覆盖下载地址,但仍须通过锁定校验值。 两个下载目录中的文件均不会被提交到 Git。 ~~~bash @@ -108,7 +116,7 @@ python3 scripts/prepare-assets.py ./scripts/smoke-test.sh weharbor:0.1.0 ~~~ -在网络需要时可设置 APT_MIRROR,例如 Ubuntu 的可信 HTTPS 镜像站地址。 +在网络需要时可设置 APT_MIRROR,例如 Debian 的可信镜像站根地址(替换 http://deb.debian.org/)。 build.sh 接受额外 Buildx 参数;一旦显式传参,需自己提供 --tag 和 --load/--push。 ## GitHub Actions 与镜像发布 @@ -127,16 +135,22 @@ image.yml 在 main 推送、PR、v* 版本标签及手动触发时运行: 首次运行无需额外配置,两个安装包均使用版本锁定文件中的 Release 附件地址。 需要镜像下载来源时,可在仓库 Settings → Secrets and variables → Actions → Variables -设置 WEFLOW_DOWNLOAD_URL / WECHAT_DOWNLOAD_URL;手动构建也可输入 WeFlow 的可信 HTTPS URL。 +设置 ALPHA_DOWNLOAD_URL / WECHAT_DOWNLOAD_URL;手动构建也可输入 Alpha 的可信 HTTPS URL。 下载内容必须通过 versions.lock.json 的 SHA256 校验。地址缺失或版本不匹配时 工作流会明确失败,不会静默跳过整个镜像任务。缓存只包含 downloads 安装包, 不包含用户配置、账号数据和凭据。 fork 的发布地址自动跟随 github.repository,不绑定某个个人账号。 -准备公开镜像时,应先确认原版 WeFlow 6.3.2 及所含原生组件的分发许可, +准备公开镜像时,应先确认 Alpha 0.2.0(闭源、许可未声明)及所含组件的分发许可, 保留微信与全部第三方声明,再补充稳定可访问的安装包来源。 构建能成功并不等于已经确认所有二进制组件的再分发授权。 源码仓库:[fliaping/weharbor](https://github.com/fliaping/weharbor)。 预构建镜像 ghcr.io/fliaping/weharbor:edge 支持匿名拉取;首次通过 完整构建、空数据目录启动与重启测试的提交标签为 sha-06cb008。 + +## 本机(无 systemd)运行提示 + +在没有 systemd 的主机上,dockerd 与 tailscaled 需要手动拉起;容器使用 +restart: unless-stopped,dockerd 启动后会自动恢复。Tailscale Serve 配置保存在 +tailscaled 状态目录中,重启 tailscaled 后仍生效。 diff --git a/docs/pwa.md b/docs/pwa.md index 0b2a457..0b89d67 100644 --- a/docs/pwa.md +++ b/docs/pwa.md @@ -1,7 +1,7 @@ # 安装为桌面应用 WeHarbor 可以安装为 PWA,以独立窗口从电脑桌面或手机主屏幕打开。 -应用名称为 **WeHarbor|微港**。微信和 WeFlow 仍在服务器上运行,需要在线连接。 +应用名称为 **WeHarbor|微港**。微信和 Alpha 仍在服务器上运行,需要在线连接。 ## 使用条件 From 2fa40cad2186dfd9dbafc69b705329692fddc576 Mon Sep 17 00:00:00 2001 From: "Grok Bot (local)" Date: Mon, 19 Oct 2026 15:00:23 +0800 Subject: [PATCH 3/7] docs: evaluate proot-apps for WeChat/Alpha; keep both native Alpha 0.2.0's key helper and key worker fail their runtime guard (status -103, KEY_RUNTIME_POLICY_REJECTED) under PRoot 5.1.0 and proot-next 0.3.2, so Alpha cannot run as a proot app. Add the reproducible evaluation under tests/proot. --- docs/operations.md | 4 +++ docs/proot-apps-evaluation.md | 62 +++++++++++++++++++++++++++++++++++ tests/proot/Dockerfile.alpha | 10 ++++++ tests/proot/reader.c | 27 +++++++++++++++ tests/proot/run-proot-eval.sh | 37 +++++++++++++++++++++ tests/proot/target.c | 11 +++++++ 6 files changed, 151 insertions(+) create mode 100644 docs/proot-apps-evaluation.md create mode 100644 tests/proot/Dockerfile.alpha create mode 100644 tests/proot/reader.c create mode 100755 tests/proot/run-proot-eval.sh create mode 100644 tests/proot/target.c diff --git a/docs/operations.md b/docs/operations.md index a1a508e..9ec1c0e 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -154,3 +154,7 @@ fork 的发布地址自动跟随 github.repository,不绑定某个个人账号 在没有 systemd 的主机上,dockerd 与 tailscaled 需要手动拉起;容器使用 restart: unless-stopped,dockerd 启动后会自动恢复。Tailscale Serve 配置保存在 tailscaled 状态目录中,重启 tailscaled 后仍生效。 + +## proot-apps + +WeChat and Alpha are not installed as proot-apps: Alpha's key helper refuses to run under proot. See [proot-apps-evaluation.md](proot-apps-evaluation.md). diff --git a/docs/proot-apps-evaluation.md b/docs/proot-apps-evaluation.md new file mode 100644 index 0000000..8bbd944 --- /dev/null +++ b/docs/proot-apps-evaluation.md @@ -0,0 +1,62 @@ +# proot-apps evaluation (WeChat + Alpha) + +Status: **rejected for Alpha; not adopted for WeChat**. WeHarbor keeps installing +the unmodified official WeChat deb and the unmodified Alpha deb natively in the +image (see `Dockerfile`, `versions.lock.json`). + +Evaluated 2026-10-19 (UTC+8) against the pinned base +`ghcr.io/linuxserver/baseimage-selkies:debiantrixie@sha256:5b3f1045…`, which ships +proot-apps 0.5.3 with classic **PRoot 5.1.0** in `/proot-apps` (copied to +`/config/.local/bin` at start; apps land in `/config/proot-apps`). proot-next +0.3.2 (`selkies-project/proot-next`) was tested too. Reproduce with +`tests/proot/run-proot-eval.sh`. + +## What proot-apps offers + +* Catalog app `wechat` exists, but it downloads the floating + `WeChatLinux_x86_64.AppImage` at image build time: not pinned to 4.1.13.23 and + not the official deb. No Alpha app exists. +* Custom apps: `proot-apps build-install-local ` builds a Dockerfile + (`FROM ghcr.io/linuxserver/proot-apps:debian-trixie`, `/entrypoint`), saves it + and unpacks the top layer into `$HOME/proot-apps//`. Each app is its own + rootfs and every `proot-apps run` is a separate proot session. Building an + app needs Docker (not available inside the container), so in-container + install would mean publishing our own image to a registry. + +## Findings + +| Test | Result | +| --- | --- | +| `alpha_xkey_helper --guard-self-check`, `alpha-key-worker --guard-self-check`, native | `status 0` | +| same under PRoot 5.1.0 (ptrace mode or `PROOT_NO_PTRACE=1`) | `status -103`, `KEY_RUNTIME_POLICY_REJECTED` | +| same under proot-next 0.3.2 (both modes; TracerPid hidden) | `status -103`, `KEY_RUNTIME_POLICY_REJECTED` | +| native reader → process in PRoot 5.1.0: `process_vm_readv`, `/proc/pid/mem` | works | +| native reader → process in PRoot: `PTRACE_ATTACH` / `PTRACE_SEIZE` | `EPERM` (already traced by proot) | +| native reader → process in PRoot: `/proc/pid/exe` | `/tmp/prooted--XXXX` (loader alias, not WeChat's binary) | +| reader and target in separate proot sessions (same or different rootfs) | memory read works, ptrace fails | +| reader and target in the same PRoot 5.1.0 session | memory read and emulated `PTRACE_ATTACH` work | + +The Alpha runtime guard (alpha-guard-sdk 2.1.0) refuses to run its key +acquisition under any proot variant. Even running the helper natively through +an explicit `ld.so` invocation trips it, so it checks the process image/loader, +not just `TracerPid`. Because of that, Alpha cannot be a proot app. This is a +property of Alpha's closed-source binaries, and we cannot change it without +modifying them. + +WeChat alone could run as a proot app with Alpha native, but it does not pay +off: + +* the proot loader alias in `/proc//exe` and proot's own ptrace hold on + WeChat break assumptions a key extractor typically makes (ptrace attach, + locating the WeChat binary). We could not confirm that Alpha still gets the + key without a logged-in session; +* every WeChat syscall that touches a path pays PRoot's ptrace cost; +* the catalog app is unpinned, and a pinned custom app would need either a + build-time unpack (no gain over the native deb) or a registry image we would + have to publish and maintain. + +## Recommendation + +Keep both WeChat and Alpha native in the image (current `feat/selkies2-alpha` +design). proot-apps remains available in the base for optional, unrelated +desktop apps the user wants to add to `/config/proot-apps`. diff --git a/tests/proot/Dockerfile.alpha b/tests/proot/Dockerfile.alpha new file mode 100644 index 0000000..720eb0f --- /dev/null +++ b/tests/proot/Dockerfile.alpha @@ -0,0 +1,10 @@ +# Test-only: Alpha 0.2.0 packaged the way a proot-apps app would be +# (proot-apps debian-trixie base, unmodified pinned deb, /entrypoint). +FROM ghcr.io/linuxserver/proot-apps:debian-trixie +COPY alpha.deb /tmp/alpha.deb +RUN echo "9d79a56b88981c496818d93600698d11f8e21d759381c66c3fec3a10b38980d6 /tmp/alpha.deb" | sha256sum -c - \ + && apt-get update -o Acquire::Retries=5 \ + && apt-get install -y --no-install-recommends /tmp/alpha.deb gnome-keyring dbus-x11 \ + && rm -rf /var/lib/apt/lists/* /tmp/alpha.deb +COPY target reader /usr/local/bin/ +RUN printf '#!/bin/sh\nexec /opt/Alpha/alpha --no-sandbox --password-store=gnome-libsecret "$@"\n' > /entrypoint && chmod +x /entrypoint diff --git a/tests/proot/reader.c b/tests/proot/reader.c new file mode 100644 index 0000000..b60daeb --- /dev/null +++ b/tests/proot/reader.c @@ -0,0 +1,27 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +int main(int argc,char**argv){ + int pid; void*addr; FILE*f=fopen(argv[1],"r"); fscanf(f,"%d %p",&pid,&addr); fclose(f); + char buf[64]={0}; char link[512]={0}; char p[64]; + struct iovec l={buf,30}, r={addr,30}; + ssize_t n=process_vm_readv(pid,&l,1,&r,1,0); + printf("process_vm_readv: %s\n", n>0?buf:strerror(errno)); + snprintf(p,64,"/proc/%d/mem",pid); int fd=open(p,O_RDONLY); memset(buf,0,64); + if(fd>=0 && pread(fd,buf,30,(off_t)addr)>0) printf("/proc/pid/mem: %s\n",buf); else printf("/proc/pid/mem: %s\n",strerror(errno)); + snprintf(p,64,"/proc/%d/exe",pid); n=readlink(p,link,511); printf("exe: %s\n", n>0?link:strerror(errno)); + snprintf(p,64,"/proc/%d/status",pid); f=fopen(p,"r"); char line[256]; while(f&&fgets(line,256,f)) if(!strncmp(line,"TracerPid",9)) printf("target %s",line); if(f)fclose(f); + f=fopen("/proc/self/status","r"); while(f&&fgets(line,256,f)) if(!strncmp(line,"TracerPid",9)) printf("self %s",line); if(f)fclose(f); + if(ptrace(PTRACE_ATTACH,pid,0,0)==0){ int st; waitpid(pid,&st,0); errno=0; long w=ptrace(PTRACE_PEEKDATA,pid,addr,0); + printf("PTRACE_ATTACH ok, PEEKDATA: %.8s (%s)\n",(char*)&w, errno?strerror(errno):"ok"); ptrace(PTRACE_DETACH,pid,0,0);} + else printf("PTRACE_ATTACH: %s\n",strerror(errno)); + if(ptrace(PTRACE_SEIZE,pid,0,0)==0){ printf("PTRACE_SEIZE ok\n"); ptrace(PTRACE_DETACH,pid,0,0);} else printf("PTRACE_SEIZE: %s\n",strerror(errno)); + return 0; +} diff --git a/tests/proot/run-proot-eval.sh b/tests/proot/run-proot-eval.sh new file mode 100755 index 0000000..0fdce14 --- /dev/null +++ b/tests/proot/run-proot-eval.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Reproduces the proot-apps evaluation in docs/proot-apps-evaluation.md. +# Usage: tests/proot/run-proot-eval.sh (needs docker, downloads/alpha.deb, gcc) +set -euo pipefail +cd "$(dirname "$0")" +DOCKER=${DOCKER:-"sudo -n docker"} +BASE=$(jq -r .base_image ../../versions.lock.json) +PROOT_NEXT_URL=https://github.com/selkies-project/proot-next/releases/download/0.3.2/proot-x86_64 +PROOT_NEXT_SHA=27361a306817d5062b092fbde6c0debbb2f3f77a4fed884256bd1e17cf4c982e +W=$(mktemp -d /tmp/weharbor-proot-eval.XXXX); chmod 755 "$W" +gcc -static -O1 -o "$W/target" target.c +gcc -static -O1 -o "$W/reader" reader.c +cp Dockerfile.alpha ../../downloads/alpha.deb "$W/" +curl -fsSLo "$W/proot-next" "$PROOT_NEXT_URL"; echo "$PROOT_NEXT_SHA $W/proot-next" | sha256sum -c -; chmod +x "$W/proot-next" +$DOCKER build -q -t weharbor-proot-eval-alpha -f "$W/Dockerfile.alpha" "$W" >/dev/null +mkdir "$W/rootfs"; cid=$($DOCKER create weharbor-proot-eval-alpha /bin/true) +$DOCKER export "$cid" | sudo -n tar -x -C "$W/rootfs"; $DOCKER rm "$cid" >/dev/null +sudo -n chown -R 911:911 "$W/rootfs" +D=/opt/Alpha/resources/bin +echo "### Alpha runtime guard, native (no proot)" +$DOCKER run --rm weharbor-proot-eval-alpha sh -c "for b in alpha_xkey_helper alpha-key-worker; do $D/\$b --guard-self-check 2>&1 | grep -o '\"status\":[-0-9]*'; done" +cat > "$W/inner.sh" <&1 | grep -o '"status":[-0-9]*\|Error.*'; done +done; done +for P in /proot-apps/proot /w/proot-next; do + echo "### WeChat-like target under \$P <- native reader" + rm -f /w/rootfs/tmp/i /tmp/i; \$P -R /w/rootfs /usr/local/bin/target /tmp/i & sleep 2 + cat /w/rootfs/tmp/i /tmp/i 2>/dev/null | head -1 > /tmp/info; /w/rootfs/usr/local/bin/reader /tmp/info | sed 's/^/ /' + pkill -f /usr/local/bin/target || true; sleep 1 +done +IN +chmod +x "$W/inner.sh" +$DOCKER run --rm --cap-add SYS_PTRACE -v "$W:/w" --entrypoint bash "$BASE" -c 'su -s /bin/bash abc -c /w/inner.sh' 2>&1 | grep -v Terminated +sudo -n rm -rf "$W" diff --git a/tests/proot/target.c b/tests/proot/target.c new file mode 100644 index 0000000..b9bf2c7 --- /dev/null +++ b/tests/proot/target.c @@ -0,0 +1,11 @@ +#include +#include +#include +#include +#include +char secret[64] = "WEHARBOR-SECRET-KEY-0123456789"; +int main(int argc,char**argv){ + /* WeChat-like: allow any same-uid process (Yama not present here anyway) */ + FILE*f=fopen(argv[1],"w"); fprintf(f,"%d %p\n",getpid(),(void*)secret); fclose(f); + for(;;) sleep(1); +} From e1cbddc42edb907fc0c43a4ac4f0cb0a2e3f5cea Mon Sep 17 00:00:00 2001 From: "Grok Bot (local)" Date: Mon, 19 Oct 2026 15:17:41 +0800 Subject: [PATCH 4/7] ci: allow an extra non-latest image tag on manual publish --- .github/workflows/image.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index eac952f..c62a6af 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -15,6 +15,10 @@ on: description: Publish the tested image to this repository's GHCR package type: boolean default: false + image_tag: + description: Extra image tag for a manual publish (not latest or edge), for example selkies2-alpha + type: string + required: false permissions: contents: read packages: write @@ -43,6 +47,15 @@ jobs: fi printf 'RELEASE_VERSION=%s\n' "$version" >> "$GITHUB_ENV" printf 'IMAGE_REPOSITORY=ghcr.io/%s\n' "${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" + - name: Validate manual image tag + if: github.event_name == 'workflow_dispatch' && inputs.image_tag != '' + env: + IMAGE_TAG: ${{ inputs.image_tag }} + run: | + if [[ ! "$IMAGE_TAG" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ || "$IMAGE_TAG" == latest || "$IMAGE_TAG" == edge ]]; then + printf 'Invalid manual image tag: %s\n' "$IMAGE_TAG" >&2 + exit 1 + fi - name: Prepare pinned assets # Cache only checksum-verified vendor archives, never a /config profile. uses: actions/cache@v4 @@ -62,6 +75,7 @@ jobs: type=ref,event=pr type=semver,pattern={{version}} type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=raw,value=${{ inputs.image_tag }},enable=${{ github.event_name == 'workflow_dispatch' && inputs.image_tag != '' }} - name: Build uses: docker/build-push-action@v7 with: From f0d45a9841aa3c41c1a7b03e04ce854418c07271 Mon Sep 17 00:00:00 2001 From: "Grok Bot (local)" Date: Mon, 19 Oct 2026 15:51:57 +0800 Subject: [PATCH 5/7] test: smoke-test password must not start with '-' The Selkies base hands PASSWORD to openssl passwd, which reads a leading '-' as an option and leaves the htpasswd file broken. --- .env.example | 1 + scripts/smoke-test.sh | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 89f4d4a..650e5a2 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ # Generated credentials and host UID/GID are filled by scripts/init.sh. WEHARBOR_IMAGE=weharbor:0.1.0 CUSTOM_USER=weharbor +# Must not start with "-" (the Selkies base passes it to `openssl passwd`). PASSWORD= PUID=1000 PGID=1000 diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index a2de95c..3e47fe4 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -15,7 +15,8 @@ cleanup() { } trap cleanup EXIT mkdir "$test_directory/config" -export PASSWORD="$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')" +# The base image hands PASSWORD to `openssl passwd`, which reads a leading "-" as an option. +export PASSWORD="$(python3 -c 'import secrets; print("w" + secrets.token_urlsafe(24))')" docker run -d --name "$container" --shm-size=1gb --cap-add SYS_PTRACE \ -e CUSTOM_USER=weharbor -e PASSWORD -e PUID="$(id -u)" -e PGID="$(id -g)" \ -e TZ=Asia/Shanghai -v "$test_directory/config:/config" \ From bb94866c71d26309d1242efab4d091dc4ce71d65 Mon Sep 17 00:00:00 2001 From: "Grok Bot (local)" Date: Mon, 19 Oct 2026 15:52:19 +0800 Subject: [PATCH 6/7] init: generated password must not start with '-' --- scripts/init.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/init.sh b/scripts/init.sh index fcd077e..c979b20 100755 --- a/scripts/init.sh +++ b/scripts/init.sh @@ -10,7 +10,7 @@ if target.exists(): print('.env already exists; preserved existing settings') else: text = Path('.env.example').read_text() - text = text.replace('PASSWORD=\n', 'PASSWORD=' + secrets.token_urlsafe(24) + '\n') + text = text.replace('PASSWORD=\n', 'PASSWORD=w' + secrets.token_urlsafe(24) + '\n') text = text.replace('PUID=1000\n', f'PUID={os.getuid()}\n') text = text.replace('PGID=1000\n', f'PGID={os.getgid()}\n') fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) From 8fb5fa20f9fd6664b8c015503653089db32961e2 Mon Sep 17 00:00:00 2001 From: "Grok Bot (local)" Date: Mon, 19 Oct 2026 16:21:35 +0800 Subject: [PATCH 7/7] docs: drop remaining WeFlow references Legacy profile migration keeps reading the old paths; only wording changes. --- AGENTS.md | 2 +- README.md | 7 +++---- docs/README.en.md | 6 +++--- docs/install-skill.md | 4 ++-- docs/operations.md | 11 ++++++----- root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/run | 4 ++-- skills/alpha-api/SKILL.md | 3 +-- 7 files changed, 18 insertions(+), 19 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8cc1358..5364011 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Alpha binds 127.0.0.1 only; nginx republishes it on ALPHA_API_PROXY_PORT (5032) and rejects browser Origin headers. - Changes to initialization must preserve existing profiles and user settings - (see init-weharbor-profile for the legacy WeFlow/wechat-selkies migration). + (see init-weharbor-profile for the legacy profile migration). - Run python3 scripts/check.py; for image changes, build and run scripts/smoke-test.sh IMAGE with an empty, temporary profile. - Keep the public skill in skills/alpha-api self-contained and portable. diff --git a/README.md b/README.md index 5fa9e92..6098bdf 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Selkies 2.0 基础镜像(Debian trixie),把官方 Linux 微信和 Alpha (ghcr.io/linuxserver/baseimage-selkies:debiantrixie,按 digest 锁定)。 目前仅支持 linux/amd64。 -> 注意:从 WeFlow 切换到 Alpha 的版本尚未发布预构建镜像,请本地构建。 +> 预构建镜像:ghcr.io/fliaping/weharbor:edge(main 分支)。 > Alpha 是闭源、未签名的二进制发行版,许可证未声明;其密钥助手内置 > 2026-11-15 的有效期,需要随上游更新。详见 [组件许可](THIRD_PARTY_NOTICES.md)。 @@ -91,8 +91,7 @@ npx --yes skills add https://github.com/fliaping/weharbor --skill alpha-api --ye ~~~ 在 AI 执行环境配置 ALPHA_API_BASE 与 ALPHA_API_TOKEN(或 ALPHA_API_TOKEN_FILE)。 -客户端最初为 WeFlow API 编写;Alpha 的路由名称一致,但参数与返回字段仍待在真实 -实例上核对。详见 [安装页](docs/install-skill.md)。 +路由来自 Alpha 0.2.0 的静态分析,参数与返回字段仍待在真实实例上核对。详见 [安装页](docs/install-skill.md)。 ## 配置 @@ -135,5 +134,5 @@ WeHarbor 的集成代码使用 MIT 许可证。微信、Alpha、Selkies 及其 各自许可;Alpha 的分发条款未明确,公开发布组合镜像前需先核实。 详见 THIRD_PARTY_NOTICES.md。 -感谢 LinuxServer.io、Selkies、wechat-selkies 与 WeFlow 项目的维护者, +感谢 LinuxServer.io、Selkies 与 wechat-selkies 项目的维护者, 以及 Alpha 的作者。WeHarbor 是独立的社区集成项目。 diff --git a/docs/README.en.md b/docs/README.en.md index 61dd5ec..72e9904 100644 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -43,7 +43,7 @@ Alpha starts minimized and WeChat receives focus; only the WeChat main window is maximized once after login. PWA installation is supported over trusted HTTPS (see [pwa.md](pwa.md)). All state persists in ./data mounted at /config. -Compared with the former wechat-selkies/WeFlow image, WeChat auto-login clicking +Compared with earlier WeHarbor releases, WeChat auto-login clicking and nightly restarts were removed, and no Chinese IME is preinstalled. Existing profiles are migrated automatically (see [operations](operations.md)). @@ -57,8 +57,8 @@ npx --yes skills add https://github.com/fliaping/weharbor --skill alpha-api --ye ~~~ Configure ALPHA_API_BASE and ALPHA_API_TOKEN (or ALPHA_API_TOKEN_FILE) locally. -The client was written for WeFlow's API; Alpha uses the same route names, but -parameters and response fields are not yet verified against a live instance. +Routes come from static analysis of Alpha 0.2.0; parameters and response +fields are not yet verified against a live instance. WeHarbor integration code is MIT licensed. Vendor components keep their own licenses; Alpha's redistribution terms are unclear, so do not publish a combined diff --git a/docs/install-skill.md b/docs/install-skill.md index dc55ae9..bab57f2 100644 --- a/docs/install-skill.md +++ b/docs/install-skill.md @@ -45,8 +45,8 @@ Python 3.10+ 即可调用 API,无需 pip 依赖。某些工具需要新会话 先在 Alpha 首次设置向导中连接微信账号、获取数据库密钥,再在 Alpha 设置中启用 HTTP API;实时订阅还需开启主动推送。API Token 与 WeHarbor 的浏览器登录密码不同。 第一次安装 WeHarbor 时,请先完成 [首次使用教程](first-run.md),在 Alpha 内确认消息可读。 -skill 不会替用户扫码登录、获取数据库密钥或配置 Alpha。客户端最初为 WeFlow API -编写,Alpha 的路由名称一致,但参数与返回字段尚待核对,遇到不一致请如实报告。 +skill 不会替用户扫码登录、获取数据库密钥或配置 Alpha。路由来自 Alpha 0.2.0 的 +静态分析,参数与返回字段尚待核对,遇到不一致请如实报告。 默认 API 地址为 http://127.0.0.1:5031。这个地址指 AI 执行环境自身:远程 AI 无法通过它连接使用者电脑。需要通过可信网络、VPN 或 SSH 隧道到达实际 diff --git a/docs/operations.md b/docs/operations.md index 9ec1c0e..f6031d8 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -52,14 +52,15 @@ docker compose exec weharbor weharbor-health 同一个微信数据目录不能同时由两个容器使用。 -### 从 WeFlow / wechat-selkies 版本升级 +### 从早期版本升级 新镜像首次启动时,init-weharbor-profile 会把引用旧脚本的 /config/.config/openbox/autostart 与 menu.xml 替换为新默认值(原文件保留为 -*.legacy-<时间戳>),并把应用条与通知设置从 .config/weflow 复制到 .config/weharbor。 -微信登录数据保持不变;WeFlow 的配置与 Token 不会迁移,需要在 Alpha 中重新完成 -首次设置并生成新的 API Token。.env 中 WEFLOW_API_PORT 改为 API_PORT, -AUTO_START_WEFLOW 改为 AUTO_START_ALPHA;自动登录与夜间重启相关变量已不再使用。 +*.legacy-<时间戳>),并把旧数据应用目录中的应用条与通知设置复制到 .config/weharbor。 +微信登录数据保持不变;旧数据应用的配置与 Token 不会迁移,需要在 Alpha 中重新完成 +首次设置并生成新的 API Token。.env 中旧的 API 端口变量改为 API_PORT, +自动启动变量改为 AUTO_START_ALPHA;自动登录与夜间重启相关变量已不再使用。 +可对照 .env.example 更新。 ## 初始窗口布局 diff --git a/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/run b/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/run index 65b74a3..bdcd71c 100644 --- a/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/run +++ b/root/etc/s6-overlay/s6-rc.d/init-weharbor-profile/run @@ -1,5 +1,5 @@ #!/usr/bin/with-contenv bash -# Migrate desktop profiles created by the former wechat-selkies/WeFlow image. +# Migrate desktop profiles created by earlier WeHarbor images. # init-selkies-config copies /defaults only when a file is missing, so legacy # autostart and menu entries would otherwise keep calling removed scripts. conf="${HOME:-/config}/.config/openbox" @@ -13,7 +13,7 @@ for name in autostart menu.xml; do echo "[weharbor] migrated legacy ${file}" fi done -# Settings moved out of the WeFlow-specific directory. +# Settings moved out of the legacy data-app directory. old="${HOME:-/config}/.config/weflow" new="${HOME:-/config}/.config/weharbor" if [ -d "${old}" ]; then diff --git a/skills/alpha-api/SKILL.md b/skills/alpha-api/SKILL.md index 75db1ad..be07a45 100644 --- a/skills/alpha-api/SKILL.md +++ b/skills/alpha-api/SKILL.md @@ -89,8 +89,7 @@ failures. It never prints server error bodies or request credentials. ## Alpha compatibility note -This client was written for the WeFlow HTTP API. Alpha 0.2.0 exposes the same -route names (/health, /api/v1/sessions, /api/v1/messages, /api/v1/contacts, +Alpha 0.2.0 exposes these route names (/health, /api/v1/sessions, /api/v1/messages, /api/v1/contacts, /api/v1/group-members, /api/v1/sns/*, /api/v1/media/*, /api/v1/push/messages, plus /mcp), bearer or access_token authentication and port 5031, but query parameters and response fields have not yet been verified against a live Alpha