diff --git a/.gitignore b/.gitignore index 695f0a4..1ab0ece 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,10 @@ __pycache__/ *.pyc # 本机 agent 工具的会话产物,不入库 +# web-ext sign 生成的工具产物(UUID 上传标识),不入库 +.amo-upload-uuid +**/.amo-upload-uuid + +# pi 任务日志/产物,不入库 +.pi/ .pi-glla/ diff --git a/Dockerfile b/Dockerfile index 4b62c02..d51c044 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,6 +84,12 @@ COPY searxng-settings.yml /etc/searxng/settings.yml COPY conf.example.yml /app/defaults/conf.yml COPY admin/index.html /app/admin/index.html COPY portal /app/portal +# 站点图标:纸飞机。覆盖 Dashy 自带的 web-icons/favicon-*.png, +# 这样即使将来上游 index.html 变了、patch 失效,标签栏仍是我们的图标。 +COPY portal/favicon.svg /app/dashy/favicon.svg +COPY portal/favicon.ico /app/dashy/favicon.ico +COPY portal/apple-touch-icon.png /app/dashy/apple-touch-icon.png +COPY portal/web-icons/ /app/dashy/web-icons/ # 浏览器扩展安装页与安装包(通过 /extension/ 访问) COPY web/extension/install.html /app/dashy/extension/install.html diff --git a/deploy/sign-firefox.sh b/deploy/sign-firefox.sh new file mode 100755 index 0000000..3a4599c --- /dev/null +++ b/deploy/sign-firefox.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# Firefox 扩展:构建 → lint → AMO unlisted 签名 → 发布到数据卷 +# +# 为什么需要这个脚本(实测踩坑,2026-10-03): +# 1. 正式版 Firefox 要求扩展经 AMO 签名,未签名一律拒绝并报成「插件损坏」。 +# 2. 签名包一旦生成就与内容绑定 —— 改一行代码就必须重新签名, +# 所以「签名」必须跟着「改代码」这一步自动发生,不能靠人记得手动做。 +# 3. 签名是 AMO 生成的产物,不该进版本库(签名与源码脱节 + 是二进制), +# 因此落在数据卷里,跨部署存活;nginx 优先供给它(见 nginx.conf)。 +# +# 凭据:只从环境变量读,绝不写进本仓库(AGENTS.md §6 脱敏铁律) +# WEB_EXT_API_KEY / WEB_EXT_API_SECRET —— AMO 开发者页面「API 密钥」里的 +# JWT issuer:secret。属于程序化签名凭据,与账号密码无关。 +# 先在 https://addons.mozilla.org/developers/ → 你的扩展 → 设置 → API 密钥 获取。 +# +# 用法: +# WEB_EXT_API_KEY=... WEB_EXT_API_SECRET=... deploy/sign-firefox.sh # 签名并发布 +# deploy/sign-firefox.sh --check # 只跑 lint,不签名 +set -euo pipefail + +SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +EXT_DIR="$SRC/extension/firefox" +WEB_EXT="${WEB_EXT:-npx --yes web-ext@8}" + +CHECK_ONLY=0 +[ "${1:-}" = "--check" ] && CHECK_ONLY=1 + +echo "== 1/4 语法与规范检查 ==" +node --check "$EXT_DIR/background.js" +node --check "$EXT_DIR/options.js" +node --check "$EXT_DIR/detect.js" +# lint 必须 0 error:AMO 用的是同一个 addons-linter,error 会被直接拒收。 +# 注意 notice 不阻塞上传,但要能看见(提示 FUTURE/MV3 之类,不静默吞掉)。 +LINT_OUT="$(mktemp)" +trap 'rm -f "$LINT_OUT"' EXIT +(cd "$EXT_DIR/.." && $WEB_EXT lint --source-dir=firefox --output text) >"$LINT_OUT" 2>&1 || { + cat "$LINT_OUT"; echo "!! lint 执行失败" >&2; exit 1; } +grep -E '^(errors|warnings|notices)' "$LINT_OUT" | sed 's/^/ /' +grep -qE '^errors +0' "$LINT_OUT" || { cat "$LINT_OUT"; echo "!! 有 error,AMO 会拒收" >&2; exit 1; } +grep -E '^(NOTICES|WARNINGS):' -A6 "$LINT_OUT" | grep -vE '^--$' | sed 's/^/ /' || true + +VER="$(python3 -c "import json;print(json.load(open('$EXT_DIR/manifest.json'))['version'])")" +echo " 版本: $VER" + +if [ "$CHECK_ONLY" = "1" ]; then + echo "--check 模式:跳过签名与发布。" + exit 0 +fi + +echo "== 2/4 AMO 签名(unlisted 自分发)==" +[ -n "${WEB_EXT_API_KEY:-}" ] && [ -n "${WEB_EXT_API_SECRET:-}" ] || { + cat >&2 <<'EOF' +!! 缺少 AMO 凭据。请设置环境变量后重试: + WEB_EXT_API_KEY='user::' \ + WEB_EXT_API_SECRET='' \ + deploy/sign-firefox.sh + 凭据在 https://addons.mozilla.org/developers/ → 你的扩展 → 设置 → API 密钥 获取。 + 不要把凭据写进仓库或任何会被提交的文件(AGENTS.md §6)。 +EOF + exit 2; } + +ART="$(mktemp -d)" +trap 'rm -rf "$ART" "$LINT_OUT"' EXIT +(cd "$EXT_DIR/.." && $WEB_EXT sign --source-dir=firefox \ + --artifacts-dir="$ART" --channel=unlisted --no-input) 2>&1 \ + | grep -vE 'DeprecationWarning|trace-deprecation|no version information' | sed 's/^/ /' + +XPI="$(ls "$ART"/*.xpi 2>/dev/null | head -1 || true)" +[ -n "$XPI" ] || { echo "!! 未产出签名包" >&2; exit 1; } + +echo "== 3/4 校验签名包 ==" +# 不能只看「web-ext 说成功」:签名条目缺失的包装上去照样报「插件损坏」。 +python3 - "$XPI" <<'PY' +import json, sys, zipfile +z = zipfile.ZipFile(sys.argv[1]) +names = z.namelist() +need = ['META-INF/mozilla.rsa', 'META-INF/mozilla.sf', 'META-INF/cose.manifest'] +missing = [n for n in need if n not in names] +if missing: + sys.exit(f'!! 签名条目缺失: {missing}') +m = json.loads(z.read('manifest.json')) +g = m['browser_specific_settings']['gecko'] +print(f" 签名条目齐备 版本={m['version']} id={g['id']}") +print(f" data_collection={g.get('data_collection_permissions')}") +PY + +echo "== 4/4 发布到数据卷 ==" +# 没给 NAVI_HOST 就只把包落到本地,供人工上传。 +if [ -z "${NAVI_HOST:-}" ]; then + OUT="${1:-$SRC}/navi-firefox-signed.xpi" + cp "$XPI" "$OUT" + echo " 未设 NAVI_HOST,仅本地输出: $OUT" + echo " (线上发布:NAVI_HOST=user@host NAVI_SSH_PASS=... deploy/sign-firefox.sh)" + exit 0 +fi +SSH_PASS="${NAVI_SSH_PASS:-}" +if [ -n "$SSH_PASS" ]; then + SSH=(sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no "$NAVI_HOST") + SCP=(sshpass -p "$SSH_PASS" scp -q -o StrictHostKeyChecking=no) +else + SSH=(ssh -o StrictHostKeyChecking=no "$NAVI_HOST") + SCP=(scp -q -o StrictHostKeyChecking=no) +fi +DATA_DIR="${NAVI_DATA_DIR:-/vol1/docker/navi-data}" +STAMP="$(date +%Y%m%d%H%M%S)" + +"${SCP[@]}" "$XPI" "$NAVI_HOST:$DATA_DIR/extension/.navi-firefox.xpi.new" +# 先备份再原子替换:中途失败不会让线上退化成半个文件 +"${SSH[@]}" "set -e + cd $DATA_DIR/extension + [ -f navi-firefox.xpi ] && cp -a navi-firefox.xpi navi-firefox.xpi.bak-\$(date +%s) + mv .navi-firefox.xpi.new navi-firefox.xpi + echo \" 已发布: \$(sha256sum navi-firefox.xpi | cut -d' ' -f1)\"" | sed 's/^/ /' + +echo "SIGN_OK version=$VER" diff --git a/deploy/zd-host.sh b/deploy/zd-host.sh index 45f2bf1..1823b6b 100644 --- a/deploy/zd-host.sh +++ b/deploy/zd-host.sh @@ -69,6 +69,11 @@ COPY searxng-settings.yml /etc/searxng/settings.yml COPY conf.example.yml /app/defaults/conf.yml COPY admin/index.html /app/admin/index.html COPY portal /app/portal +# 站点图标:纸飞机(覆盖 Dashy 自带的那套,双保险) +COPY portal/favicon.svg /app/dashy/favicon.svg +COPY portal/favicon.ico /app/dashy/favicon.ico +COPY portal/apple-touch-icon.png /app/dashy/apple-touch-icon.png +COPY portal/web-icons/ /app/dashy/web-icons/ COPY web/extension/install.html /app/dashy/extension/install.html COPY ext/navi-chrome.zip /app/dashy/extension/navi-chrome.zip COPY ext/navi-firefox.xpi /app/dashy/extension/navi-firefox.xpi diff --git a/extension/chrome/icons/icon128.png b/extension/chrome/icons/icon128.png index 76eaa6b..f381359 100644 Binary files a/extension/chrome/icons/icon128.png and b/extension/chrome/icons/icon128.png differ diff --git a/extension/chrome/icons/icon16.png b/extension/chrome/icons/icon16.png index 57e350d..d84f62c 100644 Binary files a/extension/chrome/icons/icon16.png and b/extension/chrome/icons/icon16.png differ diff --git a/extension/chrome/icons/icon48.png b/extension/chrome/icons/icon48.png index f78ca3d..48f1551 100644 Binary files a/extension/chrome/icons/icon48.png and b/extension/chrome/icons/icon48.png differ diff --git a/extension/firefox/.amo-upload-uuid b/extension/firefox/.amo-upload-uuid deleted file mode 100644 index a40d99c..0000000 --- a/extension/firefox/.amo-upload-uuid +++ /dev/null @@ -1 +0,0 @@ -{"uploadUuid":"fb05e275eefe472399d38c15fb248870","channel":"unlisted","xpiCrcHash":"e49706a0c09801097e98c94bb18b76353480053afc23f75e3590920384d8faee"} \ No newline at end of file diff --git a/extension/firefox/icons/icon128.png b/extension/firefox/icons/icon128.png index 76eaa6b..f381359 100644 Binary files a/extension/firefox/icons/icon128.png and b/extension/firefox/icons/icon128.png differ diff --git a/extension/firefox/icons/icon16.png b/extension/firefox/icons/icon16.png index 57e350d..d84f62c 100644 Binary files a/extension/firefox/icons/icon16.png and b/extension/firefox/icons/icon16.png differ diff --git a/extension/firefox/icons/icon48.png b/extension/firefox/icons/icon48.png index f78ca3d..48f1551 100644 Binary files a/extension/firefox/icons/icon48.png and b/extension/firefox/icons/icon48.png differ diff --git a/extension/firefox/icons/icon96.png b/extension/firefox/icons/icon96.png index cecfe33..acef1dd 100644 Binary files a/extension/firefox/icons/icon96.png and b/extension/firefox/icons/icon96.png differ diff --git a/portal/apple-touch-icon.png b/portal/apple-touch-icon.png new file mode 100644 index 0000000..629eb88 Binary files /dev/null and b/portal/apple-touch-icon.png differ diff --git a/portal/favicon.ico b/portal/favicon.ico index 806d4d1..8dc270d 100644 Binary files a/portal/favicon.ico and b/portal/favicon.ico differ diff --git a/portal/favicon.svg b/portal/favicon.svg index 376e686..c20f61b 100644 --- a/portal/favicon.svg +++ b/portal/favicon.svg @@ -1 +1,25 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + diff --git a/portal/web-icons/favicon-16x16.png b/portal/web-icons/favicon-16x16.png new file mode 100644 index 0000000..d84f62c Binary files /dev/null and b/portal/web-icons/favicon-16x16.png differ diff --git a/portal/web-icons/favicon-32x32.png b/portal/web-icons/favicon-32x32.png new file mode 100644 index 0000000..7ef1c94 Binary files /dev/null and b/portal/web-icons/favicon-32x32.png differ diff --git a/portal/web-icons/favicon-64x64.png b/portal/web-icons/favicon-64x64.png new file mode 100644 index 0000000..feccdff Binary files /dev/null and b/portal/web-icons/favicon-64x64.png differ diff --git a/web/patch-index.py b/web/patch-index.py index c049aed..08bf9b3 100644 --- a/web/patch-index.py +++ b/web/patch-index.py @@ -12,13 +12,24 @@ import sys CSS_TAG = '' JS_TAG = '' +# Dashy 自带的 logo(/web-icons/favicon-*.png)会被浏览器优先选中, +# 于是站点标签栏显示上游 logo 而不是我们的纸飞机。 +# 做法:删掉那些 ,再显式指向我们自己的 /favicon.svg。 +# 为什么用“删掉”而不是“改路径”:index.html 来自上游镜像,每次重建可能被重置, +# 删标签幂等,且不依赖上游将来还叫 web-icons。 +ICON_LINK_RE = re.compile(r'[ \t]*]*rel=["\']icon["\'][^>]*>\s*', re.I) +OUR_ICONS = ('\n' + '') + def patch(html: str) -> str: html = re.sub(r'[ \t]*\s*', '', html) html = re.sub(r'[ \t]*\s*', '', html) + html = re.sub(r'[ \t]*' + re.escape(OUR_ICONS) + r'\s*', '', html) + html = ICON_LINK_RE.sub('', html) if '' in html: - html = html.replace('', CSS_TAG + '', 1) + html = html.replace('', CSS_TAG + OUR_ICONS + '', 1) if '' in html: html = html.replace('', JS_TAG + '', 1) @@ -36,6 +47,12 @@ def main(path: str) -> int: if 'href="/navi.css"' not in patched or 'src="/navi.js"' not in patched: print('patch failed: tags missing after patch', file=sys.stderr) return 1 + if 'web-icons/favicon' in patched: + print('patch failed: upstream Dashy icon links survived', file=sys.stderr) + return 1 + if '/favicon.svg' not in patched: + print('patch failed: our icon link missing', file=sys.stderr) + return 1 with open(path, 'w', encoding='utf-8') as fh: fh.write(patched) print('patched %s (%s)' % (path, 'changed' if patched != original else 'unchanged'))