feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s

移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。

**目前能用的(真环境实测,非推断)**:

    profile/other : 200, 7075 字节  —— 博主主页指标(粉丝/获赞/作品数/昵称)
    aweme/detail  : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)

**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
  1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
     带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
     同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
     挑中的恰好是「批量拉作品列表」这个最敏感的动作。
  2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
     代价,过几小时要重测)。

  所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。

**排查中控住变量后得到的两条事实**(都写进注释了):
  · `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
    UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
    (我先前猜的 sec-ch-ua 不是关键。)
  · 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
    从 CDP 取齐 cookie + UA + hints,而不是各自写死。

「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。

测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
This commit is contained in:
2026-10-10 17:12:25 +08:00
parent 21ff01b894
commit 2112c1a870
1601 changed files with 367425 additions and 0 deletions
@@ -0,0 +1,742 @@
#!/usr/bin/env python3
"""
kb_api.py — 知识库管理 API 模块
集中管理知识库的所有操作,供 Dashboard 调用。
每个操作都有参数配置 + 日志记录。
版本: 1.0.0 | 更新: 2026-05-31
"""
import json, os, sys, subprocess, re, shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
HOME = Path.home()
AGENT_SYNC = Path(os.environ.get("AGENT_SYNC", str(HOME / "workbuddy-agent-os" / "agent-sync")))
AGENT_LOCAL = Path(os.environ.get("AGENT_LOCAL", str(HOME / "workbuddy-agent-os" / "agent-local")))
KNOWLEDGE = AGENT_SYNC / "03_knowledge"
TASKS_DIR = AGENT_SYNC / "04_memory" / "cross_machine" / "tasks"
# 知识库分类目录
KB_CATEGORIES = {
"00_inbox": "待提纯",
"00_stream": "流式",
"01_daily": "日志",
"01_submissions": "提交箱",
"04_ops": "运维",
"10_concepts": "概念",
"20_methods": "方法",
"30_facts": "事实",
"40_references": "参考",
"50_resources": "素材",
"60_opinions": "观点",
"90_archive": "归档",
"99_system": "系统",
}
class KBManager:
"""知识库管理——所有操作集中入口"""
def __init__(self):
self._log_path = AGENT_LOCAL / "runtime" / "kb_ops.jsonl"
self._log_path.parent.mkdir(parents=True, exist_ok=True)
# ═══════════════════════════════════════════════════════
# 统计
# ═══════════════════════════════════════════════════════
def get_stats(self) -> dict:
"""返回知识库各目录统计"""
stats = {}
total = 0
cat_order = []
for d in sorted(KNOWLEDGE.iterdir()):
if d.is_dir() and not d.name.startswith("."):
count = len(list(d.rglob("*.md")))
label = KB_CATEGORIES.get(d.name, d.name)
stats[d.name] = {"label": label, "count": count}
total += count
cat_order.append(d.name)
return {
"categories": stats,
"total": total,
"order": cat_order,
"updated_at": datetime.now().isoformat(),
}
# ═══════════════════════════════════════════════════════
# 操作:提纯收件箱 / 汇聚 / 入库 / 搜索 / 备份 / 同步
# ═══════════════════════════════════════════════════════
def run_refine(self, params: dict = None) -> dict:
"""执行 inbox_refine 提纯
params:
- auto_classify: bool (默认True)
- target_dirs: list[str] (默认["00_inbox"])
"""
params = params or {}
self._log_op("refine", params, "started")
try:
# 调用 inbox_refine 脚本
script = AGENT_SYNC / "05_tools" / "01_system" / "inbox_refine.py"
cmd = [sys.executable, str(script)]
if not params.get("auto_classify", True):
cmd.append("--no-classify")
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120,
cwd=str(script.parent))
result = {
"status": "ok" if r.returncode == 0 else "error",
"stdout": r.stdout[-1000:],
"stderr": r.stderr[-500:],
"returncode": r.returncode,
}
self._log_op("refine", params, result["status"], result)
return result
except subprocess.TimeoutExpired:
return {"status": "error", "error": "执行超时(120s)"}
except Exception as e:
return {"status": "error", "error": str(e)}
def run_collect_to_inbox(self, params: dict = None) -> dict:
"""执行 collect_to_inbox 汇聚
params:
- source_dirs: list[str] (默认全部目录)
"""
params = params or {}
self._log_op("collect_to_inbox", params, "started")
try:
script = AGENT_SYNC / "05_tools" / "01_system" / "collect_to_inbox.py"
cmd = [sys.executable, str(script)]
sources = params.get("source_dirs", [])
for s in sources:
cmd.extend(["--source", s])
r = subprocess.run(cmd, capture_output=True, text=True, timeout=180,
cwd=str(script.parent))
result = {
"status": "ok" if r.returncode == 0 else "error",
"stdout": r.stdout[-1000:],
"stderr": r.stderr[-500:],
"returncode": r.returncode,
}
self._log_op("collect_to_inbox", params, result["status"], result)
return result
except subprocess.TimeoutExpired:
return {"status": "error", "error": "执行超时(180s)"}
except Exception as e:
return {"status": "error", "error": str(e)}
def run_ingest(self, params: dict) -> dict:
"""知识入库
params:
- source: str (URL 或文本)
- category: str (目标分类目录)
- tags: str (逗号分隔的标签)
- title: str (可选标题)
"""
params = params or {}
source = params.get("source", "")
if not source:
return {"status": "error", "error": "需要提供 URL 或文本"}
self._log_op("ingest", params, "started")
try:
# 调用 kb_manager 脚本
script = AGENT_SYNC / "05_tools" / "01_system" / "kb_ingest.py"
if script.exists():
cmd = [sys.executable, str(script), source]
else:
# 降级: 直接生成 markdown 文件
cmd = self._fallback_ingest(source, params)
if isinstance(cmd, dict):
result = cmd
else:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60,
cwd=str(script.parent))
result = {
"status": "ok" if r.returncode == 0 else "error",
"stdout": r.stdout[-500:],
"stderr": r.stderr[-300:],
}
self._log_op("ingest", params, result["status"], result)
return result
except Exception as e:
return {"status": "error", "error": str(e)}
def _fallback_ingest(self, source: str, params: dict) -> dict:
"""降级入库: 直接写 markdown 文件"""
cat = params.get("category", "00_inbox")
tags = params.get("tags", "")
title = params.get("title", source[:40])
target_dir = KNOWLEDGE / cat
target_dir.mkdir(parents=True, exist_ok=True)
filename = re.sub(r'[^\w\u4e00-\u9fff]+', '_', title)[:50] + ".md"
filepath = target_dir / filename
content = f"""---
title: "{title}"
created: {datetime.now().isoformat()}
tags: [{tags}]
source: "{source[:200]}"
---
# {title}
{source}
"""
filepath.write_text(content, encoding="utf-8")
return {"status": "ok", "file": str(filepath), "method": "fallback"}
def search(self, params: dict) -> dict:
"""全文搜索知识库
params:
- query: str (搜索关键词)
- category: str (可选, 限制目录)
- limit: int (最多结果)
"""
query = (params or {}).get("query", "")
if not query:
return {"status": "error", "error": "需要搜索关键词"}
category = (params or {}).get("category", "")
limit = (params or {}).get("limit", 20)
results = []
search_dir = KNOWLEDGE / category if category else KNOWLEDGE
if not search_dir.exists():
return {"status": "ok", "results": []}
for f in search_dir.rglob("*.md"):
try:
text = f.read_text(encoding="utf-8", errors="ignore")
if query.lower() in text.lower():
# 提取匹配上下文
idx = text.lower().index(query.lower())
start = max(0, idx - 60)
end = min(len(text), idx + len(query) + 60)
context = text[start:end].replace("\n", " ")
results.append({
"file": str(f.relative_to(KNOWLEDGE)),
"category": f.parent.name,
"context": context,
})
if len(results) >= limit:
break
except:
continue
return {"status": "ok", "results": results, "total": len(results)}
def backup(self, params: dict = None) -> dict:
"""备份知识库"""
params = params or {}
label = params.get("label", f"kb_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
self._log_op("backup", params, "started")
try:
backup_dir = AGENT_LOCAL / "backups" / "knowledge" / label
backup_dir.mkdir(parents=True, exist_ok=True)
# 排除 .git 和 __pycache__
shutil.copytree(KNOWLEDGE, backup_dir,
ignore=shutil.ignore_patterns(".git", "__pycache__", "*.pyc"),
dirs_exist_ok=True)
count = len(list(backup_dir.rglob("*.md")))
result = {"status": "ok", "path": str(backup_dir), "files": count}
self._log_op("backup", params, "ok", result)
return result
except Exception as e:
return {"status": "error", "error": str(e)}
def git_sync(self, params: dict = None) -> dict:
"""Git 同步"""
params = params or {}
self._log_op("git_sync", params, "started")
try:
cmds = [
["git", "add", "03_knowledge/"],
["git", "status", "--porcelain", "03_knowledge/"],
["git", "commit", "-m", "kb: auto sync"],
["git", "push"],
]
outputs = []
for cmd in cmds:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=30,
cwd=str(AGENT_SYNC))
if r.returncode != 0 and cmd[1] == "commit":
if "nothing to commit" in r.stderr:
outputs.append("nothing to commit")
continue
outputs.append(r.stdout[-200:] or r.stderr[-200:])
result = {"status": "ok", "outputs": outputs[-3:]}
self._log_op("git_sync", params, "ok", result)
return result
except Exception as e:
return {"status": "error", "error": str(e)}
# ═══════════════════════════════════════════════════════
# 记忆库管理
# ═══════════════════════════════════════════════════════
def get_memory_stats(self) -> dict:
"""返回记忆库各层统计"""
stats = {"l1_keywords": 0, "l2_facts": 0, "l3_files": 0, "vector_count": 0}
# L1 关键词
kw_path = AGENT_LOCAL / "memory" / "vector_db" / "keyword_index.json"
if kw_path.exists():
try:
import json
kw = json.loads(kw_path.read_text())
stats["l1_keywords"] = len(kw) if isinstance(kw, (list, dict)) else 0
except:
pass
# L2 facts.db
l2_path = AGENT_SYNC / "04_memory" / "long_term" / "facts.db"
if l2_path.exists():
try:
import sqlite3
conn = sqlite3.connect(str(l2_path))
c = conn.cursor()
c.execute("SELECT count(*) FROM facts")
stats["l2_facts"] = c.fetchone()[0]
conn.close()
except:
pass
# L3 原文
l3_dir = AGENT_LOCAL / "memory" / "raw" / "04_memory"
if l3_dir.exists():
stats["l3_files"] = len([f for f in l3_dir.iterdir() if f.suffix == ".md"])
# Chroma 向量
chroma_dir = AGENT_LOCAL / "memory" / "vector_db" / "chroma"
if chroma_dir.exists():
stats["vector_count"] = len(list(chroma_dir.rglob("*.bin"))) + len(list(chroma_dir.rglob("*.pickle")))
return stats
def get_l2_facts(self, limit: int = 100, offset: int = 0, search: str = "") -> dict:
"""返回 L2 事实列表"""
l2_path = AGENT_SYNC / "04_memory" / "long_term" / "facts.db"
if not l2_path.exists():
return {"total": 0, "facts": []}
import sqlite3
conn = sqlite3.connect(str(l2_path))
conn.row_factory = sqlite3.Row
c = conn.cursor()
if search:
c.execute("SELECT count(*) FROM facts WHERE content LIKE ?", (f"%{search}%",))
total = c.fetchone()[0]
c.execute("SELECT rowid, * FROM facts WHERE content LIKE ? ORDER BY rowid DESC LIMIT ? OFFSET ?",
(f"%{search}%", limit, offset))
else:
total = c.execute("SELECT count(*) FROM facts").fetchone()[0]
c.execute("SELECT rowid, * FROM facts ORDER BY rowid DESC LIMIT ? OFFSET ?", (limit, offset))
facts = [dict(r) for r in c.fetchall()]
conn.close()
return {"total": total, "facts": facts}
def update_l2_fact(self, rowid: int, content: str, category: str = "") -> dict:
"""更新 L2 事实"""
l2_path = AGENT_SYNC / "04_memory" / "long_term" / "facts.db"
if not l2_path.exists():
return {"status": "error", "error": "facts.db 不存在"}
import sqlite3
conn = sqlite3.connect(str(l2_path))
c = conn.cursor()
try:
if category:
c.execute("UPDATE facts SET content=?, category=? WHERE rowid=?", (content, category, rowid))
else:
c.execute("UPDATE facts SET content=? WHERE rowid=?", (content, rowid))
conn.commit()
conn.close()
return {"status": "ok", "updated": rowid}
except Exception as e:
conn.close()
return {"status": "error", "error": str(e)}
def get_l3_files(self) -> list[dict]:
"""返回 L3 原文文件列表"""
l3_dir = AGENT_LOCAL / "memory" / "raw"
if not l3_dir.exists():
return []
files = []
for f in sorted(l3_dir.iterdir()):
if f.suffix == ".md":
files.append({
"name": f.name,
"size_kb": round(f.stat().st_size / 1024, 1),
"updated": datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
})
return files
def get_l3_file_content(self, filename: str) -> dict:
"""读取 L3 原文文件内容"""
l3_dir = AGENT_LOCAL / "memory" / "raw"
filepath = l3_dir / filename
if not filepath.exists() or not filename.endswith(".md"):
return {"status": "error", "error": "文件不存在"}
return {"status": "ok", "name": filename, "content": filepath.read_text(encoding="utf-8", errors="ignore")}
def get_l1_keywords(self) -> list:
"""返回 L1 关键词列表"""
kw_path = AGENT_LOCAL / "memory" / "vector_db" / "keyword_index.json"
if not kw_path.exists():
return []
try:
import json
data = json.loads(kw_path.read_text())
if isinstance(data, dict):
return [{"word": k, "freq": v.get("freq", v) if isinstance(v, dict) else v} for k, v in data.items()]
return data if isinstance(data, list) else []
except:
return []
def get_system_docs(self) -> list[dict]:
"""返回联邦系统文档目录"""
sys_dir = KNOWLEDGE / "99_system"
if not sys_dir.exists():
return []
docs = []
for f in sorted(sys_dir.iterdir()):
if f.suffix == ".md" and not f.name.startswith("."):
docs.append({
"name": f.name.replace(".md", ""),
"title": f.read_text(encoding="utf-8", errors="ignore").split("\n")[0].replace("#", "").strip()[:60],
"size_kb": round(f.stat().st_size / 1024, 1),
})
return docs
def get_system_doc_content(self, name: str) -> dict:
"""读取系统文档内容"""
sys_dir = KNOWLEDGE / "99_system"
filepath = sys_dir / (name + ".md")
if not filepath.exists():
# try without .md
filepath = sys_dir / name
if not filepath.exists() or not filepath.suffix == ".md":
return {"status": "error", "error": "文档不存在"}
return {"status": "ok", "name": name, "content": filepath.read_text(encoding="utf-8", errors="ignore")[:5000]}
def get_skills_catalog(self) -> list[dict]:
"""返回已安装技能目录"""
skills_dir = Path.home() / ".workbuddy" / "skills"
if not skills_dir.exists():
return []
skills = []
for d in sorted(skills_dir.iterdir()):
if d.is_dir():
sk = d / "SKILL.md"
if sk.exists():
content = sk.read_text(encoding="utf-8", errors="ignore")
name = ""
desc = ""
triggers = []
for line in content.split("\n"):
if line.startswith("name:"):
name = line.split(":", 1)[1].strip().strip('"').strip("'")
elif line.startswith("description:"):
desc = line.split(":", 1)[1].strip().strip('"').strip("'")
elif "trigger" in line.lower() and "-" in line:
t = line.split("-", 1)[1].strip().strip('"').strip("'")
if t:
triggers.append(t)
skills.append({
"name": name or d.name,
"dir": d.name,
"description": desc[:80],
"triggers": triggers[:5],
})
return skills
def get_tools_catalog(self) -> list[dict]:
"""返回工具目录"""
tools_dir = AGENT_SYNC / "05_tools"
if not tools_dir.exists():
return []
tools = []
for d in sorted(tools_dir.iterdir()):
if d.is_dir() and not d.name.startswith("."):
tools.append({
"name": d.name,
"path": str(d.relative_to(AGENT_SYNC)),
"items": len([f for f in d.rglob("*") if f.is_file() and f.suffix in (".py",".sh",".md")]),
})
return tools
# ═══════════════════════════════════════════════════════
# 操作日志
# ═══════════════════════════════════════════════════════
def get_logs(self, limit: int = 20) -> list[dict]:
"""返回最近操作日志"""
if not self._log_path.exists():
return []
logs = []
with open(self._log_path, "r") as f:
for line in f:
line = line.strip()
if line:
try:
logs.append(json.loads(line))
except:
pass
return logs[-limit:]
def _log_op(self, op: str, params: dict, status: str, result: dict = None):
"""记录操作日志"""
entry = {
"op": op,
"params": params,
"status": status,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
if result:
entry["result"] = {k: v for k, v in result.items()
if k in ("status", "files", "returncode", "file")}
with open(self._log_path, "a") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
# ═══════════════════════════════════════════════════════
# FastAPI router (挂载到 Dashboard app.py)
# ═══════════════════════════════════════════════════════
from fastapi import APIRouter, HTTPException
router = APIRouter(prefix="/api/kb", tags=["knowledge"])
_kb = KBManager()
@router.get("/stats")
def api_kb_stats():
return _kb.get_stats()
@router.post("/refine")
def api_kb_refine(data: dict = None):
return _kb.run_refine(data or {})
@router.post("/collect")
def api_kb_collect(data: dict = None):
return _kb.run_collect_to_inbox(data or {})
@router.post("/ingest")
def api_kb_ingest(data: dict):
return _kb.run_ingest(data)
@router.post("/search")
def api_kb_search(data: dict):
return _kb.search(data)
@router.post("/backup")
def api_kb_backup(data: dict = None):
return _kb.backup(data or {})
@router.post("/git-sync")
def api_kb_git_sync(data: dict = None):
return _kb.git_sync(data or {})
@router.get("/logs")
def api_kb_logs(limit: int = 20):
return {"logs": _kb.get_logs(limit)}
# ═══════════════════════════════════════════════════════
# 记忆库 API
# ═══════════════════════════════════════════════════════
@router.get("/memory/stats")
def api_memory_stats():
return _kb.get_memory_stats()
@router.get("/memory/l2")
def api_memory_l2(limit: int = 100, offset: int = 0, search: str = ""):
return _kb.get_l2_facts(limit, offset, search)
@router.put("/memory/l2/{rowid}")
def api_memory_l2_update(rowid: int, data: dict):
return _kb.update_l2_fact(rowid, data.get("content", ""), data.get("category", ""))
@router.get("/memory/l3")
def api_memory_l3():
return {"files": _kb.get_l3_files()}
@router.get("/memory/l3/{filename}")
def api_memory_l3_read(filename: str):
return _kb.get_l3_file_content(filename)
@router.get("/memory/l1")
def api_memory_l1():
return {"keywords": _kb.get_l1_keywords()}
# ═══════════════════════════════════════════════════════
# 联邦系统文档 API
# ═══════════════════════════════════════════════════════
@router.get("/system/docs")
def api_system_docs():
return {"docs": _kb.get_system_docs()}
@router.get("/system/docs/{name}")
def api_system_doc_content(name: str):
return _kb.get_system_doc_content(name)
# ═══════════════════════════════════════════════════════
# 技能与工具 API
# ═══════════════════════════════════════════════════════
@router.get("/skills")
def api_skills():
return {"skills": _kb.get_skills_catalog()}
@router.get("/tools")
def api_tools():
return {"tools": _kb.get_tools_catalog()}
# ═══════════════════════════════════════════════════════
# 自动化任务 API
# ═══════════════════════════════════════════════════════
@router.get("/automations")
def api_automations():
"""返回所有机器的自动化任务"""
import sqlite3
tasks = {}
# 本机 WorkBuddy 自动化
wb_db = Path.home() / ".workbuddy" / "workbuddy.db"
if wb_db.exists():
try:
conn = sqlite3.connect(str(wb_db))
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT id, name, prompt, rrule, scheduleType, scheduledAt, status, cwds FROM automations ORDER BY id")
local = []
for row in c.fetchall():
d = dict(row)
d["source"] = _kb._local_hostname
d["source_type"] = "workbuddy"
local.append(d)
if local:
tasks[_kb._local_hostname] = local
conn.close()
except:
pass
# 其他机器: 从 cross_machine/data/automation/ 读取
auto_dir = CROSS_MACHINE / "data" / "automation"
if auto_dir.exists():
for f in sorted(auto_dir.iterdir()):
if f.suffix == ".json":
try:
data = json.loads(f.read_text())
hn = data.get("hostname", f.stem)
if hn not in tasks:
tasks[hn] = []
tasks[hn].append({
"source": hn,
"source_type": "guardd_heartbeat",
"guardd_version": data.get("guardd_version", ""),
"last_seen": data.get("timestamp", ""),
})
except:
pass
# 加上 C2 命令历史
c2_dir = CROSS_MACHINE / "tasks" / "completed"
c2_history = []
if c2_dir.exists():
for f in sorted(c2_dir.iterdir(), reverse=True)[:20]:
if f.suffix == ".json":
try:
d = json.loads(f.read_text())
c2_history.append({
"type": d.get("type", ""),
"target": d.get("target_machine", ""),
"status": d.get("status", ""),
"time": (d.get("completed_at") or d.get("created_at") or "").replace("T", " ")[:19],
"output": str(d.get("output", {}))[:100],
})
except:
pass
return {"machines": tasks, "history": c2_history}
@router.post("/automations")
def api_create_automation(data: dict):
"""创建 WorkBuddy 自动化任务"""
target = data.get("target", "")
prompt = data.get("prompt", "")
command_type = data.get("type", "")
params = data.get("params", "")
schedule = data.get("schedule_at", "")
rrule = data.get("rrule", "")
if not prompt and not command_type:
return {"status": "error", "error": "需要 prompt 或命令类型"}
# 如果是跨机任务, 通过 C2 发送
if target and target != _kb._local_hostname:
try:
import importlib.util
c2_path = AGENT_SYNC / "05_tools" / "07_matrix" / "scripts" / "c2" / "command_bus.py"
spec = importlib.util.spec_from_file_location("cb", c2_path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
bus = mod.CommandBus()
result = bus.send(target, command_type, json.loads(params) if params else {}, schedule)
return {"status": "ok", "channel": "c2", "command_id": result.get("command_id")}
except Exception as e:
return {"status": "error", "error": str(e)}
# 本机任务: 写入 WorkBuddy sqlite
return _create_workbuddy_automation(prompt, rrule, schedule)
def _create_workbuddy_automation(prompt: str, rrule: str = "", schedule_at: str = "") -> dict:
"""写入本机 WorkBuddy 自动化"""
import sqlite3, uuid
wb_db = Path.home() / ".workbuddy" / "workbuddy.db"
if not wb_db.exists():
return {"status": "error", "error": "WorkBuddy DB 不存在"}
try:
conn = sqlite3.connect(str(wb_db))
c = conn.cursor()
aid = str(uuid.uuid4())[:8]
schedule_type = "once" if schedule_at else ("recurring" if rrule else "recurring")
c.execute(
"INSERT OR IGNORE INTO automations (id, name, prompt, rrule, scheduleType, scheduledAt, status, cwds) VALUES (?,?,?,?,?,?,?,?)",
(aid, prompt[:30], prompt, rrule, schedule_type, schedule_at, "ACTIVE", "")
)
conn.commit()
conn.close()
return {"status": "ok", "id": aid, "prompt": prompt[:30]}
except Exception as e:
return {"status": "error", "error": str(e)}