@lyra_wang/chrome-frequent-pages 0.4.3 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/config.example.json +2 -2
- package/inbox-deliver-mac.py +66 -0
- package/install.sh +20 -11
- package/package.json +29 -6
- package/run-if-due.sh +58 -0
- package/table_snapshot.py +46 -0
package/README.md
CHANGED
|
@@ -144,3 +144,46 @@ python3 main.py --bookmarks-from-table ~/Desktop/常用网页书签.html
|
|
|
144
144
|
```
|
|
145
145
|
|
|
146
146
|
读取飞书表每个唯一「域名」,生成 `https://<域名>/` 书签 HTML(Chrome 书签管理器导入)。
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 开机触发(每周一次)+ 投递到收件箱
|
|
151
|
+
|
|
152
|
+
数据在本机 Chrome 里,所以执行必须在 Mac 上。定时不用时钟,用**开机作为触发点**:
|
|
153
|
+
|
|
154
|
+
- `run-if-due.sh`:launchd 的 `RunAtLoad` 调用;读状态文件判断本周是否已跑,
|
|
155
|
+
没跑则执行 `chrome-frequent-pages --write`,跑完写状态(失败不写,下次开机重试)。
|
|
156
|
+
- `inbox-deliver-mac.py`:把结果写进飞书「任务收件箱」,DSH 收件箱面板会显示为「待读」。
|
|
157
|
+
- `table_snapshot.py`:执行前后各取一次「常用网页」表快照,diff 出本周**新增了哪些页面**,
|
|
158
|
+
摘要里带上「标题(域名)→ 书签「分类」」;未分类的页面导出书签时归入「其他」。
|
|
159
|
+
|
|
160
|
+
### 安装
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npm i -g @lyra_wang/chrome-frequent-pages@0.5.2 # 或 npm i -g file:$PWD
|
|
164
|
+
bash "$(dirname "$(readlink -f "$(command -v chrome-frequent-pages)")")/../install.sh"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`install.sh` 会把 `main.py` / `run-if-due.sh` / `inbox-deliver-mac.py` / `table_snapshot.py`
|
|
168
|
+
复制到 `~/.local/share/chrome-frequent-pages/`,并注册 `RunAtLoad` 的 LaunchAgent(无定时时刻)。
|
|
169
|
+
|
|
170
|
+
### 必需的 macOS 权限(否则读不到 Chrome 历史)
|
|
171
|
+
|
|
172
|
+
「完全磁盘访问权限」里需要加入三项(后台进程按二进制逐个鉴权):
|
|
173
|
+
|
|
174
|
+
| 加入项 | 作用 |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `/bin/bash` | 启动脚本 |
|
|
177
|
+
| `/usr/local/bin/node` | npm CLI 需要 |
|
|
178
|
+
| `/usr/bin/python3` | 实际读 History 的进程 |
|
|
179
|
+
|
|
180
|
+
### 行为
|
|
181
|
+
|
|
182
|
+
| 场景 | 结果 |
|
|
183
|
+
|---|---|
|
|
184
|
+
| 本周第一次开机/登录 | 执行一次 |
|
|
185
|
+
| 同周再次开机 | 状态文件命中,跳过 |
|
|
186
|
+
| 执行失败 | 不写状态,下次开机重试;失败结果也会写收件箱 |
|
|
187
|
+
|
|
188
|
+
状态文件:`~/.config/chrome-frequent-pages/last-run.json`(`{"week":"2026-43","at":"..."}`)
|
|
189
|
+
日志:`~/.config/chrome-frequent-pages/run-if-due.log`
|
package/config.example.json
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""把一条任务结果写进飞书「任务收件箱」(Mac 侧,使用本机 lark-cli 用户身份)。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
inbox-deliver-mac.py --task chrome-frequent-pages --result 成功 --summary "本周新增 3 条"
|
|
7
|
+
"""
|
|
8
|
+
import argparse, json, subprocess, sys
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
BASE = "NeujbbOfcaPEvMsPIxscaJImn9d"
|
|
12
|
+
INBOX = "tbl9LghPBTjFLH4N"
|
|
13
|
+
TASKS = "tbl4k7ggY1paqoUn"
|
|
14
|
+
LARK = "lark-cli"
|
|
15
|
+
|
|
16
|
+
def lark(args):
|
|
17
|
+
cmd = [LARK] + args + ["--as", "user", "--format", "json"]
|
|
18
|
+
res = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
|
|
19
|
+
try:
|
|
20
|
+
payload = json.loads(res.stdout or "{}")
|
|
21
|
+
except Exception:
|
|
22
|
+
payload = {}
|
|
23
|
+
if payload.get("ok") is False:
|
|
24
|
+
raise SystemExit("lark-cli failed: " + json.dumps(payload.get("error", payload), ensure_ascii=False))
|
|
25
|
+
if res.returncode != 0:
|
|
26
|
+
raise SystemExit("lark-cli exited %s: %s" % (res.returncode, (res.stderr or "")[:300]))
|
|
27
|
+
return payload
|
|
28
|
+
|
|
29
|
+
def task_record_id(name):
|
|
30
|
+
payload = lark(["base", "+record-list", "--base-token", BASE, "--table-id", TASKS, "--limit", "200"])
|
|
31
|
+
meta = payload.get("data") or {}
|
|
32
|
+
fields = meta.get("fields") or []
|
|
33
|
+
rows = meta.get("data") or []
|
|
34
|
+
ids = meta.get("record_id_list") or []
|
|
35
|
+
if "任务名称" not in fields:
|
|
36
|
+
return ""
|
|
37
|
+
idx = fields.index("任务名称")
|
|
38
|
+
for rid, row in zip(ids, rows):
|
|
39
|
+
if str(row[idx]).strip() == name:
|
|
40
|
+
return str(rid)
|
|
41
|
+
return ""
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
ap = argparse.ArgumentParser()
|
|
45
|
+
ap.add_argument("--task", required=True)
|
|
46
|
+
ap.add_argument("--result", required=True, choices=["成功", "失败", "超时"])
|
|
47
|
+
ap.add_argument("--summary", required=True)
|
|
48
|
+
args = ap.parse_args()
|
|
49
|
+
summary = args.summary.strip()
|
|
50
|
+
if len(summary) > 300:
|
|
51
|
+
summary = summary[:299] + "…"
|
|
52
|
+
record = {
|
|
53
|
+
"结果摘要": summary,
|
|
54
|
+
"结果": [args.result],
|
|
55
|
+
"状态": ["未读"],
|
|
56
|
+
"执行任务": int(datetime.now().timestamp() * 1000),
|
|
57
|
+
}
|
|
58
|
+
rid = task_record_id(args.task)
|
|
59
|
+
if rid:
|
|
60
|
+
record["关联任务"] = [rid]
|
|
61
|
+
lark(["base", "+record-batch-create", "--base-token", BASE, "--table-id", INBOX,
|
|
62
|
+
"--json", json.dumps({"create_records": [record]}, ensure_ascii=False)])
|
|
63
|
+
print(json.dumps({"ok": True, "task": args.task, "linked": bool(rid)}, ensure_ascii=False))
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
main()
|
package/install.sh
CHANGED
|
@@ -3,13 +3,15 @@ set -eu
|
|
|
3
3
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
4
4
|
BIN_DIR="$HOME/.local/bin"
|
|
5
5
|
DATA_DIR="$HOME/.local/share/chrome-frequent-pages"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
CONF_DIR="$HOME/.config/chrome-frequent-pages"
|
|
7
|
+
mkdir -p "$BIN_DIR" "$DATA_DIR" "$CONF_DIR"
|
|
8
|
+
|
|
9
|
+
# launchd 读不了 ~/Documents(TCC),脚本要放到不受保护的位置
|
|
10
|
+
cp "$HERE/main.py" "$HERE/run-if-due.sh" "$HERE/inbox-deliver-mac.py" "$HERE/table_snapshot.py" "$DATA_DIR/"
|
|
11
|
+
chmod +x "$DATA_DIR/main.py" "$DATA_DIR/run-if-due.sh" "$DATA_DIR/inbox-deliver-mac.py" "$DATA_DIR/table_snapshot.py"
|
|
10
12
|
ln -sf "$DATA_DIR/main.py" "$BIN_DIR/chrome-frequent-pages"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
[ -f "$CONF_DIR/config.json" ] || cp "$HERE/config.example.json" "$CONF_DIR/config.json"
|
|
14
|
+
|
|
13
15
|
PLIST="$HOME/Library/LaunchAgents/com.lyrawang.chrome-frequent-pages.plist"
|
|
14
16
|
cat > "$PLIST" <<EOF
|
|
15
17
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -17,14 +19,21 @@ cat > "$PLIST" <<EOF
|
|
|
17
19
|
<plist version="1.0"><dict>
|
|
18
20
|
<key>Label</key><string>com.lyrawang.chrome-frequent-pages</string>
|
|
19
21
|
<key>ProgramArguments</key><array>
|
|
20
|
-
<string>/
|
|
22
|
+
<string>/bin/bash</string><string>$DATA_DIR/run-if-due.sh</string>
|
|
21
23
|
</array>
|
|
22
|
-
<key>
|
|
23
|
-
<key>StandardOutPath</key><string>$
|
|
24
|
-
<key>StandardErrorPath</key><string>$
|
|
24
|
+
<key>RunAtLoad</key><true/>
|
|
25
|
+
<key>StandardOutPath</key><string>$CONF_DIR/launchd.out.log</string>
|
|
26
|
+
<key>StandardErrorPath</key><string>$CONF_DIR/launchd.err.log</string>
|
|
27
|
+
<key>ProcessType</key><string>Background</string>
|
|
25
28
|
</dict></plist>
|
|
26
29
|
EOF
|
|
27
30
|
launchctl bootout "gui/$(id -u)/com.lyrawang.chrome-frequent-pages" 2>/dev/null || true
|
|
28
31
|
launchctl bootstrap "gui/$(id -u)" "$PLIST" 2>/dev/null || launchctl load "$PLIST" 2>/dev/null || true
|
|
29
32
|
echo "✓ 已安装:~/.local/bin/chrome-frequent-pages"
|
|
30
|
-
echo "✓
|
|
33
|
+
echo "✓ 已注册开机触发(launchd RunAtLoad,每周首次开机跑一次)"
|
|
34
|
+
cat <<'NOTE'
|
|
35
|
+
|
|
36
|
+
⚠️ 首次安装需在「系统设置 → 隐私与安全性 → 完全磁盘访问权限」中加入:
|
|
37
|
+
/bin/bash、/usr/local/bin/node、/usr/bin/python3
|
|
38
|
+
否则读不到 Chrome History(会写一条「失败」摘要到收件箱)。
|
|
39
|
+
NOTE
|
package/package.json
CHANGED
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyra_wang/chrome-frequent-pages",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Chrome 高频网页 → 飞书「常用网页」表 + 按分类导出 Chrome 书签(根域名 / 10 天 / >5 次)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"keywords": [
|
|
7
|
+
"keywords": [
|
|
8
|
+
"chrome",
|
|
9
|
+
"history",
|
|
10
|
+
"bookmarks",
|
|
11
|
+
"feishu",
|
|
12
|
+
"lark",
|
|
13
|
+
"cli"
|
|
14
|
+
],
|
|
8
15
|
"repository": {
|
|
9
16
|
"type": "git",
|
|
10
17
|
"url": "git+https://github.com/LyraWang6688/ai-capability-store.git",
|
|
11
18
|
"directory": "external-capabilities/cli/chrome-frequent-pages"
|
|
12
19
|
},
|
|
13
|
-
"bin": {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
20
|
+
"bin": {
|
|
21
|
+
"chrome-frequent-pages": "bin/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"main.py",
|
|
25
|
+
"run-if-due.sh",
|
|
26
|
+
"inbox-deliver-mac.py",
|
|
27
|
+
"table_snapshot.py",
|
|
28
|
+
"bin/",
|
|
29
|
+
"config.example.json",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"install.sh"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
}
|
|
17
40
|
}
|
package/run-if-due.sh
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 开机触发:每周只跑一次(ISO 周去重)。由 launchd 的 RunAtLoad 调用。
|
|
3
|
+
set -u
|
|
4
|
+
STATE_DIR="$HOME/.config/chrome-frequent-pages"
|
|
5
|
+
STATE="$STATE_DIR/last-run.json"
|
|
6
|
+
LOG="$STATE_DIR/run-if-due.log"
|
|
7
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
9
|
+
WEEK="$(date +%G-%V)"
|
|
10
|
+
mkdir -p "$STATE_DIR"
|
|
11
|
+
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
|
|
12
|
+
|
|
13
|
+
LAST=""
|
|
14
|
+
if [ -f "$STATE" ]; then
|
|
15
|
+
LAST="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("week",""))' "$STATE" 2>/dev/null || true)"
|
|
16
|
+
fi
|
|
17
|
+
if [ "$LAST" = "$WEEK" ]; then
|
|
18
|
+
log "skip: week $WEEK already ran"
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
SNAP_BEFORE="$(python3 "$HERE/table_snapshot.py" 2>/dev/null || echo '{}')"
|
|
23
|
+
OUT="$(chrome-frequent-pages --write 2>&1)"
|
|
24
|
+
CODE=$?
|
|
25
|
+
if [ $CODE -ne 0 ]; then
|
|
26
|
+
log "run failed (exit $CODE): $(echo "$OUT" | tail -3 | tr '\n' ' ')"
|
|
27
|
+
python3 "$HERE/inbox-deliver-mac.py" --task chrome-frequent-pages --result 失败 \
|
|
28
|
+
--summary "chrome-frequent-pages 执行失败:$(echo "$OUT" | tail -1 | cut -c1-70)" >> "$LOG" 2>&1 || true
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
LINE="$(echo "$OUT" | grep -o '飞书写入完成:.*' | tail -1)"
|
|
33
|
+
[ -z "$LINE" ] && LINE="$(echo "$OUT" | tail -1)"
|
|
34
|
+
SNAP_AFTER="$(python3 "$HERE/table_snapshot.py" 2>/dev/null || echo '{}')"
|
|
35
|
+
DETAIL="$(python3 - "$SNAP_BEFORE" "$SNAP_AFTER" <<'PYD'
|
|
36
|
+
import json, sys
|
|
37
|
+
before = json.loads(sys.argv[1] or "{}")
|
|
38
|
+
after = json.loads(sys.argv[2] or "{}")
|
|
39
|
+
new = [(h, v) for h, v in after.items() if h not in before]
|
|
40
|
+
if not new:
|
|
41
|
+
print("本周无新增页面")
|
|
42
|
+
else:
|
|
43
|
+
parts = []
|
|
44
|
+
for host, v in new[:5]:
|
|
45
|
+
cat = v.get("cat") or "其他"
|
|
46
|
+
tag = "未分类→其他" if cat == "其他" else cat
|
|
47
|
+
parts.append(f"{v.get('title') or host}({host})→ 书签「{tag}」")
|
|
48
|
+
more = "" if len(new) <= 5 else f" 等 {len(new)} 条"
|
|
49
|
+
print("新增:" + ";".join(parts) + more)
|
|
50
|
+
PYD
|
|
51
|
+
)"
|
|
52
|
+
python3 "$HERE/inbox-deliver-mac.py" --task chrome-frequent-pages --result 成功 \
|
|
53
|
+
--summary "$DETAIL;$LINE" >> "$LOG" 2>&1 || log "inbox deliver failed"
|
|
54
|
+
python3 - "$STATE" "$WEEK" <<'PY'
|
|
55
|
+
import json, sys, datetime
|
|
56
|
+
json.dump({"week": sys.argv[2], "at": datetime.datetime.now().isoformat(timespec="seconds")}, open(sys.argv[1], "w"), ensure_ascii=False)
|
|
57
|
+
PY
|
|
58
|
+
log "done week=$WEEK result=$LINE"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""导出「常用网页」表的 域名 -> {标题, 分类} 快照,供 run-if-due.sh 做前后 diff。"""
|
|
4
|
+
import json, subprocess, sys
|
|
5
|
+
|
|
6
|
+
BASE = "ZNjDb9TrpalLuCsw0macCeVdnld"
|
|
7
|
+
TABLE = "tblDejemWsgqKSA2"
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
out = subprocess.run(
|
|
11
|
+
["lark-cli", "base", "+record-list", "--base-token", BASE, "--table-id", TABLE,
|
|
12
|
+
"--limit", "200", "--as", "user", "--format", "json"],
|
|
13
|
+
capture_output=True, text=True, timeout=120)
|
|
14
|
+
try:
|
|
15
|
+
data = json.loads(out.stdout)
|
|
16
|
+
except Exception:
|
|
17
|
+
print("{}")
|
|
18
|
+
return 1
|
|
19
|
+
meta = (data.get("data") or {})
|
|
20
|
+
cols = meta.get("fields") or []
|
|
21
|
+
rows = meta.get("data") or []
|
|
22
|
+
if "域名" not in cols:
|
|
23
|
+
print("{}")
|
|
24
|
+
return 1
|
|
25
|
+
idx = {name: i for i, name in enumerate(cols)}
|
|
26
|
+
snap = {}
|
|
27
|
+
for row in rows:
|
|
28
|
+
host = str(row[idx["域名"]] or "").strip()
|
|
29
|
+
if not host:
|
|
30
|
+
continue
|
|
31
|
+
cat = "其他"
|
|
32
|
+
if "分类" in idx:
|
|
33
|
+
raw = row[idx["分类"]]
|
|
34
|
+
if isinstance(raw, list) and raw:
|
|
35
|
+
cat = str(raw[0])
|
|
36
|
+
elif isinstance(raw, str) and raw.strip():
|
|
37
|
+
cat = raw.strip()
|
|
38
|
+
title = ""
|
|
39
|
+
if "标题" in idx and row[idx["标题"]]:
|
|
40
|
+
title = str(row[idx["标题"]]).strip()
|
|
41
|
+
snap[host] = {"title": title or host, "cat": cat}
|
|
42
|
+
print(json.dumps(snap, ensure_ascii=False))
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
sys.exit(main())
|