sophhub 0.2.2 → 0.2.4
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/package.json +1 -1
- package/skills/consensus/skill.json +20 -0
- package/skills/consensus/src/SKILL.md +93 -0
- package/skills/deepwiki/skill.json +20 -0
- package/skills/deepwiki/src/SKILL.md +45 -0
- package/skills/deepwiki/src/_meta.json +6 -0
- package/skills/deepwiki/src/scripts/deepwiki.js +135 -0
- package/skills/feishu-bitable/skill.json +20 -0
- package/skills/feishu-bitable/src/CHECKLIST.md +150 -0
- package/skills/feishu-bitable/src/README.md +178 -0
- package/skills/feishu-bitable/src/SKILL.md +113 -0
- package/skills/feishu-bitable/src/_meta.json +6 -0
- package/skills/feishu-bitable/src/api.js +381 -0
- package/skills/feishu-bitable/src/bin/cli.js +284 -0
- package/skills/feishu-bitable/src/description.md +143 -0
- package/skills/feishu-bitable/src/examples/create-records.json +52 -0
- package/skills/feishu-bitable/src/examples/create-table.json +64 -0
- package/skills/feishu-bitable/src/package-lock.json +324 -0
- package/skills/feishu-bitable/src/package.json +33 -0
- package/skills/feishu-bitable/src/publish-config.json +14 -0
- package/skills/feishu-bitable/src/test-simple.js +61 -0
- package/skills/feishu-bitable/src/utils.js +261 -0
- package/skills/flight-booking/skill.json +9 -2
- package/skills/flight-booking/src/scripts/flight_booking.py +2 -1
- package/skills/google-maps/skill.json +20 -0
- package/skills/google-maps/src/SKILL.md +237 -0
- package/skills/google-maps/src/_meta.json +6 -0
- package/skills/google-maps/src/lib/map_helper.py +912 -0
- package/skills/large-task-router/skill.json +20 -0
- package/skills/large-task-router/src/SKILL.md +79 -0
- package/skills/large-task-router/src/templates/plan.md +74 -0
- package/skills/skillhub/skill.json +11 -4
- package/skills/skillhub/src/SKILL.md +11 -1
- package/skills/sophnet-dailynews/skill.json +20 -0
- package/skills/sophnet-dailynews/src/SKILL.md +179 -0
- package/skills/sophnet-dailynews/src/cache.json +151 -0
- package/skills/sophnet-dailynews/src/sources.json +230 -0
- package/skills/sophnet-schedule/skill.json +20 -0
- package/skills/sophnet-schedule/src/ARCHITECTURE.md +321 -0
- package/skills/sophnet-schedule/src/IMPROVEMENTS.md +145 -0
- package/skills/sophnet-schedule/src/SKILL.md +1050 -0
- package/skills/sophnet-schedule/src/_meta.json +6 -0
- package/skills/sophnet-schedule/src/api/__init__.py +0 -0
- package/skills/sophnet-schedule/src/api/models.py +245 -0
- package/skills/sophnet-schedule/src/apps/add_event.py +237 -0
- package/skills/sophnet-schedule/src/apps/check_reminders.py +112 -0
- package/skills/sophnet-schedule/src/apps/check_roc.py +246 -0
- package/skills/sophnet-schedule/src/apps/generate_daily_plan.py +342 -0
- package/skills/sophnet-schedule/src/apps/import_events.py +216 -0
- package/skills/sophnet-schedule/src/apps/monitor_calendar_changes.py +140 -0
- package/skills/sophnet-schedule/src/apps/register_tasks.py +169 -0
- package/skills/sophnet-schedule/src/apps/sync_roc_to_gcal.py +174 -0
- package/skills/sophnet-schedule/src/compat.py +66 -0
- package/skills/sophnet-schedule/src/config/__init__.py +0 -0
- package/skills/sophnet-schedule/src/config/reminder_rules.yaml +96 -0
- package/skills/sophnet-schedule/src/config/roc_events.yaml +44 -0
- package/skills/sophnet-schedule/src/config/settings.py +133 -0
- package/skills/sophnet-schedule/src/config/task_registry.yaml +92 -0
- package/skills/sophnet-schedule/src/docs/FRONTEND_INTEGRATION_GUIDE.md +437 -0
- package/skills/sophnet-schedule/src/gcal/__init__.py +0 -0
- package/skills/sophnet-schedule/src/gcal/client.py +374 -0
- package/skills/sophnet-schedule/src/gcal/models.py +91 -0
- package/skills/sophnet-schedule/src/requirements.txt +6 -0
- package/skills/sophnet-schedule/src/scripts/setup_gcal_token.py +85 -0
- package/skills/sophnet-schedule/src/server.py +669 -0
- package/skills/sophnet-schedule/src/services/__init__.py +0 -0
- package/skills/sophnet-schedule/src/services/calendar_backend.py +139 -0
- package/skills/sophnet-schedule/src/services/conflict_detector.py +96 -0
- package/skills/sophnet-schedule/src/services/datetime_utils.py +117 -0
- package/skills/sophnet-schedule/src/services/event_classifier.py +100 -0
- package/skills/sophnet-schedule/src/services/event_diff.py +160 -0
- package/skills/sophnet-schedule/src/services/google_integration.py +500 -0
- package/skills/sophnet-schedule/src/services/job_store.py +100 -0
- package/skills/sophnet-schedule/src/services/local_event_store.py +266 -0
- package/skills/sophnet-schedule/src/services/reminder_planner.py +116 -0
- package/skills/sophnet-schedule/src/services/runtime_utils.py +31 -0
- package/skills/sophnet-schedule/src/services/table_parser.py +286 -0
- package/skills/sophnet-schedule/src/services/task_builder.py +167 -0
- package/skills/sophnet-schedule/src/services/time_window.py +72 -0
- package/skills/sophnet-stock/skill.json +20 -0
- package/skills/sophnet-stock/src/App-Plan.md +442 -0
- package/skills/sophnet-stock/src/README.md +214 -0
- package/skills/sophnet-stock/src/SKILL.md +236 -0
- package/skills/sophnet-stock/src/TODO.md +394 -0
- package/skills/sophnet-stock/src/_meta.json +6 -0
- package/skills/sophnet-stock/src/docs/ARCHITECTURE.md +408 -0
- package/skills/sophnet-stock/src/docs/CONCEPT.md +233 -0
- package/skills/sophnet-stock/src/docs/HOT_SCANNER.md +288 -0
- package/skills/sophnet-stock/src/docs/README.md +95 -0
- package/skills/sophnet-stock/src/docs/USAGE.md +465 -0
- package/skills/sophnet-stock/src/scripts/analyze_stock.py +2565 -0
- package/skills/sophnet-stock/src/scripts/dividends.py +365 -0
- package/skills/sophnet-stock/src/scripts/hot_scanner.py +582 -0
- package/skills/sophnet-stock/src/scripts/portfolio.py +548 -0
- package/skills/sophnet-stock/src/scripts/rumor_scanner.py +342 -0
- package/skills/sophnet-stock/src/scripts/test_stock_analysis.py +409 -0
- package/skills/sophnet-stock/src/scripts/watchlist.py +336 -0
- package/skills/xiaohongshu/skill.json +20 -0
- package/skills/xiaohongshu/src/SKILL.md +91 -0
- package/skills/xiaohongshu/src/_meta.json +6 -0
- package/skills/xiaohongshu/src/assets/card.html +216 -0
- package/skills/xiaohongshu/src/assets/cover.html +82 -0
- package/skills/xiaohongshu/src/assets/example.md +84 -0
- package/skills/xiaohongshu/src/assets/styles.css +318 -0
- package/skills/xiaohongshu/src/scripts/render_xhs_v2.py +737 -0
- package/skills/xiaohongshu/src/scripts/sign_server.py +158 -0
- package/skills/xiaohongshu/src/scripts/stealth.min.js +7 -0
- package/skills/xiaohongshu/src/scripts/xhs_tool.py +186 -0
- package/skills/xiaohongshu/src/workflow.py +185 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "yfinance>=0.2.40",
|
|
6
|
+
# ]
|
|
7
|
+
# ///
|
|
8
|
+
"""
|
|
9
|
+
Stock Watchlist with Price Alerts.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
uv run watchlist.py add AAPL # Add to watchlist
|
|
13
|
+
uv run watchlist.py add AAPL --target 200 # With price target
|
|
14
|
+
uv run watchlist.py add AAPL --stop 150 # With stop loss
|
|
15
|
+
uv run watchlist.py add AAPL --alert-on signal # Alert on signal change
|
|
16
|
+
uv run watchlist.py remove AAPL # Remove from watchlist
|
|
17
|
+
uv run watchlist.py list # Show watchlist
|
|
18
|
+
uv run watchlist.py check # Check for triggered alerts
|
|
19
|
+
uv run watchlist.py check --notify # Check and format for notification
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from dataclasses import dataclass, asdict
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Literal
|
|
29
|
+
|
|
30
|
+
import yfinance as yf
|
|
31
|
+
|
|
32
|
+
# Storage
|
|
33
|
+
WATCHLIST_DIR = Path.home() / ".clawdbot" / "skills" / "stock-analysis"
|
|
34
|
+
WATCHLIST_FILE = WATCHLIST_DIR / "watchlist.json"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class WatchlistItem:
|
|
39
|
+
ticker: str
|
|
40
|
+
added_at: str
|
|
41
|
+
price_at_add: float | None = None
|
|
42
|
+
target_price: float | None = None # Alert when price >= target
|
|
43
|
+
stop_price: float | None = None # Alert when price <= stop
|
|
44
|
+
alert_on_signal: bool = False # Alert when recommendation changes
|
|
45
|
+
last_signal: str | None = None # BUY/HOLD/SELL
|
|
46
|
+
last_check: str | None = None
|
|
47
|
+
notes: str | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Alert:
|
|
52
|
+
ticker: str
|
|
53
|
+
alert_type: Literal["target_hit", "stop_hit", "signal_change"]
|
|
54
|
+
message: str
|
|
55
|
+
current_price: float
|
|
56
|
+
trigger_value: float | str
|
|
57
|
+
timestamp: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def ensure_dirs():
|
|
61
|
+
"""Create storage directories."""
|
|
62
|
+
WATCHLIST_DIR.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load_watchlist() -> list[WatchlistItem]:
|
|
66
|
+
"""Load watchlist from file."""
|
|
67
|
+
if WATCHLIST_FILE.exists():
|
|
68
|
+
data = json.loads(WATCHLIST_FILE.read_text())
|
|
69
|
+
return [WatchlistItem(**item) for item in data]
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def save_watchlist(items: list[WatchlistItem]):
|
|
74
|
+
"""Save watchlist to file."""
|
|
75
|
+
ensure_dirs()
|
|
76
|
+
data = [asdict(item) for item in items]
|
|
77
|
+
WATCHLIST_FILE.write_text(json.dumps(data, indent=2))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_current_price(ticker: str) -> float | None:
|
|
81
|
+
"""Get current price for a ticker."""
|
|
82
|
+
try:
|
|
83
|
+
stock = yf.Ticker(ticker)
|
|
84
|
+
price = stock.info.get("regularMarketPrice") or stock.info.get("currentPrice")
|
|
85
|
+
return float(price) if price else None
|
|
86
|
+
except Exception:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def add_to_watchlist(
|
|
91
|
+
ticker: str,
|
|
92
|
+
target_price: float | None = None,
|
|
93
|
+
stop_price: float | None = None,
|
|
94
|
+
alert_on_signal: bool = False,
|
|
95
|
+
notes: str | None = None,
|
|
96
|
+
) -> dict:
|
|
97
|
+
"""Add ticker to watchlist."""
|
|
98
|
+
ticker = ticker.upper()
|
|
99
|
+
|
|
100
|
+
# Validate ticker
|
|
101
|
+
current_price = get_current_price(ticker)
|
|
102
|
+
if current_price is None:
|
|
103
|
+
return {"success": False, "error": f"Invalid ticker: {ticker}"}
|
|
104
|
+
|
|
105
|
+
# Load existing watchlist
|
|
106
|
+
watchlist = load_watchlist()
|
|
107
|
+
|
|
108
|
+
# Check if already exists
|
|
109
|
+
for item in watchlist:
|
|
110
|
+
if item.ticker == ticker:
|
|
111
|
+
# Update existing
|
|
112
|
+
item.target_price = target_price or item.target_price
|
|
113
|
+
item.stop_price = stop_price or item.stop_price
|
|
114
|
+
item.alert_on_signal = alert_on_signal or item.alert_on_signal
|
|
115
|
+
item.notes = notes or item.notes
|
|
116
|
+
save_watchlist(watchlist)
|
|
117
|
+
return {
|
|
118
|
+
"success": True,
|
|
119
|
+
"action": "updated",
|
|
120
|
+
"ticker": ticker,
|
|
121
|
+
"current_price": current_price,
|
|
122
|
+
"target_price": item.target_price,
|
|
123
|
+
"stop_price": item.stop_price,
|
|
124
|
+
"alert_on_signal": item.alert_on_signal,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Add new
|
|
128
|
+
item = WatchlistItem(
|
|
129
|
+
ticker=ticker,
|
|
130
|
+
added_at=datetime.now(timezone.utc).isoformat(),
|
|
131
|
+
price_at_add=current_price,
|
|
132
|
+
target_price=target_price,
|
|
133
|
+
stop_price=stop_price,
|
|
134
|
+
alert_on_signal=alert_on_signal,
|
|
135
|
+
notes=notes,
|
|
136
|
+
)
|
|
137
|
+
watchlist.append(item)
|
|
138
|
+
save_watchlist(watchlist)
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"success": True,
|
|
142
|
+
"action": "added",
|
|
143
|
+
"ticker": ticker,
|
|
144
|
+
"current_price": current_price,
|
|
145
|
+
"target_price": target_price,
|
|
146
|
+
"stop_price": stop_price,
|
|
147
|
+
"alert_on_signal": alert_on_signal,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def remove_from_watchlist(ticker: str) -> dict:
|
|
152
|
+
"""Remove ticker from watchlist."""
|
|
153
|
+
ticker = ticker.upper()
|
|
154
|
+
watchlist = load_watchlist()
|
|
155
|
+
|
|
156
|
+
original_len = len(watchlist)
|
|
157
|
+
watchlist = [item for item in watchlist if item.ticker != ticker]
|
|
158
|
+
|
|
159
|
+
if len(watchlist) == original_len:
|
|
160
|
+
return {"success": False, "error": f"{ticker} not in watchlist"}
|
|
161
|
+
|
|
162
|
+
save_watchlist(watchlist)
|
|
163
|
+
return {"success": True, "removed": ticker}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def list_watchlist() -> dict:
|
|
167
|
+
"""List all watchlist items with current prices."""
|
|
168
|
+
watchlist = load_watchlist()
|
|
169
|
+
|
|
170
|
+
if not watchlist:
|
|
171
|
+
return {"success": True, "items": [], "count": 0}
|
|
172
|
+
|
|
173
|
+
items = []
|
|
174
|
+
for item in watchlist:
|
|
175
|
+
current_price = get_current_price(item.ticker)
|
|
176
|
+
|
|
177
|
+
# Calculate change since added
|
|
178
|
+
change_pct = None
|
|
179
|
+
if current_price and item.price_at_add:
|
|
180
|
+
change_pct = ((current_price - item.price_at_add) / item.price_at_add) * 100
|
|
181
|
+
|
|
182
|
+
# Distance to target/stop
|
|
183
|
+
to_target = None
|
|
184
|
+
to_stop = None
|
|
185
|
+
if current_price:
|
|
186
|
+
if item.target_price:
|
|
187
|
+
to_target = ((item.target_price - current_price) / current_price) * 100
|
|
188
|
+
if item.stop_price:
|
|
189
|
+
to_stop = ((item.stop_price - current_price) / current_price) * 100
|
|
190
|
+
|
|
191
|
+
items.append({
|
|
192
|
+
"ticker": item.ticker,
|
|
193
|
+
"current_price": current_price,
|
|
194
|
+
"price_at_add": item.price_at_add,
|
|
195
|
+
"change_pct": round(change_pct, 2) if change_pct else None,
|
|
196
|
+
"target_price": item.target_price,
|
|
197
|
+
"to_target_pct": round(to_target, 2) if to_target else None,
|
|
198
|
+
"stop_price": item.stop_price,
|
|
199
|
+
"to_stop_pct": round(to_stop, 2) if to_stop else None,
|
|
200
|
+
"alert_on_signal": item.alert_on_signal,
|
|
201
|
+
"last_signal": item.last_signal,
|
|
202
|
+
"added_at": item.added_at[:10],
|
|
203
|
+
"notes": item.notes,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
return {"success": True, "items": items, "count": len(items)}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def check_alerts(notify_format: bool = False) -> dict:
|
|
210
|
+
"""Check watchlist for triggered alerts."""
|
|
211
|
+
watchlist = load_watchlist()
|
|
212
|
+
alerts: list[Alert] = []
|
|
213
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
214
|
+
|
|
215
|
+
for item in watchlist:
|
|
216
|
+
current_price = get_current_price(item.ticker)
|
|
217
|
+
if current_price is None:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
# Check target price
|
|
221
|
+
if item.target_price and current_price >= item.target_price:
|
|
222
|
+
alerts.append(Alert(
|
|
223
|
+
ticker=item.ticker,
|
|
224
|
+
alert_type="target_hit",
|
|
225
|
+
message=f"🎯 {item.ticker} hit target! ${current_price:.2f} >= ${item.target_price:.2f}",
|
|
226
|
+
current_price=current_price,
|
|
227
|
+
trigger_value=item.target_price,
|
|
228
|
+
timestamp=now,
|
|
229
|
+
))
|
|
230
|
+
|
|
231
|
+
# Check stop price
|
|
232
|
+
if item.stop_price and current_price <= item.stop_price:
|
|
233
|
+
alerts.append(Alert(
|
|
234
|
+
ticker=item.ticker,
|
|
235
|
+
alert_type="stop_hit",
|
|
236
|
+
message=f"🛑 {item.ticker} hit stop! ${current_price:.2f} <= ${item.stop_price:.2f}",
|
|
237
|
+
current_price=current_price,
|
|
238
|
+
trigger_value=item.stop_price,
|
|
239
|
+
timestamp=now,
|
|
240
|
+
))
|
|
241
|
+
|
|
242
|
+
# Check signal change (requires running analyze_stock)
|
|
243
|
+
if item.alert_on_signal:
|
|
244
|
+
try:
|
|
245
|
+
import subprocess
|
|
246
|
+
result = subprocess.run(
|
|
247
|
+
["uv", "run", str(Path(__file__).parent / "analyze_stock.py"), item.ticker, "--output", "json"],
|
|
248
|
+
capture_output=True,
|
|
249
|
+
text=True,
|
|
250
|
+
timeout=60,
|
|
251
|
+
)
|
|
252
|
+
if result.returncode == 0:
|
|
253
|
+
analysis = json.loads(result.stdout)
|
|
254
|
+
new_signal = analysis.get("recommendation")
|
|
255
|
+
|
|
256
|
+
if item.last_signal and new_signal and new_signal != item.last_signal:
|
|
257
|
+
alerts.append(Alert(
|
|
258
|
+
ticker=item.ticker,
|
|
259
|
+
alert_type="signal_change",
|
|
260
|
+
message=f"📊 {item.ticker} signal changed: {item.last_signal} → {new_signal}",
|
|
261
|
+
current_price=current_price,
|
|
262
|
+
trigger_value=f"{item.last_signal} → {new_signal}",
|
|
263
|
+
timestamp=now,
|
|
264
|
+
))
|
|
265
|
+
|
|
266
|
+
# Update last signal
|
|
267
|
+
item.last_signal = new_signal
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
item.last_check = now
|
|
272
|
+
|
|
273
|
+
# Save updated watchlist (with last_signal updates)
|
|
274
|
+
save_watchlist(watchlist)
|
|
275
|
+
|
|
276
|
+
# Format output
|
|
277
|
+
if notify_format and alerts:
|
|
278
|
+
# Format for Telegram notification
|
|
279
|
+
lines = ["📢 **Stock Alerts**\n"]
|
|
280
|
+
for alert in alerts:
|
|
281
|
+
lines.append(alert.message)
|
|
282
|
+
return {"success": True, "alerts": [asdict(a) for a in alerts], "notification": "\n".join(lines)}
|
|
283
|
+
|
|
284
|
+
return {"success": True, "alerts": [asdict(a) for a in alerts], "count": len(alerts)}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def main():
|
|
288
|
+
parser = argparse.ArgumentParser(description="Stock Watchlist with Alerts")
|
|
289
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
290
|
+
|
|
291
|
+
# Add
|
|
292
|
+
add_parser = subparsers.add_parser("add", help="Add ticker to watchlist")
|
|
293
|
+
add_parser.add_argument("ticker", help="Stock ticker")
|
|
294
|
+
add_parser.add_argument("--target", type=float, help="Target price for alert")
|
|
295
|
+
add_parser.add_argument("--stop", type=float, help="Stop loss price for alert")
|
|
296
|
+
add_parser.add_argument("--alert-on", choices=["signal"], help="Alert on signal change")
|
|
297
|
+
add_parser.add_argument("--notes", help="Notes")
|
|
298
|
+
|
|
299
|
+
# Remove
|
|
300
|
+
remove_parser = subparsers.add_parser("remove", help="Remove ticker from watchlist")
|
|
301
|
+
remove_parser.add_argument("ticker", help="Stock ticker")
|
|
302
|
+
|
|
303
|
+
# List
|
|
304
|
+
subparsers.add_parser("list", help="List watchlist")
|
|
305
|
+
|
|
306
|
+
# Check
|
|
307
|
+
check_parser = subparsers.add_parser("check", help="Check for triggered alerts")
|
|
308
|
+
check_parser.add_argument("--notify", action="store_true", help="Format for notification")
|
|
309
|
+
|
|
310
|
+
args = parser.parse_args()
|
|
311
|
+
|
|
312
|
+
if args.command == "add":
|
|
313
|
+
result = add_to_watchlist(
|
|
314
|
+
args.ticker,
|
|
315
|
+
target_price=args.target,
|
|
316
|
+
stop_price=args.stop,
|
|
317
|
+
alert_on_signal=(args.alert_on == "signal"),
|
|
318
|
+
notes=args.notes,
|
|
319
|
+
)
|
|
320
|
+
print(json.dumps(result, indent=2))
|
|
321
|
+
|
|
322
|
+
elif args.command == "remove":
|
|
323
|
+
result = remove_from_watchlist(args.ticker)
|
|
324
|
+
print(json.dumps(result, indent=2))
|
|
325
|
+
|
|
326
|
+
elif args.command == "list":
|
|
327
|
+
result = list_watchlist()
|
|
328
|
+
print(json.dumps(result, indent=2))
|
|
329
|
+
|
|
330
|
+
elif args.command == "check":
|
|
331
|
+
result = check_alerts(notify_format=args.notify)
|
|
332
|
+
print(json.dumps(result, indent=2))
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
if __name__ == "__main__":
|
|
336
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xiaohongshu",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"types": [
|
|
5
|
+
"store"
|
|
6
|
+
],
|
|
7
|
+
"displayName": "小红书创作助手",
|
|
8
|
+
"description": "支持小红书笔记的创建、读取、更新、删除与搜索,并支持在提供笔记链接或 token 时生成笔记文档。",
|
|
9
|
+
"changelog": [
|
|
10
|
+
{
|
|
11
|
+
"version": "1.0.0",
|
|
12
|
+
"date": "2026-04-14",
|
|
13
|
+
"changes": [
|
|
14
|
+
"初次提交"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"createdAt": "2026-04-14",
|
|
19
|
+
"updatedAt": "2026-04-14"
|
|
20
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xiaohongshu
|
|
3
|
+
description: Xiaohongshu content creation, publishing and operation. Use when the user wants to create, render, and publish content to Xiaohongshu platform.
|
|
4
|
+
---
|
|
5
|
+
# 小红书技能 (Xiaohongshu Skill)
|
|
6
|
+
|
|
7
|
+
小红书内容创作、发布与运营一体化技能。
|
|
8
|
+
|
|
9
|
+
## 环境
|
|
10
|
+
|
|
11
|
+
- Python venv: `{baseDir}/venv`
|
|
12
|
+
- 激活: `source {baseDir}/venv/bin/activate`
|
|
13
|
+
- social-auto-upload: `{baseDir}/social-auto-upload`
|
|
14
|
+
|
|
15
|
+
## 功能模块
|
|
16
|
+
|
|
17
|
+
### 1. 内容创作 ✅
|
|
18
|
+
AI 根据主题撰写小红书风格内容。
|
|
19
|
+
|
|
20
|
+
### 2. 渲染卡片 ✅
|
|
21
|
+
将 Markdown 内容渲染成小红书风格的图片卡片(1080×1440px)。
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd {baseDir}
|
|
25
|
+
./render <markdown文件> --style <样式> -o output/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
可用样式: `purple`(默认), `xiaohongshu`, `mint`, `sunset`, `ocean`, `elegant`, `dark`
|
|
29
|
+
|
|
30
|
+
### 3. 发布笔记 ✅ (RPA方式)
|
|
31
|
+
使用 Playwright 自动化发布,模拟真实用户操作。
|
|
32
|
+
|
|
33
|
+
**首次使用需要登录获取 Cookie:**
|
|
34
|
+
```bash
|
|
35
|
+
cd {baseDir}/social-auto-upload
|
|
36
|
+
../venv/bin/python -c "
|
|
37
|
+
import asyncio
|
|
38
|
+
from uploader.xiaohongshu_uploader.main import xiaohongshu_cookie_gen
|
|
39
|
+
asyncio.run(xiaohongshu_cookie_gen('cookies/xhs_account.json'))
|
|
40
|
+
"
|
|
41
|
+
```
|
|
42
|
+
会打开浏览器,扫码登录后 Cookie 自动保存。
|
|
43
|
+
|
|
44
|
+
**发布图文笔记:**
|
|
45
|
+
```python
|
|
46
|
+
from uploader.xiaohongshu_uploader.main import XiaoHongShuVideo
|
|
47
|
+
# 详见 examples/upload_video_to_xiaohongshu.py
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 4. 查看数据(开发中)
|
|
51
|
+
通过 RPA 方式获取笔记数据。
|
|
52
|
+
|
|
53
|
+
### 5. 学习他人笔记(开发中)
|
|
54
|
+
通过 RPA 方式浏览和分析热门笔记。
|
|
55
|
+
|
|
56
|
+
## 完整工作流
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
1. 主题构思 → 告诉我你想写什么
|
|
60
|
+
2. AI 撰写 → 生成小红书风格文案
|
|
61
|
+
3. 保存内容 → output/note.md
|
|
62
|
+
4. 渲染卡片 → 生成封面 + 内容图片
|
|
63
|
+
5. RPA 发布 → 自动上传到小红书
|
|
64
|
+
6. 数据跟踪 → 查看互动数据
|
|
65
|
+
7. 优化迭代 → 根据数据调整内容
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 目录结构
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
xiaohongshu/
|
|
72
|
+
├── SKILL.md # 本文档
|
|
73
|
+
├── .env # Cookie 配置(API方式,备用)
|
|
74
|
+
├── venv/ # Python 虚拟环境
|
|
75
|
+
├── assets/ # HTML 模板
|
|
76
|
+
├── scripts/ # 工具脚本
|
|
77
|
+
│ ├── render_xhs_v2.py # 渲染脚本
|
|
78
|
+
│ └── xhs_tool.py # API 工具(备用)
|
|
79
|
+
├── output/ # 输出目录
|
|
80
|
+
└── social-auto-upload/ # RPA 发布工具
|
|
81
|
+
├── cookies/ # 登录凭证
|
|
82
|
+
├── videos/ # 待发布内容
|
|
83
|
+
└── uploader/ # 各平台上传模块
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 注意事项
|
|
87
|
+
|
|
88
|
+
- RPA 方式需要首次扫码登录
|
|
89
|
+
- 发布间隔建议 > 30 秒,避免风控
|
|
90
|
+
- 图片尺寸 1080×1440px(小红书推荐比例)
|
|
91
|
+
- 支持定时发布
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=1080">
|
|
6
|
+
<title>小红书卡片</title>
|
|
7
|
+
<style>
|
|
8
|
+
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
|
|
9
|
+
|
|
10
|
+
* {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
18
|
+
width: 1080px;
|
|
19
|
+
min-height: 1440px;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
background: transparent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.card-container {
|
|
25
|
+
width: 1080px;
|
|
26
|
+
min-height: 1440px;
|
|
27
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
28
|
+
position: relative;
|
|
29
|
+
padding: 50px;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.card-inner {
|
|
34
|
+
background: rgba(255, 255, 255, 0.95);
|
|
35
|
+
border-radius: 20px;
|
|
36
|
+
padding: 60px;
|
|
37
|
+
min-height: calc(1440px - 100px);
|
|
38
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
39
|
+
backdrop-filter: blur(10px);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Markdown 内容样式 */
|
|
43
|
+
.card-content {
|
|
44
|
+
color: #475569;
|
|
45
|
+
font-size: 42px;
|
|
46
|
+
line-height: 1.7;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.card-content h1 {
|
|
50
|
+
font-size: 72px;
|
|
51
|
+
font-weight: 700;
|
|
52
|
+
color: #1e293b;
|
|
53
|
+
margin-bottom: 40px;
|
|
54
|
+
line-height: 1.3;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.card-content h2 {
|
|
58
|
+
font-size: 56px;
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
color: #334155;
|
|
61
|
+
margin: 50px 0 25px 0;
|
|
62
|
+
line-height: 1.4;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.card-content h3 {
|
|
66
|
+
font-size: 48px;
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
color: #475569;
|
|
69
|
+
margin: 40px 0 20px 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.card-content p {
|
|
73
|
+
margin-bottom: 35px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.card-content strong {
|
|
77
|
+
font-weight: 700;
|
|
78
|
+
color: #1e293b;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.card-content em {
|
|
82
|
+
font-style: italic;
|
|
83
|
+
color: #6366f1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.card-content a {
|
|
87
|
+
color: #6366f1;
|
|
88
|
+
text-decoration: none;
|
|
89
|
+
border-bottom: 2px solid #6366f1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.card-content ul,
|
|
93
|
+
.card-content ol {
|
|
94
|
+
margin: 30px 0;
|
|
95
|
+
padding-left: 60px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.card-content li {
|
|
99
|
+
margin-bottom: 20px;
|
|
100
|
+
line-height: 1.6;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.card-content blockquote {
|
|
104
|
+
border-left: 8px solid #6366f1;
|
|
105
|
+
padding-left: 40px;
|
|
106
|
+
background: #f1f5f9;
|
|
107
|
+
padding-top: 25px;
|
|
108
|
+
padding-bottom: 25px;
|
|
109
|
+
padding-right: 30px;
|
|
110
|
+
margin: 35px 0;
|
|
111
|
+
color: #64748b;
|
|
112
|
+
font-style: italic;
|
|
113
|
+
border-radius: 0 12px 12px 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.card-content blockquote p {
|
|
117
|
+
margin: 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.card-content code {
|
|
121
|
+
background: #f1f5f9;
|
|
122
|
+
padding: 6px 16px;
|
|
123
|
+
border-radius: 8px;
|
|
124
|
+
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
125
|
+
font-size: 38px;
|
|
126
|
+
color: #6366f1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.card-content pre {
|
|
130
|
+
background: #1e293b;
|
|
131
|
+
color: #e2e8f0;
|
|
132
|
+
padding: 40px;
|
|
133
|
+
border-radius: 16px;
|
|
134
|
+
margin: 35px 0;
|
|
135
|
+
overflow-x: visible;
|
|
136
|
+
overflow-wrap: break-word;
|
|
137
|
+
word-wrap: break-word;
|
|
138
|
+
word-break: break-all;
|
|
139
|
+
white-space: pre-wrap;
|
|
140
|
+
font-size: 36px;
|
|
141
|
+
line-height: 1.5;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.card-content pre code {
|
|
145
|
+
background: transparent;
|
|
146
|
+
color: inherit;
|
|
147
|
+
padding: 0;
|
|
148
|
+
font-size: inherit;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.card-content img {
|
|
152
|
+
max-width: 100%;
|
|
153
|
+
height: auto;
|
|
154
|
+
border-radius: 16px;
|
|
155
|
+
margin: 35px auto;
|
|
156
|
+
display: block;
|
|
157
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.card-content hr {
|
|
161
|
+
border: none;
|
|
162
|
+
height: 2px;
|
|
163
|
+
background: #e2e8f0;
|
|
164
|
+
margin: 50px 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Tags 标签样式 */
|
|
168
|
+
.tags-container {
|
|
169
|
+
margin-top: 50px;
|
|
170
|
+
padding-top: 30px;
|
|
171
|
+
border-top: 2px solid #e2e8f0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.tag {
|
|
175
|
+
display: inline-block;
|
|
176
|
+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
177
|
+
color: white;
|
|
178
|
+
padding: 12px 28px;
|
|
179
|
+
border-radius: 30px;
|
|
180
|
+
font-size: 34px;
|
|
181
|
+
margin: 10px 15px 10px 0;
|
|
182
|
+
font-weight: 500;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* 信息卡片样式 */
|
|
186
|
+
.info-card {
|
|
187
|
+
margin: 40px 0;
|
|
188
|
+
padding: 40px 50px;
|
|
189
|
+
background: linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(139, 92, 246, 0.12) 100%);
|
|
190
|
+
border: 2px solid rgba(99, 102, 241, 0.2);
|
|
191
|
+
border-radius: 20px;
|
|
192
|
+
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* 页码样式 */
|
|
196
|
+
.page-number {
|
|
197
|
+
position: absolute;
|
|
198
|
+
bottom: 80px;
|
|
199
|
+
right: 80px;
|
|
200
|
+
font-size: 36px;
|
|
201
|
+
color: rgba(255, 255, 255, 0.8);
|
|
202
|
+
font-weight: 500;
|
|
203
|
+
}
|
|
204
|
+
</style>
|
|
205
|
+
</head>
|
|
206
|
+
<body>
|
|
207
|
+
<div class="card-container">
|
|
208
|
+
<div class="card-inner">
|
|
209
|
+
<div class="card-content">
|
|
210
|
+
{{CONTENT}}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="page-number">{{PAGE_NUMBER}}</div>
|
|
214
|
+
</div>
|
|
215
|
+
</body>
|
|
216
|
+
</html>
|