evolcore 0.0.32 → 0.0.34

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.
Files changed (214) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +135 -37
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +111 -15
  6. package/dist/agents/codex/app-server-client.js +3 -0
  7. package/dist/agents/codex/diagnostics.js +112 -54
  8. package/dist/agents/codex-runner.js +241 -63
  9. package/dist/agents/ecagent-runner.js +78 -33
  10. package/dist/agents/runner-types.js +5 -6
  11. package/dist/aun/aid/identity.js +1 -1
  12. package/dist/aun/group/operations.js +0 -1
  13. package/dist/aun/msg/history.js +1 -0
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/channels/aun.js +44 -8
  17. package/dist/channels/daemon.js +261 -3
  18. package/dist/cli/agent-command.js +58 -24
  19. package/dist/cli/agent.js +1 -1
  20. package/dist/cli/aun-commands.js +368 -31
  21. package/dist/cli/contact.js +8 -2
  22. package/dist/cli/ctl-command.js +3 -1
  23. package/dist/cli/daemon-commands.js +197 -27
  24. package/dist/cli/debug-mode.js +154 -0
  25. package/dist/cli/fs-command.js +2 -1
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +98 -67
  28. package/dist/cli/init.js +2 -2
  29. package/dist/cli/integrity-command.js +196 -0
  30. package/dist/cli/managed-command-guard.js +5 -0
  31. package/dist/cli/memory-command.js +166 -146
  32. package/dist/cli/queue-command.js +9 -4
  33. package/dist/cli/session-command.js +194 -0
  34. package/dist/cli/skill-command.js +78 -2
  35. package/dist/cli/task-context.js +1 -0
  36. package/dist/cli/thread-command.js +622 -0
  37. package/dist/cli/trigger-command.js +10 -3
  38. package/dist/config/builtin-roles.js +1 -0
  39. package/dist/config/config-field-policy.js +6 -3
  40. package/dist/config/config-manager.js +26 -2
  41. package/dist/config/contact-book-store.js +5 -0
  42. package/dist/config/resolved-config-op.js +3 -0
  43. package/dist/config/role-service.js +13 -0
  44. package/dist/config/runtime-env.js +16 -0
  45. package/dist/config-store.js +45 -0
  46. package/dist/core/audit/log-integrity.js +3 -1
  47. package/dist/core/auth/agent-delegation.js +133 -36
  48. package/dist/core/auth/debug-context.js +139 -0
  49. package/dist/core/auth/delegation-diagnostics.js +414 -0
  50. package/dist/core/auth/operation-authorizer.js +25 -2
  51. package/dist/core/auth/operation-catalog.js +49 -0
  52. package/dist/core/background-session/session-cleanup.js +212 -0
  53. package/dist/core/background-session/types.js +10 -0
  54. package/dist/core/bootstrap-messages.js +1 -1
  55. package/dist/core/bootstrap-service.js +6 -3
  56. package/dist/core/command/agent-control.js +18 -26
  57. package/dist/core/command/command-handler.js +197 -27
  58. package/dist/core/command/daemon-diagnostics.js +1 -0
  59. package/dist/core/command/memory-slash.js +6 -2
  60. package/dist/core/command/menu-catalog.js +51 -1
  61. package/dist/core/command/menu-handler.js +114 -23
  62. package/dist/core/command/menu-protocol.js +25 -1
  63. package/dist/core/command/skill-slash.js +322 -0
  64. package/dist/core/command/slash-gate.js +6 -4
  65. package/dist/core/command/slash-handler.js +195 -13
  66. package/dist/core/command/topic-slash.js +528 -0
  67. package/dist/core/daemon-bootstrap.js +4 -7
  68. package/dist/core/evolagent-registry.js +20 -5
  69. package/dist/core/file-integrity/read.js +61 -0
  70. package/dist/core/file-integrity/store.js +262 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +175 -3
  74. package/dist/core/memory/batch-meta.js +132 -0
  75. package/dist/core/memory/consolidation-context.js +83 -29
  76. package/dist/core/memory/consolidation-ops-records.js +49 -16
  77. package/dist/core/memory/consolidation-queue-format.js +21 -0
  78. package/dist/core/memory/consolidation.js +264 -205
  79. package/dist/core/memory/index-manager.js +19 -28
  80. package/dist/core/memory/memory-admin.js +19 -14
  81. package/dist/core/memory/memory-document.js +53 -9
  82. package/dist/core/memory/memory-writer.js +30 -15
  83. package/dist/core/message/aun-ipc-sender.js +161 -6
  84. package/dist/core/message/im-renderer.js +169 -10
  85. package/dist/core/message/message-bridge.js +15 -4
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/peer-mode.js +6 -1
  88. package/dist/core/message/response-engine.js +929 -110
  89. package/dist/core/message/subtask-tool-projection.js +140 -0
  90. package/dist/core/model/catalog-service.js +17 -12
  91. package/dist/core/permission/ec-command-parser.js +217 -9
  92. package/dist/core/permission/protected-paths.js +5 -1
  93. package/dist/core/permission/readonly-shell-query.js +43 -5
  94. package/dist/core/permission/session-path-grants.js +334 -0
  95. package/dist/core/permission/tool-canonical.js +226 -0
  96. package/dist/core/permission/tool-policy.js +30 -62
  97. package/dist/core/role/runtime-policy.js +21 -10
  98. package/dist/core/session/session-fs-store.js +123 -8
  99. package/dist/core/session/session-idle-monitor.js +249 -22
  100. package/dist/core/session/session-manager.js +129 -14
  101. package/dist/core/skill/command-parser.js +13 -0
  102. package/dist/core/skill/index-generator.js +21 -4
  103. package/dist/core/skill/startup.js +41 -0
  104. package/dist/core/skill/switch-resolver.js +55 -29
  105. package/dist/core/system-session/grants.js +40 -0
  106. package/dist/core/system-session/memory.js +222 -0
  107. package/dist/core/system-session/registry.js +74 -0
  108. package/dist/core/system-session/types.js +14 -0
  109. package/dist/core/thread/command-parser.js +125 -0
  110. package/dist/core/thread/index.js +1 -0
  111. package/dist/core/thread/ipc-operation.js +95 -0
  112. package/dist/core/thread/thread-context-storage.js +131 -0
  113. package/dist/core/thread/thread-operation.js +258 -0
  114. package/dist/core/thread/thread-route.js +33 -0
  115. package/dist/core/thread/thread-service.js +770 -0
  116. package/dist/core/thread/types.js +1 -0
  117. package/dist/eck/kit-renderer.js +7 -2
  118. package/dist/eck/manifest-engine.js +24 -4
  119. package/dist/eck/message-renderer.js +35 -0
  120. package/dist/index.js +19 -7
  121. package/dist/ipc.js +140 -50
  122. package/dist/paths.js +93 -0
  123. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  124. package/dist/stats/billing.js +20 -8
  125. package/dist/trigger/authorization.js +57 -28
  126. package/dist/trigger/feedback.js +13 -2
  127. package/dist/trigger/parser.js +70 -1
  128. package/dist/trigger/system-definitions.js +3 -3
  129. package/dist/trigger/validation.js +14 -0
  130. package/dist/utils/cross-platform.js +11 -2
  131. package/dist/utils/error-dict.json +18 -4
  132. package/dist/utils/error-utils.js +143 -1
  133. package/dist/utils/instance-registry.js +13 -8
  134. package/dist/utils/log-writer.js +12 -0
  135. package/dist/utils/process-introspect.js +11 -0
  136. package/kits/docs/context-assembly.md +1 -1
  137. package/kits/docs/evolcore/INDEX.md +1 -0
  138. package/kits/docs/evolcore/agent-create.md +40 -28
  139. package/kits/docs/evolcore/agent.md +21 -6
  140. package/kits/docs/evolcore/config.md +2 -2
  141. package/kits/docs/evolcore/ctl.md +2 -2
  142. package/kits/docs/evolcore/ec-capabilities.md +5 -1
  143. package/kits/docs/evolcore/fs-architecture.md +4 -5
  144. package/kits/docs/evolcore/fs.md +1 -1
  145. package/kits/docs/evolcore/group-collaboration.md +4 -6
  146. package/kits/docs/evolcore/group.md +11 -12
  147. package/kits/docs/evolcore/memory-architecture.md +4 -5
  148. package/kits/docs/evolcore/memory-consolidation.md +19 -15
  149. package/kits/docs/evolcore/memory-tags.md +3 -1
  150. package/kits/docs/evolcore/memory.md +24 -12
  151. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  152. package/kits/docs/evolcore/menu-protocol.md +7 -0
  153. package/kits/docs/evolcore/model.md +16 -0
  154. package/kits/docs/evolcore/msg.md +8 -11
  155. package/kits/docs/evolcore/thread-chain.md +267 -0
  156. package/kits/docs/prompt-loading-architecture.md +1 -1
  157. package/kits/eck_manifest.json +55 -40
  158. package/kits/eck_manifest.memory.json +0 -12
  159. package/kits/eck_message_manifest.json +169 -10
  160. package/kits/rules/01-overview.md +11 -0
  161. package/kits/rules/03-identity.md +6 -3
  162. package/kits/rules/05-venue.md +1 -1
  163. package/kits/rules/06-channel.md +1 -1
  164. package/kits/rules/07-memory-and-thread.md +199 -0
  165. package/kits/schemas/_meta.json +7 -1
  166. package/kits/schemas/agent-config.schema.13.json +1 -1
  167. package/kits/schemas/daemon.schema.7.json +140 -0
  168. package/kits/schemas/relation-config.schema.9.json +18 -3
  169. package/kits/schemas/role-config.schema.1.json +6 -6
  170. package/kits/schemas/single-session.schema.1.json +6 -6
  171. package/kits/schemas/single-session.schema.2.json +6 -6
  172. package/kits/schemas/single-session.schema.3.json +6 -6
  173. package/kits/skills/.switches.json +12 -1
  174. package/kits/skills/ec-group/SKILL.md +234 -0
  175. package/kits/skills/ec-skill/SKILL.md +10 -1
  176. package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
  177. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  178. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  179. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  180. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  181. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  182. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  183. package/kits/skills/thread-ops/SKILL.md +492 -0
  184. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  185. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  186. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  187. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  188. package/kits/templates/bootstrap-welcome.md +7 -7
  189. package/kits/templates/memory-consolidation/forget.md +7 -2
  190. package/kits/templates/memory-consolidation/generalize.md +9 -4
  191. package/kits/templates/memory-consolidation/notes.md +11 -11
  192. package/kits/templates/memory-consolidation/profile.md +8 -3
  193. package/kits/templates/memory-consolidation/promote.md +8 -3
  194. package/kits/templates/memory-consolidation/workflow.md +6 -6
  195. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  196. package/kits/templates/message-fragments/item.md +2 -1
  197. package/kits/templates/message-fragments/thread-context.md +6 -0
  198. package/kits/templates/message-fragments/trigger-session.md +2 -0
  199. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  200. package/kits/templates/roles/member.json +2 -0
  201. package/kits/templates/roles/visitor.json +2 -0
  202. package/kits/templates/system-fragments/bootstrap.md +67 -72
  203. package/kits/templates/system-fragments/channel.md +3 -6
  204. package/kits/templates/system-fragments/commands.md +1 -0
  205. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  206. package/kits/templates/system-fragments/memory.md +16 -9
  207. package/kits/templates/system-fragments/reply.md +7 -6
  208. package/kits/templates/system-fragments/session.md +5 -14
  209. package/package.json +6 -3
  210. package/dist/core/memory/memory-session-policy.js +0 -41
  211. package/dist/core/memory/memory-signature.js +0 -257
  212. package/kits/docs/evolcore/group-fs.md +0 -17
  213. package/kits/docs/venues/aun-group.md +0 -45
  214. package/kits/docs/venues/group.md +0 -43
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ wechat-article-media-download
4
+ Download all videos and images embedded in a WeChat official account article.
5
+
6
+ Works WITHOUT a WeChat client and WITHOUT mp.weixin.qq.com login:
7
+ video direct links (signed mpvideo.qpic.cn URLs) are extracted from the
8
+ inline JS data blocks of the article HTML itself.
9
+
10
+ Usage:
11
+ python3 download.py <article_url> [output_dir]
12
+
13
+ Requires: Python 3.7+ (stdlib only)
14
+ """
15
+
16
+ import html as htmllib
17
+ import json
18
+ import os
19
+ import re
20
+ import sys
21
+ import threading
22
+ import urllib.request
23
+
24
+ UA = (
25
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
26
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"
27
+ )
28
+ HEADERS = {"User-Agent": UA, "Referer": "https://mp.weixin.qq.com/"}
29
+
30
+ # preferred video formats, best first (10002 = 超清 H.264)
31
+ FORMAT_PRIORITY = ["10002", "10102", "10004", "10104"]
32
+
33
+
34
+ def fetch(url, dest=None):
35
+ req = urllib.request.Request(url, headers=HEADERS)
36
+ with urllib.request.urlopen(req, timeout=120) as resp:
37
+ data = resp.read()
38
+ if dest:
39
+ with open(dest, "wb") as f:
40
+ f.write(data)
41
+ return data
42
+
43
+
44
+ def unescape_js(html_text):
45
+ """Undo the JS escaping used in article data blocks.
46
+
47
+ Video URLs are stored like `url: 'http://mpvideo.qpic.cn/...\\x26amp;dis_t=...'`.
48
+ Missing this step truncates every URL at its first `&`.
49
+ """
50
+ txt = htmllib.unescape(html_text)
51
+ txt = txt.replace("\\x26amp;", "&").replace("\\x26;", "&").replace("\\x26", "&")
52
+ return txt
53
+
54
+
55
+ def extract_images(html_text):
56
+ seen, out = set(), []
57
+ for u in re.findall(r'data-src="(https://mmbiz\.qpic\.cn/[^"]+)"', html_text):
58
+ if u not in seen:
59
+ seen.add(u)
60
+ fmt_m = re.search(r"wx_fmt=(\w+)", u)
61
+ out.append((u, fmt_m.group(1) if fmt_m else "png"))
62
+ return out
63
+
64
+
65
+ def extract_videos(html_text):
66
+ """Return an ordered list of (best_format, url) — one entry per article video.
67
+
68
+ Each video appears as ~4 format variants sharing the same base path;
69
+ order of first appearance matches article order.
70
+ """
71
+ txt = unescape_js(html_text)
72
+ blocks = re.findall(
73
+ r"format_id:\s*'(\d+)'\s*\*[^}]*?url:\s*'(http://mpvideo\.qpic\.cn/[^']+)'",
74
+ txt,
75
+ re.S,
76
+ )
77
+ groups, order = {}, []
78
+ for fid, url in blocks:
79
+ m = re.match(r"(http://mpvideo\.qpic\.cn/[a-z0-9]+)\.f\d+\.mp4", url)
80
+ if not m:
81
+ continue
82
+ base = m.group(1)
83
+ if base not in groups:
84
+ groups[base] = {}
85
+ order.append(base)
86
+ groups[base][fid] = url
87
+ plan = []
88
+ for base in order:
89
+ for fid in FORMAT_PRIORITY:
90
+ if fid in groups[base]:
91
+ plan.append((fid, groups[base][fid]))
92
+ break
93
+ return plan
94
+
95
+
96
+ def download_all(items, outdir, prefix, ext_default="bin"):
97
+ """items: list of (name_hint, url, ext). Returns list of (path, size)."""
98
+ os.makedirs(outdir, exist_ok=True)
99
+ results, lock = [], threading.Lock()
100
+
101
+ def work(i, url, ext):
102
+ path = os.path.join(outdir, f"{prefix}_{i:02d}.{ext}")
103
+ try:
104
+ fetch(url, path)
105
+ size = os.path.getsize(path)
106
+ except Exception as e: # noqa: BLE001
107
+ size = 0
108
+ with lock:
109
+ print(f" [FAIL] {path}: {e}")
110
+ with lock:
111
+ print(f" [OK] {path} ({size / 1024:.0f} KB)" if size else f" [FAIL] {path}")
112
+ results.append((path, size))
113
+
114
+ threads = [
115
+ threading.Thread(target=work, args=(i, url, ext))
116
+ for i, (url, ext) in enumerate(items, 1)
117
+ ]
118
+ for t in threads:
119
+ t.start()
120
+ for t in threads:
121
+ t.join()
122
+ return sorted(results)
123
+
124
+
125
+ def main():
126
+ if len(sys.argv) < 2:
127
+ print(__doc__)
128
+ sys.exit(1)
129
+ article_url = sys.argv[1]
130
+ outdir = sys.argv[2] if len(sys.argv) > 2 else "output"
131
+
132
+ print(f"[1/4] Fetching article: {article_url}")
133
+ html_text = fetch(article_url).decode("utf-8", errors="ignore")
134
+
135
+ title_m = re.search(r'<meta property="og:title" content="([^"]*)"', html_text)
136
+ print(f" Title: {title_m.group(1) if title_m else 'N/A'}")
137
+
138
+ print("[2/4] Extracting media links ...")
139
+ images = extract_images(html_text)
140
+ videos = extract_videos(html_text)
141
+ print(f" Found {len(videos)} videos, {len(images)} images")
142
+
143
+ print(f"[3/4] Downloading to {outdir}/ ...")
144
+ vres = download_all([(u, "mp4") for _, u in videos], os.path.join(outdir, "videos"), "video")
145
+ ires = download_all([(u, e) for u, e in images], os.path.join(outdir, "images"), "image")
146
+
147
+ print("[4/4] Verification")
148
+ ok_v = sum(1 for _, s in vres if s > 100_000)
149
+ ok_i = sum(1 for _, s in ires if s > 0)
150
+ print(f" videos: {ok_v}/{len(vres)} valid (>100KB)")
151
+ print(f" images: {ok_i}/{len(ires)} valid")
152
+
153
+ json.dump(
154
+ {"article": article_url, "videos": [u for _, u in videos], "images": [u for u, _ in images]},
155
+ open(os.path.join(outdir, "sources.json"), "w"),
156
+ ensure_ascii=False,
157
+ indent=1,
158
+ )
159
+ print("Done. Link sources saved to sources.json")
160
+
161
+
162
+ if __name__ == "__main__":
163
+ main()
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: wechat-article-reader
3
+ description: >-
4
+ 从微信公众号文章链接提取正文、图片、视频,保存为 Markdown 和本地媒体文件。
5
+ 当用户提供 mp.weixin.qq.com/s 格式的链接,并要求"阅读""保存""下载""提取正文"
6
+ "抓取文章"时使用。输出 Markdown 正文(图片/视频引用本地相对路径)、
7
+ 下载好的图片/视频文件、统计信息。零外部依赖,只需 Node.js ≥22。
8
+ 不适用于需要登录才能查看的付费文章、视频号来源的视频、已删除的文章。
9
+ license: MIT
10
+ ---
11
+
12
+ # 微信公众号文章阅读器
13
+
14
+ 从公众号文章链接提取完整内容并保存到本地,包含正文 Markdown、图片、视频。
15
+
16
+ ## 功能
17
+
18
+ - 提取文章标题、作者、发布时间、正文
19
+ - 下载所有图片(自动取原图)
20
+ - 下载所有视频(选最高清晰度)
21
+ - 输出 Markdown,图片/视频引用本地相对路径
22
+ - 生成统计信息(文字数、图片数、视频数、成功/失败数)
23
+ - 原始链接存档到 sources.json
24
+
25
+ ## 依赖
26
+
27
+ - Node.js ≥ 22
28
+ - 零外部依赖(只用 Node 标准库)
29
+
30
+ ## 用法
31
+
32
+ ```bash
33
+ node "<技能目录>/scripts/fetch.mjs" "<文章URL>" [输出目录]
34
+ ```
35
+
36
+ **示例:**
37
+
38
+ ```bash
39
+ node "<技能目录>/scripts/fetch.mjs" \
40
+ "https://mp.weixin.qq.com/s/xxxxxx"
41
+ ```
42
+
43
+ 默认输出到当前目录下的 `wechat-articles/日期_公众号_标题/`。
44
+
45
+ 输出结构:
46
+
47
+ ```
48
+ wechat-articles/
49
+ └── 2026-10-08_机器之心_豆包大模型…/
50
+ ├── article.md # 正文 Markdown
51
+ ├── images/ # 图片文件
52
+ │ ├── cover.png
53
+ │ └── 001.jpg
54
+ ├── videos/ # 视频文件
55
+ │ └── 001.mp4
56
+ └── sources.json # 原始链接存档
57
+ ```
58
+
59
+ ## 原理
60
+
61
+ ### 图片
62
+ - 从 `data-src="https://mmbiz.qpic.cn/..."` 提取 URL
63
+ - URL 中的 `/640?` 改成 `/0?` 取原图
64
+ - 无需 Referer,直接下载
65
+
66
+ ### 视频
67
+ - 从页面内嵌 JS 数据块提取签名直链
68
+ - 每个视频有多种清晰度(format_id):优先 10002(超清 H.264),
69
+ 依次回退 10102 → 10004 → 10104
70
+ - 签名 URL 短期有效,必须在提取后立即下载
71
+ - 视频号(Channels)来源无直链,无法下载
72
+
73
+ ### 正文
74
+ - 定位 `#js_content` 容器
75
+ - 转换为 Markdown:保留段落、加粗、图片、视频占位
76
+ - 图片/视频路径改写为本地相对路径
77
+
78
+ ## 统计
79
+
80
+ 文章末尾追加:
81
+
82
+ ```
83
+ ---
84
+ 📊 统计: 3120 字 | 图片 8/8 | 视频 5/9 (4 个失败: 403 签名失效)
85
+ ```
86
+
87
+ 格式:正文字数 | 图片成功/总数 | 视频成功/总数 (失败原因)
88
+
89
+ ## 边界与限制
90
+
91
+ - **视频限频**:微信 CDN 对同一出口 IP 短时间内重复请求同一视频会返回 403。
92
+ 脚本已加入随机延迟和重试,但无法完全避免。失败的视频会在统计中明确标注。
93
+ - **视频号视频**:无签名直链,跳过并标注。
94
+ - **付费/仅粉丝文章**:页面返回验证页或空内容,脚本报错退出。
95
+ - **已删除文章**:同上。
96
+ - **零依赖**:只用 Node.js 标准库,无需 npm install。
97
+ - **路径规则**:执行时必须用脚本的绝对路径,不依赖 cwd。
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ import { writeFile, mkdir } from 'node:fs/promises';
3
+ import { createWriteStream } from 'node:fs';
4
+ import { basename, join } from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
6
+
7
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
8
+ const BS = String.fromCharCode(92);
9
+
10
+ function usage() {
11
+ console.error('用法: node fetch.mjs <文章URL> [输出根目录]');
12
+ console.error('示例: node fetch.mjs "https://mp.weixin.qq.com/s/xxxxx" ./my-articles');
13
+ process.exit(1);
14
+ }
15
+
16
+ // HTML 实体解码(处理微信视频 URL 里的 \x26amp; \x26)
17
+ function decodeVideoUrl(raw) {
18
+ return raw.split(BS + 'x26amp;').join('&')
19
+ .split(BS + 'x26').join('&')
20
+ .split('&amp;').join('&');
21
+ }
22
+
23
+ // 微信图片 URL:把路径里的尺寸段换成 0 拿原图,并统一解实体
24
+ function toOriginal(src) {
25
+ return src.replace(/&amp;/g, '&').replace(/\/\d+\?/, '/0?');
26
+ }
27
+
28
+ // 安全文件名(跨平台)
29
+ function sanitize(s, maxLen = 80) {
30
+ if (!s) return '';
31
+ return s.replace(/[\[\]()/\\:*?"<>|]/g, ' ')
32
+ .replace(/\s+/g, ' ')
33
+ .trim()
34
+ .replace(/\.$/, '')
35
+ .slice(0, maxLen)
36
+ .trim();
37
+ }
38
+
39
+ // 从 js_content 开标签处切出正文:跳到标签闭合的 '>',再按 div 深度配对
40
+ function sliceJsContent(html, from) {
41
+ const openEnd = html.indexOf('>', from);
42
+ if (openEnd < 0) throw new Error('正文容器标签未闭合');
43
+ const rest = html.slice(openEnd + 1);
44
+
45
+ // 微信正文是 <div id="js_content">…</div>,内部还有大量嵌套 div
46
+ let depth = 1, pos = 0;
47
+ const re = /<(\/?)div\b[^>]*>/gi;
48
+ re.lastIndex = 0;
49
+ let m;
50
+ while ((m = re.exec(rest))) {
51
+ depth += m[1] ? -1 : 1;
52
+ if (depth === 0) return rest.slice(0, m.index);
53
+ pos = re.lastIndex;
54
+ }
55
+ // 配对失败时退回旧策略:截到第一个 <script>
56
+ const s = rest.indexOf('<script');
57
+ return s > 0 ? rest.slice(0, s) : rest;
58
+ }
59
+
60
+ // 下载文件。403 是签名失效或 CDN 限频,重试无意义,直接失败;
61
+ // 网络抖动与 5xx/429 才退避重试。
62
+ async function download(url, dest) {
63
+ let lastErr;
64
+ for (const wait of [0, 1200, 3000]) {
65
+ if (wait) await new Promise(r => setTimeout(r, wait));
66
+ try {
67
+ const r = await fetch(url, { headers: { 'User-Agent': UA } });
68
+ if (!r.ok) {
69
+ const err = new Error(`HTTP ${r.status}`);
70
+ if (r.status === 403) throw Object.assign(err, { fatal: true });
71
+ throw err;
72
+ }
73
+ await pipeline(r.body, createWriteStream(dest));
74
+ return dest;
75
+ } catch (e) {
76
+ if (e.fatal) throw e;
77
+ lastErr = e;
78
+ }
79
+ }
80
+ throw lastErr;
81
+ }
82
+
83
+ async function main() {
84
+ if (process.argv.length < 3) usage();
85
+ const url = process.argv[2];
86
+ const outRoot = process.argv[3] || 'wechat-articles';
87
+
88
+ console.log('[1/5] 抓取文章页面...');
89
+ const pageResp = await fetch(url, { headers: { 'User-Agent': UA } });
90
+ const html = await pageResp.text();
91
+
92
+ if (html.includes('secitptpage') || !html.includes('id="js_content"')) {
93
+ throw new Error('页面异常:可能遇到验证页、付费墙或文章已删除');
94
+ }
95
+
96
+ // 提取元信息
97
+ const titleMatch = html.match(/<meta property="og:title" content="([^"]*)"/);
98
+ const title = titleMatch ? titleMatch[1].replace(/&quot;/g, '"').replace(/&amp;/g, '&') : '未命名文章';
99
+
100
+ const nicknameMatch = html.match(/var nickname\s*=\s*htmlDecode\("([^"]*)"\)/);
101
+ const nickname = nicknameMatch ? nicknameMatch[1] : '未知公众号';
102
+
103
+ const createTimeMatch = html.match(/var create_time\s*=\s*"(\d+)"/);
104
+ const timestamp = createTimeMatch ? Number(createTimeMatch[1]) * 1000 : Date.now();
105
+ const date = new Date(timestamp).toISOString().slice(0, 10);
106
+
107
+ console.log(` 标题: ${title}`);
108
+ console.log(` 公众号: ${nickname}`);
109
+ console.log(` 日期: ${date}`);
110
+
111
+ // 创建输出目录
112
+ const dirname = `${date}_${sanitize(nickname, 20)}_${sanitize(title, 40)}`;
113
+ const outDir = join(outRoot, dirname);
114
+ await mkdir(join(outDir, 'images'), { recursive: true });
115
+ await mkdir(join(outDir, 'videos'), { recursive: true });
116
+
117
+ console.log(`[2/5] 提取媒体链接...`);
118
+
119
+ // 定位正文:先跳到 js_content 开标签结束的 >,再按 div 深度配对找收尾
120
+ const containerStart = html.indexOf('id="js_content"');
121
+ if (containerStart < 0) throw new Error('未找到正文容器 #js_content');
122
+ const body = sliceJsContent(html, containerStart);
123
+
124
+ // 图片:整页去重,URL 里的尺寸段换成 /0 取原图
125
+ const imgMatches = [...body.matchAll(/data-src="(https:\/\/mmbiz\.qpic\.cn\/[^"]+)"/g)];
126
+ const images = [...new Set(imgMatches.map(m => toOriginal(m[1])))];
127
+
128
+ // 视频:videoPageInfos 里每个 video_id 自带一组清晰度,
129
+ // 按 video_id 分块,避免相邻视频的清晰度互相串台。
130
+ const formatsByVid = new Map();
131
+ const chunks = html.split(/video_id:\s*'(wxv_\d+)'/);
132
+ for (let i = 1; i < chunks.length; i += 2) {
133
+ const vid = chunks[i];
134
+ const block = chunks[i + 1] || '';
135
+ if (formatsByVid.has(vid)) continue;
136
+ // 以 format_id 切段,每段里第一个 mpvideo url 就是该清晰度的直链。
137
+ // 段长随页面模板变化(实测 format_id 到 url 有 550+ 字符),不能用固定距离。
138
+ const formats = {};
139
+ const parts = block.split(/format_id:\s*'(10002|10102|10004|10104)'/);
140
+ for (let k = 1; k < parts.length; k += 2) {
141
+ const fid = parts[k];
142
+ if (formats[fid]) continue;
143
+ const u = (parts[k + 1] || '').match(/url:\s*\(?'(http:\/\/mpvideo\.qpic\.cn\/[^']+)'/);
144
+ if (u) formats[fid] = decodeVideoUrl(u[1]);
145
+ }
146
+ formatsByVid.set(vid, formats);
147
+ }
148
+
149
+ const pickBestUrl = formats => {
150
+ for (const fid of ['10002', '10102', '10004', '10104']) {
151
+ if (formats[fid]) return { format: fid, url: formats[fid] };
152
+ }
153
+ return null;
154
+ };
155
+
156
+ // 正文里视频按 iframe 出现顺序,用 data-mpvid 关联;同一视频只算一次
157
+ const orderedVids = [...new Set([...body.matchAll(/data-mpvid="(wxv_\d+)"/g)].map(m => m[1]))];
158
+ const videos = [];
159
+ for (const vid of orderedVids) {
160
+ const best = pickBestUrl(formatsByVid.get(vid) || {});
161
+ if (best) videos.push({ vid, ...best });
162
+ else videos.push({ vid, format: null, url: null, reason: '无直链(视频号或已下架)' });
163
+ }
164
+
165
+ console.log(` 图片: ${images.length} 个`);
166
+ console.log(` 视频: ${videos.length} 个`);
167
+
168
+ console.log('[3/5] 下载图片...');
169
+ const imageResults = [];
170
+ for (let i = 0; i < images.length; i++) {
171
+ const url = images[i];
172
+ const ext = url.match(/wx_fmt=(\w+)/)?.[1] || (url.includes('_gif') ? 'gif' : 'jpg');
173
+ const filename = `${String(i + 1).padStart(3, '0')}.${ext}`;
174
+ const dest = join(outDir, 'images', filename);
175
+ try {
176
+ await download(url, dest);
177
+ imageResults.push({ ok: true, url, path: `images/${filename}` });
178
+ process.stdout.write(` ✓ ${filename}\r`);
179
+ } catch (e) {
180
+ imageResults.push({ ok: false, url, error: e.message });
181
+ process.stdout.write(` ✗ ${filename} (${e.message})\n`);
182
+ }
183
+ }
184
+ console.log(` 完成: ${imageResults.filter(r => r.ok).length}/${images.length}`);
185
+
186
+ console.log('[4/5] 下载视频...');
187
+ const videoResults = [];
188
+ for (let i = 0; i < videos.length; i++) {
189
+ const { vid, format, url, reason } = videos[i];
190
+ const filename = `${String(i + 1).padStart(3, '0')}.mp4`;
191
+ const dest = join(outDir, 'videos', filename);
192
+ if (!url) {
193
+ videoResults.push({ ok: false, vid, format, url: null, error: reason });
194
+ console.log(` ✗ ${filename}: ${reason}`);
195
+ continue;
196
+ }
197
+ try {
198
+ await download(url, dest);
199
+ videoResults.push({ ok: true, vid, format, url, path: `videos/${filename}` });
200
+ console.log(` ✓ ${filename} (f${format})`);
201
+ } catch (e) {
202
+ videoResults.push({ ok: false, vid, format, url, error: e.message });
203
+ console.log(` ✗ ${filename} (f${format}, ${e.message})`);
204
+ }
205
+ if (i < videos.length - 1) await new Promise(r => setTimeout(r, 800 + Math.random() * 400));
206
+ }
207
+ console.log(` 完成: ${videoResults.filter(r => r.ok).length}/${videos.length}`);
208
+
209
+ // 远端链接 → 本地路径,供正文改写使用
210
+ const imageMap = new Map(imageResults.filter(r => r.ok).map(r => [r.url, r.path]));
211
+ const videoMap = new Map(videoResults.filter(r => r.ok).map(r => [r.vid, r.path]));
212
+
213
+ console.log('[5/5] 生成 Markdown...');
214
+
215
+ // 简化 HTML 转 Markdown
216
+ let md = body
217
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
218
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
219
+ .replace(/<iframe[^>]*data-mpvid="([^"]+)"[^>]*>(?:<\/iframe>)?/g, (_, vid) => {
220
+ const local = videoMap.get(vid);
221
+ return local ? `\n\n[▶ 视频](${local})\n\n` : `\n\n[▶ 视频: ${vid}(未能下载)]\n\n`;
222
+ })
223
+ .replace(/<img[^>]*data-src="([^"]+)"[^>]*>/g, (full, src) => {
224
+ const local = imageMap.get(toOriginal(src));
225
+ return local ? `\n\n![](${local})\n\n` : `\n\n[图片缺失]\n\n`;
226
+ })
227
+ .replace(/<br\s*\/?>/gi, '\n')
228
+ .replace(/<\/p>/gi, '\n\n')
229
+ .replace(/<[^>]+>/g, '')
230
+ .replace(/&nbsp;/g, ' ')
231
+ .replace(/&quot;/g, '"')
232
+ .replace(/&lt;/g, '<')
233
+ .replace(/&gt;/g, '>')
234
+ .replace(/&amp;/g, '&')
235
+ .replace(/\n{3,}/g, '\n\n')
236
+ .trim();
237
+
238
+ const wordCount = md.replace(/[^一-龥a-zA-Z]/g, '').length;
239
+ const imgOk = imageResults.filter(r => r.ok).length;
240
+ const vidOk = videoResults.filter(r => r.ok).length;
241
+ const vidFail = videos.length - vidOk;
242
+ const failReason = vidFail > 0 ? ` (${vidFail} 个失败: 403 签名失效/限频)` : '';
243
+
244
+ const article = `# ${title}\n\n**作者**: ${nickname} \n**日期**: ${date} \n**原文**: ${url}\n\n---\n\n${md}\n\n---\n\n📊 统计: ${wordCount} 字 | 图片 ${imgOk}/${images.length} | 视频 ${vidOk}/${videos.length}${failReason}\n`;
245
+
246
+ await writeFile(join(outDir, 'article.md'), article, 'utf8');
247
+ await writeFile(join(outDir, 'sources.json'), JSON.stringify({
248
+ url, title, nickname, date, timestamp,
249
+ images: imageResults,
250
+ videos: videoResults
251
+ }, null, 2), 'utf8');
252
+
253
+ console.log(`\n✅ 完成! 输出目录: ${outDir}/`);
254
+ }
255
+
256
+ main().catch(e => { console.error('\n❌', e.message); process.exit(1); });
@@ -1,17 +1,17 @@
1
1
  你好,我是 {{agentName}}。
2
2
 
3
- 我刚刚完成连接,需要先和你确认我的基础信息,然后再进入正常工作状态。
3
+ 我刚刚完成连接,先补齐基础信息就可以开始工作。创建时已经填写的有效信息会直接沿用,不需要重复提供或确认。
4
4
 
5
- 请帮我确认三项内容:
6
- 1. 显示名:你希望我叫什么?
7
- 2. 简介:我主要负责什么?一句到两句话即可。
8
- 3. 标签:给我 3 到 6 个标签,例如 `代码审查`、`客服助手`、`项目管理`。
5
+ 如果还有缺失,或你想修改,可以一次补充相应项:
6
+ 1. 显示名:希望使用的名称。
7
+ 2. 简介:主要职责或定位,一到两句话即可。
8
+ 3. 标签:3 到 6 个检索标签,例如 `代码审查`、`客服助手`、`项目管理`。
9
9
 
10
10
  当前信息:
11
11
  - AID: {{agentAid}}
12
12
  - 渠道: {{channel}}
13
13
  - Baseagent: {{baseagent}}
14
14
 
15
- 如果希望沿用已有 agent.md 信息,可以直接回复“沿用现有信息”;我会先读取并回显现有值,请你确认是否保持或修改,不需要重新输入。
15
+ 也可以顺便补充人格属性、话术风格、工作流程、任务偏好(persona,支持文本或 `.md`/`.txt` 文件),以及可选头像(HTTPS 图片地址,或放在当前项目目录/本会话临时目录中的图片文件)。这些都不必现在处理,未提供不影响激活,之后也可以修改。
16
16
 
17
- 如果希望重新设定,也可以直接按上面的三项回复。
17
+ 如果希望沿用现有信息,直接回复“沿用现有信息”即可;我只会追问缺失或有歧义的部分。
@@ -10,6 +10,11 @@
10
10
 
11
11
  上下文文件里列出的候选:mtime 超过 `memory.forgetAfterDays` 的记忆。**候选只给路径 + 描述。**
12
12
 
13
+ | 来源 | 位置 | 说明 |
14
+ |---|---|---|
15
+ | 候选记忆 | 上下文文件里列出的候选清单 | mtime 超过 `forgetAfterDays` 的条目,只给路径+描述 |
16
+ | 对话历史 | 上下文文件里列出的 `threads/` 快照文件(若有,仅 relation 层) | 上次整理以来的增量对话,辅助判断超期记忆是否仍然有效;agent/owner 层无此文件 |
17
+
13
18
  > **打了 `important` / `urgent` / `profile` 的记忆同样会出现在候选里。**
14
19
  > 「永不淘汰」指的是索引截断时不会被截掉(只要它在,就保证进索引),
15
20
  > **不代表它免于遗忘审查**。审查的是「这条内容还成不成立」,与它在不在索引里是两个维度。
@@ -58,9 +63,9 @@ ec memory archive <路径>
58
63
  若干条超期记忆**单独每一条都不值得续期,但合起来值得留一条**时,产出一条合并版:
59
64
 
60
65
  ```bash
61
- ec memory create --scope <本次会话的 scope> --slug <slug> --desc "一句话描述" \
66
+ ec memory create --scope <本次会话的 scope> --slug <slug> --desc '一句话描述' \
62
67
  --tags <...> [--keywords <...>] \
63
- --replaces "<旧文件1>,<旧文件2>,..." \
68
+ --replaces '<旧文件1>,<旧文件2>,...' \
64
69
  "<合并后的完整内容>"
65
70
  ```
66
71
 
@@ -10,6 +10,11 @@
10
10
 
11
11
  上下文文件里列出的候选:关系级记忆里带 `to-generalize` 标签的条目。**候选只给路径 + 描述**,先只读描述判断;某条描述太简、拿不准的,再读那一条正文。
12
12
 
13
+ | 来源 | 位置 | 说明 |
14
+ |---|---|---|
15
+ | 候选记忆 | 上下文文件里列出的候选清单 | 关系级 consolidated/ 里带 `to-generalize` 的条目,只给路径+描述 |
16
+ | 对话历史 | 上下文文件里列出的 `threads/` 快照文件(若有) | 上次本模式整理以来的增量对话,辅助判断泛化结论是否成立;无增量时为空,可忽略 |
17
+
13
18
  ## 产出
14
19
 
15
20
  写到 **agent 作用域**(`--scope agent`)。
@@ -37,7 +42,7 @@
37
42
  **评估后认为不该泛化的,用 `update` 把 `to-generalize` 标签去掉。** 否则每次定时检查都会重新扫到它,反复评估同一条。
38
43
 
39
44
  ```bash
40
- ec memory update <关系级记忆路径> --tags "<去掉 to-generalize 后的标签>"
45
+ ec memory update <关系级记忆路径> --tags '<去掉 to-generalize 后的标签>'
41
46
  ```
42
47
 
43
48
  ### 已经存在同类 agent 级记忆
@@ -45,9 +50,9 @@ ec memory update <关系级记忆路径> --tags "<去掉 to-generalize 后的标
45
50
  如果 agent 级里已经有讲同一件事的记忆,**用 `--replaces` 同时替换两边**,产出合并后的版本:
46
51
 
47
52
  ```bash
48
- ec memory create --scope agent --slug commit-chinese --desc "commit 消息用中文" \
49
- --tags "rule,long-term" --keywords "commit,git" \
50
- --replaces "consolidated/20260901-120000-commit.md,$RELATIONS_DIR/<peerKey>/memory/consolidated/20261002-103100-commit-chinese.md" \
53
+ ec memory create --scope agent --slug commit-chinese --desc 'commit 消息用中文' \
54
+ --tags 'rule,long-term' --keywords 'commit,git' \
55
+ --replaces 'consolidated/20260901-120000-commit.md,$RELATIONS_DIR/<peerKey>/memory/consolidated/20261002-103100-commit-chinese.md' \
51
56
  "(合并后的完整内容)"
52
57
  ```
53
58
 
@@ -25,17 +25,17 @@
25
25
  ### ① 是新的稳定结论 → create
26
26
 
27
27
  ```bash
28
- ec memory create --scope relation --slug commit-chinese --desc "对端要求 commit 消息用中文" \
29
- --tags "rule,long-term,verified" --keywords "commit,git" \
28
+ ec memory create --scope relation --slug commit-chinese --desc '对端要求 commit 消息用中文' \
29
+ --tags 'rule,long-term,verified' --keywords 'commit,git' \
30
30
  "对端明确要求:commit message 必须用中文,并列出所有功能改动点。"
31
31
  ```
32
32
 
33
33
  ### ② 和已有记忆是同一条,但内容更全或更准 → create --replaces
34
34
 
35
35
  ```bash
36
- ec memory create --scope relation --slug commit-chinese --desc "对端要求 commit 用中文,注释不限" \
37
- --tags "rule,long-term,verified" --keywords "commit,git" \
38
- --replaces "consolidated/20261001-103100-commit-chinese.md" \
36
+ ec memory create --scope relation --slug commit-chinese --desc '对端要求 commit 用中文,注释不限' \
37
+ --tags 'rule,long-term,verified' --keywords 'commit,git' \
38
+ --replaces 'consolidated/20261001-103100-commit-chinese.md' \
39
39
  "对端要求 commit message 用中文;代码注释可以是英文。"
40
40
  ```
41
41
 
@@ -44,7 +44,7 @@ ec memory create --scope relation --slug commit-chinese --desc "对端要求 com
44
44
  ### ③ 已有记忆还成立,只是状态变了 → update
45
45
 
46
46
  ```bash
47
- ec memory update consolidated/20261002-090000-refactor-plan.md --tags "rule,done"
47
+ ec memory update consolidated/20261002-090000-refactor-plan.md --tags 'rule,done'
48
48
  ```
49
49
 
50
50
  ### ④ 不值得长期保留 → 丢弃
@@ -70,9 +70,9 @@ ec memory update consolidated/20261002-090000-refactor-plan.md --tags "rule,done
70
70
 
71
71
  ```bash
72
72
  ec memory create --scope relation --slug commit-convention \
73
- --desc "对端要求 commit 用中文、按功能列出改动" \
74
- --tags "rule,long-term,verified" --keywords "commit,git" \
75
- --replaces "consolidated/a.md,consolidated/b.md" \
73
+ --desc '对端要求 commit 用中文、按功能列出改动' \
74
+ --tags 'rule,long-term,verified' --keywords 'commit,git' \
75
+ --replaces 'consolidated/a.md,consolidated/b.md' \
76
76
  "(合并后的完整内容)"
77
77
  ```
78
78
 
@@ -92,13 +92,13 @@ note 的 frontmatter 里如果有 `chatType: group`,整理时需要按信息
92
92
  ```bash
93
93
  # 关于群本身的记忆(默认路由,不加 --peer)
94
94
  ec memory create --scope relation --slug group-meeting-schedule \
95
- --desc "群每周一开技术会" --tags "rule,verified" \
95
+ --desc '群每周一开技术会' --tags 'rule,verified' \
96
96
  "该群约定每周一 10:00 召开技术同步会。"
97
97
 
98
98
  # 关于发言人自身的记忆(指定发言人的 relation)
99
99
  ec memory create --scope relation --peer aun#alice.agentid.pub \
100
100
  --slug alice-ts-preference \
101
- --desc "Alice 偏好 TypeScript,不喜欢注释" --tags "preference,verified" \
101
+ --desc 'Alice 偏好 TypeScript,不喜欢注释' --tags 'preference,verified' \
102
102
  "Alice 明确表示偏好 TypeScript,认为代码自解释,不需要额外注释。"
103
103
  ```
104
104