skills/windpy-sdk/scripts/wind_server.py
wangyitong a65adcc2e5 Initial commit: merged, deduplicated, and vetted skill collection
Sources: extracted from two upstream archives (skill-repo, skills-main),
merged with the following policy:

- 15 broken symlinks (pointing to /Users/jameslee/.cc-switch/skills or
  ../../.agents/skills on a foreign machine) discarded
- 3 real name collisions with identical content (ai-pair, ifind-http-api,
  zhipu-websearch) kept as one copy
- Functional overlaps deduped keeping the strongest variant:
  - docx family: kept docx (official, full toolchain) + docx-cn
    (GB/T 9704 Chinese official-document constants),
    dropped docx_writer (no scripts, name collided with docx)
  - humanizer family: kept humanizer-zh (6 zh reference docs),
    dropped humanizer (en, redundant for CN workflow)
- Skills that only ran in a foreign environment removed:
  ablemind-ops, app-publish, hlb-design-system, openclaw-adj-skill,
  claude-driver
- alphapai excluded from this public repo because its SKILL.md hard-coded
  live credentials

Result: 25 skills, 572 files, ~7.5 MB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:47:12 +08:00

342 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Wind 常驻服务 — 保持 WindPy 长连接,通过本地 HTTP 接口提供数据查询。
启动: python wind_server.py [--host 127.0.0.1] [--port 18888] [--pidfile <path>]
停止: curl http://localhost:18888/shutdown 或 kill $(cat <pidfile>)
环境变量 (优先级低于命令行参数):
WIND_SERVER_HOST — 绑定地址 (默认 127.0.0.1)
WIND_SERVER_PORT — 端口 (默认 18888)
WIND_SERVER_PIDFILE — PID 文件路径 (默认 系统临时目录/wind_server.pid)
支持的接口:
POST /wsd — w.wsd() 日级时间序列
POST /wss — w.wss() 截面快照
POST /wset — w.wset() 报表数据集
POST /wsq — w.wsq() 实时行情快照
POST /edb — w.edb() 宏观经济指标
POST /wsi — w.wsi() 分钟K线序列
POST /wst — w.wst() 日内Tick跳价
POST /wsee — w.wsee() 板块多维数据
POST /wses — w.wses() 板块时间序列
POST /wsed — w.wsed() 板块查询
POST /tdays — w.tdays() 交易日列表
POST /tdaysoffset — w.tdaysoffset() 交易日偏移
POST /tdayscount — w.tdayscount() 交易日计数
POST /weqs — w.weqs() 条件选股
POST /htocode — w.htocode() 代码转换
POST /wai — w.wai() 智能API
POST /wgel — w.wgel() 企业库
GET /health — 健康检查
GET /shutdown — 优雅关闭
"""
import argparse
import json
import os
import signal
import sys
import tempfile
import threading
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
# ---------------------------------------------------------------------------
# Wind 连接管理
# ---------------------------------------------------------------------------
_wind_lock = threading.Lock()
_w = None
def _ensure_connected():
"""确保 Wind 已连接,断线自动重连。"""
global _w
with _wind_lock:
if _w is None:
from WindPy import w as _wind
_w = _wind
if not _w.isconnected():
ret = _w.start(waitTime=30)
if not _w.isconnected():
raise RuntimeError(f"Wind connect failed: {ret}")
return _w
def _wind_data_to_dict(result):
"""将 WindData 对象转成可序列化的 dict。"""
if hasattr(result, 'ErrorCode') and result.ErrorCode != 0:
return {"error": True, "code": result.ErrorCode, "message": str(getattr(result, 'Data', ''))}
out = {
"ErrorCode": result.ErrorCode,
"Codes": result.Codes if hasattr(result, 'Codes') else [],
"Fields": result.Fields if hasattr(result, 'Fields') else [],
"Times": [str(t) for t in result.Times] if hasattr(result, 'Times') and result.Times else [],
}
# Data: list of lists, 需要处理日期等不可序列化类型
if hasattr(result, 'Data') and result.Data:
data = []
for row in result.Data:
data.append([_serialize(v) for v in row])
out["Data"] = data
else:
out["Data"] = []
return out
def _serialize(v):
"""将单个值转为 JSON 可序列化类型。"""
if v is None:
return None
if isinstance(v, (int, float, str, bool)):
return v
return str(v)
# ---------------------------------------------------------------------------
# HTTP Handler
# ---------------------------------------------------------------------------
class WindHandler(BaseHTTPRequestHandler):
"""处理 Wind 查询请求。"""
def log_message(self, fmt, *args):
# 静默日志,避免刷屏
pass
def _json_response(self, data, status=200):
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _read_body(self):
length = int(self.headers.get("Content-Length", 0))
if length == 0:
return {}
return json.loads(self.rfile.read(length))
# --- GET ---
def do_GET(self):
if self.path == "/health":
try:
w = _ensure_connected()
self._json_response({"status": "ok", "connected": w.isconnected()})
except Exception as e:
self._json_response({"status": "error", "message": str(e)}, 503)
elif self.path == "/shutdown":
self._json_response({"status": "shutting down"})
threading.Thread(target=self.server.shutdown, daemon=True).start()
else:
self._json_response({"error": "not found"}, 404)
# --- POST ---
def do_POST(self):
try:
params = self._read_body()
w = _ensure_connected()
if self.path == "/wsd":
result = w.wsd(
params["codes"], params["fields"],
params.get("beginTime", ""), params.get("endTime", ""),
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/wss":
result = w.wss(
params["codes"], params["fields"],
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/wset":
result = w.wset(
params["tableName"],
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/wsq":
result = w.wsq(
params["codes"], params["fields"],
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/edb":
result = w.edb(
params["codes"],
params.get("beginTime", ""), params.get("endTime", ""),
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/wsi":
result = w.wsi(
params["codes"], params["fields"],
params.get("beginTime", ""), params.get("endTime", ""),
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/wst":
result = w.wst(
params["codes"], params["fields"],
params.get("beginTime", ""), params.get("endTime", ""),
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/wsee":
result = w.wsee(
params["codes"], params["fields"],
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/wses":
result = w.wses(
params["codes"], params["fields"],
params.get("beginTime", ""), params.get("endTime", ""),
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/wsed":
result = w.wsed(
params["codes"], params["fields"],
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/tdays":
result = w.tdays(
params.get("beginTime", ""), params.get("endTime", ""),
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/tdaysoffset":
result = w.tdaysoffset(
params["offset"],
params.get("beginTime", ""),
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/tdayscount":
result = w.tdayscount(
params.get("beginTime", ""), params.get("endTime", ""),
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/weqs":
result = w.weqs(
params["filtername"],
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/htocode":
result = w.htocode(
params["codes"], params["sec_type"],
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/wai":
result = w.wai(
params["func"], params["input"],
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
elif self.path == "/wgel":
result = w.wgel(
params["funname"], params["windid"],
params.get("options", ""),
)
self._json_response(_wind_data_to_dict(result))
else:
self._json_response({"error": "unknown endpoint"}, 404)
except Exception as e:
self._json_response({"error": True, "message": str(e)}, 500)
# ---------------------------------------------------------------------------
# 启动入口
# ---------------------------------------------------------------------------
def main():
default_port = int(os.environ.get("WIND_SERVER_PORT", 18888))
default_host = os.environ.get("WIND_SERVER_HOST", "127.0.0.1")
default_pid = os.environ.get(
"WIND_SERVER_PIDFILE",
os.path.join(tempfile.gettempdir(), "wind_server.pid"),
)
parser = argparse.ArgumentParser(description="Wind 常驻服务")
parser.add_argument("--port", type=int, default=default_port)
parser.add_argument("--host", type=str, default=default_host)
parser.add_argument("--pidfile", type=str, default=default_pid)
args = parser.parse_args()
pid_file = args.pidfile
# 写 PID 文件
with open(pid_file, "w") as f:
f.write(str(os.getpid()))
# macOS: 检测 WindPy 环境,自动修复 symlink
try:
from setup_windpy import ensure_windpy
ensure_windpy(auto_fix=True, verbose=True)
except ImportError:
# setup_windpy.py 不在同目录时跳过Windows 等场景)
pass
except RuntimeError as e:
print(f"[wind_server] WindPy 环境配置失败: {e}", file=sys.stderr, flush=True)
sys.exit(1)
# 先建立 Wind 连接
print(f"[wind_server] Connecting to Wind...", flush=True)
try:
_ensure_connected()
print(f"[wind_server] Wind connected.", flush=True)
except Exception as e:
print(f"[wind_server] Wind connect failed: {e}", file=sys.stderr, flush=True)
sys.exit(1)
server = HTTPServer((args.host, args.port), WindHandler)
print(f"[wind_server] Listening on http://{args.host}:{args.port}", flush=True)
print(f"[wind_server] PID={os.getpid()}, pid_file={pid_file}", flush=True)
def _shutdown(sig, frame):
print(f"\n[wind_server] Shutting down...", flush=True)
threading.Thread(target=server.shutdown, daemon=True).start()
if hasattr(signal, 'SIGTERM'):
signal.signal(signal.SIGTERM, _shutdown)
signal.signal(signal.SIGINT, _shutdown)
try:
server.serve_forever()
finally:
if _w and _w.isconnected():
_w.stop()
if os.path.exists(pid_file):
os.remove(pid_file)
print("[wind_server] Stopped.", flush=True)
if __name__ == "__main__":
main()