codebee 0.1.95 → 0.1.97

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 (144) hide show
  1. package/CHANGELOG.md +733 -720
  2. package/LICENSE +21 -21
  3. package/README.md +516 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +118 -0
  6. package/app/core/aiflavor.py +197 -197
  7. package/app/core/analytics.py +38 -38
  8. package/app/core/artifacts.py +148 -148
  9. package/app/core/assets.py +110 -110
  10. package/app/core/attachments.py +623 -623
  11. package/app/core/audit.py +30 -30
  12. package/app/core/backup.py +470 -470
  13. package/app/core/beekeeper.py +183 -183
  14. package/app/core/benchstore.py +102 -95
  15. package/app/core/board.py +246 -209
  16. package/app/core/bookmeta.py +1036 -1036
  17. package/app/core/bookmeta_catalog.py +180 -180
  18. package/app/core/branching.py +137 -137
  19. package/app/core/builtin_agent.py +2055 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +648 -0
  23. package/app/core/cleanup.py +303 -303
  24. package/app/core/compaction.py +201 -201
  25. package/app/core/contracts.py +255 -255
  26. package/app/core/covergen.py +356 -356
  27. package/app/core/defectretro.py +43 -43
  28. package/app/core/diagnostics.py +114 -114
  29. package/app/core/dispatch.py +198 -191
  30. package/app/core/dispatch_log.py +128 -128
  31. package/app/core/env_scrub.py +85 -85
  32. package/app/core/errorlog.py +137 -137
  33. package/app/core/eval_matrix.py +82 -0
  34. package/app/core/evalbench.py +184 -91
  35. package/app/core/evaluation.py +174 -174
  36. package/app/core/failure_notify.py +290 -290
  37. package/app/core/flow_graph.py +33 -0
  38. package/app/core/goal_service.py +167 -167
  39. package/app/core/health.py +389 -389
  40. package/app/core/history.py +38 -38
  41. package/app/core/hooks.py +162 -162
  42. package/app/core/jobs.py +25 -4
  43. package/app/core/knowledge_pipeline.py +86 -0
  44. package/app/core/manager.py +2232 -2232
  45. package/app/core/mcp_client.py +200 -29
  46. package/app/core/mocks.py +64 -64
  47. package/app/core/notify.py +201 -201
  48. package/app/core/novel_quality.py +181 -189
  49. package/app/core/openapi.py +177 -154
  50. package/app/core/operations.py +228 -228
  51. package/app/core/paihang.py +145 -145
  52. package/app/core/pipeline.py +319 -59
  53. package/app/core/planner.py +17 -11
  54. package/app/core/policy.py +84 -0
  55. package/app/core/portguard.py +111 -111
  56. package/app/core/presence.py +77 -77
  57. package/app/core/preview.py +293 -293
  58. package/app/core/project_memory.py +139 -0
  59. package/app/core/prompt_guard.py +78 -78
  60. package/app/core/publish/__init__.py +7 -7
  61. package/app/core/publish/auto.py +106 -22
  62. package/app/core/publish/browser.py +694 -655
  63. package/app/core/publish/chaprows.py +138 -0
  64. package/app/core/publish/fanqie.py +212 -165
  65. package/app/core/publish/flow.py +832 -640
  66. package/app/core/publish/flows-fanqie-calibrated.json +361 -336
  67. package/app/core/publish/ledger.py +51 -2
  68. package/app/core/publish/manager.py +1150 -1118
  69. package/app/core/publish/qimao.py +176 -176
  70. package/app/core/publish/volumes.py +108 -0
  71. package/app/core/publish/ws.py +139 -139
  72. package/app/core/quality_gate.py +194 -194
  73. package/app/core/redact.py +38 -38
  74. package/app/core/repeat_guard.py +129 -129
  75. package/app/core/retrieval.py +137 -137
  76. package/app/core/router.py +398 -398
  77. package/app/core/runner.py +2239 -2238
  78. package/app/core/selfupdate.py +384 -384
  79. package/app/core/session_log.py +161 -161
  80. package/app/core/sessions.py +312 -312
  81. package/app/core/settings.py +33 -5
  82. package/app/core/settings_schema.py +282 -271
  83. package/app/core/share_page.py +114 -114
  84. package/app/core/skill_scan.py +139 -139
  85. package/app/core/skillpacks/fanqie-novel.md +8 -6
  86. package/app/core/skillpacks/market/character-bible.md +66 -66
  87. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  88. package/app/core/skillpacks/market/git-workflow.md +57 -57
  89. package/app/core/skillpacks/market/release-notes.md +72 -72
  90. package/app/core/skillpacks/market/weekly-report.md +71 -71
  91. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  92. package/app/core/skillpacks/qimao-signing.md +14 -11
  93. package/app/core/skills.py +32 -21
  94. package/app/core/step_runner.py +132 -132
  95. package/app/core/store.py +24 -0
  96. package/app/core/story_tracking.py +275 -275
  97. package/app/core/task_compile.py +199 -199
  98. package/app/core/telemetry.py +292 -292
  99. package/app/core/tlsctx.py +64 -64
  100. package/app/core/token_meter.py +147 -147
  101. package/app/core/tool_outputs.py +107 -0
  102. package/app/core/tracing.py +207 -144
  103. package/app/core/upstream.py +41 -41
  104. package/app/core/usage.py +1038 -1038
  105. package/app/core/volumes.py +389 -389
  106. package/app/core/webhooks.py +89 -0
  107. package/app/core/wxdigest.py +865 -858
  108. package/app/core/zentao.py +2692 -2692
  109. package/app/main.py +179 -10
  110. package/app/mcp_server.py +244 -244
  111. package/app/pick_dialog.py +110 -110
  112. package/app/ui/app.js +563 -21
  113. package/app/ui/artdiff.js +74 -74
  114. package/app/ui/assets/community-qr.png +0 -0
  115. package/app/ui/assets/donate-alipay.jpg +0 -0
  116. package/app/ui/assets/donate-qr.jpg +0 -0
  117. package/app/ui/assets/workbench-reference.png +0 -0
  118. package/app/ui/board.css +451 -666
  119. package/app/ui/board.html +165 -162
  120. package/app/ui/board.js +509 -305
  121. package/app/ui/hive3d.js +242 -0
  122. package/app/ui/i18n.js +125 -9
  123. package/app/ui/icons/bee.svg +79 -79
  124. package/app/ui/index.html +60 -3
  125. package/app/ui/manifest.json +16 -16
  126. package/app/ui/qrcode.js +2297 -2297
  127. package/app/ui/style.css +189 -9
  128. package/bin/tutti.js +147 -147
  129. package/package.json +49 -48
  130. package/tools/wx_reader.py +261 -261
  131. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  132. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  133. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  134. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  135. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  136. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  137. package/app/.playwright-cli/run-detail-after.png +0 -0
  138. package/app/output/_createbook_body.json +0 -1
  139. package/app/output/_probe_fq_click.py +0 -50
  140. package/app/output/_probe_fq_form.py +0 -42
  141. package/app/output/_probe_fq_modal.py +0 -55
  142. package/app/output/_probe_fq_points.py +0 -71
  143. package/app/output/_probe_fq_submit.py +0 -34
  144. package/app/output/_probe_fq_watch.py +0 -62
@@ -1,261 +1,261 @@
1
- # -*- coding: utf-8 -*-
2
- """微信读取 sidecar:由 64 位 Python ≥3.9(配 wechatauto-replica)运行,
3
- 把本机微信 4.x 群消息增量拉成本仓库已有的「导出 txt」格式。
4
-
5
- 为什么是独立进程:wechatauto 依赖链(winsdk 等)需要 64 位 Python ≥3.9,
6
- 而 CodeBee 服务钉在 3.8-32。sidecar 通过文件与主程序解耦——拉到的新消息
7
- 按「2024-01-01 12:34:56 昵称」行格式追加进监控文件夹,wxdigest 现有的
8
- 解析/增量游标/摘要/桌宠提醒整条链路零改动复用。
9
-
10
- 用法(两个模式,互斥):
11
- 列群(供前端选择):
12
- python wx_reader.py --list-groups --result OUT.json
13
- 拉增量(每次扫描前调用):
14
- python wx_reader.py --pull --out DIR --state STATE.json \
15
- --groups-file GROUPS.json --result OUT.json
16
- GROUPS.json = [{"username": "...@chatroom", "name": "群名"}, ...]
17
-
18
- 只读保证:只调用 get_groups/get_new_messages/get_group_members/get_self_info,
19
- 不调用任何发送/删除接口。
20
- 结果 JSON 写 --result 文件(stdout 被库日志污染,不作为契约)。
21
- 语法保持 3.8 兼容(测试解释器要 import 本文件的纯函数)。
22
- """
23
- from __future__ import annotations
24
-
25
- import argparse
26
- import json
27
- import os
28
- import re
29
- import sys
30
- import time
31
-
32
- # 群消息 content 自带「<wxid>: 前缀」,剥掉后用成员表翻译成昵称
33
- _PREFIX_RE = re.compile(r"^([A-Za-z0-9_+@\-.]{3,}):\s*\n?")
34
- _ILLEGAL_FN = re.compile(r'[\\/:*?"<>|]')
35
-
36
- SELF_NAME = "我"
37
-
38
-
39
- def sanitize_filename(name):
40
- """群名 → 合法文件名(Windows 非法字符换下划线;空名回落 username)。"""
41
- out = _ILLEGAL_FN.sub("_", str(name or "").strip())
42
- return out[:80] or "unnamed"
43
-
44
-
45
- def strip_sender_prefix(content, sender_username):
46
- """群消息正文剥掉自带的「wxid: 」前缀(前缀与发送者一致才可信地剥)。"""
47
- text = str(content or "")
48
- m = _PREFIX_RE.match(text)
49
- if m and (not sender_username or m.group(1) == sender_username):
50
- return text[m.end():].strip()
51
- return text.strip()
52
-
53
-
54
- def member_map(members):
55
- """get_group_members 结果 → {username: 显示名}。备注优先(微信 UI 同款)。"""
56
- out = {}
57
- for m in members or []:
58
- if not isinstance(m, dict):
59
- continue
60
- uid = m.get("username") or ""
61
- if not uid:
62
- continue
63
- out[uid] = m.get("remark") or m.get("nick_name") or uid
64
- return out
65
-
66
-
67
- def format_line(ts_epoch, sender_name, text):
68
- """一条消息 → 导出格式行(时间戳行与正文之间不留空行,与现有解析器对齐)。"""
69
- stamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts_epoch or 0))
70
- return "%s %s\n%s\n" % (stamp, sender_name or "?", str(text or "").strip())
71
-
72
-
73
- def type_placeholder(mtype):
74
- """非文本消息 → 占位符(摘要时让模型知道有图/语音存在)。None=跳过不入正文。"""
75
- t = str(mtype or "")
76
- if "文本" in t:
77
- return None # 文本走正文,不是占位符
78
- for key, ph in (("图片", "[图片]"), ("语音", "[语音]"), ("视频", "[视频]"),
79
- ("文件", "[文件]"), ("链接", "[链接]"), ("名片", "[名片]"),
80
- ("位置", "[位置]"), ("动画表情", "[表情]"), ("系统", None)):
81
- if key in t:
82
- return ph
83
- return "[%s]" % t if t and len(t) <= 8 else None
84
-
85
-
86
- def _quiet_lib_logs():
87
- try:
88
- import logging
89
- logging.getLogger("wechatauto").setLevel(logging.WARNING)
90
- except Exception:
91
- pass
92
-
93
-
94
- def _write_result(path, obj):
95
- tmp = str(path) + ".tmp"
96
- with open(tmp, "w", encoding="utf-8") as f:
97
- json.dump(obj, f, ensure_ascii=False)
98
- os.replace(tmp, path)
99
-
100
-
101
- def _load_json(path, default):
102
- try:
103
- with open(path, "r", encoding="utf-8") as f:
104
- return json.load(f)
105
- except Exception:
106
- return default
107
-
108
-
109
- def load_state(path):
110
- return _load_json(path, {}) or {}
111
-
112
-
113
- def save_state(path, state):
114
- tmp = str(path) + ".tmp"
115
- with open(tmp, "w", encoding="utf-8") as f:
116
- json.dump(state, f, ensure_ascii=False)
117
- os.replace(tmp, path)
118
-
119
-
120
- def cmd_list_groups(result_path):
121
- _quiet_lib_logs()
122
- import wechatauto
123
- db = wechatauto.WeChatDB()
124
- groups, seen = [], set()
125
- for g in db.get_groups():
126
- if isinstance(g, dict) and g.get("username"):
127
- groups.append({"username": g["username"],
128
- "name": g.get("name") or g["username"],
129
- "member_count": str(g.get("member_count") or "")})
130
- seen.add(g["username"])
131
- # 新群兜底(2026-09-21 用户实测「缺一个新建的群」):刚建的群可能还没
132
- # 落进 chat_room/contact,但只要有过消息就在会话列表里——合并补进,
133
- # 昵称未同步时先显示 wxid,微信同步后「刷新群列表」即见真名。
134
- try:
135
- for s in db.get_sessions(200):
136
- u = str((s or {}).get("username") or "")
137
- if u.endswith("@chatroom") and u not in seen:
138
- groups.append({"username": u,
139
- "name": db.get_nickname(u) or u,
140
- "member_count": ""})
141
- seen.add(u)
142
- except Exception:
143
- pass # 会话库不可用不影响主清单
144
- _write_result(result_path, {"ok": True, "groups": groups, "count": len(groups)})
145
-
146
-
147
- def cmd_pull(out_dir, state_path, groups_file, result_path):
148
- _quiet_lib_logs()
149
- import wechatauto
150
- sel = _load_json(groups_file, [])
151
- sel = [g for g in (sel or []) if isinstance(g, dict) and g.get("username")]
152
- state = load_state(state_path)
153
- os.makedirs(out_dir, exist_ok=True)
154
- db = wechatauto.WeChatDB()
155
- try:
156
- self_wxid = db.wxid
157
- except Exception:
158
- self_wxid = ""
159
- pulled, errors = {}, []
160
- for g in sel:
161
- username, name = g["username"], g.get("name") or g["username"]
162
- since = 0
163
- try:
164
- since = int((state.get(username) or {}).get("since_seq") or 0)
165
- except (TypeError, ValueError):
166
- since = 0
167
- try:
168
- msgs = db.get_new_messages(username, since_seq=since, limit=500)
169
- except Exception as e:
170
- errors.append("%s: %s" % (name, e))
171
- continue
172
- if not msgs:
173
- pulled[username] = 0
174
- continue
175
- members = {}
176
- try:
177
- members = member_map(db.get_group_members(username))
178
- except Exception:
179
- members = {}
180
- # 昵称缓存:退群成员不在成员表里,靠 get_nickname 兜底一次后永久缓存
181
- gstate = state.get(username) or {}
182
- names = dict(gstate.get("names") or {})
183
-
184
- def resolve(uid):
185
- if not uid:
186
- return "?"
187
- if self_wxid and uid == self_wxid:
188
- return SELF_NAME
189
- if uid in members:
190
- return members[uid]
191
- if uid in names:
192
- return names[uid]
193
- try:
194
- nick = str(db.get_nickname(uid) or "").strip()
195
- except Exception:
196
- nick = ""
197
- name2 = nick or uid
198
- if len(names) < 5000: # 缓存上限防无限膨胀
199
- names[uid] = name2
200
- return name2
201
-
202
- fname = os.path.join(out_dir, sanitize_filename(name) + ".txt")
203
- header = "【%s】\n" % name
204
- need_header = not os.path.exists(fname)
205
- lines = []
206
- max_seq = since
207
- for m in msgs:
208
- seq = m.get("sort_seq") or 0
209
- if seq <= since:
210
- continue
211
- max_seq = max(max_seq, seq)
212
- uid = m.get("sender_username") or ""
213
- who = resolve(uid)
214
- mtype = m.get("type")
215
- text = None
216
- if "文本" in str(mtype or ""):
217
- text = strip_sender_prefix(m.get("content"), uid)
218
- else:
219
- text = type_placeholder(mtype)
220
- if not text:
221
- continue
222
- lines.append(format_line(m.get("create_time"), who, text))
223
- if lines:
224
- with open(fname, "a", encoding="utf-8") as f:
225
- if need_header:
226
- f.write(header)
227
- f.write("\n".join(lines) + "\n")
228
- state[username] = {"name": name, "since_seq": max_seq,
229
- "names": names, "t": time.time()}
230
- pulled[username] = len(lines)
231
- save_state(state_path, state)
232
- _write_result(result_path, {"ok": not errors, "pulled": pulled,
233
- "errors": errors, "count": sum(pulled.values())})
234
-
235
-
236
- def main():
237
- ap = argparse.ArgumentParser(description="CodeBee 微信群消息只读 sidecar")
238
- ap.add_argument("--list-groups", action="store_true")
239
- ap.add_argument("--pull", action="store_true")
240
- ap.add_argument("--out", default="")
241
- ap.add_argument("--state", default="")
242
- ap.add_argument("--groups-file", default="")
243
- ap.add_argument("--result", required=True, help="结果 JSON 落点(stdout 不可靠)")
244
- args = ap.parse_args()
245
- try:
246
- if args.list_groups:
247
- cmd_list_groups(args.result)
248
- elif args.pull:
249
- cmd_pull(args.out, args.state, args.groups_file, args.result)
250
- else:
251
- ap.error("需要 --list-groups 或 --pull")
252
- except Exception as e:
253
- # 顶层异常也落 result:微信未登录/DB 锁住等真实原因必须透传给主程序
254
- _write_result(args.result, {"ok": False, "errors": [str(e)],
255
- "groups": [], "pulled": {}, "count": 0})
256
- return 1
257
- return 0
258
-
259
-
260
- if __name__ == "__main__":
261
- sys.exit(main())
1
+ # -*- coding: utf-8 -*-
2
+ """微信读取 sidecar:由 64 位 Python ≥3.9(配 wechatauto-replica)运行,
3
+ 把本机微信 4.x 群消息增量拉成本仓库已有的「导出 txt」格式。
4
+
5
+ 为什么是独立进程:wechatauto 依赖链(winsdk 等)需要 64 位 Python ≥3.9,
6
+ 而 CodeBee 服务钉在 3.8-32。sidecar 通过文件与主程序解耦——拉到的新消息
7
+ 按「2024-01-01 12:34:56 昵称」行格式追加进监控文件夹,wxdigest 现有的
8
+ 解析/增量游标/摘要/桌宠提醒整条链路零改动复用。
9
+
10
+ 用法(两个模式,互斥):
11
+ 列群(供前端选择):
12
+ python wx_reader.py --list-groups --result OUT.json
13
+ 拉增量(每次扫描前调用):
14
+ python wx_reader.py --pull --out DIR --state STATE.json \
15
+ --groups-file GROUPS.json --result OUT.json
16
+ GROUPS.json = [{"username": "...@chatroom", "name": "群名"}, ...]
17
+
18
+ 只读保证:只调用 get_groups/get_new_messages/get_group_members/get_self_info,
19
+ 不调用任何发送/删除接口。
20
+ 结果 JSON 写 --result 文件(stdout 被库日志污染,不作为契约)。
21
+ 语法保持 3.8 兼容(测试解释器要 import 本文件的纯函数)。
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import json
27
+ import os
28
+ import re
29
+ import sys
30
+ import time
31
+
32
+ # 群消息 content 自带「<wxid>: 前缀」,剥掉后用成员表翻译成昵称
33
+ _PREFIX_RE = re.compile(r"^([A-Za-z0-9_+@\-.]{3,}):\s*\n?")
34
+ _ILLEGAL_FN = re.compile(r'[\\/:*?"<>|]')
35
+
36
+ SELF_NAME = "我"
37
+
38
+
39
+ def sanitize_filename(name):
40
+ """群名 → 合法文件名(Windows 非法字符换下划线;空名回落 username)。"""
41
+ out = _ILLEGAL_FN.sub("_", str(name or "").strip())
42
+ return out[:80] or "unnamed"
43
+
44
+
45
+ def strip_sender_prefix(content, sender_username):
46
+ """群消息正文剥掉自带的「wxid: 」前缀(前缀与发送者一致才可信地剥)。"""
47
+ text = str(content or "")
48
+ m = _PREFIX_RE.match(text)
49
+ if m and (not sender_username or m.group(1) == sender_username):
50
+ return text[m.end():].strip()
51
+ return text.strip()
52
+
53
+
54
+ def member_map(members):
55
+ """get_group_members 结果 → {username: 显示名}。备注优先(微信 UI 同款)。"""
56
+ out = {}
57
+ for m in members or []:
58
+ if not isinstance(m, dict):
59
+ continue
60
+ uid = m.get("username") or ""
61
+ if not uid:
62
+ continue
63
+ out[uid] = m.get("remark") or m.get("nick_name") or uid
64
+ return out
65
+
66
+
67
+ def format_line(ts_epoch, sender_name, text):
68
+ """一条消息 → 导出格式行(时间戳行与正文之间不留空行,与现有解析器对齐)。"""
69
+ stamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts_epoch or 0))
70
+ return "%s %s\n%s\n" % (stamp, sender_name or "?", str(text or "").strip())
71
+
72
+
73
+ def type_placeholder(mtype):
74
+ """非文本消息 → 占位符(摘要时让模型知道有图/语音存在)。None=跳过不入正文。"""
75
+ t = str(mtype or "")
76
+ if "文本" in t:
77
+ return None # 文本走正文,不是占位符
78
+ for key, ph in (("图片", "[图片]"), ("语音", "[语音]"), ("视频", "[视频]"),
79
+ ("文件", "[文件]"), ("链接", "[链接]"), ("名片", "[名片]"),
80
+ ("位置", "[位置]"), ("动画表情", "[表情]"), ("系统", None)):
81
+ if key in t:
82
+ return ph
83
+ return "[%s]" % t if t and len(t) <= 8 else None
84
+
85
+
86
+ def _quiet_lib_logs():
87
+ try:
88
+ import logging
89
+ logging.getLogger("wechatauto").setLevel(logging.WARNING)
90
+ except Exception:
91
+ pass
92
+
93
+
94
+ def _write_result(path, obj):
95
+ tmp = str(path) + ".tmp"
96
+ with open(tmp, "w", encoding="utf-8") as f:
97
+ json.dump(obj, f, ensure_ascii=False)
98
+ os.replace(tmp, path)
99
+
100
+
101
+ def _load_json(path, default):
102
+ try:
103
+ with open(path, "r", encoding="utf-8") as f:
104
+ return json.load(f)
105
+ except Exception:
106
+ return default
107
+
108
+
109
+ def load_state(path):
110
+ return _load_json(path, {}) or {}
111
+
112
+
113
+ def save_state(path, state):
114
+ tmp = str(path) + ".tmp"
115
+ with open(tmp, "w", encoding="utf-8") as f:
116
+ json.dump(state, f, ensure_ascii=False)
117
+ os.replace(tmp, path)
118
+
119
+
120
+ def cmd_list_groups(result_path):
121
+ _quiet_lib_logs()
122
+ import wechatauto
123
+ db = wechatauto.WeChatDB()
124
+ groups, seen = [], set()
125
+ for g in db.get_groups():
126
+ if isinstance(g, dict) and g.get("username"):
127
+ groups.append({"username": g["username"],
128
+ "name": g.get("name") or g["username"],
129
+ "member_count": str(g.get("member_count") or "")})
130
+ seen.add(g["username"])
131
+ # 新群兜底(2026-09-21 用户实测「缺一个新建的群」):刚建的群可能还没
132
+ # 落进 chat_room/contact,但只要有过消息就在会话列表里——合并补进,
133
+ # 昵称未同步时先显示 wxid,微信同步后「刷新群列表」即见真名。
134
+ try:
135
+ for s in db.get_sessions(200):
136
+ u = str((s or {}).get("username") or "")
137
+ if u.endswith("@chatroom") and u not in seen:
138
+ groups.append({"username": u,
139
+ "name": db.get_nickname(u) or u,
140
+ "member_count": ""})
141
+ seen.add(u)
142
+ except Exception:
143
+ pass # 会话库不可用不影响主清单
144
+ _write_result(result_path, {"ok": True, "groups": groups, "count": len(groups)})
145
+
146
+
147
+ def cmd_pull(out_dir, state_path, groups_file, result_path):
148
+ _quiet_lib_logs()
149
+ import wechatauto
150
+ sel = _load_json(groups_file, [])
151
+ sel = [g for g in (sel or []) if isinstance(g, dict) and g.get("username")]
152
+ state = load_state(state_path)
153
+ os.makedirs(out_dir, exist_ok=True)
154
+ db = wechatauto.WeChatDB()
155
+ try:
156
+ self_wxid = db.wxid
157
+ except Exception:
158
+ self_wxid = ""
159
+ pulled, errors = {}, []
160
+ for g in sel:
161
+ username, name = g["username"], g.get("name") or g["username"]
162
+ since = 0
163
+ try:
164
+ since = int((state.get(username) or {}).get("since_seq") or 0)
165
+ except (TypeError, ValueError):
166
+ since = 0
167
+ try:
168
+ msgs = db.get_new_messages(username, since_seq=since, limit=500)
169
+ except Exception as e:
170
+ errors.append("%s: %s" % (name, e))
171
+ continue
172
+ if not msgs:
173
+ pulled[username] = 0
174
+ continue
175
+ members = {}
176
+ try:
177
+ members = member_map(db.get_group_members(username))
178
+ except Exception:
179
+ members = {}
180
+ # 昵称缓存:退群成员不在成员表里,靠 get_nickname 兜底一次后永久缓存
181
+ gstate = state.get(username) or {}
182
+ names = dict(gstate.get("names") or {})
183
+
184
+ def resolve(uid):
185
+ if not uid:
186
+ return "?"
187
+ if self_wxid and uid == self_wxid:
188
+ return SELF_NAME
189
+ if uid in members:
190
+ return members[uid]
191
+ if uid in names:
192
+ return names[uid]
193
+ try:
194
+ nick = str(db.get_nickname(uid) or "").strip()
195
+ except Exception:
196
+ nick = ""
197
+ name2 = nick or uid
198
+ if len(names) < 5000: # 缓存上限防无限膨胀
199
+ names[uid] = name2
200
+ return name2
201
+
202
+ fname = os.path.join(out_dir, sanitize_filename(name) + ".txt")
203
+ header = "【%s】\n" % name
204
+ need_header = not os.path.exists(fname)
205
+ lines = []
206
+ max_seq = since
207
+ for m in msgs:
208
+ seq = m.get("sort_seq") or 0
209
+ if seq <= since:
210
+ continue
211
+ max_seq = max(max_seq, seq)
212
+ uid = m.get("sender_username") or ""
213
+ who = resolve(uid)
214
+ mtype = m.get("type")
215
+ text = None
216
+ if "文本" in str(mtype or ""):
217
+ text = strip_sender_prefix(m.get("content"), uid)
218
+ else:
219
+ text = type_placeholder(mtype)
220
+ if not text:
221
+ continue
222
+ lines.append(format_line(m.get("create_time"), who, text))
223
+ if lines:
224
+ with open(fname, "a", encoding="utf-8") as f:
225
+ if need_header:
226
+ f.write(header)
227
+ f.write("\n".join(lines) + "\n")
228
+ state[username] = {"name": name, "since_seq": max_seq,
229
+ "names": names, "t": time.time()}
230
+ pulled[username] = len(lines)
231
+ save_state(state_path, state)
232
+ _write_result(result_path, {"ok": not errors, "pulled": pulled,
233
+ "errors": errors, "count": sum(pulled.values())})
234
+
235
+
236
+ def main():
237
+ ap = argparse.ArgumentParser(description="CodeBee 微信群消息只读 sidecar")
238
+ ap.add_argument("--list-groups", action="store_true")
239
+ ap.add_argument("--pull", action="store_true")
240
+ ap.add_argument("--out", default="")
241
+ ap.add_argument("--state", default="")
242
+ ap.add_argument("--groups-file", default="")
243
+ ap.add_argument("--result", required=True, help="结果 JSON 落点(stdout 不可靠)")
244
+ args = ap.parse_args()
245
+ try:
246
+ if args.list_groups:
247
+ cmd_list_groups(args.result)
248
+ elif args.pull:
249
+ cmd_pull(args.out, args.state, args.groups_file, args.result)
250
+ else:
251
+ ap.error("需要 --list-groups 或 --pull")
252
+ except Exception as e:
253
+ # 顶层异常也落 result:微信未登录/DB 锁住等真实原因必须透传给主程序
254
+ _write_result(args.result, {"ok": False, "errors": [str(e)],
255
+ "groups": [], "pulled": {}, "count": 0})
256
+ return 1
257
+ return 0
258
+
259
+
260
+ if __name__ == "__main__":
261
+ sys.exit(main())
@@ -1,88 +0,0 @@
1
- [ 154ms] [VERBOSE] [DOM] Password field is not contained in a form: (More info: https://goo.gl/9p2vKq) %o @ http://127.0.0.1:8766/board.html:0
2
- [ 4086ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
3
- [ 8084ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
4
- [ 12084ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
5
- [ 16071ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
6
- [ 20083ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
7
- [ 24080ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
8
- [ 28074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
9
- [ 32078ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
10
- [ 36084ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
11
- [ 40087ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
12
- [ 44075ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
13
- [ 48074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
14
- [ 52070ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
15
- [ 56083ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
16
- [ 60070ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
17
- [ 64074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
18
- [ 68084ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
19
- [ 72076ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
20
- [ 76083ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
21
- [ 80075ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
22
- [ 84072ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
23
- [ 88077ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
24
- [ 92073ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
25
- [ 96084ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
26
- [ 100086ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
27
- [ 104085ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
28
- [ 108083ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
29
- [ 112083ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
30
- [ 116076ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
31
- [ 120074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
32
- [ 124084ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
33
- [ 128080ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
34
- [ 132074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
35
- [ 136074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
36
- [ 140070ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
37
- [ 144080ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
38
- [ 148075ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
39
- [ 152077ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
40
- [ 156075ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
41
- [ 160072ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
42
- [ 164074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
43
- [ 168083ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
44
- [ 172076ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
45
- [ 176073ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
46
- [ 180085ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
47
- [ 184071ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
48
- [ 188079ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
49
- [ 192075ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
50
- [ 196085ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
51
- [ 200073ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
52
- [ 204080ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
53
- [ 208077ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
54
- [ 212074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
55
- [ 216084ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
56
- [ 220077ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
57
- [ 224077ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
58
- [ 228079ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
59
- [ 232072ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
60
- [ 236083ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
61
- [ 240069ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
62
- [ 244077ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
63
- [ 248085ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
64
- [ 252072ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
65
- [ 256073ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
66
- [ 260078ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
67
- [ 264072ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
68
- [ 268074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
69
- [ 272082ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
70
- [ 276076ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
71
- [ 280075ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
72
- [ 284079ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
73
- [ 288083ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
74
- [ 292069ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
75
- [ 296083ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
76
- [ 300075ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
77
- [ 304077ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
78
- [ 308074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
79
- [ 312079ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
80
- [ 316074ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
81
- [ 320076ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
82
- [ 324084ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
83
- [ 328076ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
84
- [ 332080ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
85
- [ 336077ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
86
- [ 340085ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8766/api/board:0
87
- [ 438049ms] [VERBOSE] [DOM] Password field is not contained in a form: (More info: https://goo.gl/9p2vKq) %o @ http://127.0.0.1:8766/board.html:0
88
- [ 578323ms] [VERBOSE] [DOM] Password field is not contained in a form: (More info: https://goo.gl/9p2vKq) %o @ http://127.0.0.1:8766/board.html:0