weflow-cli 1.17.0 → 1.19.1
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 +46 -0
- package/README.md +3 -0
- package/bin/weflow-cli.ts +167 -0
- package/dist/bin/weflow-cli.js +199 -0
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/src/services/diagnosticReport.d.ts +6 -0
- package/dist/src/services/diagnosticReport.d.ts.map +1 -1
- package/dist/src/services/diagnosticReport.js +16 -0
- package/dist/src/services/diagnosticReport.js.map +1 -1
- package/dist/src/services/wechatCompat.d.ts +29 -0
- package/dist/src/services/wechatCompat.d.ts.map +1 -0
- package/dist/src/services/wechatCompat.js +65 -0
- package/dist/src/services/wechatCompat.js.map +1 -0
- package/docs/DATA_CONTRACT.md +25 -25
- package/docs/DECISIONS.md +26 -0
- package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +365 -365
- package/docs/HEALTH-CHECK.md +104 -104
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -310
- package/docs/NEXT_ACTIONS.md +236 -236
- package/docs/NPM-PUBLISH.md +130 -130
- package/docs/NT_DATABASES.md +47 -0
- package/docs/POSITIONING.md +68 -68
- package/docs/PROJECT_STATE.md +3 -2
- package/docs/RELEASING.md +4 -0
- package/package.json +1 -1
- package/requirements-3x.txt +4 -4
- package/requirements-voice.txt +15 -15
- package/resources/js/graph3d/NOTICE.txt +35 -35
- package/resources/js/graph3d/d3-binarytree.min.js +2 -2
- package/resources/js/graph3d/d3-dispatch.min.js +2 -2
- package/resources/js/graph3d/d3-force-3d.min.js +2 -2
- package/resources/js/graph3d/d3-octree.min.js +2 -2
- package/resources/js/graph3d/d3-quadtree.min.js +2 -2
- package/resources/js/graph3d/d3-timer.min.js +2 -2
- package/resources/js/graph3d/three.min.js +6 -6
- package/resources/panel/quick-menu.cjs +197 -197
- package/scripts/_utils.py +1338 -1338
- package/scripts/annual_report.py +3 -1
- package/scripts/article_notes.py +423 -423
- package/scripts/contact_schema.py +339 -339
- package/scripts/create_reading_notes.py +11 -11
- package/scripts/daily_mail.py +51 -5
- package/scripts/daily_task.ps1 +216 -213
- package/scripts/extract_todos.py +3 -1
- package/scripts/fav_notes.py +321 -321
- package/scripts/health_check.py +473 -473
- package/scripts/imported_chats.py +68 -68
- package/scripts/install_tasks.ps1 +105 -105
- package/scripts/key_cache.py +232 -0
- package/scripts/leads.py +179 -179
- package/scripts/mcp_bridge.py +3 -1
- package/scripts/media_index.py +150 -0
- package/scripts/network.py +206 -206
- package/scripts/nt_common.py +32 -0
- package/scripts/nt_decrypt.py +2023 -1954
- package/scripts/opinion_timeline.py +138 -138
- package/scripts/replay.py +803 -803
- package/scripts/run_hidden.vbs +24 -24
- package/scripts/semantic_search.py +5 -1
- package/scripts/user_notes.py +315 -315
- package/scripts/voice_transcribe.py +139 -139
- package/scripts/watch_issues.py +212 -212
- package/scripts/watch_mail.py +131 -131
- package/scripts/wechat_avatar.py +185 -0
- package/scripts/wechat_ledger.py +190 -0
- package/src/services/cursorProbe.ts +222 -222
- package/src/services/diagnosticReport.ts +22 -0
- package/src/services/qchatImport.ts +327 -327
- package/src/services/setupVenv.ts +238 -238
- package/src/services/syncService.ts +363 -363
- package/src/services/syncState.ts +275 -275
- package/src/services/wechatCompat.ts +67 -0
- package/src/services/wechatMedia.ts +166 -166
package/scripts/watch_issues.py
CHANGED
|
@@ -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())
|