self-evolve-framework 1.0.7 → 1.1.0
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 +12 -8
- package/bin/cli.js +63 -35
- package/package.json +1 -1
- package/template/rules/CodeGraph.mdc +23 -0
- package/template/rules/Svelte_5.mdc +167 -0
- package/template/rules/Svelte_Flow.mdc +176 -0
- package/template/rules/Tailwind_CSS_v4.mdc +187 -0
- package/template/rules/Tauri.mdc +145 -0
- package/template/rules/app-error-pattern.mdc +65 -0
- package/template/rules/invoke-safe-pattern.mdc +53 -0
- package/template/rules/js.mdc +10 -0
- package/template/rules/powershell.mdc +9 -0
- package/template/rules/self-evolve.mdc +4 -4
- package/template/rules//346/227/245/345/277/227.mdc +15 -0
- package/template/rules//350/257/267/346/261/202.mdc +49 -0
- package/template/skills/caveman/SKILL.md +49 -0
- package/template/skills/check/SKILL.md +393 -0
- package/template/skills/check/agents/reviewer-architecture.md +39 -0
- package/template/skills/check/agents/reviewer-security.md +39 -0
- package/template/skills/check/references/persona-catalog.md +56 -0
- package/template/skills/check/references/project-context.md +120 -0
- package/template/skills/check/references/public-reply.md +14 -0
- package/template/skills/check/scripts/audit_signals.py +666 -0
- package/template/skills/check/scripts/run-tests.sh +19 -0
- package/template/skills/design/SKILL.md +173 -0
- package/template/skills/design/references/design-aesthetic-quality.md +67 -0
- package/template/skills/design/references/design-data-viz.md +34 -0
- package/template/skills/design/references/design-reference.md +295 -0
- package/template/skills/design/references/design-tokens.md +45 -0
- package/template/skills/design/references/design-traps.md +43 -0
- package/template/skills/design-an-interface/SKILL.md +94 -0
- package/template/skills/diagnose/SKILL.md +117 -0
- package/template/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/template/skills/edit-article/SKILL.md +14 -0
- package/template/skills/git-guardrails-claude-code/SKILL.md +95 -0
- package/template/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +25 -0
- package/template/skills/grill-me/SKILL.md +10 -0
- package/template/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/template/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/template/skills/grill-with-docs/SKILL.md +88 -0
- package/template/skills/handoff/SKILL.md +15 -0
- package/template/skills/health/SKILL.md +260 -0
- package/template/skills/health/agents/inspector-context.md +119 -0
- package/template/skills/health/agents/inspector-control.md +84 -0
- package/template/skills/health/agents/inspector-maintainability.md +55 -0
- package/template/skills/health/scripts/check-agent-context.sh +5 -0
- package/template/skills/health/scripts/check-doc-refs.sh +8 -0
- package/template/skills/health/scripts/check-maintainability.sh +8 -0
- package/template/skills/health/scripts/check-verifier-output.sh +5 -0
- package/template/skills/health/scripts/check_agent_context.py +444 -0
- package/template/skills/health/scripts/check_doc_refs.py +110 -0
- package/template/skills/health/scripts/check_maintainability.py +635 -0
- package/template/skills/health/scripts/check_verifier_output.py +116 -0
- package/template/skills/health/scripts/collect-data.sh +751 -0
- package/template/skills/hunt/SKILL.md +232 -0
- package/template/skills/hunt/references/failure-patterns.md +138 -0
- package/template/skills/hunt/references/ime-unicode.md +58 -0
- package/template/skills/hunt/references/logging-techniques.md +72 -0
- package/template/skills/hunt/references/rendering-debug.md +34 -0
- package/template/skills/impeccable/SKILL.md +47 -0
- package/template/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/template/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/template/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/template/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/template/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/template/skills/learn/SKILL.md +140 -0
- package/template/skills/migrate-to-shoehorn/SKILL.md +118 -0
- package/template/skills/obsidian-vault/SKILL.md +59 -0
- package/template/skills/prototype/LOGIC.md +79 -0
- package/template/skills/prototype/SKILL.md +30 -0
- package/template/skills/prototype/UI.md +112 -0
- package/template/skills/qa/SKILL.md +130 -0
- package/template/skills/read/SKILL.md +141 -0
- package/template/skills/read/references/read-methods.md +129 -0
- package/template/skills/read/scripts/fetch.sh +106 -0
- package/template/skills/read/scripts/fetch_feishu.py +251 -0
- package/template/skills/read/scripts/fetch_local.py +218 -0
- package/template/skills/read/scripts/fetch_weixin.py +107 -0
- package/template/skills/request-refactor-plan/SKILL.md +68 -0
- package/template/skills/review/SKILL.md +78 -0
- package/template/skills/rust-auto-fix/SKILL.md +94 -0
- package/template/skills/scaffold-exercises/SKILL.md +106 -0
- package/template/skills/sdd-dev/SKILL.md +114 -0
- package/template/skills/setup-matt-pocock-skills/SKILL.md +121 -0
- package/template/skills/setup-matt-pocock-skills/domain.md +51 -0
- package/template/skills/setup-matt-pocock-skills/issue-tracker-github.md +22 -0
- package/template/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md +23 -0
- package/template/skills/setup-matt-pocock-skills/issue-tracker-local.md +19 -0
- package/template/skills/setup-matt-pocock-skills/triage-labels.md +15 -0
- package/template/skills/setup-pre-commit/SKILL.md +91 -0
- package/template/skills/skillopt-sleep/SKILL.md +1 -1
- package/template/skills/svelte-warnings-fix/SKILL.md +94 -0
- package/template/skills/tauri-nsis-installer-icon/SKILL.md +92 -0
- package/template/skills/tauri-nsis-installer-icon/references/tauri-nsis-schema.md +71 -0
- package/template/skills/tb/SKILL.md +62 -0
- package/template/skills/tdd/SKILL.md +109 -0
- package/template/skills/tdd/deep-modules.md +33 -0
- package/template/skills/tdd/interface-design.md +31 -0
- package/template/skills/tdd/mocking.md +59 -0
- package/template/skills/tdd/refactoring.md +10 -0
- package/template/skills/tdd/tests.md +61 -0
- package/template/skills/teach/GLOSSARY-FORMAT.md +35 -0
- package/template/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/template/skills/teach/MISSION-FORMAT.md +31 -0
- package/template/skills/teach/RESOURCES-FORMAT.md +32 -0
- package/template/skills/teach/SKILL.md +91 -0
- package/template/skills/think/SKILL.md +184 -0
- package/template/skills/to-issues/SKILL.md +83 -0
- package/template/skills/to-prd/SKILL.md +74 -0
- package/template/skills/triage/AGENT-BRIEF.md +168 -0
- package/template/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/template/skills/triage/SKILL.md +103 -0
- package/template/skills/ubiquitous-language/SKILL.md +93 -0
- package/template/skills/ver/SKILL.md +62 -0
- package/template/skills/write/SKILL.md +209 -0
- package/template/skills/write/references/write-en.md +199 -0
- package/template/skills/write/references/write-product-localization.md +43 -0
- package/template/skills/write/references/write-zh-bilingual.md +59 -0
- package/template/skills/write/references/write-zh-prose.md +50 -0
- package/template/skills/write/references/write-zh-release-notes.md +40 -0
- package/template/skills/write/references/write-zh.md +721 -0
- package/template/skills/write-a-skill/SKILL.md +117 -0
- package/template/skills/writing-beats/SKILL.md +52 -0
- package/template/skills/writing-fragments/SKILL.md +75 -0
- package/template/skills/writing-shape/SKILL.md +64 -0
- package/template/skills/zoom-out/SKILL.md +7 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Fetch a URL as Markdown.
|
|
3
|
+
#
|
|
4
|
+
# Privacy-first cascade:
|
|
5
|
+
# Default (no --use-proxy): local extractor only. URL is never sent to a
|
|
6
|
+
# third party. Best quality when readability-lxml + html2text are pip-
|
|
7
|
+
# installed; degrades to a stdlib-only stripper otherwise.
|
|
8
|
+
#
|
|
9
|
+
# With --use-proxy: tries local first, then defuddle.md, then r.jina.ai.
|
|
10
|
+
# Use this for JS-heavy pages, X/Twitter, paywalls, or anything the local
|
|
11
|
+
# extractor cannot reach. Be aware: the URL is sent to those third-party
|
|
12
|
+
# services and may be cached or logged. Never feed sensitive URLs through
|
|
13
|
+
# --use-proxy.
|
|
14
|
+
#
|
|
15
|
+
# Every tier writes a structured stderr line:
|
|
16
|
+
# [fetch] tier=<local|defuddle|jina> status=<ok|fail|skip> reason="..."
|
|
17
|
+
#
|
|
18
|
+
# Special thanks to joeseesun for the qiaomu-markdown-proxy project, which
|
|
19
|
+
# inspired the proxy cascade design:
|
|
20
|
+
# https://github.com/joeseesun/qiaomu-markdown-proxy
|
|
21
|
+
#
|
|
22
|
+
# Usage:
|
|
23
|
+
# fetch.sh <url> [proxy_url]
|
|
24
|
+
# fetch.sh --use-proxy <url> [proxy_url]
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
USE_PROXY=0
|
|
28
|
+
if [ "${1:-}" = "--use-proxy" ]; then
|
|
29
|
+
USE_PROXY=1
|
|
30
|
+
shift
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
URL="${1:?Usage: fetch.sh [--use-proxy] <url> [proxy_url]}"
|
|
34
|
+
PROXY="${2:-}"
|
|
35
|
+
|
|
36
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
37
|
+
|
|
38
|
+
LOCAL_ERR="$(mktemp)"
|
|
39
|
+
trap 'rm -f "$LOCAL_ERR"' EXIT
|
|
40
|
+
|
|
41
|
+
# shellcheck disable=SC2329,SC2317 # called indirectly via _with_retry / _try_once
|
|
42
|
+
_curl() {
|
|
43
|
+
if [ -n "$PROXY" ]; then
|
|
44
|
+
https_proxy="$PROXY" http_proxy="$PROXY" curl -sfL --connect-timeout 10 --max-time 30 "$@"
|
|
45
|
+
else
|
|
46
|
+
curl -sfL --connect-timeout 10 --max-time 30 "$@"
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_has_content() {
|
|
51
|
+
local content="$1"
|
|
52
|
+
[ "$(printf '%s' "$content" | wc -l)" -gt 5 ] || return 1
|
|
53
|
+
# Reject pages dominated by login walls, captchas, or bot challenges that
|
|
54
|
+
# otherwise pass the line-count check. Add new markers here, not new branches.
|
|
55
|
+
if printf '%s' "$content" | grep -qE "Don't miss what's happening|Sign in to continue|Please sign in|Log in to continue|请登录|登录后查看|机器人验证|人机验证|Just a moment\.\.\.|Checking your browser" 2>/dev/null; then
|
|
56
|
+
return 1
|
|
57
|
+
fi
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_try_once() {
|
|
62
|
+
local out
|
|
63
|
+
out=$("$@" 2>/dev/null || true)
|
|
64
|
+
if _has_content "$out"; then echo "$out"; return 0; fi
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_with_retry() {
|
|
69
|
+
_try_once "$@" && return 0
|
|
70
|
+
sleep 2
|
|
71
|
+
_try_once "$@" && return 0
|
|
72
|
+
return 1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Tier 1: local extractor. Always tried first.
|
|
76
|
+
if OUT=$(python3 "$SCRIPT_DIR/fetch_local.py" "$URL" 2>"$LOCAL_ERR"); then
|
|
77
|
+
cat "$LOCAL_ERR" >&2 2>/dev/null || true
|
|
78
|
+
echo "$OUT"
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
cat "$LOCAL_ERR" >&2 2>/dev/null || true
|
|
82
|
+
|
|
83
|
+
# Without --use-proxy, stop here. URL never leaves the machine.
|
|
84
|
+
if [ "$USE_PROXY" -eq 0 ]; then
|
|
85
|
+
echo "[fetch] status=fail reason=\"local extractor failed; rerun with --use-proxy to try defuddle.md and r.jina.ai (URL will be sent to those services)\"" >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Tier 2: defuddle.md (third party; user opted in via --use-proxy).
|
|
90
|
+
if OUT=$(_with_retry _curl "https://defuddle.md/$URL"); then
|
|
91
|
+
echo "[fetch] tier=defuddle status=ok" >&2
|
|
92
|
+
echo "$OUT"
|
|
93
|
+
exit 0
|
|
94
|
+
fi
|
|
95
|
+
echo "[fetch] tier=defuddle status=fail reason=\"empty or paywall-like response\"" >&2
|
|
96
|
+
|
|
97
|
+
# Tier 3: r.jina.ai (third party; user opted in via --use-proxy).
|
|
98
|
+
if OUT=$(_with_retry _curl "https://r.jina.ai/$URL"); then
|
|
99
|
+
echo "[fetch] tier=jina status=ok" >&2
|
|
100
|
+
echo "$OUT"
|
|
101
|
+
exit 0
|
|
102
|
+
fi
|
|
103
|
+
echo "[fetch] tier=jina status=fail reason=\"empty or paywall-like response\"" >&2
|
|
104
|
+
|
|
105
|
+
echo "[fetch] status=fail reason=\"all tiers (local, defuddle, jina) failed for $URL\"" >&2
|
|
106
|
+
exit 1
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fetch Feishu/Lark document as Markdown via Feishu Open API.
|
|
3
|
+
|
|
4
|
+
Special thanks to joeseesun for the excellent qiaomu-markdown-proxy project,
|
|
5
|
+
which inspired the Feishu API integration and document parsing approach here.
|
|
6
|
+
https://github.com/joeseesun/qiaomu-markdown-proxy
|
|
7
|
+
|
|
8
|
+
Requirements:
|
|
9
|
+
pip install requests
|
|
10
|
+
|
|
11
|
+
Setup:
|
|
12
|
+
export FEISHU_APP_ID=your_app_id
|
|
13
|
+
export FEISHU_APP_SECRET=your_app_secret
|
|
14
|
+
App needs: docx:document:readonly, wiki:wiki:readonly
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python3 fetch_feishu.py <feishu_url>
|
|
18
|
+
python3 fetch_feishu.py <feishu_url> --json
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import sys
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import urllib.parse
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import requests
|
|
29
|
+
except ImportError:
|
|
30
|
+
print("Error: requests not installed. Run: pip install requests", file=sys.stderr)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
API = "https://open.feishu.cn/open-apis"
|
|
34
|
+
TIMEOUT = 20
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def yaml_string(value):
|
|
38
|
+
return json.dumps("" if value is None else str(value), ensure_ascii=False)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_token():
|
|
42
|
+
app_id = os.environ.get("FEISHU_APP_ID")
|
|
43
|
+
app_secret = os.environ.get("FEISHU_APP_SECRET")
|
|
44
|
+
if not app_id or not app_secret:
|
|
45
|
+
return None, "FEISHU_APP_ID or FEISHU_APP_SECRET not set"
|
|
46
|
+
resp = requests.post(f"{API}/auth/v3/tenant_access_token/internal",
|
|
47
|
+
json={"app_id": app_id, "app_secret": app_secret},
|
|
48
|
+
timeout=TIMEOUT)
|
|
49
|
+
d = resp.json()
|
|
50
|
+
if d.get("code") != 0:
|
|
51
|
+
return None, f"Auth failed: {d.get('msg', resp.text)}"
|
|
52
|
+
return d["tenant_access_token"], None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_url(url):
|
|
56
|
+
patterns = [
|
|
57
|
+
(r"feishu\.cn/docx/([A-Za-z0-9]+)", "docx"),
|
|
58
|
+
(r"feishu\.cn/docs/([A-Za-z0-9]+)", "legacy_doc"),
|
|
59
|
+
(r"feishu\.cn/wiki/([A-Za-z0-9]+)", "wiki"),
|
|
60
|
+
(r"larksuite\.com/docx/([A-Za-z0-9]+)", "docx"),
|
|
61
|
+
(r"larksuite\.com/docs/([A-Za-z0-9]+)", "legacy_doc"),
|
|
62
|
+
(r"larksuite\.com/wiki/([A-Za-z0-9]+)", "wiki"),
|
|
63
|
+
]
|
|
64
|
+
for pattern, doc_type in patterns:
|
|
65
|
+
m = re.search(pattern, url)
|
|
66
|
+
if m:
|
|
67
|
+
return m.group(1), doc_type
|
|
68
|
+
return url, "docx"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve_wiki(token, wiki_token):
|
|
72
|
+
resp = requests.get(f"{API}/wiki/v2/spaces/get_node",
|
|
73
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
74
|
+
params={"token": wiki_token},
|
|
75
|
+
timeout=TIMEOUT)
|
|
76
|
+
d = resp.json()
|
|
77
|
+
if d.get("code") == 0:
|
|
78
|
+
node = d["data"]["node"]
|
|
79
|
+
return node.get("obj_token"), node.get("obj_type")
|
|
80
|
+
return None, None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_blocks(token, doc_id):
|
|
84
|
+
blocks, page_token = [], None
|
|
85
|
+
while True:
|
|
86
|
+
params = {"page_size": 500}
|
|
87
|
+
if page_token:
|
|
88
|
+
params["page_token"] = page_token
|
|
89
|
+
resp = requests.get(f"{API}/docx/v1/documents/{doc_id}/blocks",
|
|
90
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
91
|
+
params=params,
|
|
92
|
+
timeout=TIMEOUT)
|
|
93
|
+
d = resp.json()
|
|
94
|
+
if d.get("code") != 0:
|
|
95
|
+
return None, f"Blocks fetch failed: {d.get('msg', resp.text)}"
|
|
96
|
+
blocks.extend(d["data"].get("items", []))
|
|
97
|
+
if not d["data"].get("has_more"):
|
|
98
|
+
break
|
|
99
|
+
page_token = d["data"].get("page_token")
|
|
100
|
+
return blocks, None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def extract_text(elements):
|
|
104
|
+
if not elements:
|
|
105
|
+
return ""
|
|
106
|
+
parts = []
|
|
107
|
+
for el in elements:
|
|
108
|
+
if "text_run" in el:
|
|
109
|
+
tr = el["text_run"]
|
|
110
|
+
text = tr.get("content", "")
|
|
111
|
+
s = tr.get("text_element_style", {})
|
|
112
|
+
if s.get("bold"): text = f"**{text}**"
|
|
113
|
+
if s.get("italic"): text = f"*{text}*"
|
|
114
|
+
if s.get("inline_code"): text = f"`{text}`"
|
|
115
|
+
if s.get("link", {}).get("url"):
|
|
116
|
+
text = f"[{text}]({urllib.parse.unquote(s['link']['url'])})"
|
|
117
|
+
parts.append(text)
|
|
118
|
+
elif "mention_user" in el:
|
|
119
|
+
parts.append(f"@{el['mention_user'].get('user_id', 'user')}")
|
|
120
|
+
elif "equation" in el:
|
|
121
|
+
parts.append(f"${el['equation'].get('content', '')}$")
|
|
122
|
+
return "".join(parts)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
LANG_MAP = {
|
|
126
|
+
7: "bash", 8: "c", 9: "csharp", 10: "cpp", 14: "css", 19: "dockerfile",
|
|
127
|
+
25: "go", 29: "html", 31: "java", 32: "javascript", 33: "json",
|
|
128
|
+
35: "kotlin", 40: "markdown", 46: "php", 50: "python", 52: "ruby",
|
|
129
|
+
53: "rust", 58: "sql", 59: "swift", 62: "typescript", 68: "xml", 69: "yaml",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def blocks_to_md(blocks):
|
|
134
|
+
lines = []
|
|
135
|
+
counters = {}
|
|
136
|
+
for block in blocks:
|
|
137
|
+
bt = block.get("block_type")
|
|
138
|
+
pid = block.get("parent_id", "")
|
|
139
|
+
|
|
140
|
+
if bt == 2:
|
|
141
|
+
text = extract_text(block.get("text", {}).get("elements", []))
|
|
142
|
+
lines.append(text if text.strip() else "")
|
|
143
|
+
elif bt in range(3, 10):
|
|
144
|
+
level = bt - 2
|
|
145
|
+
key = f"heading{level}"
|
|
146
|
+
data = block.get(key) or block.get("heading", {})
|
|
147
|
+
text = extract_text(data.get("elements", []))
|
|
148
|
+
lines.append(f"{'#' * min(level, 6)} {text}")
|
|
149
|
+
elif bt == 10:
|
|
150
|
+
text = extract_text(block.get("bullet", {}).get("elements", []))
|
|
151
|
+
lines.append(f"- {text}")
|
|
152
|
+
elif bt == 11:
|
|
153
|
+
text = extract_text(block.get("ordered", {}).get("elements", []))
|
|
154
|
+
n = counters.get(pid, 0) + 1
|
|
155
|
+
counters[pid] = n
|
|
156
|
+
lines.append(f"{n}. {text}")
|
|
157
|
+
elif bt == 12:
|
|
158
|
+
code_data = block.get("code", {})
|
|
159
|
+
text = extract_text(code_data.get("elements", []))
|
|
160
|
+
lang = LANG_MAP.get(code_data.get("style", {}).get("language", 0), "")
|
|
161
|
+
lines.extend([f"```{lang}", text, "```"])
|
|
162
|
+
elif bt == 13:
|
|
163
|
+
text = extract_text(block.get("quote", {}).get("elements", []))
|
|
164
|
+
lines.append(f"> {text}")
|
|
165
|
+
elif bt == 15:
|
|
166
|
+
todo_data = block.get("todo", {})
|
|
167
|
+
text = extract_text(todo_data.get("elements", []))
|
|
168
|
+
done = todo_data.get("style", {}).get("done", False)
|
|
169
|
+
lines.append(f"- {'[x]' if done else '[ ]'} {text}")
|
|
170
|
+
elif bt == 16:
|
|
171
|
+
lines.append("---")
|
|
172
|
+
elif bt == 17:
|
|
173
|
+
tok = block.get("image", {}).get("token", "")
|
|
174
|
+
lines.append(f"")
|
|
175
|
+
elif bt == 1:
|
|
176
|
+
pass
|
|
177
|
+
else:
|
|
178
|
+
for key, val in block.items():
|
|
179
|
+
if isinstance(val, dict) and "elements" in val:
|
|
180
|
+
text = extract_text(val["elements"])
|
|
181
|
+
if text.strip():
|
|
182
|
+
lines.append(text)
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
return "\n\n".join(lines)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def fetch_feishu(url):
|
|
189
|
+
doc_id, doc_type = parse_url(url)
|
|
190
|
+
|
|
191
|
+
if doc_type == "legacy_doc":
|
|
192
|
+
return {
|
|
193
|
+
"error": (
|
|
194
|
+
"Legacy Feishu /docs/ pages are not supported by this script. "
|
|
195
|
+
"Convert the document to docx first, or use a public-page fallback if the page is accessible without the API."
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
token, err = get_token()
|
|
200
|
+
if err:
|
|
201
|
+
return {"error": err}
|
|
202
|
+
|
|
203
|
+
if doc_type == "wiki":
|
|
204
|
+
real_id, real_type = resolve_wiki(token, doc_id)
|
|
205
|
+
if not real_id:
|
|
206
|
+
return {"error": f"Cannot resolve wiki node: {doc_id}"}
|
|
207
|
+
doc_id, doc_type = real_id, real_type or "docx"
|
|
208
|
+
|
|
209
|
+
info_resp = requests.get(f"{API}/docx/v1/documents/{doc_id}",
|
|
210
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
211
|
+
timeout=TIMEOUT)
|
|
212
|
+
doc_info = (info_resp.json().get("data") or {}).get("document") or {}
|
|
213
|
+
title = doc_info.get("title", "")
|
|
214
|
+
|
|
215
|
+
blocks, err = get_blocks(token, doc_id)
|
|
216
|
+
if err:
|
|
217
|
+
return {"error": err}
|
|
218
|
+
|
|
219
|
+
return {"title": title, "document_id": doc_id, "url": url, "content": blocks_to_md(blocks)}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def to_markdown(r):
|
|
223
|
+
if "error" in r:
|
|
224
|
+
return f"Error: {r['error']}"
|
|
225
|
+
parts = [
|
|
226
|
+
"---",
|
|
227
|
+
f"title: {yaml_string(r.get('title', ''))}",
|
|
228
|
+
f"document_id: {yaml_string(r.get('document_id', ''))}",
|
|
229
|
+
f"url: {yaml_string(r.get('url', ''))}",
|
|
230
|
+
"---",
|
|
231
|
+
"",
|
|
232
|
+
f"# {r['title']}" if r.get("title") else "",
|
|
233
|
+
"",
|
|
234
|
+
r.get("content", ""),
|
|
235
|
+
]
|
|
236
|
+
return "\n".join(parts)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
if len(sys.argv) < 2:
|
|
241
|
+
print("Usage: fetch_feishu.py <feishu_url> [--json]", file=sys.stderr)
|
|
242
|
+
print(" Requires: FEISHU_APP_ID, FEISHU_APP_SECRET", file=sys.stderr)
|
|
243
|
+
sys.exit(1)
|
|
244
|
+
|
|
245
|
+
result = fetch_feishu(sys.argv[1])
|
|
246
|
+
if "--json" in sys.argv:
|
|
247
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
248
|
+
else:
|
|
249
|
+
print(to_markdown(result))
|
|
250
|
+
if "error" in result:
|
|
251
|
+
sys.exit(1)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Local URL → Markdown extractor. Privacy-preserving tier 1 for fetch.sh.
|
|
3
|
+
|
|
4
|
+
Two paths, picked at runtime:
|
|
5
|
+
|
|
6
|
+
1. **Best path**: `readability-lxml` + `html2text` installed. Extract main
|
|
7
|
+
content via readability scoring, convert to clean Markdown. Quality close
|
|
8
|
+
to defuddle.md / r.jina.ai for static pages.
|
|
9
|
+
|
|
10
|
+
2. **Fallback path**: stdlib only. Strip HTML tags, collapse whitespace, drop
|
|
11
|
+
<script>/<style>/<nav>/<footer>. Quality is poor for JS-heavy pages and
|
|
12
|
+
complex layouts, but works for simple article-style HTML without any
|
|
13
|
+
third-party dependency.
|
|
14
|
+
|
|
15
|
+
JS-rendered pages (X/Twitter, paywalled news, SPA) are out of reach for both
|
|
16
|
+
paths. Use `fetch.sh --use-proxy` for those.
|
|
17
|
+
|
|
18
|
+
Exit codes:
|
|
19
|
+
0 success, markdown on stdout
|
|
20
|
+
1 fetch or extraction failed; reason on stderr
|
|
21
|
+
2 invocation error (missing URL)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
import urllib.error
|
|
30
|
+
import urllib.request
|
|
31
|
+
from html.parser import HTMLParser
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
USER_AGENT = (
|
|
35
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 12_0) "
|
|
36
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36"
|
|
37
|
+
)
|
|
38
|
+
FETCH_TIMEOUT_SECS = 20
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def fetch_html(url: str) -> str:
|
|
42
|
+
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
43
|
+
with urllib.request.urlopen(req, timeout=FETCH_TIMEOUT_SECS) as resp:
|
|
44
|
+
raw = resp.read()
|
|
45
|
+
# Detect charset from Content-Type header; fall back to utf-8 with replace.
|
|
46
|
+
charset = "utf-8"
|
|
47
|
+
ctype = resp.headers.get("Content-Type", "")
|
|
48
|
+
m = re.search(r"charset=([\w\-]+)", ctype, re.IGNORECASE)
|
|
49
|
+
if m:
|
|
50
|
+
charset = m.group(1).lower()
|
|
51
|
+
try:
|
|
52
|
+
return raw.decode(charset, errors="replace")
|
|
53
|
+
except LookupError:
|
|
54
|
+
return raw.decode("utf-8", errors="replace")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def extract_with_readability(html: str, url: str) -> str | None:
|
|
58
|
+
"""Best path: readability-lxml + html2text. Returns Markdown or None if
|
|
59
|
+
deps missing. Raises only on genuine extraction failure."""
|
|
60
|
+
try:
|
|
61
|
+
from readability import Document # type: ignore
|
|
62
|
+
import html2text # type: ignore
|
|
63
|
+
except ImportError:
|
|
64
|
+
return None
|
|
65
|
+
doc = Document(html)
|
|
66
|
+
cleaned_html = doc.summary(html_partial=True)
|
|
67
|
+
title = (doc.short_title() or "").strip()
|
|
68
|
+
converter = html2text.HTML2Text()
|
|
69
|
+
converter.body_width = 0
|
|
70
|
+
converter.unicode_snob = True
|
|
71
|
+
converter.ignore_links = False
|
|
72
|
+
converter.ignore_images = False
|
|
73
|
+
body = converter.handle(cleaned_html).strip()
|
|
74
|
+
if not body:
|
|
75
|
+
return None
|
|
76
|
+
header = ""
|
|
77
|
+
if title:
|
|
78
|
+
header = f"# {title}\n\n"
|
|
79
|
+
return f"{header}> Source: {url}\n\n{body}\n"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _StdlibStripper(HTMLParser):
|
|
83
|
+
"""Fallback HTML → text converter using only stdlib. Quality is poor but
|
|
84
|
+
deterministic; intended as a last resort when readability isn't installed.
|
|
85
|
+
Drops common non-content blocks (script/style/nav/footer/aside)."""
|
|
86
|
+
|
|
87
|
+
DROP_TAGS = {"script", "style", "nav", "footer", "aside", "noscript", "form"}
|
|
88
|
+
BLOCK_TAGS = {
|
|
89
|
+
"p", "div", "section", "article", "li", "ul", "ol",
|
|
90
|
+
"h1", "h2", "h3", "h4", "h5", "h6", "br", "tr",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
super().__init__()
|
|
95
|
+
self._buf: list[str] = []
|
|
96
|
+
self._drop_depth = 0
|
|
97
|
+
self._title = ""
|
|
98
|
+
self._in_title = False
|
|
99
|
+
|
|
100
|
+
def handle_starttag(self, tag, attrs):
|
|
101
|
+
if tag in self.DROP_TAGS:
|
|
102
|
+
self._drop_depth += 1
|
|
103
|
+
elif tag == "title":
|
|
104
|
+
self._in_title = True
|
|
105
|
+
elif tag in self.BLOCK_TAGS:
|
|
106
|
+
self._buf.append("\n")
|
|
107
|
+
|
|
108
|
+
def handle_endtag(self, tag):
|
|
109
|
+
if tag in self.DROP_TAGS:
|
|
110
|
+
self._drop_depth = max(0, self._drop_depth - 1)
|
|
111
|
+
elif tag == "title":
|
|
112
|
+
self._in_title = False
|
|
113
|
+
elif tag in self.BLOCK_TAGS:
|
|
114
|
+
self._buf.append("\n")
|
|
115
|
+
|
|
116
|
+
def handle_data(self, data):
|
|
117
|
+
if self._drop_depth:
|
|
118
|
+
return
|
|
119
|
+
if self._in_title:
|
|
120
|
+
self._title += data
|
|
121
|
+
return
|
|
122
|
+
self._buf.append(data)
|
|
123
|
+
|
|
124
|
+
def text(self) -> str:
|
|
125
|
+
raw = "".join(self._buf)
|
|
126
|
+
lines = [line.strip() for line in raw.splitlines()]
|
|
127
|
+
# Drop empty lines but keep paragraph breaks.
|
|
128
|
+
out: list[str] = []
|
|
129
|
+
prev_blank = False
|
|
130
|
+
for line in lines:
|
|
131
|
+
if not line:
|
|
132
|
+
if not prev_blank:
|
|
133
|
+
out.append("")
|
|
134
|
+
prev_blank = True
|
|
135
|
+
else:
|
|
136
|
+
out.append(line)
|
|
137
|
+
prev_blank = False
|
|
138
|
+
return "\n".join(out).strip()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def extract_with_stdlib(html: str, url: str) -> str:
|
|
142
|
+
p = _StdlibStripper()
|
|
143
|
+
p.feed(html)
|
|
144
|
+
body = p.text()
|
|
145
|
+
if not body:
|
|
146
|
+
body = "(no text content extracted)"
|
|
147
|
+
header = ""
|
|
148
|
+
title = (p._title or "").strip()
|
|
149
|
+
if title:
|
|
150
|
+
header = f"# {title}\n\n"
|
|
151
|
+
return f"{header}> Source: {url}\n\n{body}\n"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main() -> int:
|
|
155
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
156
|
+
parser.add_argument("url", help="URL to fetch")
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"--prefer",
|
|
159
|
+
choices=("auto", "readability", "stdlib"),
|
|
160
|
+
default="auto",
|
|
161
|
+
help="Force a specific extractor (default: auto = readability if installed, else stdlib)",
|
|
162
|
+
)
|
|
163
|
+
args = parser.parse_args()
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
html = fetch_html(args.url)
|
|
167
|
+
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError) as exc:
|
|
168
|
+
print(
|
|
169
|
+
f"[fetch] tier=local status=fail reason=\"fetch failed: {exc}\"",
|
|
170
|
+
file=sys.stderr,
|
|
171
|
+
)
|
|
172
|
+
return 1
|
|
173
|
+
|
|
174
|
+
used = ""
|
|
175
|
+
body: str | None = None
|
|
176
|
+
|
|
177
|
+
if args.prefer in ("auto", "readability"):
|
|
178
|
+
body = extract_with_readability(html, args.url)
|
|
179
|
+
if body is not None:
|
|
180
|
+
used = "readability"
|
|
181
|
+
elif args.prefer == "readability":
|
|
182
|
+
print(
|
|
183
|
+
"[fetch] tier=local status=fail "
|
|
184
|
+
"reason=\"--prefer readability but readability-lxml or html2text not installed; "
|
|
185
|
+
"install with: pip install --user readability-lxml html2text\"",
|
|
186
|
+
file=sys.stderr,
|
|
187
|
+
)
|
|
188
|
+
return 1
|
|
189
|
+
|
|
190
|
+
if body is None:
|
|
191
|
+
body = extract_with_stdlib(html, args.url)
|
|
192
|
+
used = "stdlib"
|
|
193
|
+
|
|
194
|
+
# Sanity floor: if even the stdlib extractor returns essentially nothing,
|
|
195
|
+
# treat as failure so the proxy fallback (if --use-proxy) gets a chance.
|
|
196
|
+
body_lines = [l for l in body.splitlines() if l.strip()]
|
|
197
|
+
if len(body_lines) < 4:
|
|
198
|
+
print(
|
|
199
|
+
f"[fetch] tier=local status=fail reason=\"extractor={used} produced <4 non-empty lines\"",
|
|
200
|
+
file=sys.stderr,
|
|
201
|
+
)
|
|
202
|
+
return 1
|
|
203
|
+
|
|
204
|
+
if used == "stdlib":
|
|
205
|
+
print(
|
|
206
|
+
"[fetch] tier=local status=ok extractor=stdlib "
|
|
207
|
+
"hint=\"install readability-lxml + html2text for cleaner output\"",
|
|
208
|
+
file=sys.stderr,
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
print(f"[fetch] tier=local status=ok extractor={used}", file=sys.stderr)
|
|
212
|
+
|
|
213
|
+
sys.stdout.write(body)
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
sys.exit(main())
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fetch WeChat public account article as Markdown using Playwright + BeautifulSoup.
|
|
3
|
+
|
|
4
|
+
Special thanks to joeseesun for the excellent qiaomu-markdown-proxy project,
|
|
5
|
+
which inspired the Playwright-based WeChat scraping approach in this script.
|
|
6
|
+
https://github.com/joeseesun/qiaomu-markdown-proxy
|
|
7
|
+
|
|
8
|
+
Requirements:
|
|
9
|
+
pip install playwright beautifulsoup4 lxml
|
|
10
|
+
playwright install chromium
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python3 fetch_weixin.py <url>
|
|
14
|
+
python3 fetch_weixin.py <url> --json
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
import json
|
|
19
|
+
import asyncio
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def yaml_string(value: str) -> str:
|
|
23
|
+
return json.dumps("" if value is None else str(value), ensure_ascii=False)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def fetch(url: str) -> dict:
|
|
27
|
+
try:
|
|
28
|
+
from playwright.async_api import async_playwright
|
|
29
|
+
from bs4 import BeautifulSoup
|
|
30
|
+
except ImportError as e:
|
|
31
|
+
return {"error": str(e) + "\nRun: pip install playwright beautifulsoup4 lxml && playwright install chromium"}
|
|
32
|
+
|
|
33
|
+
async with async_playwright() as p:
|
|
34
|
+
browser = await p.chromium.launch(headless=True)
|
|
35
|
+
page = await browser.new_page(
|
|
36
|
+
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
37
|
+
)
|
|
38
|
+
try:
|
|
39
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
|
|
40
|
+
await page.wait_for_selector("#js_content", timeout=15000)
|
|
41
|
+
html = await page.content()
|
|
42
|
+
except Exception as e:
|
|
43
|
+
await browser.close()
|
|
44
|
+
return {"error": f"Page load failed: {e}"}
|
|
45
|
+
await browser.close()
|
|
46
|
+
|
|
47
|
+
soup = BeautifulSoup(html, "lxml")
|
|
48
|
+
|
|
49
|
+
title = (soup.select_one("#activity-name") or soup.new_tag("x")).get_text(strip=True)
|
|
50
|
+
author = (soup.select_one("#js_author_name") or soup.new_tag("x")).get_text(strip=True)
|
|
51
|
+
date = (soup.select_one("#publish_time") or soup.new_tag("x")).get_text(strip=True)
|
|
52
|
+
|
|
53
|
+
content_el = soup.select_one("#js_content")
|
|
54
|
+
if not content_el:
|
|
55
|
+
return {"error": "Could not find #js_content"}
|
|
56
|
+
|
|
57
|
+
for tag in content_el.find_all(["script", "style"]):
|
|
58
|
+
tag.decompose()
|
|
59
|
+
|
|
60
|
+
for img in content_el.find_all("img"):
|
|
61
|
+
src = img.get("data-src") or img.get("src") or ""
|
|
62
|
+
img.replace_with(f"\n\n" if src else "")
|
|
63
|
+
|
|
64
|
+
lines = []
|
|
65
|
+
for el in content_el.find_all(["p", "h1", "h2", "h3", "h4", "section", "blockquote"]):
|
|
66
|
+
text = el.get_text(strip=True)
|
|
67
|
+
if not text:
|
|
68
|
+
continue
|
|
69
|
+
if el.name in ("h1", "h2", "h3", "h4"):
|
|
70
|
+
lines.append(f"{'#' * int(el.name[1])} {text}")
|
|
71
|
+
elif el.name == "blockquote":
|
|
72
|
+
lines.append(f"> {text}")
|
|
73
|
+
else:
|
|
74
|
+
lines.append(text)
|
|
75
|
+
|
|
76
|
+
content = "\n\n".join(lines) or content_el.get_text("\n", strip=True)
|
|
77
|
+
return {"title": title, "author": author, "date": date, "url": url, "content": content}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def to_markdown(r: dict) -> str:
|
|
81
|
+
if "error" in r:
|
|
82
|
+
return f"Error: {r['error']}"
|
|
83
|
+
parts = [
|
|
84
|
+
"---",
|
|
85
|
+
f"title: {yaml_string(r.get('title', ''))}",
|
|
86
|
+
*([f"author: {yaml_string(r['author'])}"] if r.get("author") else []),
|
|
87
|
+
*([f"date: {yaml_string(r['date'])}"] if r.get("date") else []),
|
|
88
|
+
f"url: {yaml_string(r.get('url', ''))}",
|
|
89
|
+
"---",
|
|
90
|
+
"",
|
|
91
|
+
f"# {r['title']}" if r.get("title") else "",
|
|
92
|
+
"",
|
|
93
|
+
r.get("content", ""),
|
|
94
|
+
]
|
|
95
|
+
return "\n".join(parts)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
if len(sys.argv) < 2:
|
|
100
|
+
print("Usage: fetch_weixin.py <url> [--json]", file=sys.stderr)
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
result = asyncio.run(fetch(sys.argv[1]))
|
|
104
|
+
if "--json" in sys.argv:
|
|
105
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
106
|
+
else:
|
|
107
|
+
print(to_markdown(result))
|