Initial commit with translated description

This commit is contained in:
2026-03-29 13:04:57 +08:00
commit e33b5cbac6
9 changed files with 1333 additions and 0 deletions

273
scripts/monitor_task.py Normal file
View File

@@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""
Monitor a long-running task and send periodic status updates every 5 seconds.
Usage:
# Start monitoring
python monitor_task.py start "<task_name>" "<status_type>" [--interval <seconds>]
# Stop monitoring (sends final status)
python monitor_task.py stop "<task_name>" "<final_status>" "<final_message>"
Status Types (for final status):
progress - Ongoing work (shows 🔄)
success - Task complete (shows ✅)
error - Failed task (shows ❌)
warning - Issue but continuing (shows ⚠️)
Examples:
# Start monitoring a video processing task
python monitor_task.py start "video_proc" "progress"
# Later, stop with success
python monitor_task.py stop "video_proc" "success" "Processing complete!"
# Or with an error
python monitor_task.py stop "video_proc" "error" "Failed: Corrupt file"
"""
import sys
sys.stdout.reconfigure(encoding='utf-8')
import time
import json
import threading
from pathlib import Path
from datetime import datetime
from send_status import send_status, can_encode_emoji
# State file to track active monitors
# Path: C:\Users\Luffy\clawd\task-status\scripts\monitor_task.py
# Parent: C:\Users\Luffy\clawd\task-status\scripts
# Parent.parent: C:\Users\Luffy\clawd\task-status
STATE_FILE = Path(__file__).parent.parent / ".task_status_state.json"
def load_state():
"""Load active monitors from state file."""
if STATE_FILE.exists():
try:
with open(STATE_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except:
pass
return {}
def save_state(state):
"""Save active monitors to state file."""
with open(STATE_FILE, 'w', encoding='utf-8') as f:
json.dump(state, f, indent=2)
def monitor_worker(task_name: str, interval: int = 5):
"""
Background worker that sends periodic updates.
Runs until stopped via state file check.
"""
import sys
sys.stdout.reconfigure(encoding='utf-8')
print(f"[DEBUG] monitor_worker started for '{task_name}' with {interval}s interval", file=sys.stderr)
sys.stderr.flush()
last_update = time.time()
print(f"[DEBUG] Initial last_update: {last_update}", file=sys.stderr)
sys.stderr.flush()
iteration = 0
while True:
iteration += 1
# Check if task still exists in state
state = load_state()
print(f"[DEBUG] Iteration {iteration}: State keys = {list(state.keys())}", file=sys.stderr)
sys.stderr.flush()
print(f"[DEBUG] Checking if '{task_name}' in state: {task_name in state}", file=sys.stderr)
sys.stderr.flush()
if task_name not in state:
print(f"[DEBUG] Task '{task_name}' not in state, stopping worker", file=sys.stderr)
sys.stderr.flush()
# Task was stopped
break
# Send "Still working..." update if enough time has passed
current_time = time.time()
elapsed = current_time - last_update
print(f"[DEBUG] Iteration {iteration}: elapsed={elapsed:.1f}s, interval={interval}s", file=sys.stderr)
sys.stderr.flush()
if elapsed >= interval:
print(f"[DEBUG] Time to send update! elapsed={elapsed:.1f}s >= interval={interval}s", file=sys.stderr)
sys.stderr.flush()
# Send progress update
message = f"Still working..."
status = "progress"
# Try to send status
try:
print(f"[DEBUG] Sending status: {message} {status} {task_name}", file=sys.stderr)
sys.stderr.flush()
send_status(message, status, task_name)
print(f"[DEBUG] Status sent successfully", file=sys.stderr)
sys.stderr.flush()
except Exception as e:
# If send fails, log but continue
print(f"[DEBUG] Monitor warning: {e}", file=sys.stderr)
sys.stderr.flush()
last_update = current_time
print(f"[DEBUG] Updated last_update: {last_update}", file=sys.stderr)
sys.stderr.flush()
else:
print(f"[DEBUG] Not time yet, sleeping...", file=sys.stderr)
sys.stderr.flush()
# Sleep briefly to avoid tight loop (reduced from 0.5 to 0.1)
time.sleep(0.1)
print(f"[DEBUG] monitor_worker finished", file=sys.stderr)
sys.stderr.flush()
def start_monitor(task_name: str, status_type: str = "progress", interval: int = 5):
"""
Start a new monitor for the given task.
"""
import sys
sys.stdout.reconfigure(encoding='utf-8')
print(f"[DEBUG] start_monitor called: task_name='{task_name}', status_type='{status_type}', interval={interval}", file=sys.stderr)
# Validate status type
if status_type not in ["progress", "success", "error", "warning"]:
print(f"Invalid status type: {status_type}", file=sys.stderr)
sys.exit(1)
# Check if already monitoring
state = load_state()
print(f"[DEBUG] Current state: {state}", file=sys.stderr)
if task_name in state:
print(f"Already monitoring task '{task_name}'", file=sys.stderr)
sys.exit(1)
# Add to state
state[task_name] = {
"status_type": status_type,
"interval": interval,
"started_at": datetime.now().isoformat()
}
print(f"[DEBUG] Saving new state: {state}", file=sys.stderr)
save_state(state)
# Send initial status
print(f"[DEBUG] Sending initial status", file=sys.stderr)
send_status(f"Monitoring started (updates every {interval}s)", status_type, task_name)
# Start background monitor thread
print(f"[DEBUG] Starting monitor thread", file=sys.stderr)
thread = threading.Thread(
target=monitor_worker,
args=(task_name, interval),
daemon=True
)
thread.start()
print(f"[DEBUG] Monitor thread started: {thread}", file=sys.stderr)
print(f"Monitor started for '{task_name}' with {interval}s interval")
def stop_monitor(task_name: str, final_status: str, final_message: str):
"""
Stop monitoring and send final status.
"""
import sys
sys.stdout.reconfigure(encoding='utf-8')
print(f"[DEBUG] stop_monitor called: task_name='{task_name}', final_status='{final_status}', final_message='{final_message}'", file=sys.stderr)
# Check if monitoring
state = load_state()
print(f"[DEBUG] Current state: {state}", file=sys.stderr)
if task_name not in state:
print(f"No active monitor for task '{task_name}'", file=sys.stderr)
sys.exit(1)
# Remove from state
print(f"[DEBUG] Removing '{task_name}' from state", file=sys.stderr)
del state[task_name]
print(f"[DEBUG] New state: {state}", file=sys.stderr)
save_state(state)
# Send final status
print(f"[DEBUG] Sending final status", file=sys.stderr)
send_status(final_message, final_status, task_name)
print(f"Monitor stopped for '{task_name}'")
print(f"Final status: {final_message}")
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
action = sys.argv[1].lower()
if action == "start":
if len(sys.argv) < 4:
print("Usage: monitor_task.py start <task_name> <status_type> [--interval <seconds>]", file=sys.stderr)
sys.exit(1)
task_name = sys.argv[2]
status_type = sys.argv[3]
interval = 5
# Check for --interval flag
if len(sys.argv) > 5 and sys.argv[4] == "--interval":
try:
interval = int(sys.argv[5])
except ValueError:
print("Interval must be an integer", file=sys.stderr)
sys.exit(1)
start_monitor(task_name, status_type, interval)
elif action == "stop":
if len(sys.argv) < 5:
print("Usage: monitor_task.py stop <task_name> <final_status> <final_message>", file=sys.stderr)
sys.exit(1)
task_name = sys.argv[2]
final_status = sys.argv[3]
final_message = sys.argv[4]
stop_monitor(task_name, final_status, final_message)
elif action == "status":
# Show active monitors
state = load_state()
if not state:
print("No active monitors")
else:
print("Active monitors:")
for task, info in state.items():
print(f" {task}: {info['status_type']} (interval: {info['interval']}s)")
elif action == "cancel_all":
# Stop all monitors without sending final status
state = load_state()
if state:
for task_name in list(state.keys()):
del state[task_name]
save_state(state)
print(f"Cancelled all monitors: {list(state.keys())}")
else:
print("No active monitors to cancel")
else:
print(f"Unknown action: {action}", file=sys.stderr)
print(__doc__)
sys.exit(1)
if __name__ == "__main__":
main()

180
scripts/send_status.py Normal file
View File

@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""
Send status messages for long-running tasks.
Usage:
python send_status.py "<message>" "<status_type>" "<step_name>" [--details "<details>"]
Status Types:
progress - Ongoing work (shows 🔄)
success - Task complete (shows ✅)
error - Failed task (shows ❌)
warning - Issue but continuing (shows ⚠️)
Example:
python send_status.py "Fetching data..." "progress" "fetch"
python send_status.py "Complete!" "success" "final"
python send_status.py "Error: Missing file" "error" "file_check"
"""
import sys
sys.stdout.reconfigure(encoding='utf-8')
import json
import os
import websocket
# Status type to emoji mapping
STATUS_EMOJIS = {
"progress": "🔄",
"success": "",
"error": "",
"warning": "⚠️"
}
def can_encode_emoji(text: str, encoding: str = None) -> bool:
"""Check if text can be encoded with the given encoding."""
if encoding is None:
encoding = sys.stdout.encoding
try:
text.encode(encoding)
return True
except (UnicodeEncodeError, LookupError):
return False
def send_status(message: str, status_type: str, step_name: str, details: str = None):
"""
Format and send a status message to Telegram.
Args:
message: Short status message (< 140 chars)
status_type: Type of status (progress, success, error, warning)
step_name: Name of the step being reported
details: Optional additional context
"""
if status_type not in STATUS_EMOJIS:
raise ValueError(f"Invalid status_type: {status_type}")
emoji = STATUS_EMOJIS[status_type]
# Choose emoji or ASCII based on encoding capability
if can_encode_emoji(emoji):
prefix = emoji
else:
prefix = emoji # Use emoji anyway, most modern terminals support it
# Build the message
formatted = f"{prefix} [{step_name}] {message}"
if details:
formatted += f" ({details})"
# Keep it concise (under 140 chars)
if len(formatted) > 140:
formatted = formatted[:137] + "..."
# Try WebSocket first (fastest)
gateway_token = os.environ.get("CLAWDBOT_GATEWAY_TOKEN")
if gateway_token:
try:
gateway_port = os.environ.get("CLAWDBOT_GATEWAY_PORT", "18789")
target = os.environ.get("TELEGRAM_TARGET", "7590912486")
ws_url = f"ws://127.0.0.1:{gateway_port}/ws"
# Connect and send
ws = websocket.create_connection(ws_url, timeout=10)
# Send message directly (no handshake needed for simple messages)
msg = {
"type": "message",
"action": "send",
"target": target,
"message": formatted,
"channel": "telegram"
}
ws.send(json.dumps(msg))
# Try to receive response but don't wait too long
try:
response = ws.recv()
result = json.loads(response)
# If we got a challenge, respond to it
if result.get("event") == "connect.challenge":
# Send handshake with token
handshake = {"type": "handshake", "token": gateway_token}
ws.send(json.dumps(handshake))
# Try to receive again
response = ws.recv()
result = json.loads(response)
except:
# If we can't receive, assume message was sent
pass
ws.close()
return formatted
except Exception as e:
print(f"⚠️ WebSocket failed: {e}", file=sys.stderr)
# Fallback: try CLI
import subprocess
import shutil
clawdbot_path = shutil.which("clawdbot")
if not clawdbot_path:
clawdbot_paths = [
"C:\\Users\\Luffy\\AppData\\Roaming\\npm\\clawdbot.cmd",
"C:\\Users\\Luffy\\AppData\\Roaming\\npm\\clawdbot"
]
for path in clawdbot_paths:
if os.path.exists(path):
clawdbot_path = path
break
if clawdbot_path:
try:
target = os.environ.get("TELEGRAM_TARGET", "7590912486")
result = subprocess.run(
[
clawdbot_path,
"message",
"send",
"--target", target,
"--message", formatted,
"--channel", "telegram"
],
capture_output=True,
text=True,
timeout=20
)
if result.returncode == 0:
return formatted
except Exception as e:
print(f"⚠️ CLI failed: {e}", file=sys.stderr)
# Final fallback: print to console
print(formatted, file=sys.stderr)
return formatted
def main():
if len(sys.argv) < 4:
print(__doc__)
sys.exit(1)
message = sys.argv[1]
status_type = sys.argv[2]
step_name = sys.argv[3]
details = sys.argv[4] if len(sys.argv) > 4 else None
try:
result = send_status(message, status_type, step_name, details)
print(f"Status sent: {result}")
except Exception as e:
print(f"Error sending status: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Send status messages using Clawdbot WebSocket API (fast).
"""
import sys
sys.stdout.reconfigure(encoding='utf-8')
import os
import json
import websocket
# Status type to emoji mapping
STATUS_EMOJIS = {
"progress": "🔄",
"success": "",
"error": "",
"warning": "⚠️"
}
def send_status(message: str, status_type: str, step_name: str, details: str = None):
"""
Format and send a status message to Telegram via WebSocket.
Args:
message: Short status message (< 140 chars)
status_type: Type of status (progress, success, error, warning)
step_name: Name of the step being reported
details: Optional additional context
"""
if status_type not in STATUS_EMOJIS:
raise ValueError(f"Invalid status_type: {status_type}")
emoji = STATUS_EMOJIS[status_type]
# Build the message
formatted = f"{emoji} [{step_name}] {message}"
if details:
formatted += f" ({details})"
# Keep it concise (under 140 chars)
if len(formatted) > 140:
formatted = formatted[:137] + "..."
# Send via WebSocket
gateway_port = os.environ.get("CLAWDBOT_GATEWAY_PORT", "18789")
gateway_token = os.environ.get("CLAWDBOT_GATEWAY_TOKEN")
target = os.environ.get("TELEGRAM_TARGET", "7590912486")
if not gateway_token:
print(f"✗ CLAWDBOT_GATEWAY_TOKEN not found", file=sys.stderr)
return formatted
ws_url = f"ws://127.0.0.1:{gateway_port}/ws"
try:
# Connect and send
ws = websocket.create_connection(ws_url, timeout=10)
# Handshake
handshake = {"type": "handshake", "token": gateway_token}
ws.send(json.dumps(handshake))
# Send message
msg = {
"type": "message",
"action": "send",
"target": target,
"message": formatted,
"channel": "telegram"
}
ws.send(json.dumps(msg))
# Get response
response = ws.recv()
result = json.loads(response)
ws.close()
# Check if message was sent
if result.get("event") == "message.sent" or result.get("type") == "ack":
return formatted
else:
print(f"⚠️ Response: {result}", file=sys.stderr)
return formatted
except Exception as e:
print(f"✗ WebSocket error: {e}", file=sys.stderr)
# Fallback: print to console
print(formatted, file=sys.stderr)
return formatted
def main():
if len(sys.argv) < 4:
print("Usage: send_status_websocket.py <message> <status_type> <step_name>")
print("\nStatus Types: progress, success, error, warning")
sys.exit(1)
message = sys.argv[1]
status_type = sys.argv[2]
step_name = sys.argv[3]
details = sys.argv[4] if len(sys.argv) > 4 else None
try:
result = send_status(message, status_type, step_name, details)
print(f"✓ Status sent: {result}")
except Exception as e:
print(f"✗ Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""
Send status messages to Telegram with automatic logging.
This is the production version of send_status.py.
"""
import sys
sys.stdout.reconfigure(encoding='utf-8')
import json
import os
import websocket
from datetime import datetime
from pathlib import Path
# Configuration
LOG_DIR = Path("C:/Users/Luffy/clawd/logs")
LOG_FILE = LOG_DIR / "telegram_messages.jsonl"
TASK_LOG_FILE = LOG_DIR / "task_status.jsonl"
# Status type to emoji mapping
STATUS_EMOJIS = {
"progress": "🔄",
"success": "",
"error": "",
"warning": "⚠️"
}
def ensure_log_dir():
"""Create log directory if it doesn't exist."""
LOG_DIR.mkdir(exist_ok=True)
def log_message(message: str, direction: str = "out", task_name: str = None, status_type: str = None):
"""Log a message to the file."""
ensure_log_dir()
try:
data = {
"timestamp": datetime.now().isoformat(),
"direction": direction,
"message": message
}
if task_name:
data["task"] = task_name
if status_type:
data["status"] = status_type
with open(LOG_FILE, 'a', encoding='utf-8') as f:
f.write(json.dumps(data, ensure_ascii=False) + '\n')
return True
except Exception as e:
print(f"[LOG] Error: {e}", file=sys.stderr)
return False
def can_encode_emoji(text: str, encoding: str = None) -> bool:
"""Check if text can be encoded with the given encoding."""
if encoding is None:
encoding = sys.stdout.encoding
try:
text.encode(encoding)
return True
except (UnicodeEncodeError, LookupError):
return False
def send_status(message: str, status_type: str, step_name: str, details: str = None):
"""
Format and send a status message to Telegram with logging.
Args:
message: Short status message (< 140 chars)
status_type: Type of status (progress, success, error, warning)
step_name: Name of the step being reported
details: Optional additional context
"""
if status_type not in STATUS_EMOJIS:
raise ValueError(f"Invalid status_type: {status_type}")
emoji = STATUS_EMOJIS[status_type]
# Choose emoji or ASCII based on encoding capability
if can_encode_emoji(emoji):
prefix = emoji
else:
prefix = emoji # Most modern terminals support emojis
# Build the message
formatted = f"{prefix} [{step_name}] {message}"
if details:
formatted += f" ({details})"
# Keep it concise (under 140 chars)
if len(formatted) > 140:
formatted = formatted[:137] + "..."
# Log the message (before sending)
log_message(formatted, direction="out", task_name=step_name, status_type=status_type)
# Try WebSocket first (fastest)
gateway_token = os.environ.get("CLAWDBOT_GATEWAY_TOKEN")
if gateway_token:
try:
gateway_port = os.environ.get("CLAWDBOT_GATEWAY_PORT", "18789")
target = os.environ.get("TELEGRAM_TARGET", "7590912486")
ws_url = f"ws://127.0.0.1:{gateway_port}/ws"
# Connect and send
ws = websocket.create_connection(ws_url, timeout=10)
# Send message directly (no handshake needed for simple messages)
msg = {
"type": "message",
"action": "send",
"target": target,
"message": formatted,
"channel": "telegram"
}
ws.send(json.dumps(msg))
# Try to receive response but don't wait too long
try:
response = ws.recv()
result = json.loads(response)
# If we got a challenge, respond to it
if result.get("event") == "connect.challenge":
# Send handshake with token
handshake = {
"type": "handshake",
"token": gateway_token,
"nonce": result.get("payload", {}).get("nonce")
}
ws.send(json.dumps(handshake))
# Try to receive again
response = ws.recv()
result = json.loads(response)
except:
# If we can't receive, assume message was sent
pass
ws.close()
# Log successful send
log_message(formatted, direction="out_sent", task_name=step_name, status_type=status_type)
return formatted
except Exception as e:
print(f"[LOG] WebSocket failed: {e}", file=sys.stderr)
# Fallback: try CLI
import subprocess
import shutil
clawdbot_path = shutil.which("clawdbot")
if not clawdbot_path:
clawdbot_paths = [
"C:\\Users\\Luffy\\AppData\\Roaming\\npm\\clawdbot.cmd",
"C:\\Users\\Luffy\\AppData\\Roaming\\npm\\clawdbot"
]
for path in clawdbot_paths:
if os.path.exists(path):
clawdbot_path = path
break
if clawdbot_path:
try:
target = os.environ.get("TELEGRAM_TARGET", "7590912486")
result = subprocess.run(
[
clawdbot_path,
"message",
"send",
"--target", target,
"--message", formatted,
"--channel", "telegram"
],
capture_output=True,
text=True,
timeout=20
)
if result.returncode == 0:
# Log successful send
log_message(formatted, direction="out_sent", task_name=step_name, status_type=status_type)
return formatted
except Exception as e:
print(f"[LOG] CLI failed: {e}", file=sys.stderr)
# Final fallback: print to console
print(formatted, file=sys.stderr)
log_message(formatted, direction="out_failed", task_name=step_name, status_type=status_type)
return formatted
def main():
if len(sys.argv) < 4:
print("Usage: send_status_with_logging.py <message> <status_type> <step_name>")
print("\nStatus Types: progress, success, error, warning")
sys.exit(1)
message = sys.argv[1]
status_type = sys.argv[2]
step_name = sys.argv[3]
try:
result = send_status(message, status_type, step_name)
print(f"✓ Sent and logged: {result}")
except Exception as e:
print(f"✗ Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""
Test script for send_status.py
"""
import sys
import subprocess
from pathlib import Path
def test_send_status():
"""Test the status sending functionality."""
script_path = Path(__file__).parent / "send_status.py"
# Test cases
test_cases = [
("Test progress message", "progress", "test_step", None),
("Test success message", "success", "test_step", None),
("Test error message", "error", "test_step", None),
("Test warning message", "warning", "test_step", None),
("Long message test", "progress", "step1", "with details about the step"),
]
print("Testing send_status.py...")
print("=" * 50)
for message, status_type, step_name, details in test_cases:
print(f"\nTest: {message}")
print(f" Type: {status_type}, Step: {step_name}")
# Build command
cmd = [sys.executable, str(script_path), message, status_type, step_name]
if details:
cmd.append(details)
try:
# Run the command
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
print(f" PASS: Success")
output = result.stdout.strip()
if output:
print(f" Output: {output}")
else:
print(f" FAIL: Failed with return code {result.returncode}")
if result.stderr:
print(f" Error: {result.stderr.strip()}")
except subprocess.TimeoutExpired:
print(f" FAIL: Timed out")
except Exception as e:
print(f" FAIL: Exception: {e}")
print("\n" + "=" * 50)
print("Testing complete")
if __name__ == "__main__":
test_send_status()