mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-20 12:13:38 +08:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
42
CONTRIBUTING.md
Normal file
42
CONTRIBUTING.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Contributing
|
||||
|
||||
This is a curated teaching repository. Each chapter (`sNN_topic/`) builds one idea on top of the last, and the code is deliberately minimal. The simplifications are intentional, so the lesson stays legible. Contributions are welcome when they serve that goal.
|
||||
|
||||
## Before opening a PR
|
||||
|
||||
1. Tie it to a specific issue. Say what is wrong and why your change is the right fix. One PR addresses one issue.
|
||||
2. Keep teaching code minimal. Do not add production hardening, defensive parsing, error handling layers, abstractions, or a test framework unless the chapter is specifically about that. A simplification that looks like a bug is often deliberate, so ask in an issue first.
|
||||
3. Keep the three languages in sync. Any change to a chapter's `code.py` or a README must be reflected in all three language READMEs in that chapter folder, with the code blocks identical across them.
|
||||
4. Target the current course files (`sNN_topic/`), not legacy mirrors.
|
||||
5. Disclose AI assistance. Using an agent is fine. Say so, and make sure you have read and stand behind the change.
|
||||
|
||||
## What gets closed without detailed review
|
||||
|
||||
To keep the review queue useful, we close:
|
||||
|
||||
1. Bulk PRs generated by an agent and opened across many unrelated repositories at once. We cannot responsibly review code the author has not engaged with.
|
||||
2. Changes that expand a chapter's scope or structure against the teaching design.
|
||||
3. PRs not tied to an issue, or that restate a deliberate simplification as a bug.
|
||||
|
||||
Focused, clearly explained contributions from people who have actually worked with the material are always welcome.
|
||||
|
||||
|
||||
# 贡献指南
|
||||
|
||||
这是一个**教学仓库**。每一章(`sNN_topic/`)在上一章的基础上只讲清一个点,代码刻意保持最简。简化是有意为之,为的是让讲解保持清晰。欢迎服务于这个目标的贡献。
|
||||
|
||||
## 提 PR 之前
|
||||
|
||||
1. 对应一个具体 issue。说清问题是什么、为什么你的改法是对的。一个 PR 只解决一个问题。
|
||||
2. 教学代码保持最简。不要加生产级的防御解析、错误处理层、抽象或测试框架,除非这一章本身就在讲这些。某处"像 bug"的简化往往是刻意的,先在 issue 里问一声。
|
||||
3. 三语保持同步。对某章 `code.py` 或 README 的改动,必须同步到该章目录下三种语言的 README,且其中代码块完全一致。
|
||||
4. 改现行课程文件(`sNN_topic/`),不要改旧的镜像目录。
|
||||
5. 声明 AI 协助。用 agent 没问题,但请说明,并确保你读过、也认可这份改动。
|
||||
|
||||
## 哪些会被直接关闭(不做详细评审)
|
||||
|
||||
1. 在大量互不相关的仓库里一次性批量提交、由 agent 生成的 PR。作者自己都没参与的代码,我们无法负责任地评审。
|
||||
2. 让某章范围或结构超出教学设计的改动。
|
||||
3. 没有对应 issue、或把刻意的简化当成 bug 来"修"的 PR。
|
||||
|
||||
真正用过这份材料、聚焦且讲清理由的贡献,我们始终欢迎。
|
||||
@@ -409,13 +409,13 @@ learn-claude-code/
|
||||
|
||||
Skill & LSP 対応、Windows 対応、GLM / MiniMax / DeepSeek 等のオープンモデルに接続可能。インストールしてすぐ使える。
|
||||
|
||||
GitHub: **[shareAI-lab/Kode-cli](https://github.com/shareAI-lab/Kode-cli)**
|
||||
GitHub: **[shareAI-lab/Kode-CLI](https://github.com/shareAI-lab/Kode-CLI)**
|
||||
|
||||
### Kode Agent SDK -- アプリにエージェント機能を埋め込む
|
||||
|
||||
公式 Claude Code Agent SDK は内部で完全な CLI プロセスと通信する -- 同時ユーザーごとに独立のターミナルプロセスが必要。Kode SDK は独立ライブラリでユーザーごとのプロセスオーバーヘッドがなく、バックエンド、ブラウザ拡張、組み込みデバイス等に埋め込み可能。
|
||||
|
||||
GitHub: **[shareAI-lab/Kode-agent-sdk](https://github.com/shareAI-lab/Kode-agent-sdk)**
|
||||
GitHub: **[shareAI-lab/kode-agent-sdk](https://github.com/shareAI-lab/kode-agent-sdk)**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -410,13 +410,13 @@ learn-claude-code/
|
||||
|
||||
支持 Skill & LSP, 适配 Windows, 可接 GLM / MiniMax / DeepSeek 等开放模型。装完即用。
|
||||
|
||||
GitHub: **[shareAI-lab/Kode-cli](https://github.com/shareAI-lab/Kode-cli)**
|
||||
GitHub: **[shareAI-lab/Kode-CLI](https://github.com/shareAI-lab/Kode-CLI)**
|
||||
|
||||
### Kode Agent SDK -- 把 Agent 能力嵌入你的应用
|
||||
|
||||
官方 Claude Code Agent SDK 底层与完整 CLI 进程通信 -- 每个并发用户 = 一个终端进程。Kode SDK 是独立库, 无 per-user 进程开销, 可嵌入后端、浏览器插件、嵌入式设备等任意运行时。
|
||||
|
||||
GitHub: **[shareAI-lab/Kode-agent-sdk](https://github.com/shareAI-lab/Kode-agent-sdk)**
|
||||
GitHub: **[shareAI-lab/kode-agent-sdk](https://github.com/shareAI-lab/kode-agent-sdk)**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -415,7 +415,7 @@ After 20 lessons, you understand harness engineering from the inside out. Two pa
|
||||
|
||||
Skill and LSP support, Windows compatible, works with GLM / MiniMax / DeepSeek and other open models. Install and go.
|
||||
|
||||
GitHub: **[shareAI-lab/Kode-Agent](https://github.com/shareAI-lab/Kode-Agent)**
|
||||
GitHub: **[shareAI-lab/Kode-CLI](https://github.com/shareAI-lab/Kode-CLI)**
|
||||
|
||||
### Kode Agent SDK -- Embed Agent Capabilities in Your Application
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ def _safe_path(p: str) -> Path:
|
||||
|
||||
|
||||
def _run_bash(command: str) -> str:
|
||||
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot"]
|
||||
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
|
||||
if any(d in command for d in dangerous):
|
||||
return "Error: Dangerous command blocked"
|
||||
try:
|
||||
|
||||
@@ -301,7 +301,7 @@ def _safe_path(p: str) -> Path:
|
||||
|
||||
|
||||
def _run_bash(command: str) -> str:
|
||||
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot"]
|
||||
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
|
||||
if any(d in command for d in dangerous):
|
||||
return "Error: Dangerous command blocked"
|
||||
try:
|
||||
|
||||
@@ -389,7 +389,7 @@ def _safe_path(p: str) -> Path:
|
||||
|
||||
|
||||
def _run_bash(command: str) -> str:
|
||||
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot"]
|
||||
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
|
||||
if any(d in command for d in dangerous):
|
||||
return "Error: Dangerous command blocked"
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# s01: The Agent Loop
|
||||
|
||||
`[ s01 ] s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`
|
||||
`[ s01 ] > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`
|
||||
|
||||
> *"One loop & Bash is all you need"* -- one tool + one loop = an agent.
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# s02: Tool Use
|
||||
|
||||
`s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`
|
||||
`s01 > [ s02 ] > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`
|
||||
|
||||
> *"Adding a tool means adding one handler"* -- the loop stays the same; new tools register into the dispatch map.
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# s03: TodoWrite
|
||||
|
||||
`s01 > s02 > [ s03 ] s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`
|
||||
`s01 > s02 > [ s03 ] > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`
|
||||
|
||||
> *"An agent without a plan drifts"* -- list the steps first, then execute.
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# s04: Subagents
|
||||
|
||||
`s01 > s02 > s03 > [ s04 ] s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`
|
||||
`s01 > s02 > s03 > [ s04 ] > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`
|
||||
|
||||
> *"Break big tasks down; each subtask gets a clean context"* -- subagents use independent messages[], keeping the main conversation clean.
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# s05: Skills
|
||||
|
||||
`s01 > s02 > s03 > s04 > [ s05 ] s06 | s07 > s08 > s09 > s10 > s11 > s12`
|
||||
`s01 > s02 > s03 > s04 > [ s05 ] > s06 | s07 > s08 > s09 > s10 > s11 > s12`
|
||||
|
||||
> *"Load knowledge when you need it, not upfront"* -- inject via tool_result, not the system prompt.
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# s07: Task System
|
||||
|
||||
`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] s08 > s09 > s10 > s11 > s12`
|
||||
`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] > s08 > s09 > s10 > s11 > s12`
|
||||
|
||||
> *"Break big goals into small tasks, order them, persist to disk"* -- a file-based task graph with dependencies, laying the foundation for multi-agent collaboration.
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# s08: Background Tasks
|
||||
|
||||
`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] s09 > s10 > s11 > s12`
|
||||
`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] > s09 > s10 > s11 > s12`
|
||||
|
||||
> *"Run slow operations in the background; the agent keeps thinking"* -- daemon threads run commands, inject notifications on completion.
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# s09: Agent Teams
|
||||
|
||||
`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] s10 > s11 > s12`
|
||||
`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] > s10 > s11 > s12`
|
||||
|
||||
> *"When the task is too big for one, delegate to teammates"* -- persistent teammates + async mailboxes.
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# s10: Team Protocols
|
||||
|
||||
`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] s11 > s12`
|
||||
`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] > s11 > s12`
|
||||
|
||||
> *"Teammates need shared communication rules"* -- one request-response pattern drives all negotiation.
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# s11: Autonomous Agents
|
||||
|
||||
`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] s12`
|
||||
`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] > s12`
|
||||
|
||||
> *"Teammates scan the board and claim tasks themselves"* -- no need for the lead to assign each one.
|
||||
>
|
||||
|
||||
@@ -373,4 +373,6 @@ if __name__ == "__main__":
|
||||
for block in history[-1]["content"]:
|
||||
if getattr(block, "type", None) == "text":
|
||||
print(block.text)
|
||||
elif isinstance(block, dict) and block.get("type") == "text":
|
||||
print(block.get("text", ""))
|
||||
print()
|
||||
|
||||
@@ -475,4 +475,6 @@ if __name__ == "__main__":
|
||||
for block in history[-1]["content"]:
|
||||
if getattr(block, "type", None) == "text":
|
||||
print(block.text)
|
||||
elif isinstance(block, dict) and block.get("type") == "text":
|
||||
print(block.get("text", ""))
|
||||
print()
|
||||
|
||||
@@ -973,6 +973,8 @@ if __name__ == "__main__":
|
||||
for block in history[-1]["content"]:
|
||||
if getattr(block, "type", None) == "text":
|
||||
print(block.text)
|
||||
elif isinstance(block, dict) and block.get("type") == "text":
|
||||
print(block.get("text", ""))
|
||||
|
||||
# Announce once when every teammate has finished and its output drained.
|
||||
if active_teammates:
|
||||
|
||||
@@ -868,6 +868,8 @@ if __name__ == "__main__":
|
||||
for block in history[-1]["content"]:
|
||||
if getattr(block, "type", None) == "text":
|
||||
print(block.text)
|
||||
elif isinstance(block, dict) and block.get("type") == "text":
|
||||
print(block.get("text", ""))
|
||||
|
||||
# Check inbox → route protocol + inject into history
|
||||
inbox_msgs = consume_lead_inbox(route_protocol=True)
|
||||
|
||||
@@ -42,13 +42,13 @@ After completing a task, the teammate doesn't exit. It enters the IDLE phase —
|
||||
IDLE_POLL_INTERVAL = 5 # seconds
|
||||
IDLE_TIMEOUT = 60 # seconds
|
||||
|
||||
def idle_poll(agent_name, messages, name, role) -> str:
|
||||
def idle_poll(name, messages, role) -> str:
|
||||
"""Return 'work', 'shutdown', or 'timeout'."""
|
||||
for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL):
|
||||
time.sleep(IDLE_POLL_INTERVAL)
|
||||
|
||||
# ① Check inbox (priority)
|
||||
inbox = BUS.read_inbox(agent_name)
|
||||
inbox = BUS.read_inbox(name)
|
||||
if inbox:
|
||||
# shutdown_request handled immediately
|
||||
for msg in inbox:
|
||||
@@ -63,7 +63,7 @@ def idle_poll(agent_name, messages, name, role) -> str:
|
||||
unclaimed = scan_unclaimed_tasks()
|
||||
if unclaimed:
|
||||
task = unclaimed[0]
|
||||
result = claim_task(task["id"], agent_name)
|
||||
result = claim_task(task["id"], name)
|
||||
if "Claimed" in result:
|
||||
messages.append(...)
|
||||
return "work"
|
||||
@@ -126,7 +126,7 @@ while True:
|
||||
break # WORK phase ends
|
||||
|
||||
# IDLE phase
|
||||
idle_result = idle_poll(name, messages, name, role)
|
||||
idle_result = idle_poll(name, messages, role)
|
||||
if idle_result == "shutdown":
|
||||
break
|
||||
if idle_result == "timeout":
|
||||
@@ -268,4 +268,4 @@ Teaching version's `idle_poll()` merges CC's four mechanisms into one polling fu
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
|
||||
@@ -42,13 +42,13 @@ S16 の教学版 MessageBus とプロトコルツールを踏襲。本章の追
|
||||
IDLE_POLL_INTERVAL = 5 # seconds
|
||||
IDLE_TIMEOUT = 60 # seconds
|
||||
|
||||
def idle_poll(agent_name, messages, name, role) -> str:
|
||||
def idle_poll(name, messages, role) -> str:
|
||||
"""Return 'work', 'shutdown', or 'timeout'."""
|
||||
for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL):
|
||||
time.sleep(IDLE_POLL_INTERVAL)
|
||||
|
||||
# ① 受信箱確認(優先)
|
||||
inbox = BUS.read_inbox(agent_name)
|
||||
inbox = BUS.read_inbox(name)
|
||||
if inbox:
|
||||
# shutdown_request は即座に処理
|
||||
for msg in inbox:
|
||||
@@ -63,7 +63,7 @@ def idle_poll(agent_name, messages, name, role) -> str:
|
||||
unclaimed = scan_unclaimed_tasks()
|
||||
if unclaimed:
|
||||
task = unclaimed[0]
|
||||
result = claim_task(task["id"], agent_name)
|
||||
result = claim_task(task["id"], name)
|
||||
if "Claimed" in result:
|
||||
messages.append(...)
|
||||
return "work"
|
||||
@@ -126,7 +126,7 @@ while True:
|
||||
break # WORK フェーズ終了
|
||||
|
||||
# IDLE フェーズ
|
||||
idle_result = idle_poll(name, messages, name, role)
|
||||
idle_result = idle_poll(name, messages, role)
|
||||
if idle_result == "shutdown":
|
||||
break
|
||||
if idle_result == "timeout":
|
||||
@@ -268,4 +268,4 @@ s18 Worktree Isolation → 各タスクに専用の作業ディレクトリ、
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
|
||||
@@ -42,13 +42,13 @@ s16 的队友能通信、能握手关机。但每个队友等 Lead 分配任务
|
||||
IDLE_POLL_INTERVAL = 5 # seconds
|
||||
IDLE_TIMEOUT = 60 # seconds
|
||||
|
||||
def idle_poll(agent_name, messages, name, role) -> str:
|
||||
def idle_poll(name, messages, role) -> str:
|
||||
"""Return 'work', 'shutdown', or 'timeout'."""
|
||||
for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL):
|
||||
time.sleep(IDLE_POLL_INTERVAL)
|
||||
|
||||
# ① 检查收件箱(优先)
|
||||
inbox = BUS.read_inbox(agent_name)
|
||||
inbox = BUS.read_inbox(name)
|
||||
if inbox:
|
||||
# shutdown_request 立即处理
|
||||
for msg in inbox:
|
||||
@@ -63,7 +63,7 @@ def idle_poll(agent_name, messages, name, role) -> str:
|
||||
unclaimed = scan_unclaimed_tasks()
|
||||
if unclaimed:
|
||||
task = unclaimed[0]
|
||||
result = claim_task(task["id"], agent_name)
|
||||
result = claim_task(task["id"], name)
|
||||
if "Claimed" in result:
|
||||
messages.append(...)
|
||||
return "work"
|
||||
@@ -126,7 +126,7 @@ while True:
|
||||
break # WORK 阶段结束
|
||||
|
||||
# IDLE phase
|
||||
idle_result = idle_poll(name, messages, name, role)
|
||||
idle_result = idle_poll(name, messages, role)
|
||||
if idle_result == "shutdown":
|
||||
break
|
||||
if idle_result == "timeout":
|
||||
@@ -268,4 +268,4 @@ s18 Worktree Isolation → 每个任务有自己的工作目录,互不干扰
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
|
||||
@@ -301,14 +301,13 @@ def scan_unclaimed_tasks() -> list[dict]:
|
||||
return unclaimed
|
||||
|
||||
|
||||
def idle_poll(agent_name: str, messages: list,
|
||||
name: str, role: str) -> str:
|
||||
def idle_poll(name: str, messages: list, role: str) -> str:
|
||||
"""Poll for 60s. Return 'work', 'shutdown', or 'timeout'."""
|
||||
for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL):
|
||||
time.sleep(IDLE_POLL_INTERVAL)
|
||||
|
||||
# Check inbox — dispatch protocol messages first
|
||||
inbox = BUS.read_inbox(agent_name)
|
||||
inbox = BUS.read_inbox(name)
|
||||
if inbox:
|
||||
# Check for shutdown_request
|
||||
for msg in inbox:
|
||||
@@ -331,7 +330,7 @@ def idle_poll(agent_name: str, messages: list,
|
||||
unclaimed = scan_unclaimed_tasks()
|
||||
if unclaimed:
|
||||
task = unclaimed[0]
|
||||
result = claim_task(task["id"], agent_name)
|
||||
result = claim_task(task["id"], name)
|
||||
if "Claimed" in result:
|
||||
messages.append({"role": "user",
|
||||
"content": f"<auto-claimed>Task {task['id']}: "
|
||||
@@ -498,7 +497,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
|
||||
break
|
||||
|
||||
# IDLE phase (s17 new)
|
||||
idle_result = idle_poll(name, messages, name, role)
|
||||
idle_result = idle_poll(name, messages, role)
|
||||
if idle_result == "shutdown":
|
||||
break
|
||||
if idle_result == "timeout":
|
||||
@@ -800,6 +799,8 @@ if __name__ == "__main__":
|
||||
for block in history[-1]["content"]:
|
||||
if getattr(block, "type", None) == "text":
|
||||
print(block.text)
|
||||
elif isinstance(block, dict) and block.get("type") == "text":
|
||||
print(block.get("text", ""))
|
||||
|
||||
# Consume lead inbox: route protocol + inject into history
|
||||
inbox = consume_lead_inbox(route_protocol=True)
|
||||
|
||||
@@ -984,6 +984,8 @@ if __name__ == "__main__":
|
||||
for block in history[-1]["content"]:
|
||||
if getattr(block, "type", None) == "text":
|
||||
print(block.text)
|
||||
elif isinstance(block, dict) and block.get("type") == "text":
|
||||
print(block.get("text", ""))
|
||||
|
||||
# Consume lead inbox: route protocol + inject into history
|
||||
inbox = consume_lead_inbox(route_protocol=True)
|
||||
|
||||
@@ -1013,6 +1013,8 @@ if __name__ == "__main__":
|
||||
for block in history[-1]["content"]:
|
||||
if getattr(block, "type", None) == "text":
|
||||
print(block.text)
|
||||
elif isinstance(block, dict) and block.get("type") == "text":
|
||||
print(block.get("text", ""))
|
||||
|
||||
inbox = consume_lead_inbox(route_protocol=True)
|
||||
if inbox:
|
||||
|
||||
@@ -2063,8 +2063,8 @@ def print_turn_assistants(messages: list, turn_start: int):
|
||||
if msg.get("role") != "assistant":
|
||||
continue
|
||||
for block in msg.get("content", []):
|
||||
if getattr(block, "type", None) == "text":
|
||||
terminal_print(block.text)
|
||||
if block_type(block) == "text":
|
||||
terminal_print(block["text"] if isinstance(block, dict) else block.text)
|
||||
|
||||
|
||||
def cron_autorun_loop(history: list, context: dict):
|
||||
|
||||
Reference in New Issue
Block a user