evolcore 0.0.31 → 0.0.33
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/CHANGELOG.md +35 -0
- package/bin/install-codex-managed-hooks.mjs +124 -37
- package/dist/agents/claude-runner.js +29 -2
- package/dist/agents/codex/app-server-client.js +3 -0
- package/dist/agents/codex/diagnostics.js +76 -44
- package/dist/agents/codex-runner.js +51 -24
- package/dist/agents/ecagent-runner.js +9 -13
- package/dist/agents/gemini-runner.js +4 -0
- package/dist/aun/aid/store.js +0 -81
- package/dist/aun/group/fs-download.js +1 -10
- package/dist/aun/group/operations.js +0 -1
- package/dist/aun/msg/history.js +1 -0
- package/dist/aun/msg/managed-operation.js +130 -32
- package/dist/aun/msg/p2p.js +0 -1
- package/dist/aun/outbox.js +50 -1
- package/dist/aun/service-proxy.js +12 -18
- package/dist/aun/storage/index.js +29 -0
- package/dist/channels/aun.js +38 -137
- package/dist/channels/daemon.js +257 -2
- package/dist/cli/agent-command.js +64 -31
- package/dist/cli/agent-create.js +43 -24
- package/dist/cli/agent.js +16 -70
- package/dist/cli/aun-commands.js +512 -32
- package/dist/cli/contact.js +8 -2
- package/dist/cli/ctl-command.js +1 -2
- package/dist/cli/daemon-commands.js +82 -13
- package/dist/cli/debug-mode.js +154 -0
- package/dist/cli/diag.js +2 -2
- package/dist/cli/handoff-command.js +18 -5
- package/dist/cli/index.js +81 -64
- package/dist/cli/init.js +2 -2
- package/dist/cli/managed-command-guard.js +5 -0
- package/dist/cli/memory-command.js +93 -12
- package/dist/cli/model.js +14 -138
- package/dist/cli/queue-command.js +51 -0
- package/dist/cli/response.js +5 -54
- package/dist/cli/session-command.js +194 -0
- package/dist/cli/skill-command.js +85 -3
- package/dist/cli/stats.js +10 -4
- package/dist/cli/task-command.js +1 -0
- package/dist/cli/task-context.js +16 -0
- package/dist/cli/thread-command.js +620 -0
- package/dist/cli/trigger-command.js +76 -50
- package/dist/config/builtin-roles.js +9 -7
- package/dist/config/config-field-policy.js +2 -3
- package/dist/config/config-manager.js +7 -2
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config-store.js +15 -0
- package/dist/core/agent-ipc-registration.js +65 -22
- package/dist/core/auth/agent-delegation.js +42 -2
- package/dist/core/auth/debug-context.js +139 -0
- package/dist/core/auth/delegation-authorization.js +1 -0
- package/dist/core/auth/operation-authorizer.js +24 -1
- package/dist/core/auth/operation-catalog.js +44 -3
- package/dist/core/background-session/session-cleanup.js +212 -0
- package/dist/core/background-session/types.js +10 -0
- package/dist/core/bootstrap-messages.js +1 -1
- package/dist/core/bootstrap-service.js +6 -3
- package/dist/core/command/command-handler.js +224 -45
- package/dist/core/command/diag-service.js +5 -1
- package/dist/core/command/memory-slash.js +6 -2
- package/dist/core/command/menu-catalog.js +48 -1
- package/dist/core/command/menu-handler.js +92 -16
- package/dist/core/command/menu-protocol.js +24 -1
- package/dist/core/command/skill-slash.js +322 -0
- package/dist/core/command/slash-gate.js +6 -4
- package/dist/core/command/slash-handler.js +193 -13
- package/dist/core/command/topic-slash.js +528 -0
- package/dist/core/evolagent-registry.js +20 -5
- package/dist/core/handoff/ipc-registration.js +38 -0
- package/dist/core/handoff/runtime.js +5 -11
- package/dist/core/handoff/store.js +5 -0
- package/dist/core/ipc-registration.js +210 -9
- package/dist/core/memory/consolidation-context.js +49 -16
- package/dist/core/memory/consolidation-ops-records.js +35 -0
- package/dist/core/memory/consolidation-queue-format.js +21 -0
- package/dist/core/memory/consolidation.js +174 -180
- package/dist/core/message/aun-ipc-sender.js +207 -8
- package/dist/core/message/im-renderer.js +149 -6
- package/dist/core/message/message-bridge.js +13 -3
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +131 -3
- package/dist/core/message/peer-mode.js +6 -1
- package/dist/core/message/response-engine.js +379 -69
- package/dist/core/message/subtask-tool-projection.js +140 -0
- package/dist/core/model/catalog-service.js +17 -12
- package/dist/core/permission/ec-command-parser.js +126 -7
- package/dist/core/permission/sandbox-runtime.js +14 -1
- package/dist/core/session/session-idle-monitor.js +249 -22
- package/dist/core/session/session-manager.js +54 -9
- package/dist/core/skill/command-parser.js +13 -0
- package/dist/core/skill/index-generator.js +4 -1
- package/dist/core/skill/startup.js +41 -0
- package/dist/core/skill/switch-resolver.js +55 -29
- package/dist/core/task/task-recovery-service.js +2 -0
- package/dist/core/task/task-resume-operation.js +11 -0
- package/dist/core/thread/command-parser.js +125 -0
- package/dist/core/thread/index.js +1 -0
- package/dist/core/thread/ipc-operation.js +95 -0
- package/dist/core/thread/thread-context-storage.js +131 -0
- package/dist/core/thread/thread-operation.js +258 -0
- package/dist/core/thread/thread-route.js +33 -0
- package/dist/core/thread/thread-service.js +777 -0
- package/dist/core/thread/types.js +1 -0
- package/dist/eck/message-renderer.js +35 -0
- package/dist/index.js +10 -7
- package/dist/ipc.js +234 -17
- package/dist/paths.js +23 -0
- package/dist/response-system/engines/v1/proactive-flow.js +169 -80
- package/dist/trigger/authorization.js +59 -29
- package/dist/trigger/feedback.js +7 -1
- package/dist/trigger/ipc-service.js +49 -35
- package/dist/trigger/parser.js +63 -1
- package/dist/trigger/script-sandbox.js +58 -77
- package/dist/trigger/validation.js +14 -0
- package/dist/utils/error-utils.js +22 -1
- package/kits/docs/AUTHORING.md +1 -1
- package/kits/docs/channels/aun.md +8 -0
- package/kits/docs/context-assembly.md +1 -0
- package/kits/docs/evolcore/INDEX.md +3 -2
- package/kits/docs/evolcore/agent-create.md +39 -28
- package/kits/docs/evolcore/agent.md +19 -4
- package/kits/docs/evolcore/ec-capabilities.md +4 -0
- package/kits/docs/evolcore/group-collaboration.md +3 -5
- package/kits/docs/evolcore/group.md +3 -4
- package/kits/docs/evolcore/memory-consolidation.md +1 -1
- package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
- package/kits/docs/evolcore/menu-protocol.md +7 -0
- package/kits/docs/evolcore/msg.md +5 -8
- package/kits/docs/evolcore/task.md +2 -0
- package/kits/docs/evolcore/thread-chain.md +267 -0
- package/kits/docs/evolcore/trigger.md +2 -2
- package/kits/docs/prompt-loading-architecture.md +1 -0
- package/kits/eck_manifest.json +61 -26
- package/kits/eck_message_manifest.json +169 -10
- package/kits/rules/01-overview.md +0 -9
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +7 -1
- package/kits/rules/07-memory-and-thread.md +202 -0
- package/kits/schemas/relation-config.schema.9.json +18 -3
- package/kits/schemas/role-config.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.2.json +6 -6
- package/kits/schemas/single-session.schema.3.json +6 -6
- package/kits/skills/.switches.json +12 -1
- package/kits/skills/ec-group/SKILL.md +234 -0
- package/kits/skills/ec-skill/SKILL.md +10 -1
- package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
- package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
- package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
- package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
- package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
- package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
- package/kits/skills/thread-ops/SKILL.md +492 -0
- package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
- package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
- package/kits/skills/wechat-article-reader/SKILL.md +97 -0
- package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
- package/kits/templates/bootstrap-welcome.md +7 -7
- package/kits/templates/memory-consolidation/forget.md +5 -0
- package/kits/templates/memory-consolidation/generalize.md +5 -0
- package/kits/templates/memory-consolidation/profile.md +5 -0
- package/kits/templates/memory-consolidation/promote.md +5 -0
- package/kits/templates/message-fragments/item.md +2 -1
- package/kits/templates/message-fragments/thread-context.md +6 -0
- package/kits/templates/message-fragments/trigger-session.md +2 -0
- package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
- package/kits/templates/roles/member.json +2 -0
- package/kits/templates/roles/visitor.json +2 -0
- package/kits/templates/system-fragments/bootstrap.md +66 -72
- package/kits/templates/system-fragments/channel.md +3 -6
- package/kits/templates/system-fragments/commands.md +2 -1
- package/kits/templates/system-fragments/managed-runtime.md +15 -0
- package/kits/templates/system-fragments/reply.md +3 -2
- package/kits/templates/system-fragments/session.md +5 -14
- package/package.json +3 -1
- package/kits/docs/evolcore/group-fs.md +0 -17
- package/kits/docs/venues/aun-group.md +0 -45
- package/kits/docs/venues/group.md +0 -43
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Windows 终端默认 GBK,强制 stdout/stderr 用 UTF-8
|
|
7
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
8
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
9
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
10
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_reply(title: str, src_url: str, markdown: str) -> str:
|
|
14
|
+
body = markdown.strip()
|
|
15
|
+
return (
|
|
16
|
+
"```markdown\n"
|
|
17
|
+
f"# {title}\n\n"
|
|
18
|
+
f"原文链接:{src_url}\n\n"
|
|
19
|
+
f"{body}\n"
|
|
20
|
+
"```"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> int:
|
|
25
|
+
if len(sys.argv) != 4:
|
|
26
|
+
print("usage: render_markdown_reply.py <title> <src_url> <markdown_file>", file=sys.stderr)
|
|
27
|
+
return 2
|
|
28
|
+
title, src_url, markdown_file = sys.argv[1:4]
|
|
29
|
+
markdown = Path(markdown_file).read_text(encoding="utf-8")
|
|
30
|
+
print(render_reply(title, src_url, markdown))
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# 自定位:从脚本自身位置推导同目录脚本,不依赖 cwd 或任何绝对路径。
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
|
|
7
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
8
|
+
PYTHON_BIN=python3
|
|
9
|
+
elif command -v python >/dev/null 2>&1; then
|
|
10
|
+
PYTHON_BIN=python
|
|
11
|
+
else
|
|
12
|
+
echo 'Python not found' >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
"$PYTHON_BIN" "$SCRIPT_DIR/check_python_env.py"
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import urllib.request
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
# Windows 终端默认 GBK,强制 stdout/stderr 用 UTF-8
|
|
10
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
11
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
12
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
13
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
14
|
+
|
|
15
|
+
import html2text
|
|
16
|
+
import lxml.html
|
|
17
|
+
import lxml.cssselect # noqa: F401 — pulls cssselect into lxml's registry
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Thin compatibility shim — mimics the scrapling page/node API surface used
|
|
22
|
+
# in this script, backed by urllib + lxml so patchright is never needed.
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
_HEADERS = {
|
|
26
|
+
"User-Agent": (
|
|
27
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
28
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
29
|
+
"Chrome/124.0.0.0 Safari/537.36"
|
|
30
|
+
),
|
|
31
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
32
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _NodeList:
|
|
37
|
+
def __init__(self, nodes):
|
|
38
|
+
self._nodes = nodes
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def first(self):
|
|
42
|
+
return _Node(self._nodes[0]) if self._nodes else None
|
|
43
|
+
|
|
44
|
+
def __bool__(self):
|
|
45
|
+
return bool(self._nodes)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _Node:
|
|
49
|
+
def __init__(self, el):
|
|
50
|
+
self._el = el
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def html_content(self):
|
|
54
|
+
return lxml.html.tostring(self._el, encoding="unicode", with_tail=False)
|
|
55
|
+
|
|
56
|
+
def css(self, sel):
|
|
57
|
+
try:
|
|
58
|
+
return _NodeList(self._el.cssselect(sel))
|
|
59
|
+
except Exception:
|
|
60
|
+
return _NodeList([])
|
|
61
|
+
|
|
62
|
+
def get(self, attr, default=None):
|
|
63
|
+
return self._el.get(attr, default)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def attributes(self):
|
|
67
|
+
return dict(self._el.attrib)
|
|
68
|
+
|
|
69
|
+
def __str__(self):
|
|
70
|
+
return self.html_content
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _Page:
|
|
74
|
+
def __init__(self, html_bytes: bytes, status: int, url: str):
|
|
75
|
+
self.status = status
|
|
76
|
+
self._url = url
|
|
77
|
+
self._doc = lxml.html.fromstring(html_bytes, base_url=url)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def html_content(self):
|
|
81
|
+
return lxml.html.tostring(self._doc, encoding="unicode", with_tail=False)
|
|
82
|
+
|
|
83
|
+
def css(self, sel):
|
|
84
|
+
try:
|
|
85
|
+
return _NodeList(self._doc.cssselect(sel))
|
|
86
|
+
except Exception:
|
|
87
|
+
return _NodeList([])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Fetcher:
|
|
91
|
+
@staticmethod
|
|
92
|
+
def get(url: str) -> _Page:
|
|
93
|
+
req = urllib.request.Request(url, headers=_HEADERS)
|
|
94
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
95
|
+
status = resp.status
|
|
96
|
+
html_bytes = resp.read()
|
|
97
|
+
return _Page(html_bytes, status, url)
|
|
98
|
+
|
|
99
|
+
DEFAULT_SELECTORS = [
|
|
100
|
+
"article",
|
|
101
|
+
"main",
|
|
102
|
+
".post-content",
|
|
103
|
+
'[class*="body"]',
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
WECHAT_SELECTORS = [
|
|
107
|
+
"#js_content",
|
|
108
|
+
".rich_media_content",
|
|
109
|
+
".rich_media_area_primary",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
WECHAT_TITLE_SELECTORS = [
|
|
113
|
+
"#activity-name",
|
|
114
|
+
".rich_media_title",
|
|
115
|
+
"h1",
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
WECHAT_COVER_SELECTORS = [
|
|
119
|
+
'.rich_media_thumb[style*="background-image"]',
|
|
120
|
+
'.rich_media_thumb',
|
|
121
|
+
'meta[property="og:image"]',
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
NOISE_MARKERS = [
|
|
125
|
+
"Scan with Weixin to",
|
|
126
|
+
"微信扫一扫可打开此内容",
|
|
127
|
+
"轻触阅读原文",
|
|
128
|
+
"预览时标签不可点",
|
|
129
|
+
"Got It",
|
|
130
|
+
"Cancel",
|
|
131
|
+
"Allow",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def choose_selectors(url: str):
|
|
136
|
+
host = (urlparse(url).hostname or "").lower()
|
|
137
|
+
if host.endswith("mp.weixin.qq.com"):
|
|
138
|
+
return WECHAT_SELECTORS + DEFAULT_SELECTORS
|
|
139
|
+
return DEFAULT_SELECTORS
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def normalize_html_for_images(html: str) -> str:
|
|
143
|
+
return re.sub(
|
|
144
|
+
r'<img([^>]*?)\sdata-src="([^"]+)"([^>]*?)>',
|
|
145
|
+
lambda m: f'<img{m.group(1)} src="{m.group(2)}"{m.group(3)}>',
|
|
146
|
+
html,
|
|
147
|
+
flags=re.IGNORECASE,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def strip_tail_noise(markdown: str) -> str:
|
|
152
|
+
cut_positions = [markdown.find(marker) for marker in NOISE_MARKERS if marker in markdown]
|
|
153
|
+
cut_positions = [p for p in cut_positions if p >= 0]
|
|
154
|
+
if cut_positions:
|
|
155
|
+
markdown = markdown[: min(cut_positions)]
|
|
156
|
+
|
|
157
|
+
while "\n\n\n" in markdown:
|
|
158
|
+
markdown = markdown.replace("\n\n\n", "\n\n")
|
|
159
|
+
|
|
160
|
+
return markdown.strip()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def clean_text(text: str) -> str:
|
|
164
|
+
text = re.sub(r"<[^>]+>", " ", text)
|
|
165
|
+
return re.sub(r"\s+", " ", text).strip()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_text(node) -> str:
|
|
169
|
+
if not node:
|
|
170
|
+
return ""
|
|
171
|
+
for attr in ("text", "text_content", "html_content"):
|
|
172
|
+
value = getattr(node, attr, None)
|
|
173
|
+
if isinstance(value, str) and value.strip():
|
|
174
|
+
return clean_text(value)
|
|
175
|
+
if callable(value):
|
|
176
|
+
try:
|
|
177
|
+
out = value()
|
|
178
|
+
if isinstance(out, str) and out.strip():
|
|
179
|
+
return clean_text(out)
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
return clean_text(str(node))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_attr(node, attr: str) -> str:
|
|
186
|
+
if not node:
|
|
187
|
+
return ""
|
|
188
|
+
attributes = getattr(node, "attributes", None)
|
|
189
|
+
if isinstance(attributes, dict) and attributes.get(attr):
|
|
190
|
+
return attributes[attr]
|
|
191
|
+
attrib = getattr(node, "attrib", None)
|
|
192
|
+
if isinstance(attrib, dict) and attrib.get(attr):
|
|
193
|
+
return attrib[attr]
|
|
194
|
+
fn = getattr(node, "get", None)
|
|
195
|
+
if callable(fn):
|
|
196
|
+
try:
|
|
197
|
+
value = fn(attr)
|
|
198
|
+
if value:
|
|
199
|
+
return value
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
html = getattr(node, "html_content", None) or str(node)
|
|
203
|
+
m = re.search(fr'{attr}="([^"]+)"', html)
|
|
204
|
+
return m.group(1) if m else ""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def extract_title(page, url: str, markdown: str = "") -> str:
|
|
208
|
+
host = (urlparse(url).hostname or "").lower()
|
|
209
|
+
if host.endswith("mp.weixin.qq.com"):
|
|
210
|
+
for sel in WECHAT_TITLE_SELECTORS:
|
|
211
|
+
nodes = page.css(sel)
|
|
212
|
+
node = nodes.first if nodes else None
|
|
213
|
+
text = get_text(node)
|
|
214
|
+
if text:
|
|
215
|
+
return text.strip()
|
|
216
|
+
|
|
217
|
+
html = getattr(page, "html_content", None) or ""
|
|
218
|
+
for pattern in [
|
|
219
|
+
r'<meta[^>]+property="og:title"[^>]+content="([^"]+)"',
|
|
220
|
+
r'<meta[^>]+name="twitter:title"[^>]+content="([^"]+)"',
|
|
221
|
+
r'<title>(.*?)</title>',
|
|
222
|
+
]:
|
|
223
|
+
m = re.search(pattern, html, re.I | re.S)
|
|
224
|
+
if m:
|
|
225
|
+
text = re.sub(r"\s+", " ", m.group(1)).strip()
|
|
226
|
+
if text:
|
|
227
|
+
return text
|
|
228
|
+
|
|
229
|
+
if markdown:
|
|
230
|
+
for line in markdown.splitlines():
|
|
231
|
+
line = line.strip()
|
|
232
|
+
if not line:
|
|
233
|
+
continue
|
|
234
|
+
if re.match(r"!\[[^\]]*\]\(([^)]+)\)", line):
|
|
235
|
+
continue
|
|
236
|
+
line = re.sub(r"^>\s*", "", line)
|
|
237
|
+
line = re.sub(r"^[#\s]+", "", line)
|
|
238
|
+
line = re.sub(r"^\*\*(.*?)\*\*$", r"\1", line)
|
|
239
|
+
if line:
|
|
240
|
+
return line.strip()
|
|
241
|
+
return ""
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def extract_cover_url(page, url: str, markdown: str = "") -> str:
|
|
245
|
+
host = (urlparse(url).hostname or "").lower()
|
|
246
|
+
html = getattr(page, "html_content", None) or ""
|
|
247
|
+
if host.endswith("mp.weixin.qq.com"):
|
|
248
|
+
for sel in WECHAT_COVER_SELECTORS:
|
|
249
|
+
nodes = page.css(sel)
|
|
250
|
+
node = nodes.first if nodes else None
|
|
251
|
+
if not node:
|
|
252
|
+
continue
|
|
253
|
+
if sel.startswith("meta"):
|
|
254
|
+
content = get_attr(node, "content")
|
|
255
|
+
if content:
|
|
256
|
+
return content.strip()
|
|
257
|
+
style = get_attr(node, "style")
|
|
258
|
+
if style:
|
|
259
|
+
m = re.search(r'background-image:\s*url\([\"\']?([^\)\"\']+)', style)
|
|
260
|
+
if m:
|
|
261
|
+
return m.group(1).strip()
|
|
262
|
+
for attr in ("data-src", "src"):
|
|
263
|
+
value = get_attr(node, attr)
|
|
264
|
+
if value:
|
|
265
|
+
return value.strip()
|
|
266
|
+
|
|
267
|
+
m = re.search(r'<meta[^>]+property="og:image"[^>]+content="([^"]+)"', html, re.I)
|
|
268
|
+
if m:
|
|
269
|
+
return m.group(1).strip()
|
|
270
|
+
|
|
271
|
+
if markdown:
|
|
272
|
+
m = re.search(r'!\[[^\]]*\]\(([^)]+)\)', markdown)
|
|
273
|
+
if m:
|
|
274
|
+
return m.group(1).strip()
|
|
275
|
+
return ""
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def extract_markdown(url: str, max_chars: int = 30000) -> str:
|
|
279
|
+
return extract_article(url, max_chars)["markdown"]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def extract_article(url: str, max_chars: int = 30000) -> dict:
|
|
283
|
+
page = Fetcher.get(url)
|
|
284
|
+
status = getattr(page, "status", None)
|
|
285
|
+
if isinstance(status, int) and status >= 400:
|
|
286
|
+
raise RuntimeError(f"HTTP {status} for {url}")
|
|
287
|
+
|
|
288
|
+
chosen_html = None
|
|
289
|
+
for sel in choose_selectors(url):
|
|
290
|
+
nodes = page.css(sel)
|
|
291
|
+
node = nodes.first if nodes else None
|
|
292
|
+
if node:
|
|
293
|
+
chosen_html = getattr(node, "html_content", None) or str(node)
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
if not chosen_html:
|
|
297
|
+
chosen_html = getattr(page, "html_content", None) or ""
|
|
298
|
+
|
|
299
|
+
chosen_html = normalize_html_for_images(chosen_html)
|
|
300
|
+
|
|
301
|
+
parser = html2text.HTML2Text()
|
|
302
|
+
parser.ignore_links = False
|
|
303
|
+
parser.ignore_images = False
|
|
304
|
+
parser.body_width = 0
|
|
305
|
+
|
|
306
|
+
markdown = parser.handle(chosen_html).strip()
|
|
307
|
+
markdown = strip_tail_noise(markdown)
|
|
308
|
+
title = extract_title(page, url, markdown)
|
|
309
|
+
cover_url = extract_cover_url(page, url, markdown)
|
|
310
|
+
if cover_url and cover_url not in markdown:
|
|
311
|
+
markdown = f"\n\n{markdown}".strip()
|
|
312
|
+
markdown = markdown[:max_chars]
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
"title": title,
|
|
316
|
+
"cover_url": cover_url,
|
|
317
|
+
"markdown": markdown,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main() -> int:
|
|
322
|
+
if len(sys.argv) < 2:
|
|
323
|
+
print("Usage: python scrapling_fetch.py <url> [max_chars] [--json]", file=sys.stderr)
|
|
324
|
+
return 1
|
|
325
|
+
|
|
326
|
+
args = [a for a in sys.argv[1:] if a != "--json"]
|
|
327
|
+
as_json = "--json" in sys.argv[1:]
|
|
328
|
+
url = args[0]
|
|
329
|
+
max_chars = int(args[1]) if len(args) > 1 else 30000
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
article = extract_article(url, max_chars)
|
|
333
|
+
if as_json:
|
|
334
|
+
print(json.dumps(article, ensure_ascii=False))
|
|
335
|
+
else:
|
|
336
|
+
print(article["markdown"])
|
|
337
|
+
return 0
|
|
338
|
+
except Exception as exc:
|
|
339
|
+
print(f"[ERROR] {exc}", file=sys.stderr)
|
|
340
|
+
return 2
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if __name__ == "__main__":
|
|
344
|
+
raise SystemExit(main())
|