fix(update): 聚合 Spark 和 APM 升级通知

This commit is contained in:
2026-04-15 20:49:15 +08:00
parent 44587e299a
commit bed2d43e0e
3 changed files with 431 additions and 52 deletions

View File

@@ -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 升级通知"
```

View File

@@ -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}' <pkg>`
### 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: <version>]` and legacy `[from: <version>]` variants when extracting the current version
- Hold check: `amber-pm-debug dpkg-query -W -f='${db:Status-Want}' <pkg>`
## 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.

View File

@@ -5,6 +5,10 @@ load_transhell_debug
############################################################# #############################################################
function has-command() {
command -v "$1" >/dev/null 2>&1
}
# 发送通知 # 发送通知
function notify-send() { function notify-send() {
local user local user
@@ -86,6 +90,30 @@ function load-ignored-apps() {
done 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() { function network-check() {
# 超时时间 # 超时时间
@@ -106,6 +134,21 @@ function network-check() {
fi 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 秒 initial_wait_time=15 # 初始等待时间 15 秒
max_wait_time=$((12 * 3600)) # 最大等待时间 12 小时 max_wait_time=$((12 * 3600)) # 最大等待时间 12 小时
@@ -123,16 +166,19 @@ while ! network-check; do
fi fi
done done
load-ignored-apps
spark_update_count=0
if [ "$has_aptss" -eq 1 ]; then
# 每日更新星火源文件 # 每日更新星火源文件
aptss update aptss update
updatetext=`LANGUAGE=en_US aptss ssupdate 2>&1` updatetext=$(LANGUAGE=en_US aptss ssupdate 2>&1)
# 在网络恢复后,继续更新操作 # 在网络恢复后,继续更新操作
retry_count=0 retry_count=0
max_retries=12 # 最大重试次数,防止死循环 max_retries=12 # 最大重试次数,防止死循环
until ! echo $updatetext | grep -q "E:"; do until ! echo "$updatetext" | grep -q "E:"; do
if [ $retry_count -ge $max_retries ]; then if [ $retry_count -ge $max_retries ]; then
echo "Reached maximum retry limit for aptss ssupdate." echo "Reached maximum retry limit for aptss ssupdate."
exit 1 exit 1
@@ -140,53 +186,100 @@ until ! echo $updatetext | grep -q "E:"; do
echo "${TRANSHELL_CONTENT_UPDATE_ERROR_AND_WAIT_15_SEC}" echo "${TRANSHELL_CONTENT_UPDATE_ERROR_AND_WAIT_15_SEC}"
sleep 15 sleep 15
updatetext=`LANGUAGE=en_US aptss ssupdate 2>&1` updatetext=$(LANGUAGE=en_US aptss ssupdate 2>&1)
retry_count=$((retry_count + 1)) retry_count=$((retry_count + 1))
done 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) 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 [ "$update_app_number" -le 0 ]; then
exit 0
fi
load-ignored-apps
if [ "$spark_update_count" -gt 0 ]; then
# 获取用户选择的要更新的应用 # 获取用户选择的要更新的应用
PKG_LIST="$(/opt/durapps/spark-store/bin/update-upgrade/ss-do-upgrade-worker.sh upgradable-list)" PKG_LIST="$(/opt/durapps/spark-store/bin/update-upgrade/ss-do-upgrade-worker.sh upgradable-list)"
# 指定分隔符为 \n
IFS_OLD="$IFS" IFS_OLD="$IFS"
IFS=$'\n' IFS=$'\n'
for line in $PKG_LIST; do for line in $PKG_LIST; do
PKG_NAME=$(echo $line | awk -F ' ' '{print $1}') PKG_NAME=$(echo "$line" | awk -F ' ' '{print $1}')
PKG_NEW_VER=$(echo $line | awk -F ' ' '{print $2}') PKG_NEW_VER=$(echo "$line" | awk -F ' ' '{print $2}')
PKG_CUR_VER=$(echo $line | awk -F ' ' '{print $3}') PKG_CUR_VER=$(echo "$line" | awk -F ' ' '{print $3}')
dpkg --compare-versions $PKG_NEW_VER le $PKG_CUR_VER
dpkg --compare-versions "$PKG_NEW_VER" le "$PKG_CUR_VER"
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
let update_app_number=$update_app_number-1 spark_update_count=$((spark_update_count - 1))
continue continue
fi fi
# 检测是否是 hold 状态 PKG_STA=$(dpkg-query -W -f='${db:Status-Want}' "$PKG_NAME")
PKG_STA=$(dpkg-query -W -f='${db:Status-Want}' $PKG_NAME)
if [ "$PKG_STA" = "hold" ]; then if [ "$PKG_STA" = "hold" ]; then
let update_app_number=$update_app_number-1 spark_update_count=$((spark_update_count - 1))
continue continue
fi fi
# 检测是否在忽略列表中
if [ -n "${ignored_apps["$PKG_NAME|$PKG_NEW_VER"]}" ]; then if [ -n "${ignored_apps["$PKG_NAME|$PKG_NEW_VER"]}" ]; then
let update_app_number=$update_app_number-1 spark_update_count=$((spark_update_count - 1))
continue continue
fi fi
done done
# 还原分隔符
IFS="$IFS_OLD" IFS="$IFS_OLD"
if [ $update_app_number -le 0 ]; then fi
fi
apm_update_count=0
if [ "$has_apm" -eq 1 ]; then
updatetext=$(LANGUAGE=en_US apm update 2>&1)
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 apm update."
exit 1
fi
echo "Update failed...Will retry in 15sec"
sleep 15
updatetext=$(LANGUAGE=en_US apm update 2>&1)
retry_count=$((retry_count + 1))
done
apm clean
PKG_LIST="$(get-apm-upgradable-list)"
apm_update_count=$(printf '%s\n' "$PKG_LIST" | awk 'NF { count++ } END { print count + 0 }')
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
update_app_number=$((spark_update_count + apm_update_count))
if [ "$update_app_number" -le 0 ]; then
exit 0 exit 0
fi fi
update_transhell update_transhell