weflow-cli 1.17.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +3 -0
  3. package/bin/weflow-cli.ts +167 -0
  4. package/dist/bin/weflow-cli.js +199 -0
  5. package/dist/bin/weflow-cli.js.map +1 -1
  6. package/dist/src/services/diagnosticReport.d.ts +6 -0
  7. package/dist/src/services/diagnosticReport.d.ts.map +1 -1
  8. package/dist/src/services/diagnosticReport.js +16 -0
  9. package/dist/src/services/diagnosticReport.js.map +1 -1
  10. package/dist/src/services/wechatCompat.d.ts +29 -0
  11. package/dist/src/services/wechatCompat.d.ts.map +1 -0
  12. package/dist/src/services/wechatCompat.js +65 -0
  13. package/dist/src/services/wechatCompat.js.map +1 -0
  14. package/docs/DATA_CONTRACT.md +25 -25
  15. package/docs/DECISIONS.md +26 -0
  16. package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +365 -365
  17. package/docs/HEALTH-CHECK.md +104 -104
  18. package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -310
  19. package/docs/NEXT_ACTIONS.md +236 -236
  20. package/docs/NPM-PUBLISH.md +130 -130
  21. package/docs/NT_DATABASES.md +47 -0
  22. package/docs/POSITIONING.md +68 -68
  23. package/docs/PROJECT_STATE.md +2 -1
  24. package/docs/RELEASING.md +4 -0
  25. package/package.json +1 -1
  26. package/requirements-3x.txt +4 -4
  27. package/requirements-voice.txt +15 -15
  28. package/resources/js/graph3d/NOTICE.txt +35 -35
  29. package/resources/js/graph3d/d3-binarytree.min.js +2 -2
  30. package/resources/js/graph3d/d3-dispatch.min.js +2 -2
  31. package/resources/js/graph3d/d3-force-3d.min.js +2 -2
  32. package/resources/js/graph3d/d3-octree.min.js +2 -2
  33. package/resources/js/graph3d/d3-quadtree.min.js +2 -2
  34. package/resources/js/graph3d/d3-timer.min.js +2 -2
  35. package/resources/js/graph3d/three.min.js +6 -6
  36. package/resources/panel/quick-menu.cjs +197 -197
  37. package/scripts/_utils.py +1338 -1338
  38. package/scripts/annual_report.py +3 -1
  39. package/scripts/article_notes.py +423 -423
  40. package/scripts/contact_schema.py +339 -339
  41. package/scripts/create_reading_notes.py +11 -11
  42. package/scripts/daily_task.ps1 +213 -213
  43. package/scripts/extract_todos.py +3 -1
  44. package/scripts/fav_notes.py +321 -321
  45. package/scripts/health_check.py +473 -473
  46. package/scripts/imported_chats.py +68 -68
  47. package/scripts/install_tasks.ps1 +105 -105
  48. package/scripts/leads.py +179 -179
  49. package/scripts/mcp_bridge.py +3 -1
  50. package/scripts/media_index.py +150 -0
  51. package/scripts/network.py +206 -206
  52. package/scripts/nt_common.py +32 -0
  53. package/scripts/nt_decrypt.py +19 -15
  54. package/scripts/opinion_timeline.py +138 -138
  55. package/scripts/replay.py +803 -803
  56. package/scripts/run_hidden.vbs +24 -24
  57. package/scripts/semantic_search.py +5 -1
  58. package/scripts/user_notes.py +315 -315
  59. package/scripts/voice_transcribe.py +139 -139
  60. package/scripts/watch_issues.py +212 -212
  61. package/scripts/watch_mail.py +131 -131
  62. package/scripts/wechat_avatar.py +185 -0
  63. package/scripts/wechat_ledger.py +190 -0
  64. package/src/services/cursorProbe.ts +222 -222
  65. package/src/services/diagnosticReport.ts +22 -0
  66. package/src/services/qchatImport.ts +327 -327
  67. package/src/services/setupVenv.ts +238 -238
  68. package/src/services/syncService.ts +363 -363
  69. package/src/services/syncState.ts +275 -275
  70. package/src/services/wechatCompat.ts +67 -0
  71. package/src/services/wechatMedia.ts +166 -166
@@ -1,212 +1,212 @@
1
- #!/usr/bin/env python3
2
- """Watch a GitHub repo's issues and email new activity.
3
-
4
- Meant to run from Windows Task Scheduler rather than from an editor session, so
5
- notifications arrive whether or not anything else is running.
6
-
7
- Credentials are read from outside the repository:
8
-
9
- ~/.weflow-issue-watch.json
10
- {
11
- "smtp_user": "1473517806@qq.com",
12
- "smtp_auth": "<QQ 邮箱 SMTP 授权码,不是登录密码>",
13
- "mail_to": "1473517806@qq.com",
14
- "repo": "zhuobichen/weflow-cli"
15
- }
16
-
17
- Only `smtp_auth` is required; the rest default to the values above. The file is
18
- never written by this script and never belongs in the repo.
19
-
20
- First run seeds the state file without sending anything, so a fresh install does
21
- not mail the entire issue history.
22
- """
23
- import json
24
- import os
25
- import smtplib
26
- import subprocess
27
- import sys
28
- from email.header import Header
29
- from email.mime.text import MIMEText
30
- from email.utils import formataddr
31
-
32
- CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.weflow-issue-watch.json')
33
- STATE_PATH = os.path.join(os.path.expanduser('~'), '.weflow-issue-watch-state.json')
34
-
35
- DEFAULTS = {
36
- 'repo': 'zhuobichen/weflow-cli',
37
- 'smtp_host': 'smtp.qq.com',
38
- 'smtp_port': 465,
39
- 'smtp_user': '1473517806@qq.com',
40
- 'mail_to': '1473517806@qq.com',
41
- }
42
-
43
-
44
- def load_config():
45
- config = dict(DEFAULTS)
46
- try:
47
- with open(CONFIG_PATH, encoding='utf-8') as handle:
48
- config.update(json.load(handle))
49
- except (OSError, ValueError):
50
- pass
51
- if os.environ.get('WEFLOW_SMTP_AUTH'):
52
- config['smtp_auth'] = os.environ['WEFLOW_SMTP_AUTH']
53
- return config
54
-
55
-
56
- # Task Scheduler starts with a thinner PATH than an interactive shell, so `gh`
57
- # is not always resolvable by name there.
58
- GH_CANDIDATES = ['gh', r'C:\Program Files\GitHub CLI\gh.exe']
59
-
60
-
61
- def _runnable(candidate):
62
- if os.sep in candidate:
63
- return os.path.isfile(candidate)
64
- from shutil import which
65
- return which(candidate) is not None
66
-
67
-
68
- def gh(args):
69
- """Run `gh` and parse its JSON output, or None when it fails."""
70
- executable = next((c for c in GH_CANDIDATES if _runnable(c)), GH_CANDIDATES[0])
71
- try:
72
- result = subprocess.run(
73
- [executable, *args], capture_output=True, timeout=60, encoding='utf-8')
74
- except (OSError, subprocess.SubprocessError):
75
- return None
76
- if result.returncode != 0 or not result.stdout.strip():
77
- return None
78
- try:
79
- return json.loads(result.stdout)
80
- except ValueError:
81
- return None
82
-
83
-
84
- def fetch_activity(repo, since_issue=0):
85
- """(issues, comments) newer than what has already been reported.
86
-
87
- Comments are fetched per issue rather than from the repo-wide endpoint so an
88
- issue that is merely edited does not look like new activity.
89
- """
90
- issues = gh(['api', f'repos/{repo}/issues?state=all&sort=created&direction=desc&per_page=30'])
91
- if issues is None:
92
- return None, None
93
-
94
- new_issues = [i for i in issues if not i.get('pull_request') and i['number'] > since_issue]
95
- comments = []
96
- # Only the busiest recent issues can have new replies; 30 is plenty.
97
- for issue in issues:
98
- if issue.get('pull_request'):
99
- continue
100
- rows = gh(['api', f'repos/{repo}/issues/{issue["number"]}/comments?per_page=100'])
101
- if not rows:
102
- continue
103
- for row in rows:
104
- if row['user']['login'] != 'zhuobichen':
105
- comments.append({
106
- 'issue': issue['number'],
107
- 'title': issue['title'],
108
- 'id': row['id'],
109
- 'author': row['user']['login'],
110
- 'body': row['body'],
111
- })
112
- return new_issues, comments
113
-
114
-
115
- def load_state():
116
- try:
117
- with open(STATE_PATH, encoding='utf-8') as handle:
118
- return json.load(handle)
119
- except (OSError, ValueError):
120
- return None
121
-
122
-
123
- def save_state(state):
124
- try:
125
- with open(STATE_PATH, 'w', encoding='utf-8') as handle:
126
- json.dump(state, handle, ensure_ascii=False, indent=2)
127
- except OSError:
128
- pass
129
-
130
-
131
- def send_mail(config, subject, body):
132
- message = MIMEText(body, 'plain', 'utf-8')
133
- message['Subject'] = Header(subject, 'utf-8')
134
- message['From'] = formataddr(('WeFlow Issue Watch', config['smtp_user']))
135
- message['To'] = config['mail_to']
136
- with smtplib.SMTP_SSL(config['smtp_host'], config['smtp_port'], timeout=30) as server:
137
- server.login(config['smtp_user'], config['smtp_auth'])
138
- server.sendmail(config['smtp_user'], [config['mail_to']], message.as_string())
139
-
140
-
141
- def main():
142
- config = load_config()
143
- if not config.get('smtp_auth'):
144
- print(f'未配置邮件授权码,跳过。请创建 {CONFIG_PATH},填入 smtp_auth。')
145
- return 0
146
-
147
- repo = config['repo']
148
- state = load_state()
149
- issues = gh(['api', f'repos/{repo}/issues?state=all&sort=created&direction=desc&per_page=30'])
150
- if issues is None:
151
- print('无法访问 GitHub(gh 未登录或网络不通),本次跳过。')
152
- return 0
153
-
154
- # Seed on first run: remember what exists, send nothing.
155
- if state is None:
156
- highest = max([i['number'] for i in issues if not i.get('pull_request')] or [0])
157
- seen_comments = []
158
- for issue in issues:
159
- if issue.get('pull_request'):
160
- continue
161
- rows = gh(['api', f'repos/{repo}/issues/{issue["number"]}/comments?per_page=100']) or []
162
- seen_comments.extend(r['id'] for r in rows if r['user']['login'] != 'zhuobichen')
163
- save_state({'last_issue': highest, 'seen_comments': seen_comments})
164
- print(f'首次运行,已记录基线(最新 issue #{highest},{len(seen_comments)} 条历史回复),不发送邮件。')
165
- return 0
166
-
167
- new_issues, comments = fetch_activity(repo, state.get('last_issue', 0))
168
- if new_issues is None:
169
- return 0
170
-
171
- seen = set(state.get('seen_comments', []))
172
- fresh = [c for c in comments if c['id'] not in seen]
173
-
174
- if not new_issues and not fresh:
175
- print('无新动态。')
176
- return 0
177
-
178
- lines = []
179
- for issue in new_issues:
180
- lines.append(f'【新 issue】#{issue["number"]} {issue["title"]}')
181
- lines.append(f' 来自: {issue["user"]["login"]}')
182
- lines.append(f' {issue["html_url"]}')
183
- lines.append('')
184
- for comment in fresh:
185
- lines.append(f'【新回复】#{comment["issue"]} {comment["title"]}')
186
- lines.append(f' 来自: {comment["author"]}')
187
- lines.append(' ' + comment['body'][:600].replace('\n', '\n '))
188
- lines.append(f' https://github.com/{repo}/issues/{comment["issue"]}')
189
- lines.append('')
190
-
191
- body = '\n'.join(lines)
192
- summary = []
193
- if new_issues:
194
- summary.append(f'{len(new_issues)} 个新 issue')
195
- if fresh:
196
- summary.append(f'{len(fresh)} 条新回复')
197
- try:
198
- send_mail(config, f'[WeFlow] {" / ".join(summary)}', body)
199
- except Exception as error:
200
- # Leave the state untouched so the next run retries instead of
201
- # silently swallowing the notification.
202
- print(f'发送失败,本次不记录状态,下次重试: {type(error).__name__}: {error}')
203
- return 1
204
-
205
- highest = max([i['number'] for i in issues if not i.get('pull_request')] + [state.get('last_issue', 0)])
206
- save_state({'last_issue': highest, 'seen_comments': list(seen | {c['id'] for c in fresh})})
207
- print(f'已发送通知:{" / ".join(summary)}')
208
- return 0
209
-
210
-
211
- if __name__ == '__main__':
212
- sys.exit(main())
1
+ #!/usr/bin/env python3
2
+ """Watch a GitHub repo's issues and email new activity.
3
+
4
+ Meant to run from Windows Task Scheduler rather than from an editor session, so
5
+ notifications arrive whether or not anything else is running.
6
+
7
+ Credentials are read from outside the repository:
8
+
9
+ ~/.weflow-issue-watch.json
10
+ {
11
+ "smtp_user": "1473517806@qq.com",
12
+ "smtp_auth": "<QQ 邮箱 SMTP 授权码,不是登录密码>",
13
+ "mail_to": "1473517806@qq.com",
14
+ "repo": "zhuobichen/weflow-cli"
15
+ }
16
+
17
+ Only `smtp_auth` is required; the rest default to the values above. The file is
18
+ never written by this script and never belongs in the repo.
19
+
20
+ First run seeds the state file without sending anything, so a fresh install does
21
+ not mail the entire issue history.
22
+ """
23
+ import json
24
+ import os
25
+ import smtplib
26
+ import subprocess
27
+ import sys
28
+ from email.header import Header
29
+ from email.mime.text import MIMEText
30
+ from email.utils import formataddr
31
+
32
+ CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.weflow-issue-watch.json')
33
+ STATE_PATH = os.path.join(os.path.expanduser('~'), '.weflow-issue-watch-state.json')
34
+
35
+ DEFAULTS = {
36
+ 'repo': 'zhuobichen/weflow-cli',
37
+ 'smtp_host': 'smtp.qq.com',
38
+ 'smtp_port': 465,
39
+ 'smtp_user': '1473517806@qq.com',
40
+ 'mail_to': '1473517806@qq.com',
41
+ }
42
+
43
+
44
+ def load_config():
45
+ config = dict(DEFAULTS)
46
+ try:
47
+ with open(CONFIG_PATH, encoding='utf-8') as handle:
48
+ config.update(json.load(handle))
49
+ except (OSError, ValueError):
50
+ pass
51
+ if os.environ.get('WEFLOW_SMTP_AUTH'):
52
+ config['smtp_auth'] = os.environ['WEFLOW_SMTP_AUTH']
53
+ return config
54
+
55
+
56
+ # Task Scheduler starts with a thinner PATH than an interactive shell, so `gh`
57
+ # is not always resolvable by name there.
58
+ GH_CANDIDATES = ['gh', r'C:\Program Files\GitHub CLI\gh.exe']
59
+
60
+
61
+ def _runnable(candidate):
62
+ if os.sep in candidate:
63
+ return os.path.isfile(candidate)
64
+ from shutil import which
65
+ return which(candidate) is not None
66
+
67
+
68
+ def gh(args):
69
+ """Run `gh` and parse its JSON output, or None when it fails."""
70
+ executable = next((c for c in GH_CANDIDATES if _runnable(c)), GH_CANDIDATES[0])
71
+ try:
72
+ result = subprocess.run(
73
+ [executable, *args], capture_output=True, timeout=60, encoding='utf-8')
74
+ except (OSError, subprocess.SubprocessError):
75
+ return None
76
+ if result.returncode != 0 or not result.stdout.strip():
77
+ return None
78
+ try:
79
+ return json.loads(result.stdout)
80
+ except ValueError:
81
+ return None
82
+
83
+
84
+ def fetch_activity(repo, since_issue=0):
85
+ """(issues, comments) newer than what has already been reported.
86
+
87
+ Comments are fetched per issue rather than from the repo-wide endpoint so an
88
+ issue that is merely edited does not look like new activity.
89
+ """
90
+ issues = gh(['api', f'repos/{repo}/issues?state=all&sort=created&direction=desc&per_page=30'])
91
+ if issues is None:
92
+ return None, None
93
+
94
+ new_issues = [i for i in issues if not i.get('pull_request') and i['number'] > since_issue]
95
+ comments = []
96
+ # Only the busiest recent issues can have new replies; 30 is plenty.
97
+ for issue in issues:
98
+ if issue.get('pull_request'):
99
+ continue
100
+ rows = gh(['api', f'repos/{repo}/issues/{issue["number"]}/comments?per_page=100'])
101
+ if not rows:
102
+ continue
103
+ for row in rows:
104
+ if row['user']['login'] != 'zhuobichen':
105
+ comments.append({
106
+ 'issue': issue['number'],
107
+ 'title': issue['title'],
108
+ 'id': row['id'],
109
+ 'author': row['user']['login'],
110
+ 'body': row['body'],
111
+ })
112
+ return new_issues, comments
113
+
114
+
115
+ def load_state():
116
+ try:
117
+ with open(STATE_PATH, encoding='utf-8') as handle:
118
+ return json.load(handle)
119
+ except (OSError, ValueError):
120
+ return None
121
+
122
+
123
+ def save_state(state):
124
+ try:
125
+ with open(STATE_PATH, 'w', encoding='utf-8') as handle:
126
+ json.dump(state, handle, ensure_ascii=False, indent=2)
127
+ except OSError:
128
+ pass
129
+
130
+
131
+ def send_mail(config, subject, body):
132
+ message = MIMEText(body, 'plain', 'utf-8')
133
+ message['Subject'] = Header(subject, 'utf-8')
134
+ message['From'] = formataddr(('WeFlow Issue Watch', config['smtp_user']))
135
+ message['To'] = config['mail_to']
136
+ with smtplib.SMTP_SSL(config['smtp_host'], config['smtp_port'], timeout=30) as server:
137
+ server.login(config['smtp_user'], config['smtp_auth'])
138
+ server.sendmail(config['smtp_user'], [config['mail_to']], message.as_string())
139
+
140
+
141
+ def main():
142
+ config = load_config()
143
+ if not config.get('smtp_auth'):
144
+ print(f'未配置邮件授权码,跳过。请创建 {CONFIG_PATH},填入 smtp_auth。')
145
+ return 0
146
+
147
+ repo = config['repo']
148
+ state = load_state()
149
+ issues = gh(['api', f'repos/{repo}/issues?state=all&sort=created&direction=desc&per_page=30'])
150
+ if issues is None:
151
+ print('无法访问 GitHub(gh 未登录或网络不通),本次跳过。')
152
+ return 0
153
+
154
+ # Seed on first run: remember what exists, send nothing.
155
+ if state is None:
156
+ highest = max([i['number'] for i in issues if not i.get('pull_request')] or [0])
157
+ seen_comments = []
158
+ for issue in issues:
159
+ if issue.get('pull_request'):
160
+ continue
161
+ rows = gh(['api', f'repos/{repo}/issues/{issue["number"]}/comments?per_page=100']) or []
162
+ seen_comments.extend(r['id'] for r in rows if r['user']['login'] != 'zhuobichen')
163
+ save_state({'last_issue': highest, 'seen_comments': seen_comments})
164
+ print(f'首次运行,已记录基线(最新 issue #{highest},{len(seen_comments)} 条历史回复),不发送邮件。')
165
+ return 0
166
+
167
+ new_issues, comments = fetch_activity(repo, state.get('last_issue', 0))
168
+ if new_issues is None:
169
+ return 0
170
+
171
+ seen = set(state.get('seen_comments', []))
172
+ fresh = [c for c in comments if c['id'] not in seen]
173
+
174
+ if not new_issues and not fresh:
175
+ print('无新动态。')
176
+ return 0
177
+
178
+ lines = []
179
+ for issue in new_issues:
180
+ lines.append(f'【新 issue】#{issue["number"]} {issue["title"]}')
181
+ lines.append(f' 来自: {issue["user"]["login"]}')
182
+ lines.append(f' {issue["html_url"]}')
183
+ lines.append('')
184
+ for comment in fresh:
185
+ lines.append(f'【新回复】#{comment["issue"]} {comment["title"]}')
186
+ lines.append(f' 来自: {comment["author"]}')
187
+ lines.append(' ' + comment['body'][:600].replace('\n', '\n '))
188
+ lines.append(f' https://github.com/{repo}/issues/{comment["issue"]}')
189
+ lines.append('')
190
+
191
+ body = '\n'.join(lines)
192
+ summary = []
193
+ if new_issues:
194
+ summary.append(f'{len(new_issues)} 个新 issue')
195
+ if fresh:
196
+ summary.append(f'{len(fresh)} 条新回复')
197
+ try:
198
+ send_mail(config, f'[WeFlow] {" / ".join(summary)}', body)
199
+ except Exception as error:
200
+ # Leave the state untouched so the next run retries instead of
201
+ # silently swallowing the notification.
202
+ print(f'发送失败,本次不记录状态,下次重试: {type(error).__name__}: {error}')
203
+ return 1
204
+
205
+ highest = max([i['number'] for i in issues if not i.get('pull_request')] + [state.get('last_issue', 0)])
206
+ save_state({'last_issue': highest, 'seen_comments': list(seen | {c['id'] for c in fresh})})
207
+ print(f'已发送通知:{" / ".join(summary)}')
208
+ return 0
209
+
210
+
211
+ if __name__ == '__main__':
212
+ sys.exit(main())