illusion-code 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.
Files changed (214) hide show
  1. illusion/__init__.py +24 -0
  2. illusion/__main__.py +15 -0
  3. illusion/_frontend/dist/index.mjs +39208 -0
  4. illusion/_frontend/package.json +27 -0
  5. illusion/_frontend/src/App.tsx +624 -0
  6. illusion/_frontend/src/components/CommandPicker.tsx +98 -0
  7. illusion/_frontend/src/components/Composer.tsx +55 -0
  8. illusion/_frontend/src/components/ComposerController.tsx +128 -0
  9. illusion/_frontend/src/components/ConversationView.tsx +750 -0
  10. illusion/_frontend/src/components/Footer.tsx +25 -0
  11. illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
  12. illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
  13. illusion/_frontend/src/components/ModalHost.tsx +425 -0
  14. illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
  15. illusion/_frontend/src/components/PromptInput.tsx +64 -0
  16. illusion/_frontend/src/components/SelectModal.tsx +78 -0
  17. illusion/_frontend/src/components/SidePanel.tsx +175 -0
  18. illusion/_frontend/src/components/Spinner.tsx +77 -0
  19. illusion/_frontend/src/components/StatusBar.tsx +142 -0
  20. illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
  21. illusion/_frontend/src/components/TodoPanel.tsx +126 -0
  22. illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
  23. illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
  24. illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
  25. illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
  26. illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
  27. illusion/_frontend/src/i18n.ts +78 -0
  28. illusion/_frontend/src/index.tsx +42 -0
  29. illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
  30. illusion/_frontend/src/theme/builtinThemes.ts +89 -0
  31. illusion/_frontend/src/types.ts +110 -0
  32. illusion/_frontend/src/utils/markdown.ts +33 -0
  33. illusion/_frontend/src/utils/thinking.ts +191 -0
  34. illusion/_frontend/tsconfig.json +13 -0
  35. illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
  36. illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
  37. illusion/_web_dist/index.html +16 -0
  38. illusion/api/__init__.py +36 -0
  39. illusion/api/client.py +568 -0
  40. illusion/api/codex_client.py +563 -0
  41. illusion/api/compat.py +138 -0
  42. illusion/api/effort.py +128 -0
  43. illusion/api/errors.py +57 -0
  44. illusion/api/openai_client.py +819 -0
  45. illusion/api/provider.py +148 -0
  46. illusion/api/registry.py +479 -0
  47. illusion/api/usage.py +45 -0
  48. illusion/auth/__init__.py +50 -0
  49. illusion/auth/copilot.py +419 -0
  50. illusion/auth/external.py +612 -0
  51. illusion/auth/flows.py +58 -0
  52. illusion/auth/manager.py +214 -0
  53. illusion/auth/storage.py +372 -0
  54. illusion/bridge/__init__.py +38 -0
  55. illusion/bridge/manager.py +190 -0
  56. illusion/bridge/session_runner.py +84 -0
  57. illusion/bridge/types.py +113 -0
  58. illusion/bridge/work_secret.py +131 -0
  59. illusion/cli.py +1228 -0
  60. illusion/commands/__init__.py +32 -0
  61. illusion/commands/registry.py +1934 -0
  62. illusion/config/__init__.py +39 -0
  63. illusion/config/i18n.py +522 -0
  64. illusion/config/paths.py +259 -0
  65. illusion/config/settings.py +564 -0
  66. illusion/coordinator/__init__.py +41 -0
  67. illusion/coordinator/agent_definitions.py +1093 -0
  68. illusion/coordinator/coordinator_mode.py +127 -0
  69. illusion/engine/__init__.py +95 -0
  70. illusion/engine/cost_tracker.py +55 -0
  71. illusion/engine/messages.py +369 -0
  72. illusion/engine/query.py +632 -0
  73. illusion/engine/query_engine.py +343 -0
  74. illusion/engine/stream_events.py +169 -0
  75. illusion/hooks/__init__.py +67 -0
  76. illusion/hooks/events.py +43 -0
  77. illusion/hooks/executor.py +397 -0
  78. illusion/hooks/hot_reload.py +74 -0
  79. illusion/hooks/loader.py +133 -0
  80. illusion/hooks/schemas.py +121 -0
  81. illusion/hooks/types.py +86 -0
  82. illusion/mcp/__init__.py +104 -0
  83. illusion/mcp/client.py +377 -0
  84. illusion/mcp/config.py +140 -0
  85. illusion/mcp/types.py +175 -0
  86. illusion/memory/__init__.py +36 -0
  87. illusion/memory/manager.py +94 -0
  88. illusion/memory/memdir.py +58 -0
  89. illusion/memory/paths.py +57 -0
  90. illusion/memory/scan.py +120 -0
  91. illusion/memory/search.py +83 -0
  92. illusion/memory/types.py +43 -0
  93. illusion/output_styles/__init__.py +15 -0
  94. illusion/output_styles/loader.py +64 -0
  95. illusion/permissions/__init__.py +39 -0
  96. illusion/permissions/checker.py +174 -0
  97. illusion/permissions/modes.py +38 -0
  98. illusion/platforms.py +148 -0
  99. illusion/plugins/__init__.py +71 -0
  100. illusion/plugins/bundled/__init__.py +0 -0
  101. illusion/plugins/installer.py +59 -0
  102. illusion/plugins/loader.py +301 -0
  103. illusion/plugins/schemas.py +51 -0
  104. illusion/plugins/types.py +56 -0
  105. illusion/prompts/__init__.py +29 -0
  106. illusion/prompts/claudemd.py +74 -0
  107. illusion/prompts/context.py +187 -0
  108. illusion/prompts/environment.py +189 -0
  109. illusion/prompts/system_prompt.py +155 -0
  110. illusion/py.typed +0 -0
  111. illusion/sandbox/__init__.py +29 -0
  112. illusion/sandbox/adapter.py +174 -0
  113. illusion/services/__init__.py +59 -0
  114. illusion/services/compact/__init__.py +1015 -0
  115. illusion/services/cron.py +338 -0
  116. illusion/services/cron_scheduler.py +715 -0
  117. illusion/services/file_history.py +258 -0
  118. illusion/services/lsp/__init__.py +455 -0
  119. illusion/services/session_storage.py +237 -0
  120. illusion/services/token_estimation.py +72 -0
  121. illusion/skills/__init__.py +60 -0
  122. illusion/skills/bundled/__init__.py +110 -0
  123. illusion/skills/bundled/content/batch.md +86 -0
  124. illusion/skills/bundled/content/coding-guidelines.md +70 -0
  125. illusion/skills/bundled/content/debug.md +38 -0
  126. illusion/skills/bundled/content/loop.md +82 -0
  127. illusion/skills/bundled/content/remember.md +105 -0
  128. illusion/skills/bundled/content/simplify.md +53 -0
  129. illusion/skills/bundled/content/skillify.md +113 -0
  130. illusion/skills/bundled/content/stuck.md +54 -0
  131. illusion/skills/bundled/content/update-config.md +329 -0
  132. illusion/skills/bundled/content/verify.md +74 -0
  133. illusion/skills/loader.py +219 -0
  134. illusion/skills/registry.py +40 -0
  135. illusion/skills/types.py +24 -0
  136. illusion/state/__init__.py +18 -0
  137. illusion/state/app_state.py +67 -0
  138. illusion/state/store.py +93 -0
  139. illusion/swarm/__init__.py +71 -0
  140. illusion/swarm/agent_executor.py +857 -0
  141. illusion/swarm/in_process.py +259 -0
  142. illusion/swarm/subprocess_backend.py +136 -0
  143. illusion/swarm/team_helpers.py +123 -0
  144. illusion/swarm/types.py +159 -0
  145. illusion/swarm/worktree.py +347 -0
  146. illusion/tasks/__init__.py +33 -0
  147. illusion/tasks/local_agent_task.py +42 -0
  148. illusion/tasks/local_shell_task.py +27 -0
  149. illusion/tasks/manager.py +377 -0
  150. illusion/tasks/stop_task.py +21 -0
  151. illusion/tasks/types.py +88 -0
  152. illusion/tools/__init__.py +126 -0
  153. illusion/tools/agent_tool.py +388 -0
  154. illusion/tools/ask_user_question_tool.py +186 -0
  155. illusion/tools/base.py +149 -0
  156. illusion/tools/bash_tool.py +413 -0
  157. illusion/tools/config_tool.py +90 -0
  158. illusion/tools/cron_tool.py +473 -0
  159. illusion/tools/enter_plan_mode_tool.py +147 -0
  160. illusion/tools/enter_worktree_tool.py +188 -0
  161. illusion/tools/exit_plan_mode_tool.py +69 -0
  162. illusion/tools/exit_worktree_tool.py +225 -0
  163. illusion/tools/file_edit_tool.py +283 -0
  164. illusion/tools/file_read_tool.py +294 -0
  165. illusion/tools/file_write_tool.py +184 -0
  166. illusion/tools/glob_tool.py +165 -0
  167. illusion/tools/grep_tool.py +190 -0
  168. illusion/tools/list_mcp_resources_tool.py +80 -0
  169. illusion/tools/lsp_tool.py +333 -0
  170. illusion/tools/mcp_auth_tool.py +100 -0
  171. illusion/tools/mcp_tool.py +75 -0
  172. illusion/tools/notebook_edit_tool.py +242 -0
  173. illusion/tools/powershell_tool.py +334 -0
  174. illusion/tools/read_mcp_resource_tool.py +63 -0
  175. illusion/tools/repl_tool.py +100 -0
  176. illusion/tools/send_message_tool.py +112 -0
  177. illusion/tools/shell_common.py +187 -0
  178. illusion/tools/skill_tool.py +86 -0
  179. illusion/tools/sleep_tool.py +62 -0
  180. illusion/tools/structured_output_tool.py +58 -0
  181. illusion/tools/task_create_tool.py +98 -0
  182. illusion/tools/task_get_tool.py +94 -0
  183. illusion/tools/task_list_tool.py +94 -0
  184. illusion/tools/task_output_tool.py +55 -0
  185. illusion/tools/task_stop_tool.py +52 -0
  186. illusion/tools/task_update_tool.py +224 -0
  187. illusion/tools/team_create_tool.py +236 -0
  188. illusion/tools/team_delete_tool.py +104 -0
  189. illusion/tools/todo_write_tool.py +198 -0
  190. illusion/tools/tool_search_tool.py +156 -0
  191. illusion/tools/web_fetch_tool.py +264 -0
  192. illusion/tools/web_search_tool.py +186 -0
  193. illusion/ui/__init__.py +23 -0
  194. illusion/ui/app.py +258 -0
  195. illusion/ui/backend_host.py +1180 -0
  196. illusion/ui/input.py +86 -0
  197. illusion/ui/output.py +363 -0
  198. illusion/ui/permission_dialog.py +47 -0
  199. illusion/ui/permission_store.py +99 -0
  200. illusion/ui/protocol.py +384 -0
  201. illusion/ui/react_launcher.py +280 -0
  202. illusion/ui/runtime.py +787 -0
  203. illusion/ui/textual_app.py +603 -0
  204. illusion/ui/web/__init__.py +10 -0
  205. illusion/ui/web/server.py +87 -0
  206. illusion/ui/web/ws_host.py +1197 -0
  207. illusion/utils/__init__.py +0 -0
  208. illusion/utils/ripgrep.py +299 -0
  209. illusion/utils/shell.py +248 -0
  210. illusion_code-0.1.0.dist-info/METADATA +1159 -0
  211. illusion_code-0.1.0.dist-info/RECORD +214 -0
  212. illusion_code-0.1.0.dist-info/WHEEL +4 -0
  213. illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
  214. illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,338 @@
1
+ """
2
+ Cron 任务注册表模块
3
+ ==================
4
+
5
+ 对齐 openclaw cron 数据模型,提供定时任务的增删改查功能。
6
+
7
+ 数据模型字段(对齐 openclaw CronJob):
8
+ - id: 唯一标识符(UUID)
9
+ - name: 人类可读的任务名称
10
+ - schedule: 标准 5 字段 cron 表达式(本地时间)
11
+ - prompt: 每次触发时执行的提示词
12
+ - enabled: 是否启用
13
+ - recurring: 是否重复执行(False 为一次性任务)
14
+ - delete_after_run: 执行后是否自动删除
15
+ - cwd: 工作目录
16
+ - created_at: 创建时间(本地时间 ISO 格式)
17
+ - updated_at: 最后更新时间
18
+ - next_run: 下次运行时间
19
+ - last_run: 上次运行时间
20
+ - last_status: 上次执行状态 ("success" | "failed" | "timeout" | "error")
21
+ - consecutive_errors: 连续错误次数(成功时重置为 0)
22
+
23
+ 主要函数:
24
+ - load_cron_jobs: 加载任务列表(自动迁移旧格式)
25
+ - save_cron_jobs: 持久化任务列表
26
+ - validate_cron_expression: 验证 cron 表达式
27
+ - next_run_time: 计算下次运行时间(本地时间)
28
+ - upsert_cron_job: 插入或更新任务
29
+ - delete_cron_job: 删除任务
30
+ - get_cron_job: 按 ID 获取任务
31
+ - get_cron_job_by_name: 按名称获取任务
32
+ - set_job_enabled: 启用/禁用任务
33
+ - mark_job_run: 标记执行结果并更新状态
34
+ - remove_expired_jobs: 清理过期任务
35
+
36
+ 使用示例:
37
+ >>> from illusion.services.cron import upsert_cron_job, load_cron_jobs
38
+ >>> upsert_cron_job({"name": "daily-report", "schedule": "0 9 * * *", "prompt": "生成日报"})
39
+ >>> jobs = load_cron_jobs()
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import json
45
+ import uuid
46
+ from datetime import datetime
47
+ from typing import Any
48
+
49
+ from croniter import croniter
50
+
51
+ from illusion.config.paths import get_cron_registry_path
52
+
53
+ # 存储格式版本号,用于未来迁移
54
+ _STORE_VERSION = 1
55
+
56
+
57
+ def _generate_id() -> str:
58
+ """生成短 UUID 作为任务 ID。"""
59
+ return uuid.uuid4().hex[:12]
60
+
61
+
62
+ def _now_local() -> datetime:
63
+ """返回本地时间(无时区信息),对齐用户本地时间。"""
64
+ return datetime.now().replace(microsecond=0)
65
+
66
+
67
+ def _now_iso() -> str:
68
+ """返回本地时间的 ISO 格式字符串。"""
69
+ return _now_local().isoformat()
70
+
71
+
72
+ def _normalize_job(job: dict[str, Any]) -> dict[str, Any]:
73
+ """规范化单个任务字段,处理旧格式迁移。
74
+
75
+ 旧格式使用 'command' 字段,新格式统一为 'prompt'。
76
+ 旧格式缺少 'id' 字段,自动生成。
77
+ """
78
+ # 旧格式 command -> 新格式 prompt
79
+ if "command" in job and "prompt" not in job:
80
+ job["prompt"] = job.pop("command")
81
+
82
+ # 缺少 id 则自动生成
83
+ if not job.get("id"):
84
+ job["id"] = _generate_id()
85
+
86
+ # 确保必要字段存在
87
+ job.setdefault("name", job["id"])
88
+ job.setdefault("enabled", True)
89
+ job.setdefault("recurring", True)
90
+ job.setdefault("delete_after_run", False)
91
+ job.setdefault("consecutive_errors", 0)
92
+ job.setdefault("created_at", _now_iso())
93
+ job.setdefault("updated_at", job["created_at"])
94
+
95
+ return job
96
+
97
+
98
+ def load_cron_jobs() -> list[dict[str, Any]]:
99
+ """加载已保存的 Cron 任务列表。
100
+
101
+ 自动处理旧格式迁移(command -> prompt,缺少 id 等)。
102
+ 返回规范化后的任务列表。
103
+ """
104
+ path = get_cron_registry_path()
105
+ if not path.exists():
106
+ return []
107
+ try:
108
+ data = json.loads(path.read_text(encoding="utf-8"))
109
+ except (json.JSONDecodeError, OSError):
110
+ return []
111
+
112
+ # 兼容旧格式:直接是列表
113
+ if isinstance(data, list):
114
+ jobs = data
115
+ # 新格式:带 version 的字典
116
+ elif isinstance(data, dict):
117
+ jobs = data.get("jobs", [])
118
+ if not isinstance(jobs, list):
119
+ return []
120
+ else:
121
+ return []
122
+
123
+ return [_normalize_job(job) for job in jobs if isinstance(job, dict)]
124
+
125
+
126
+ def save_cron_jobs(jobs: list[dict[str, Any]]) -> None:
127
+ """将 Cron 任务列表持久化到磁盘。
128
+
129
+ 使用带版本号的新格式存储。
130
+ """
131
+ path = get_cron_registry_path()
132
+ path.parent.mkdir(parents=True, exist_ok=True)
133
+ store = {"version": _STORE_VERSION, "jobs": jobs}
134
+ path.write_text(json.dumps(store, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
135
+
136
+
137
+ def validate_cron_expression(expression: str) -> bool:
138
+ """验证是否为有效的 cron 表达式。"""
139
+ if not expression or not isinstance(expression, str):
140
+ return False
141
+ return croniter.is_valid(expression.strip())
142
+
143
+
144
+ def next_run_time(expression: str, base: datetime | None = None) -> datetime:
145
+ """返回 cron 表达式的下次运行时间(本地时间)。
146
+
147
+ Args:
148
+ expression: 5 字段 cron 表达式
149
+ base: 基准时间,默认为当前本地时间
150
+
151
+ Returns:
152
+ datetime: 下次运行的本地时间
153
+ """
154
+ base = base or _now_local()
155
+ return croniter(expression, base).get_next(datetime)
156
+
157
+
158
+ def upsert_cron_job(job: dict[str, Any]) -> str:
159
+ """插入或替换一个 Cron 任务。
160
+
161
+ 自动生成 id(如未提供),设置默认值,计算下次运行时间。
162
+ 返回任务的 id。
163
+
164
+ Args:
165
+ job: 任务字典,至少包含 schedule 和 prompt
166
+
167
+ Returns:
168
+ str: 任务 ID
169
+ """
170
+ # 确保有 id
171
+ if not job.get("id"):
172
+ job["id"] = _generate_id()
173
+
174
+ # 规范化字段
175
+ job = _normalize_job(job)
176
+ job["updated_at"] = _now_iso()
177
+
178
+ # 设置首次创建时间
179
+ if "created_at" not in job:
180
+ job["created_at"] = _now_iso()
181
+
182
+ # 如果是新的一次性任务且 delete_after_run 未显式设置为 True,自动设置为 True
183
+ if not job.get("recurring", True) and not job.get("delete_after_run", False):
184
+ job["delete_after_run"] = True
185
+
186
+ # 验证并计算下次运行时间
187
+ schedule = job.get("schedule", "")
188
+ if validate_cron_expression(schedule):
189
+ job["next_run"] = next_run_time(schedule).isoformat()
190
+
191
+ # 按 id 和 name 去重,保留更新
192
+ job_id = job["id"]
193
+ job_name = job.get("name")
194
+ jobs = [
195
+ j for j in load_cron_jobs()
196
+ if j.get("id") != job_id and j.get("name") != job_name
197
+ ]
198
+ jobs.append(job)
199
+ jobs.sort(key=lambda item: str(item.get("name", "")))
200
+
201
+ save_cron_jobs(jobs)
202
+ return job_id
203
+
204
+
205
+ def delete_cron_job(identifier: str) -> bool:
206
+ """按 ID 或名称删除一个 Cron 任务。
207
+
208
+ 先按 id 匹配,再按 name 匹配。
209
+ 如果找到并删除则返回 True,否则返回 False。
210
+ """
211
+ jobs = load_cron_jobs()
212
+ filtered = [
213
+ j for j in jobs
214
+ if j.get("id") != identifier and j.get("name") != identifier
215
+ ]
216
+ if len(filtered) == len(jobs):
217
+ return False
218
+ save_cron_jobs(filtered)
219
+ return True
220
+
221
+
222
+ def get_cron_job(identifier: str) -> dict[str, Any] | None:
223
+ """按 ID 或名称返回一个 Cron 任务。
224
+
225
+ 先按 id 匹配,再按 name 匹配。
226
+ """
227
+ for job in load_cron_jobs():
228
+ if job.get("id") == identifier or job.get("name") == identifier:
229
+ return job
230
+ return None
231
+
232
+
233
+ def get_cron_job_by_name(name: str) -> dict[str, Any] | None:
234
+ """按名称返回一个 Cron 任务。"""
235
+ for job in load_cron_jobs():
236
+ if job.get("name") == name:
237
+ return job
238
+ return None
239
+
240
+
241
+ def set_job_enabled(identifier: str, enabled: bool) -> bool:
242
+ """启用或禁用 Cron 任务。
243
+
244
+ 按 id 或 name 查找任务。
245
+ 如果找到则更新并返回 True,否则返回 False。
246
+ """
247
+ jobs = load_cron_jobs()
248
+ for job in jobs:
249
+ if job.get("id") == identifier or job.get("name") == identifier:
250
+ job["enabled"] = enabled
251
+ job["updated_at"] = _now_iso()
252
+ save_cron_jobs(jobs)
253
+ return True
254
+ return False
255
+
256
+
257
+ def mark_job_run(
258
+ identifier: str,
259
+ *,
260
+ success: bool,
261
+ status: str | None = None,
262
+ ) -> None:
263
+ """任务执行后更新状态并重新计算下次运行时间。
264
+
265
+ 自动处理:
266
+ - 更新 last_run 和 last_status
267
+ - 成功时重置 consecutive_errors 为 0
268
+ - 失败时递增 consecutive_errors
269
+ - 一次性任务(recurring=False)执行后禁用
270
+ - delete_after_run 任务执行后标记待删除
271
+
272
+ Args:
273
+ identifier: 任务 ID 或名称
274
+ success: 是否执行成功
275
+ status: 自定义状态字符串(默认 success/failed)
276
+ """
277
+ jobs = load_cron_jobs()
278
+ now = _now_local()
279
+
280
+ for job in jobs:
281
+ if job.get("id") != identifier and job.get("name") != identifier:
282
+ continue
283
+
284
+ # 更新执行状态
285
+ job["last_run"] = now.isoformat()
286
+ job["last_status"] = status or ("success" if success else "failed")
287
+ job["updated_at"] = now.isoformat()
288
+
289
+ # 连续错误计数
290
+ if success:
291
+ job["consecutive_errors"] = 0
292
+ else:
293
+ job["consecutive_errors"] = job.get("consecutive_errors", 0) + 1
294
+
295
+ # 一次性任务执行后禁用
296
+ if not job.get("recurring", True):
297
+ job["enabled"] = False
298
+
299
+ # 标记 delete_after_run
300
+ if job.get("delete_after_run") and not job.get("recurring", True):
301
+ job["_pending_delete"] = True
302
+
303
+ # 重新计算下次运行时间(仅对重复任务)
304
+ schedule = job.get("schedule", "")
305
+ if job.get("recurring", True) and validate_cron_expression(schedule):
306
+ job["next_run"] = next_run_time(schedule, now).isoformat()
307
+
308
+ save_cron_jobs(jobs)
309
+ return
310
+
311
+
312
+ def remove_expired_jobs() -> list[str]:
313
+ """清理标记为待删除的一次性任务。
314
+
315
+ 返回被删除的任务 ID 列表。
316
+ """
317
+ jobs = load_cron_jobs()
318
+ remaining: list[dict[str, Any]] = []
319
+ removed_ids: list[str] = []
320
+
321
+ for job in jobs:
322
+ if job.get("_pending_delete"):
323
+ removed_ids.append(job.get("id", ""))
324
+ else:
325
+ remaining.append(job)
326
+
327
+ if removed_ids:
328
+ save_cron_jobs(remaining)
329
+
330
+ return removed_ids
331
+
332
+
333
+ def get_consecutive_error_count(identifier: str) -> int:
334
+ """获取任务的连续错误次数。"""
335
+ job = get_cron_job(identifier)
336
+ if job is None:
337
+ return 0
338
+ return job.get("consecutive_errors", 0)