From bed2d43e0e234fd1720257af195550baf0189de8 Mon Sep 17 00:00:00 2001 From: momen Date: Wed, 15 Apr 2026 20:49:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(update):=20=E8=81=9A=E5=90=88=20Spark=20?= =?UTF-8?q?=E5=92=8C=20APM=20=E5=8D=87=E7=BA=A7=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...6-04-15-update-notifier-apm-aggregation.md | 157 ++++++++++++++ ...-update-notifier-apm-aggregation-design.md | 129 ++++++++++++ tool/update-upgrade/ss-update-notifier.sh | 197 +++++++++++++----- 3 files changed, 431 insertions(+), 52 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-15-update-notifier-apm-aggregation.md create mode 100644 docs/superpowers/specs/2026-04-15-update-notifier-apm-aggregation-design.md diff --git a/docs/superpowers/plans/2026-04-15-update-notifier-apm-aggregation.md b/docs/superpowers/plans/2026-04-15-update-notifier-apm-aggregation.md new file mode 100644 index 00000000..c32cf06a --- /dev/null +++ b/docs/superpowers/plans/2026-04-15-update-notifier-apm-aggregation.md @@ -0,0 +1,157 @@ +# Update Notifier APM Aggregation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Extend `tool/update-upgrade/ss-update-notifier.sh` so one notifier aggregates effective Spark and APM updates, honoring `hold` status and shared ignored entries while skipping the Spark branch when `aptss` is unavailable. + +**Architecture:** Keep the current notifier script as the single entrypoint and add a second APM counting branch beside the existing Spark branch. Reuse the existing ignored-entry loading logic, count Spark and APM updates independently after source-specific filtering, then combine the remaining counts into one notification. + +**Tech Stack:** Bash, aptss, apm, amber-pm-debug, dpkg-query + +--- + +## File Structure + +- Modify: `tool/update-upgrade/ss-update-notifier.sh` + Responsibility: add APM update parsing and counting, guard Spark execution behind `aptss` availability, reuse ignored-entry filtering for both branches, and keep one aggregated notification path. + +### Task 1: Add Source-Specific Counting Helpers + +**Files:** + +- Modify: `tool/update-upgrade/ss-update-notifier.sh` + +- [ ] **Step 1: Write the failing shell behavior expectation as comments in the plan** + +```bash +# Expected behavior after implementation: +# 1. If aptss is missing, the script does not call aptss update/ssupdate. +# 2. If apm reports upgradable apps, ignored pkg|newVersion entries suppress them. +# 3. Spark and APM effective counts are added into one final count. +``` + +- [ ] **Step 2: Run syntax check before changes** + +Run: `bash -n tool/update-upgrade/ss-update-notifier.sh` +Expected: exit 0 + +- [ ] **Step 3: Add minimal helper functions for APM parsing and per-source counting** + +```bash +function has-command() { + command -v "$1" >/dev/null 2>&1 +} + +function get_apm_upgradable_list() { + local output + output=$(env LANGUAGE=en_US apm list --upgradable 2>/dev/null | awk 'NR>1') + local ifs_old="$IFS" + IFS=$'\n' + for line in $output; do + local pkg_name + local pkg_new_ver + local pkg_cur_ver + pkg_name=$(echo "$line" | awk -F '/' '{print $1}') + pkg_new_ver=$(echo "$line" | awk '{print $2}') + pkg_cur_ver=$(printf '%s\n' "$line" | sed -n 's/.*\[\(upgradable from\|from\):[[:space:]]*\([^]]*\)\].*/\2/p') + if [ -n "$pkg_name" ] && [ -n "$pkg_new_ver" ] && [ -n "$pkg_cur_ver" ]; then + echo "$pkg_name $pkg_new_ver $pkg_cur_ver" + fi + done + IFS="$ifs_old" +} +``` + +- [ ] **Step 4: Re-run syntax check after helper changes** + +Run: `bash -n tool/update-upgrade/ss-update-notifier.sh` +Expected: exit 0 + +### Task 2: Aggregate Spark And APM Effective Counts + +**Files:** + +- Modify: `tool/update-upgrade/ss-update-notifier.sh` + +- [ ] **Step 1: Guard Spark refresh and counting behind aptss availability** + +```bash +spark_update_count=0 +if has-command aptss; then + # existing aptss update / aptss ssupdate logic + # existing spark upgradable counting logic +fi +``` + +- [ ] **Step 2: Add APM refresh and counting branch with hold + ignored filtering** + +```bash +apm_update_count=0 +if has-command apm; then + updatetext=$(LANGUAGE=en_US apm update 2>&1) + # retry loop matching current script style + apm clean + PKG_LIST="$(get_apm_upgradable_list)" + apm_update_count=$(printf '%s\n' "$PKG_LIST" | awk 'NF { count++ } END { print count + 0 }') + # for each package: + # - skip if new <= current + # - skip if amber-pm-debug dpkg-query says hold + # - skip if ignored_apps["$PKG_NAME|$PKG_NEW_VER"] exists + # - otherwise increment apm_update_count +fi +``` + +- [ ] **Step 3: Replace single-source final count with aggregated count** + +```bash +update_app_number=$((spark_update_count + apm_update_count)) +if [ "$update_app_number" -le 0 ]; then + exit 0 +fi +``` + +- [ ] **Step 4: Keep one final notification path** + +```bash +notify-send -a spark-store \ + "${TRANSHELL_CONTENT_SPARK_STORE_UPGRADE_NOTIFY}" \ + "${TRANSHELL_CONTENT_THERE_ARE_APPS_TO_UPGRADE}" || true +``` + +- [ ] **Step 5: Re-run syntax check after aggregation changes** + +Run: `bash -n tool/update-upgrade/ss-update-notifier.sh` +Expected: exit 0 + +### Task 3: Verification And Commit + +**Files:** + +- Modify: `tool/update-upgrade/ss-update-notifier.sh` + +- [ ] **Step 1: Run notifier syntax verification** + +Run: `bash -n tool/update-upgrade/ss-update-notifier.sh` +Expected: exit 0 + +- [ ] **Step 2: Run repository lint** + +Run: `npm run lint` +Expected: exit 0 + +- [ ] **Step 3: Run repository build** + +Run: `npm run build:vite` +Expected: exit 0 + +- [ ] **Step 4: Review final diff** + +Run: `git diff -- tool/update-upgrade/ss-update-notifier.sh docs/superpowers/specs/2026-04-15-update-notifier-apm-aggregation-design.md docs/superpowers/plans/2026-04-15-update-notifier-apm-aggregation.md` +Expected: only notifier aggregation and spec/plan changes appear + +- [ ] **Step 5: Commit** + +```bash +git add tool/update-upgrade/ss-update-notifier.sh docs/superpowers/specs/2026-04-15-update-notifier-apm-aggregation-design.md docs/superpowers/plans/2026-04-15-update-notifier-apm-aggregation.md +git commit -m "fix(update): 聚合 Spark 和 APM 升级通知" +``` diff --git a/docs/superpowers/specs/2026-04-15-update-notifier-apm-aggregation-design.md b/docs/superpowers/specs/2026-04-15-update-notifier-apm-aggregation-design.md new file mode 100644 index 00000000..77779be5 --- /dev/null +++ b/docs/superpowers/specs/2026-04-15-update-notifier-apm-aggregation-design.md @@ -0,0 +1,129 @@ +# Update Notifier APM Aggregation Design + +## Background + +`tool/update-upgrade/ss-update-notifier.sh` currently counts Spark (`aptss`) updates, filters them through `hold` state and `~/.config/spark-store/ignored_apps.conf`, then sends one desktop notification. A separate APM-side notifier pattern exists, but it is not merged into the current Spark notifier script. + +The goal is to let the current notifier aggregate both Spark and APM upgradable items into one notification, while keeping the existing user-level ignored-update behavior and avoiding hard failures on systems that do not provide `aptss`. + +## Goals + +1. Keep a single notifier script: `tool/update-upgrade/ss-update-notifier.sh`. +2. Count both Spark and APM upgradable applications in that script. +3. Continue to use one shared ignored-update file: `~/.config/spark-store/ignored_apps.conf`. +4. Apply ignored filtering to both Spark and APM using exact `pkgname|newVersion` keys. +5. Apply `hold` filtering independently for Spark and APM. +6. Aggregate the remaining Spark and APM counts into one notification. +7. If `aptss` is unavailable, skip the Spark branch without failing the script. + +## Non-Goals + +1. Do not create a second notifier service or script. +2. Do not change the ignored-update file format. +3. Do not change Electron or update-center UI behavior in this task. +4. Do not add a compatibility layer for `/etc/spark-store/ignored_apps.conf`. + +## Recommended Approach + +Extend the existing notifier in place and keep Spark and APM as two counting branches inside the same script. + +Spark keeps its current `aptss`-based flow. APM adds a second branch that parses `apm list --upgradable`, applies APM `hold` detection via `amber-pm-debug dpkg-query`, and reuses the same ignored-entry set already loaded from user config files. The final notification count becomes `spark_count + apm_count`. + +This keeps the script small, preserves the current Spark path, and avoids introducing a second source of notification truth. + +## Data Sources + +### Spark branch + +- Command availability gate: `command -v aptss` +- Refresh commands: `aptss update`, `LANGUAGE=en_US aptss ssupdate` +- Upgradable list source: `/opt/durapps/spark-store/bin/update-upgrade/ss-do-upgrade-worker.sh upgradable-list` +- Hold check: `dpkg-query -W -f='${db:Status-Want}' ` + +### APM branch + +- Command availability gate: `command -v apm` +- Refresh commands: `LANGUAGE=en_US apm update`, followed by `apm clean` +- Upgradable list source: `env LANGUAGE=en_US apm list --upgradable` +- Output compatibility: support both `[upgradable from: ]` and legacy `[from: ]` variants when extracting the current version +- Hold check: `amber-pm-debug dpkg-query -W -f='${db:Status-Want}' ` + +## Filtering Rules + +### Ignored entries + +The script continues to load ignored entries from `~/.config/spark-store/ignored_apps.conf`, using the existing user-detection plus `/home/*` scan behavior. + +Each valid line is still interpreted as: + +```text +pkgname|version +``` + +Matching rule: + +- Spark item is ignored when `pkgname|sparkNewVersion` exists in the ignored set. +- APM item is ignored when `pkgname|apmNewVersion` exists in the ignored set. + +Ignored matching is intentionally source-agnostic. If Spark and APM expose the same package name and target version, one ignore entry suppresses both. + +### Hold entries + +- Spark item is excluded if `dpkg-query` reports `hold`. +- APM item is excluded if `amber-pm-debug dpkg-query` reports `hold`. + +### Invalid or stale version entries + +Each branch keeps its own version sanity check before counting: + +- Spark continues to skip items where `newVersion <= currentVersion`. +- APM does the same after parsing `apm list --upgradable` output from either supported bracket variant. + +## Availability Rules + +### Missing `aptss` + +If `aptss` is not installed or not in `PATH`: + +1. Skip Spark refresh commands entirely. +2. Skip Spark upgradable counting entirely. +3. Continue with APM counting if `apm` is available. + +### Missing `apm` + +If `apm` is not installed or not in `PATH`: + +1. Skip APM refresh commands entirely. +2. Skip APM upgradable counting entirely. +3. Continue with Spark counting if `aptss` is available. + +### Both unavailable + +If both `aptss` and `apm` are unavailable, the script exits without sending a notification. + +## Notification Behavior + +The script sends one notification only when: + +```text +spark_effective_count + apm_effective_count > 0 +``` + +The notification remains a single desktop message. The implementation may update the wording to mention both Spark and APM updates, but the key requirement is one aggregated notification rather than separate per-source notifications. + +## Implementation Boundaries + +1. Keep the current `detect-notify-user` and ignored-config discovery logic. +2. Add APM parsing as a second source-specific helper path instead of rewriting the whole script. +3. Keep the shell implementation POSIX-compatible with the current Bash usage already present in the file. +4. Avoid changing unrelated installer or update-center code in this task. + +## Verification + +1. `bash -n tool/update-upgrade/ss-update-notifier.sh` +2. Manual dry-run reasoning for all four cases: + - Spark only + - APM only + - Spark + APM + - neither available +3. Confirm ignored entries suppress both branches via exact `pkg|newVersion` matching. diff --git a/tool/update-upgrade/ss-update-notifier.sh b/tool/update-upgrade/ss-update-notifier.sh index 817c2724..6b4ff7d1 100755 --- a/tool/update-upgrade/ss-update-notifier.sh +++ b/tool/update-upgrade/ss-update-notifier.sh @@ -5,6 +5,10 @@ load_transhell_debug ############################################################# +function has-command() { + command -v "$1" >/dev/null 2>&1 +} + # 发送通知 function notify-send() { local user @@ -86,6 +90,30 @@ function load-ignored-apps() { done } +function get-apm-upgradable-list() { + local output + output=$(env LANGUAGE=en_US apm list --upgradable 2>/dev/null | awk 'NR>1') + + local ifs_old="$IFS" + IFS=$'\n' + + local line + for line in $output; do + local pkg_name + local pkg_new_ver + local pkg_cur_ver + pkg_name=$(echo "$line" | awk -F '/' '{print $1}') + pkg_new_ver=$(echo "$line" | awk '{print $2}') + pkg_cur_ver=$(printf '%s\n' "$line" | sed -n 's/.*\[\(upgradable from\|from\):[[:space:]]*\([^]]*\)\].*/\2/p') + + if [ -n "$pkg_name" ] && [ -n "$pkg_new_ver" ] && [ -n "$pkg_cur_ver" ]; then + echo "${pkg_name} ${pkg_new_ver} ${pkg_cur_ver}" + fi + done + + IFS="$ifs_old" +} + # 检测网络链接畅通 function network-check() { # 超时时间 @@ -106,6 +134,21 @@ function network-check() { fi } +has_aptss=0 +has_apm=0 + +if has-command aptss; then + has_aptss=1 +fi + +if has-command apm; then + has_apm=1 +fi + +if [ "$has_aptss" -eq 0 ] && [ "$has_apm" -eq 0 ]; then + exit 0 +fi + # 初始化等待时间和最大等待时间 initial_wait_time=15 # 初始等待时间 15 秒 max_wait_time=$((12 * 3600)) # 最大等待时间 12 小时 @@ -123,70 +166,120 @@ while ! network-check; do fi done -# 每日更新星火源文件 -aptss update +load-ignored-apps +spark_update_count=0 +if [ "$has_aptss" -eq 1 ]; then + # 每日更新星火源文件 + aptss update -updatetext=`LANGUAGE=en_US aptss ssupdate 2>&1` + updatetext=$(LANGUAGE=en_US aptss ssupdate 2>&1) -# 在网络恢复后,继续更新操作 -retry_count=0 -max_retries=12 # 最大重试次数,防止死循环 + # 在网络恢复后,继续更新操作 + retry_count=0 + max_retries=12 # 最大重试次数,防止死循环 -until ! echo $updatetext | grep -q "E:"; do - if [ $retry_count -ge $max_retries ]; then - echo "Reached maximum retry limit for aptss ssupdate." - exit 1 + until ! echo "$updatetext" | grep -q "E:"; do + if [ $retry_count -ge $max_retries ]; then + echo "Reached maximum retry limit for aptss ssupdate." + exit 1 + fi + + echo "${TRANSHELL_CONTENT_UPDATE_ERROR_AND_WAIT_15_SEC}" + sleep 15 + updatetext=$(LANGUAGE=en_US aptss ssupdate 2>&1) + retry_count=$((retry_count + 1)) + done + + spark_update_count=$(env LANGUAGE=en_US /usr/bin/apt -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf list --upgradable -o Dir::Etc::sourcelist="/opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/aptss.list" -o Dir::Etc::sourceparts="/dev/null" -o APT::Get::List-Cleanup="0" 2>/dev/null | grep -c upgradable) + + if [ "$spark_update_count" -gt 0 ]; then + # 获取用户选择的要更新的应用 + PKG_LIST="$(/opt/durapps/spark-store/bin/update-upgrade/ss-do-upgrade-worker.sh upgradable-list)" + IFS_OLD="$IFS" + IFS=$'\n' + + for line in $PKG_LIST; do + PKG_NAME=$(echo "$line" | awk -F ' ' '{print $1}') + PKG_NEW_VER=$(echo "$line" | awk -F ' ' '{print $2}') + PKG_CUR_VER=$(echo "$line" | awk -F ' ' '{print $3}') + + dpkg --compare-versions "$PKG_NEW_VER" le "$PKG_CUR_VER" + if [ $? -eq 0 ]; then + spark_update_count=$((spark_update_count - 1)) + continue + fi + + PKG_STA=$(dpkg-query -W -f='${db:Status-Want}' "$PKG_NAME") + if [ "$PKG_STA" = "hold" ]; then + spark_update_count=$((spark_update_count - 1)) + continue + fi + + if [ -n "${ignored_apps["$PKG_NAME|$PKG_NEW_VER"]}" ]; then + spark_update_count=$((spark_update_count - 1)) + continue + fi + done + + IFS="$IFS_OLD" fi - - echo "${TRANSHELL_CONTENT_UPDATE_ERROR_AND_WAIT_15_SEC}" - sleep 15 - updatetext=`LANGUAGE=en_US aptss ssupdate 2>&1` - retry_count=$((retry_count + 1)) -done - -update_app_number=$(env LANGUAGE=en_US /usr/bin/apt -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf list --upgradable -o Dir::Etc::sourcelist="/opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/aptss.list" -o Dir::Etc::sourceparts="/dev/null" -o APT::Get::List-Cleanup="0" 2>/dev/null | grep -c upgradable) - -if [ "$update_app_number" -le 0 ]; then - exit 0 fi -load-ignored-apps +apm_update_count=0 +if [ "$has_apm" -eq 1 ]; then + updatetext=$(LANGUAGE=en_US apm update 2>&1) + retry_count=0 + max_retries=12 -# 获取用户选择的要更新的应用 -PKG_LIST="$(/opt/durapps/spark-store/bin/update-upgrade/ss-do-upgrade-worker.sh upgradable-list)" -# 指定分隔符为 \n -IFS_OLD="$IFS" -IFS=$'\n' + until ! echo "$updatetext" | grep -q "E:"; do + if [ $retry_count -ge $max_retries ]; then + echo "Reached maximum retry limit for apm update." + exit 1 + fi -for line in $PKG_LIST; do - PKG_NAME=$(echo $line | awk -F ' ' '{print $1}') - PKG_NEW_VER=$(echo $line | awk -F ' ' '{print $2}') - PKG_CUR_VER=$(echo $line | awk -F ' ' '{print $3}') + echo "Update failed...Will retry in 15sec" + sleep 15 + updatetext=$(LANGUAGE=en_US apm update 2>&1) + retry_count=$((retry_count + 1)) + done - dpkg --compare-versions $PKG_NEW_VER le $PKG_CUR_VER + apm clean + PKG_LIST="$(get-apm-upgradable-list)" + apm_update_count=$(printf '%s\n' "$PKG_LIST" | awk 'NF { count++ } END { print count + 0 }') - if [ $? -eq 0 ]; then - let update_app_number=$update_app_number-1 - continue + if [ "$apm_update_count" -gt 0 ]; then + IFS_OLD="$IFS" + IFS=$'\n' + + for line in $PKG_LIST; do + PKG_NAME=$(echo "$line" | awk -F ' ' '{print $1}') + PKG_NEW_VER=$(echo "$line" | awk -F ' ' '{print $2}') + PKG_CUR_VER=$(echo "$line" | awk -F ' ' '{print $3}') + + amber-pm-debug dpkg --compare-versions "$PKG_NEW_VER" le "$PKG_CUR_VER" + if [ $? -eq 0 ]; then + apm_update_count=$((apm_update_count - 1)) + continue + fi + + PKG_STA=$(amber-pm-debug dpkg-query -W -f='${db:Status-Want}' "$PKG_NAME") + if [ "$PKG_STA" = "hold" ]; then + apm_update_count=$((apm_update_count - 1)) + continue + fi + + if [ -n "${ignored_apps["$PKG_NAME|$PKG_NEW_VER"]}" ]; then + apm_update_count=$((apm_update_count - 1)) + continue + fi + done + + IFS="$IFS_OLD" fi +fi - # 检测是否是 hold 状态 - PKG_STA=$(dpkg-query -W -f='${db:Status-Want}' $PKG_NAME) - if [ "$PKG_STA" = "hold" ]; then - let update_app_number=$update_app_number-1 - continue - fi - - # 检测是否在忽略列表中 - if [ -n "${ignored_apps["$PKG_NAME|$PKG_NEW_VER"]}" ]; then - let update_app_number=$update_app_number-1 - continue - fi -done - -# 还原分隔符 -IFS="$IFS_OLD" -if [ $update_app_number -le 0 ]; then +update_app_number=$((spark_update_count + apm_update_count)) +if [ "$update_app_number" -le 0 ]; then exit 0 fi update_transhell