feat(icon): 纸飞机图标(站点 favicon + 扩展各尺寸)
站点首页此前显示的是 Dashy 上游的 logo —— index.html 里那几条
<link rel=icon href=/web-icons/favicon-*.png> 优先级高于我们的门户图标,
而扩展的 PNG 是占位图(128px 只有 9 种颜色、alpha 通道还是乱的)。
- portal/favicon.svg:纸飞机(圆角渐变底 + 白色折纸机头朝右上 + 两道尾迹),
几何收在 8..56 安全区,避免贴边被圆角裁切
- 由该 SVG 派生 favicon.ico(16/32/48)、apple-touch-icon(180)、
web-icons/favicon-{16,32,64}、扩展 icon{16,48,96,128}
- patch-index.py:删掉上游 icon link 并显式指向 /favicon.svg(幂等,
重复执行 unchanged);上游 web-icons 那批也覆盖一份作双保险
- Dockerfile 与 zd-host.sh 都 COPY 这些图标进镜像
- 验证:nginx 沙箱实测 /favicon.svg、/web-icons/*、/apple-touch-icon.png 均 200;
16px 下人眼仍可辨识为纸飞机
- .amo-upload-uuid(web-ext 生成的 UUID)改为忽略
6
.gitignore
vendored
@ -28,4 +28,10 @@ __pycache__/
|
||||
*.pyc
|
||||
|
||||
# 本机 agent 工具的会话产物,不入库
|
||||
# web-ext sign 生成的工具产物(UUID 上传标识),不入库
|
||||
.amo-upload-uuid
|
||||
**/.amo-upload-uuid
|
||||
|
||||
# pi 任务日志/产物,不入库
|
||||
.pi/
|
||||
.pi-glla/
|
||||
|
||||
@ -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
|
||||
|
||||
115
deploy/sign-firefox.sh
Executable file
@ -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:<id>:<num>' \
|
||||
WEB_EXT_API_SECRET='<jwt 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"
|
||||
@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 79 B After Width: | Height: | Size: 552 B |
|
Before Width: | Height: | Size: 124 B After Width: | Height: | Size: 1.7 KiB |
@ -1 +0,0 @@
|
||||
{"uploadUuid":"fb05e275eefe472399d38c15fb248870","channel":"unlisted","xpiCrcHash":"e49706a0c09801097e98c94bb18b76353480053afc23f75e3590920384d8faee"}
|
||||
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 79 B After Width: | Height: | Size: 552 B |
|
Before Width: | Height: | Size: 124 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 223 B After Width: | Height: | Size: 3.7 KiB |
BIN
portal/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 556 B |
@ -1 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#0f172a"/><path stroke="#38bdf8" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M20 26h24l-5 7M20 26l15-12h6l-5 7M44 33l-9 3M20 26l7 18l10-6M27 44l11-6"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="HomeDash">
|
||||
<!-- 纸飞机图标:圆角方形底 + 白色纸飞机 + 尾迹。
|
||||
同一份图形用于站点 favicon 与扩展各尺寸图标(16/48/96/128),保持品牌一致。
|
||||
几何刻意收在 8..56 的安全区内,避免任何笔画贴边被圆角裁切。 -->
|
||||
<defs>
|
||||
<linearGradient id="sky" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#38bdf8"/>
|
||||
<stop offset="1" stop-color="#818cf8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 底:圆角方块,保证在浅色/深色标签栏都可辨识 -->
|
||||
<rect width="64" height="64" rx="14" fill="url(#sky)"/>
|
||||
|
||||
<!-- 尾迹:机身后方的两道速度线(小尺寸下自然淡出,不影响识别) -->
|
||||
<g stroke="#ffffff" stroke-width="2.6" stroke-linecap="round" opacity=".5" fill="none">
|
||||
<path d="M11 45c5-2 9-5 12-9"/>
|
||||
<path d="M13 52c6-2 11-5 15-10"/>
|
||||
</g>
|
||||
|
||||
<!-- 纸飞机主体:机头朝右上,全部坐标落在 8..56 内 -->
|
||||
<path fill="#ffffff" d="M12 30 54 12 40 54 30 40 12 30Z"/>
|
||||
<!-- 折纸的机翼分界线:从机尾折向机头,让小尺寸下也看得出是折纸而非纯三角 -->
|
||||
<path fill="#e0f2fe" d="M30 40 54 12 44 44 30 40Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 1.3 KiB |
BIN
portal/web-icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 552 B |
BIN
portal/web-icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
portal/web-icons/favicon-64x64.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@ -12,13 +12,24 @@ import sys
|
||||
CSS_TAG = '<link rel="stylesheet" href="/navi.css">'
|
||||
JS_TAG = '<script src="/navi.js"></script>'
|
||||
|
||||
# Dashy 自带的 logo(/web-icons/favicon-*.png)会被浏览器优先选中,
|
||||
# 于是站点标签栏显示上游 logo 而不是我们的纸飞机。
|
||||
# 做法:删掉那些 <link rel=icon>,再显式指向我们自己的 /favicon.svg。
|
||||
# 为什么用“删掉”而不是“改路径”:index.html 来自上游镜像,每次重建可能被重置,
|
||||
# 删标签幂等,且不依赖上游将来还叫 web-icons。
|
||||
ICON_LINK_RE = re.compile(r'[ \t]*<link[^>]*rel=["\']icon["\'][^>]*>\s*', re.I)
|
||||
OUR_ICONS = ('<link rel="icon" type="image/svg+xml" href="/favicon.svg">\n'
|
||||
'<link rel="apple-touch-icon" href="/apple-touch-icon.png">')
|
||||
|
||||
|
||||
def patch(html: str) -> str:
|
||||
html = re.sub(r'[ \t]*<link rel="stylesheet" href="/navi\.css">\s*', '', html)
|
||||
html = re.sub(r'[ \t]*<script src="/navi\.js"></script>\s*', '', html)
|
||||
html = re.sub(r'[ \t]*' + re.escape(OUR_ICONS) + r'\s*', '', html)
|
||||
html = ICON_LINK_RE.sub('', html)
|
||||
|
||||
if '</head>' in html:
|
||||
html = html.replace('</head>', CSS_TAG + '</head>', 1)
|
||||
html = html.replace('</head>', CSS_TAG + OUR_ICONS + '</head>', 1)
|
||||
|
||||
if '</body>' in html:
|
||||
html = html.replace('</body>', JS_TAG + '</body>', 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'))
|
||||
|
||||