codeabc 0.1.0__py3-none-any.whl
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.
- backend/__init__.py +0 -0
- backend/app.py +95 -0
- backend/desktop_server.py +46 -0
- backend/models.py +629 -0
- backend/prompts/__init__.py +0 -0
- backend/prompts/annotate.py +44 -0
- backend/prompts/edit.py +62 -0
- backend/prompts/overview.py +57 -0
- backend/prompts/qa.py +45 -0
- backend/routers/__init__.py +0 -0
- backend/routers/analyze.py +271 -0
- backend/routers/project.py +872 -0
- backend/services/__init__.py +0 -0
- backend/services/action_plan.py +366 -0
- backend/services/activity.py +313 -0
- backend/services/apimap.py +391 -0
- backend/services/async_surface.py +123 -0
- backend/services/cache.py +183 -0
- backend/services/churn.py +244 -0
- backend/services/ci_checks.py +317 -0
- backend/services/codemap_export.py +136 -0
- backend/services/commands.py +276 -0
- backend/services/complexity.py +111 -0
- backend/services/config_files.py +190 -0
- backend/services/contributing.py +308 -0
- backend/services/coverage.py +283 -0
- backend/services/datamodels.py +190 -0
- backend/services/deep_nesting.py +110 -0
- backend/services/dependencies.py +270 -0
- backend/services/docs.py +255 -0
- backend/services/docstrings.py +163 -0
- backend/services/duplicate_code.py +165 -0
- backend/services/entrypoints.py +152 -0
- backend/services/envscan.py +167 -0
- backend/services/error_handling.py +239 -0
- backend/services/filenames.py +261 -0
- backend/services/github_clone.py +129 -0
- backend/services/glossary.py +96 -0
- backend/services/health_score.py +381 -0
- backend/services/importgraph.py +986 -0
- backend/services/integrations.py +263 -0
- backend/services/licenses.py +505 -0
- backend/services/llm.py +117 -0
- backend/services/long_functions.py +101 -0
- backend/services/ownership.py +194 -0
- backend/services/package_purposes.py +112 -0
- backend/services/pr_reading.py +181 -0
- backend/services/release_map.py +402 -0
- backend/services/report_export.py +232 -0
- backend/services/risk.py +88 -0
- backend/services/scanner.py +546 -0
- backend/services/schedules.py +369 -0
- backend/services/security.py +445 -0
- backend/services/settings_map.py +234 -0
- backend/services/symbols.py +605 -0
- backend/services/techdebt.py +124 -0
- backend/services/too_many_params.py +102 -0
- backend/services/typing_coverage.py +184 -0
- codeabc-0.1.0.dist-info/METADATA +283 -0
- codeabc-0.1.0.dist-info/RECORD +62 -0
- codeabc-0.1.0.dist-info/WHEEL +4 -0
- codeabc-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Scheduled & automated tasks map — what this project runs on its own.
|
|
2
|
+
|
|
3
|
+
Entry points answer "how do I *start* this myself" and CLI commands answer
|
|
4
|
+
"what verbs can I *type*". This answers a question a non-programmer rarely thinks
|
|
5
|
+
to ask but very much wants the answer to: **does this project do anything by
|
|
6
|
+
itself — on a timer, on a schedule, automatically?** A repo that quietly emails a
|
|
7
|
+
report every morning, retries a queue every 30 seconds, or runs a CI job nightly
|
|
8
|
+
behaves very differently from one that only ever acts when you press a button,
|
|
9
|
+
and that difference is invisible unless you know the handful of libraries people
|
|
10
|
+
use to wire it up.
|
|
11
|
+
|
|
12
|
+
This collects those automated triggers straight from the source CodeABC already
|
|
13
|
+
loaded — no LLM, nothing to install — by recognising the common scheduling
|
|
14
|
+
mechanisms across the stacks CodeABC sees most:
|
|
15
|
+
|
|
16
|
+
GitHub Actions ``on: schedule: - cron: "*/5 * * * *"`` (.github/workflows)
|
|
17
|
+
APScheduler ``@scheduler.scheduled_job("cron", hour=0)`` / ``add_job(...)``
|
|
18
|
+
Celery beat ``@periodic_task(...)`` / ``crontab(...)`` / ``beat_schedule``
|
|
19
|
+
schedule (lib) ``schedule.every(10).minutes.do(job)``
|
|
20
|
+
FastAPI ``@repeat_every(seconds=3600)``
|
|
21
|
+
node-cron ``cron.schedule("0 * * * *", task)``
|
|
22
|
+
NestJS ``@Cron(...)`` / ``@Interval(...)`` / ``@Timeout(...)``
|
|
23
|
+
Browser/Node ``setInterval(fn, 5000)``
|
|
24
|
+
|
|
25
|
+
For each hit it reports *what* runs, by *which* mechanism, *how often* (the raw
|
|
26
|
+
schedule as written) and — the文科生-friendly touch — a plain-language gloss of
|
|
27
|
+
common cron expressions (``*/5 * * * *`` → ``每 5 分钟``).
|
|
28
|
+
|
|
29
|
+
:func:`find_scheduled_tasks` is pure over the file contents, so it is
|
|
30
|
+
unit-testable with plain strings and needs no repository.
|
|
31
|
+
|
|
32
|
+
Limitations (kept honest on purpose):
|
|
33
|
+
|
|
34
|
+
* Pattern-based, not a full parse. A schedule whose decorator arguments span
|
|
35
|
+
several lines, or one built dynamically at runtime, may be missed.
|
|
36
|
+
* ``setInterval`` is reported as a recurring timer; a bare ``setTimeout``
|
|
37
|
+
(usually a one-shot delay) is intentionally not, to avoid flooding the map.
|
|
38
|
+
* The plain-language gloss only covers the common cron shapes; anything
|
|
39
|
+
unusual is shown verbatim without a translation rather than guessed at.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import re
|
|
45
|
+
|
|
46
|
+
_PY_SUFFIX = ".py"
|
|
47
|
+
_JS_SUFFIXES = (".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs")
|
|
48
|
+
_YAML_SUFFIXES = (".yml", ".yaml")
|
|
49
|
+
|
|
50
|
+
# A trailing function/method captured after one or more stacked decorators.
|
|
51
|
+
_DEF_TAIL = r"[^\n]*\n(?:[ \t]*@[^\n]*\n)*[ \t]*(?:async[ \t]+)?def[ \t]+(\w+)"
|
|
52
|
+
|
|
53
|
+
# --- GitHub Actions / any YAML cron: key -----------------------------------
|
|
54
|
+
# Matches `- cron: "*/5 * * * *"` and unquoted forms; the backref keeps the
|
|
55
|
+
# quotes balanced so a trailing `# comment` is not swallowed into the value.
|
|
56
|
+
_YAML_CRON = re.compile(
|
|
57
|
+
r"""^[ \t]*-?[ \t]*cron:[ \t]*(['"]?)([^\n#]*?)\1[ \t]*(?:#.*)?$""",
|
|
58
|
+
re.MULTILINE,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# --- Python --------------------------------------------------------------
|
|
62
|
+
_APS_DECORATOR = re.compile(r"@[\w.]*?scheduled_job\(([^)]*)\)" + _DEF_TAIL)
|
|
63
|
+
_PERIODIC_TASK = re.compile(r"@(?:[\w.]+\.)?periodic_task\(([^)]*)\)" + _DEF_TAIL)
|
|
64
|
+
_REPEAT_EVERY = re.compile(r"@(?:[\w.]+\.)?repeat_every\(([^)]*)\)" + _DEF_TAIL)
|
|
65
|
+
_ADD_JOB = re.compile(r"\.add_job\(\s*([\w.]+)?")
|
|
66
|
+
_SCHEDULE_LIB = re.compile(
|
|
67
|
+
r"\bschedule\.every\(([^)]*)\)((?:\.\w+(?:\([^)]*\))?)+?)\.do\(\s*([\w.]+)?"
|
|
68
|
+
)
|
|
69
|
+
_CRONTAB = re.compile(r"\bcrontab\(([^)]*)\)")
|
|
70
|
+
_BEAT_SCHEDULE = re.compile(r"\bbeat_schedule\b\s*=\s*\{")
|
|
71
|
+
|
|
72
|
+
# --- JS / TS -------------------------------------------------------------
|
|
73
|
+
_NODE_CRON = re.compile(r"""\bcron\.schedule\(\s*(['"`])([^'"`]+)\1""")
|
|
74
|
+
_NEST = re.compile(
|
|
75
|
+
r"@(Cron|Interval|Timeout)\(([^)]*)\)"
|
|
76
|
+
r"[^\n]*\n(?:[ \t]*@[^\n]*\n)*[ \t]*"
|
|
77
|
+
r"(?:public |private |protected |async |readonly )*(\w+)\s*\("
|
|
78
|
+
)
|
|
79
|
+
_SET_INTERVAL = re.compile(r"\bsetInterval\b\s*\(([^,]*),\s*(\d[\d_]*)?")
|
|
80
|
+
|
|
81
|
+
_MECH_LABEL = {
|
|
82
|
+
"github-actions": "GitHub Actions 定时",
|
|
83
|
+
"apscheduler": "APScheduler",
|
|
84
|
+
"celery": "Celery 定时",
|
|
85
|
+
"schedule": "schedule 库",
|
|
86
|
+
"repeat-every": "FastAPI 周期任务",
|
|
87
|
+
"node-cron": "node-cron",
|
|
88
|
+
"nestjs": "NestJS 定时",
|
|
89
|
+
"interval": "setInterval 定时器",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_SCHEDULE_UNIT = {
|
|
93
|
+
"second": "秒",
|
|
94
|
+
"seconds": "秒",
|
|
95
|
+
"minute": "分钟",
|
|
96
|
+
"minutes": "分钟",
|
|
97
|
+
"hour": "小时",
|
|
98
|
+
"hours": "小时",
|
|
99
|
+
"day": "天",
|
|
100
|
+
"days": "天",
|
|
101
|
+
"week": "周",
|
|
102
|
+
"weeks": "周",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _line_of(content: str, pos: int) -> int:
|
|
107
|
+
return content.count("\n", 0, pos) + 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _stem(path: str) -> str:
|
|
111
|
+
base = path.replace("\\", "/").rsplit("/", 1)[-1]
|
|
112
|
+
return base.rsplit(".", 1)[0] or base
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _cron_to_human(expr: str) -> str:
|
|
116
|
+
"""Plain-language gloss of the common 5-field cron shapes, else ``""``."""
|
|
117
|
+
parts = expr.split()
|
|
118
|
+
if len(parts) != 5:
|
|
119
|
+
return ""
|
|
120
|
+
minute, hour, dom, month, dow = parts
|
|
121
|
+
star_rest = dom == month == dow == "*"
|
|
122
|
+
|
|
123
|
+
if parts == ["*", "*", "*", "*", "*"]:
|
|
124
|
+
return "每分钟"
|
|
125
|
+
m = re.fullmatch(r"\*/(\d+)", minute)
|
|
126
|
+
if m and hour == "*" and star_rest:
|
|
127
|
+
return f"每 {int(m.group(1))} 分钟"
|
|
128
|
+
if hour == "*" and star_rest and minute.isdigit():
|
|
129
|
+
return "每小时整点" if minute == "0" else f"每小时第 {int(minute)} 分"
|
|
130
|
+
mh = re.fullmatch(r"\*/(\d+)", hour)
|
|
131
|
+
if mh and star_rest and minute.isdigit():
|
|
132
|
+
head = f"每 {int(mh.group(1))} 小时"
|
|
133
|
+
return head if minute == "0" else head + f"第 {int(minute)} 分"
|
|
134
|
+
if minute.isdigit() and hour.isdigit() and star_rest:
|
|
135
|
+
return f"每天 {int(hour):02d}:{int(minute):02d}"
|
|
136
|
+
if minute.isdigit() and hour.isdigit() and dom == "*" and month == "*" and dow.isdigit():
|
|
137
|
+
names = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
|
|
138
|
+
return f"每{names[int(dow) % 7]} {int(hour):02d}:{int(minute):02d}"
|
|
139
|
+
if minute.isdigit() and hour.isdigit() and dom.isdigit() and month == "*" and dow == "*":
|
|
140
|
+
return f"每月 {int(dom)} 日 {int(hour):02d}:{int(minute):02d}"
|
|
141
|
+
return ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _amount_to_human(amount: int, unit_zh: str) -> str:
|
|
145
|
+
return f"每 {amount} {unit_zh}" if amount != 1 else f"每{unit_zh}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _seconds_to_human(raw: str) -> str:
|
|
149
|
+
try:
|
|
150
|
+
n = int(raw)
|
|
151
|
+
except ValueError:
|
|
152
|
+
return ""
|
|
153
|
+
if n % 86400 == 0:
|
|
154
|
+
return _amount_to_human(n // 86400, "天")
|
|
155
|
+
if n % 3600 == 0:
|
|
156
|
+
return _amount_to_human(n // 3600, "小时")
|
|
157
|
+
if n % 60 == 0:
|
|
158
|
+
return _amount_to_human(n // 60, "分钟")
|
|
159
|
+
return _amount_to_human(n, "秒")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _ms_to_human(raw: str) -> str:
|
|
163
|
+
digits = raw.replace("_", "")
|
|
164
|
+
if not digits.isdigit():
|
|
165
|
+
return ""
|
|
166
|
+
n = int(digits)
|
|
167
|
+
if n % 3_600_000 == 0:
|
|
168
|
+
return _amount_to_human(n // 3_600_000, "小时")
|
|
169
|
+
if n % 60_000 == 0:
|
|
170
|
+
return _amount_to_human(n // 60_000, "分钟")
|
|
171
|
+
if n % 1000 == 0:
|
|
172
|
+
return _amount_to_human(n // 1000, "秒")
|
|
173
|
+
return _amount_to_human(n, "毫秒")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
_APS_TIMING_KW = frozenset(
|
|
177
|
+
{
|
|
178
|
+
"seconds",
|
|
179
|
+
"minutes",
|
|
180
|
+
"hours",
|
|
181
|
+
"days",
|
|
182
|
+
"weeks",
|
|
183
|
+
"second",
|
|
184
|
+
"minute",
|
|
185
|
+
"hour",
|
|
186
|
+
"day",
|
|
187
|
+
"day_of_week",
|
|
188
|
+
"month",
|
|
189
|
+
"week",
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _aps_trigger(text: str) -> str:
|
|
195
|
+
"""Summarise an APScheduler trigger spec (positional kind + timing kwargs).
|
|
196
|
+
|
|
197
|
+
Timing kwargs are emitted in source order so the gloss reads the way the
|
|
198
|
+
author wrote it (``cron, hour=0, minute=30``), not in a fixed reshuffle.
|
|
199
|
+
"""
|
|
200
|
+
parts: list[str] = []
|
|
201
|
+
kind = re.search(r"""['"](cron|interval|date)['"]""", text)
|
|
202
|
+
if kind:
|
|
203
|
+
parts.append(kind.group(1))
|
|
204
|
+
for km in re.finditer(r"\b(\w+)\s*=\s*([^\s,)]+)", text):
|
|
205
|
+
if km.group(1) in _APS_TIMING_KW:
|
|
206
|
+
parts.append(f"{km.group(1)}={km.group(2)}")
|
|
207
|
+
return ", ".join(parts)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _find_in_yaml(path: str, content: str) -> list[dict]:
|
|
211
|
+
found: list[dict] = []
|
|
212
|
+
name = _stem(path)
|
|
213
|
+
for m in _YAML_CRON.finditer(content):
|
|
214
|
+
expr = m.group(2).strip()
|
|
215
|
+
if not expr:
|
|
216
|
+
continue
|
|
217
|
+
found.append(
|
|
218
|
+
{
|
|
219
|
+
"name": name,
|
|
220
|
+
"mechanism": "github-actions",
|
|
221
|
+
"schedule": expr,
|
|
222
|
+
"schedule_human": _cron_to_human(expr),
|
|
223
|
+
"line": _line_of(content, m.start()),
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
return found
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _find_in_python(content: str) -> list[dict]:
|
|
230
|
+
found: list[dict] = []
|
|
231
|
+
|
|
232
|
+
for m in _APS_DECORATOR.finditer(content):
|
|
233
|
+
found.append(
|
|
234
|
+
_task(m.group(2), "apscheduler", _aps_trigger(m.group(1)), "", content, m.start())
|
|
235
|
+
)
|
|
236
|
+
for m in _PERIODIC_TASK.finditer(content):
|
|
237
|
+
found.append(_task(m.group(2), "celery", _aps_trigger(m.group(1)), "", content, m.start()))
|
|
238
|
+
for m in _REPEAT_EVERY.finditer(content):
|
|
239
|
+
secs = re.search(r"seconds\s*=\s*([\d_]+)", m.group(1))
|
|
240
|
+
raw = m.group(1).strip()
|
|
241
|
+
human = _seconds_to_human(secs.group(1).replace("_", "")) if secs else ""
|
|
242
|
+
found.append(_task(m.group(2), "repeat-every", raw, human, content, m.start()))
|
|
243
|
+
for m in _ADD_JOB.finditer(content):
|
|
244
|
+
window = content[m.start() : m.start() + 220]
|
|
245
|
+
name = m.group(1) or "add_job 任务"
|
|
246
|
+
found.append(_task(name, "apscheduler", _aps_trigger(window), "", content, m.start()))
|
|
247
|
+
for m in _SCHEDULE_LIB.finditer(content):
|
|
248
|
+
amount, chain, target = m.group(1).strip(), m.group(2), m.group(3)
|
|
249
|
+
sched = f"every({amount}){chain}"
|
|
250
|
+
unit = chain.lstrip(".").split(".")[0]
|
|
251
|
+
human = ""
|
|
252
|
+
if amount.isdigit() and unit in _SCHEDULE_UNIT:
|
|
253
|
+
human = _amount_to_human(int(amount), _SCHEDULE_UNIT[unit])
|
|
254
|
+
found.append(_task(target or "schedule 任务", "schedule", sched, human, content, m.start()))
|
|
255
|
+
for m in _CRONTAB.finditer(content):
|
|
256
|
+
args = m.group(1).strip()
|
|
257
|
+
found.append(_task("Celery 周期任务", "celery", f"crontab({args})", "", content, m.start()))
|
|
258
|
+
for m in _BEAT_SCHEDULE.finditer(content):
|
|
259
|
+
found.append(_task("周期任务表 (beat_schedule)", "celery", "", "", content, m.start()))
|
|
260
|
+
|
|
261
|
+
return found
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _find_in_js(content: str) -> list[dict]:
|
|
265
|
+
found: list[dict] = []
|
|
266
|
+
for m in _NODE_CRON.finditer(content):
|
|
267
|
+
expr = m.group(2).strip()
|
|
268
|
+
found.append(
|
|
269
|
+
_task("node-cron 任务", "node-cron", expr, _cron_to_human(expr), content, m.start())
|
|
270
|
+
)
|
|
271
|
+
for m in _NEST.finditer(content):
|
|
272
|
+
decorator, args, method = m.group(1), m.group(2).strip(), m.group(3)
|
|
273
|
+
sched, human = _nest_schedule(decorator, args)
|
|
274
|
+
found.append(_task(method, "nestjs", sched, human, content, m.start()))
|
|
275
|
+
for m in _SET_INTERVAL.finditer(content):
|
|
276
|
+
ms = m.group(2)
|
|
277
|
+
human = _ms_to_human(ms) if ms else ""
|
|
278
|
+
sched = f"{ms} ms" if ms else ""
|
|
279
|
+
found.append(_task("周期定时器", "interval", sched, human, content, m.start()))
|
|
280
|
+
return found
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _nest_schedule(decorator: str, args: str) -> tuple[str, str]:
|
|
284
|
+
quoted = re.search(r"""['"]([^'"]+)['"]""", args)
|
|
285
|
+
if quoted and len(quoted.group(1).split()) == 5:
|
|
286
|
+
expr = quoted.group(1)
|
|
287
|
+
return expr, _cron_to_human(expr)
|
|
288
|
+
if decorator in ("Interval", "Timeout"):
|
|
289
|
+
num = re.search(r"\b(\d[\d_]*)\b", args)
|
|
290
|
+
if num:
|
|
291
|
+
return f"{decorator}({args})", _ms_to_human(num.group(1))
|
|
292
|
+
return args or decorator, ""
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _task(name: str, mechanism: str, schedule: str, human: str, content: str, pos: int) -> dict:
|
|
296
|
+
return {
|
|
297
|
+
"name": name,
|
|
298
|
+
"mechanism": mechanism,
|
|
299
|
+
"schedule": schedule,
|
|
300
|
+
"schedule_human": human,
|
|
301
|
+
"line": _line_of(content, pos),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def find_scheduled_tasks(file_contents: dict[str, str], *, limit: int = 50) -> dict:
|
|
306
|
+
"""Collect the automated / scheduled triggers a project wires up.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
file_contents: mapping of path to file text (as CodeABC already read it).
|
|
310
|
+
limit: how many tasks to return in the sorted list.
|
|
311
|
+
|
|
312
|
+
Returns ``{"total", "mechanisms", "tasks"}`` where each task is
|
|
313
|
+
``{"name", "mechanism", "schedule", "schedule_human", "path", "line"}``.
|
|
314
|
+
"""
|
|
315
|
+
seen: set[tuple] = set()
|
|
316
|
+
tasks: list[dict] = []
|
|
317
|
+
|
|
318
|
+
for path, content in file_contents.items():
|
|
319
|
+
if not content:
|
|
320
|
+
continue
|
|
321
|
+
if path.endswith(_YAML_SUFFIXES):
|
|
322
|
+
entries = _find_in_yaml(path, content)
|
|
323
|
+
elif path.endswith(_PY_SUFFIX):
|
|
324
|
+
entries = _find_in_python(content)
|
|
325
|
+
elif path.endswith(_JS_SUFFIXES):
|
|
326
|
+
entries = _find_in_js(content)
|
|
327
|
+
else:
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
for entry in entries:
|
|
331
|
+
key = (path, entry["line"], entry["name"], entry["mechanism"])
|
|
332
|
+
if key in seen:
|
|
333
|
+
continue
|
|
334
|
+
seen.add(key)
|
|
335
|
+
tasks.append({**entry, "path": path})
|
|
336
|
+
|
|
337
|
+
tasks.sort(key=lambda t: (t["path"], t["line"]))
|
|
338
|
+
mechanisms = sorted({t["mechanism"] for t in tasks})
|
|
339
|
+
return {"total": len(tasks), "mechanisms": mechanisms, "tasks": tasks[:limit]}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def render_schedules_markdown(project_name: str, data: dict | None) -> str:
|
|
343
|
+
"""Render the scheduled-tasks map as Markdown, or ``""`` if none were found."""
|
|
344
|
+
tasks = (data or {}).get("tasks") or []
|
|
345
|
+
if not tasks:
|
|
346
|
+
return ""
|
|
347
|
+
|
|
348
|
+
lines = [
|
|
349
|
+
f"# {project_name} — 会自己定时跑的任务(自动化 / 定时)",
|
|
350
|
+
"",
|
|
351
|
+
"> 入口是“我怎么手动启动”,这里是“项目会不会自己定时做点什么”——"
|
|
352
|
+
"比如每天发一封报告、每 30 秒重试一次队列、每晚跑一次 CI。"
|
|
353
|
+
"这类任务不用你按按钮就会发生,看清它跑什么、多久跑一次,再决定要不要改。",
|
|
354
|
+
"",
|
|
355
|
+
]
|
|
356
|
+
current_path = None
|
|
357
|
+
for task in tasks:
|
|
358
|
+
if task["path"] != current_path:
|
|
359
|
+
current_path = task["path"]
|
|
360
|
+
lines.append(f"## `{current_path}`")
|
|
361
|
+
label = _MECH_LABEL.get(task["mechanism"], task["mechanism"])
|
|
362
|
+
bits = [f"- `{task['name']}` — {label}"]
|
|
363
|
+
if task["schedule"]:
|
|
364
|
+
bits.append(f":`{task['schedule']}`")
|
|
365
|
+
if task["schedule_human"]:
|
|
366
|
+
bits.append(f"({task['schedule_human']})")
|
|
367
|
+
bits.append(f" 第 {task['line']} 行")
|
|
368
|
+
lines.append("".join(bits))
|
|
369
|
+
return "\n".join(lines).rstrip() + "\n"
|