@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 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`
@@ -9,8 +9,8 @@
9
9
  "localhost",
10
10
  "127.0.0.1"
11
11
  ],
12
- "base_token": "ZNjDb9TrpalLuCsw0macCeVdnld",
13
- "table_id": "tblDejemWsgqKSA2",
12
+ "base_token": "<feishu-base-token>",
13
+ "table_id": "<table-id>",
14
14
  "identity": "user",
15
15
  "rules": {
16
16
  "drop_hosts": [
@@ -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
- mkdir -p "$BIN_DIR" "$DATA_DIR" "$HOME/.config/chrome-frequent-pages"
7
- # launchd 读不了 ~/Documents(TCC),所以把脚本复制到不受保护的目录再跑
8
- cp "$HERE/main.py" "$DATA_DIR/main.py"
9
- chmod +x "$DATA_DIR/main.py"
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
- chmod +x "$HERE/main.py"
12
- [ -f "$HOME/.config/chrome-frequent-pages/config.json" ] || cp "$HERE/config.example.json" "$HOME/.config/chrome-frequent-pages/config.json"
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>/usr/bin/python3</string><string>$DATA_DIR/main.py</string><string>--write</string>
22
+ <string>/bin/bash</string><string>$DATA_DIR/run-if-due.sh</string>
21
23
  </array>
22
- <key>StartCalendarInterval</key><dict><key>Weekday</key><integer>1</integer><key>Hour</key><integer>9</integer><key>Minute</key><integer>30</integer></dict>
23
- <key>StandardOutPath</key><string>$HOME/.config/chrome-frequent-pages/run.log</string>
24
- <key>StandardErrorPath</key><string>$HOME/.config/chrome-frequent-pages/run.log</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 "✓ 已注册每周一 09:30 定时任务(launchd)"
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.4.3",
3
+ "version": "0.5.2",
4
4
  "description": "Chrome 高频网页 → 飞书「常用网页」表 + 按分类导出 Chrome 书签(根域名 / 10 天 / >5 次)",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "keywords": ["chrome", "history", "bookmarks", "feishu", "lark", "cli"],
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": { "chrome-frequent-pages": "bin/cli.js" },
14
- "files": ["main.py", "bin/", "config.example.json", "README.md", "LICENSE", "install.sh"],
15
- "publishConfig": { "access": "public" },
16
- "engines": { "node": ">=20" }
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())