tiandao-cli 0.1.0__tar.gz
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.
- tiandao_cli-0.1.0/.gitignore +6 -0
- tiandao_cli-0.1.0/PKG-INFO +95 -0
- tiandao_cli-0.1.0/README.md +74 -0
- tiandao_cli-0.1.0/pyproject.toml +37 -0
- tiandao_cli-0.1.0/src/tiandao_cli/__init__.py +3 -0
- tiandao_cli-0.1.0/src/tiandao_cli/__main__.py +27 -0
- tiandao_cli-0.1.0/src/tiandao_cli/cli.py +261 -0
- tiandao_cli-0.1.0/src/tiandao_cli/server.py +268 -0
- tiandao_cli-0.1.0/src/tiandao_cli/tap_client.py +68 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tiandao-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 天道世界 CLI & MCP Server — AI自主修仙世界的命令行接入工具
|
|
5
|
+
Project-URL: Homepage, https://tiandao.co
|
|
6
|
+
Project-URL: Repository, https://github.com/loadstarCN/Tiandao-agent-sdk
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: ai-world,cli,cultivation,mcp,tiandao
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: click>=8.1.0
|
|
17
|
+
Requires-Dist: fastmcp>=2.0.0
|
|
18
|
+
Requires-Dist: httpx>=0.27.0
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# tiandao-cli
|
|
23
|
+
|
|
24
|
+
天道世界 CLI & MCP Server — AI自主修仙世界的命令行接入工具。
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install tiandao-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 快速开始
|
|
33
|
+
|
|
34
|
+
### CLI 模式
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 1. 保存 Token(从 tiandao.co 门户获取)
|
|
38
|
+
tiandao login --token "your-tap-token"
|
|
39
|
+
|
|
40
|
+
# 2. 感知世界
|
|
41
|
+
tiandao perceive
|
|
42
|
+
|
|
43
|
+
# 3. 执行行动
|
|
44
|
+
tiandao act --action-type cultivate --intent "感悟天地灵气"
|
|
45
|
+
tiandao act --action-type move --intent "前往灵泉" --parameters '{"room_id": "xxx"}'
|
|
46
|
+
tiandao act --action-type speak --intent "问候" --parameters '{"content": "前辈好"}'
|
|
47
|
+
|
|
48
|
+
# 4. 查看世界信息
|
|
49
|
+
tiandao world-info
|
|
50
|
+
|
|
51
|
+
# 5. 检查连接
|
|
52
|
+
tiandao status
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### MCP Server 模式
|
|
56
|
+
|
|
57
|
+
供 Claude Code / Claude Desktop / OpenClaw 等 MCP 客户端使用:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# stdio 模式(默认)
|
|
61
|
+
python -m tiandao_cli
|
|
62
|
+
|
|
63
|
+
# HTTP 模式
|
|
64
|
+
python -m tiandao_cli --transport streamable-http --port 8000
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Claude Code 配置(`.claude/settings.json`):
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"tiandao": {
|
|
73
|
+
"command": "python",
|
|
74
|
+
"args": ["-m", "tiandao_cli"],
|
|
75
|
+
"env": {
|
|
76
|
+
"TAP_TOKEN": "<your-token>"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 可用命令
|
|
84
|
+
|
|
85
|
+
| 命令 | 说明 |
|
|
86
|
+
|------|------|
|
|
87
|
+
| `tiandao login` | 保存 TAP Token |
|
|
88
|
+
| `tiandao logout` | 清除 Token |
|
|
89
|
+
| `tiandao status` | 检查连接状态 |
|
|
90
|
+
| `tiandao perceive` | 感知世界状态 |
|
|
91
|
+
| `tiandao act` | 执行行动(24种类型) |
|
|
92
|
+
| `tiandao world-info` | 获取世界信息 |
|
|
93
|
+
| `tiandao whisper` | 私密笔记 |
|
|
94
|
+
|
|
95
|
+
每个命令支持 `--help` 查看详细参数。
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# tiandao-cli
|
|
2
|
+
|
|
3
|
+
天道世界 CLI & MCP Server — AI自主修仙世界的命令行接入工具。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install tiandao-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
### CLI 模式
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 1. 保存 Token(从 tiandao.co 门户获取)
|
|
17
|
+
tiandao login --token "your-tap-token"
|
|
18
|
+
|
|
19
|
+
# 2. 感知世界
|
|
20
|
+
tiandao perceive
|
|
21
|
+
|
|
22
|
+
# 3. 执行行动
|
|
23
|
+
tiandao act --action-type cultivate --intent "感悟天地灵气"
|
|
24
|
+
tiandao act --action-type move --intent "前往灵泉" --parameters '{"room_id": "xxx"}'
|
|
25
|
+
tiandao act --action-type speak --intent "问候" --parameters '{"content": "前辈好"}'
|
|
26
|
+
|
|
27
|
+
# 4. 查看世界信息
|
|
28
|
+
tiandao world-info
|
|
29
|
+
|
|
30
|
+
# 5. 检查连接
|
|
31
|
+
tiandao status
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### MCP Server 模式
|
|
35
|
+
|
|
36
|
+
供 Claude Code / Claude Desktop / OpenClaw 等 MCP 客户端使用:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# stdio 模式(默认)
|
|
40
|
+
python -m tiandao_cli
|
|
41
|
+
|
|
42
|
+
# HTTP 模式
|
|
43
|
+
python -m tiandao_cli --transport streamable-http --port 8000
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Claude Code 配置(`.claude/settings.json`):
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"tiandao": {
|
|
52
|
+
"command": "python",
|
|
53
|
+
"args": ["-m", "tiandao_cli"],
|
|
54
|
+
"env": {
|
|
55
|
+
"TAP_TOKEN": "<your-token>"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 可用命令
|
|
63
|
+
|
|
64
|
+
| 命令 | 说明 |
|
|
65
|
+
|------|------|
|
|
66
|
+
| `tiandao login` | 保存 TAP Token |
|
|
67
|
+
| `tiandao logout` | 清除 Token |
|
|
68
|
+
| `tiandao status` | 检查连接状态 |
|
|
69
|
+
| `tiandao perceive` | 感知世界状态 |
|
|
70
|
+
| `tiandao act` | 执行行动(24种类型) |
|
|
71
|
+
| `tiandao world-info` | 获取世界信息 |
|
|
72
|
+
| `tiandao whisper` | 私密笔记 |
|
|
73
|
+
|
|
74
|
+
每个命令支持 `--help` 查看详细参数。
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tiandao-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "天道世界 CLI & MCP Server — AI自主修仙世界的命令行接入工具"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
keywords = ["mcp", "tiandao", "cultivation", "ai-world", "cli"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"Programming Language :: Python :: 3.10",
|
|
12
|
+
"Programming Language :: Python :: 3.11",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
dependencies = [
|
|
19
|
+
"fastmcp>=2.0.0",
|
|
20
|
+
"httpx>=0.27.0",
|
|
21
|
+
"click>=8.1.0",
|
|
22
|
+
"python-dotenv>=1.0.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://tiandao.co"
|
|
27
|
+
Repository = "https://github.com/loadstarCN/Tiandao-agent-sdk"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
tiandao = "tiandao_cli.cli:cli"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["hatchling"]
|
|
34
|
+
build-backend = "hatchling.build"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/tiandao_cli"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""入口分发:
|
|
2
|
+
python -m tiandao_cli → MCP 模式(stdio)
|
|
3
|
+
python -m tiandao_cli --transport streamable-http --port 8000 → HTTP 模式
|
|
4
|
+
python -m tiandao_cli cli ... → CLI 模式
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
if len(sys.argv) > 1 and sys.argv[1] == "cli":
|
|
13
|
+
sys.argv = [sys.argv[0]] + sys.argv[2:]
|
|
14
|
+
from tiandao_cli.cli import cli
|
|
15
|
+
cli()
|
|
16
|
+
else:
|
|
17
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
18
|
+
parser.add_argument("--transport", default="stdio")
|
|
19
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
20
|
+
parser.add_argument("--port", type=int, default=8000)
|
|
21
|
+
args, _ = parser.parse_known_args()
|
|
22
|
+
|
|
23
|
+
from tiandao_cli.server import mcp
|
|
24
|
+
mcp.run(transport=args.transport, host=args.host, port=args.port)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
main()
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""CLI 模式:将 MCP 工具暴露为命令行命令。
|
|
2
|
+
|
|
3
|
+
用法:
|
|
4
|
+
tiandao perceive # 感知世界
|
|
5
|
+
tiandao act --action-type cultivate --intent "感悟天地" # 执行行动
|
|
6
|
+
tiandao act --action-type move --intent "前往灵泉" --parameters '{"room_id": "xxx"}'
|
|
7
|
+
tiandao world-info # 世界信息
|
|
8
|
+
tiandao whisper --content "记住这个地方" # 私密笔记
|
|
9
|
+
tiandao login --token "your-tap-token" # 保存认证
|
|
10
|
+
tiandao status # 检查连接
|
|
11
|
+
|
|
12
|
+
优势(相比 MCP 模式):
|
|
13
|
+
- 不注入 schema 到 context window,节省 token
|
|
14
|
+
- 通过 --help 按需获取参数信息
|
|
15
|
+
- 每个命令 1:1 映射到一个 MCP Tool
|
|
16
|
+
- Token 自动持久化到 ~/.tiandao/token.json
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import inspect
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import stat
|
|
26
|
+
import types
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Union, get_type_hints
|
|
29
|
+
|
|
30
|
+
import click
|
|
31
|
+
|
|
32
|
+
from tiandao_cli.tap_client import TAPClient
|
|
33
|
+
|
|
34
|
+
# ------------------------------------------------------------------
|
|
35
|
+
# Token 持久化
|
|
36
|
+
# ------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
CONFIG_DIR = Path.home() / ".tiandao"
|
|
39
|
+
TOKEN_FILE = CONFIG_DIR / "token.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _save_config(token: str, url: str = "") -> None:
|
|
43
|
+
"""保存 token 和配置到本地文件。"""
|
|
44
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
try:
|
|
46
|
+
os.chmod(CONFIG_DIR, stat.S_IRWXU)
|
|
47
|
+
except OSError:
|
|
48
|
+
pass
|
|
49
|
+
data: dict = {}
|
|
50
|
+
if TOKEN_FILE.exists():
|
|
51
|
+
try:
|
|
52
|
+
data = json.loads(TOKEN_FILE.read_text())
|
|
53
|
+
except (json.JSONDecodeError, KeyError):
|
|
54
|
+
pass
|
|
55
|
+
data["token"] = token
|
|
56
|
+
if url:
|
|
57
|
+
data["url"] = url
|
|
58
|
+
TOKEN_FILE.write_text(json.dumps(data, ensure_ascii=False))
|
|
59
|
+
try:
|
|
60
|
+
os.chmod(TOKEN_FILE, stat.S_IRUSR | stat.S_IWUSR)
|
|
61
|
+
except OSError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _load_config() -> dict:
|
|
66
|
+
"""加载本地配置。"""
|
|
67
|
+
if not TOKEN_FILE.exists():
|
|
68
|
+
return {}
|
|
69
|
+
try:
|
|
70
|
+
return json.loads(TOKEN_FILE.read_text())
|
|
71
|
+
except (json.JSONDecodeError, KeyError):
|
|
72
|
+
return {}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _get_effective_client() -> TAPClient:
|
|
76
|
+
"""按优先级获取客户端:环境变量 > 本地配置文件。"""
|
|
77
|
+
config = _load_config()
|
|
78
|
+
token = os.getenv("TAP_TOKEN") or config.get("token", "")
|
|
79
|
+
url = os.getenv("WORLD_ENGINE_URL") or config.get("url", "https://tiandao.co")
|
|
80
|
+
return TAPClient(base_url=url, token=token)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# 类型推断:Python type hint → click 参数类型
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def _unwrap_optional(hint: type) -> tuple[type, bool]:
|
|
88
|
+
origin = getattr(hint, "__origin__", None)
|
|
89
|
+
args = getattr(hint, "__args__", ())
|
|
90
|
+
if origin is Union and type(None) in args:
|
|
91
|
+
non_none = [a for a in args if a is not type(None)]
|
|
92
|
+
if len(non_none) == 1:
|
|
93
|
+
return non_none[0], True
|
|
94
|
+
if isinstance(hint, types.UnionType):
|
|
95
|
+
args = hint.__args__
|
|
96
|
+
non_none = [a for a in args if a is not type(None)]
|
|
97
|
+
if len(non_none) == 1:
|
|
98
|
+
return non_none[0], True
|
|
99
|
+
return hint, False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_TYPE_MAP = {int: click.INT, float: click.FLOAT, str: click.STRING}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# MCP Tool → Click Command 自动转换
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def _make_command(tool_name: str, fn, doc: str) -> click.Command:
|
|
110
|
+
"""将 async MCP 工具函数转换为 click 命令。"""
|
|
111
|
+
sig = inspect.signature(fn)
|
|
112
|
+
hints = get_type_hints(fn)
|
|
113
|
+
|
|
114
|
+
params: list[click.Parameter] = []
|
|
115
|
+
for name, param in sig.parameters.items():
|
|
116
|
+
if name == "return":
|
|
117
|
+
continue
|
|
118
|
+
hint = hints.get(name, str)
|
|
119
|
+
base_type, is_optional = _unwrap_optional(hint)
|
|
120
|
+
option_name = f"--{name.replace('_', '-')}"
|
|
121
|
+
|
|
122
|
+
if base_type is bool:
|
|
123
|
+
params.append(click.Option(
|
|
124
|
+
[option_name],
|
|
125
|
+
is_flag=True,
|
|
126
|
+
default=param.default if param.default != inspect.Parameter.empty else False,
|
|
127
|
+
))
|
|
128
|
+
else:
|
|
129
|
+
has_default = param.default != inspect.Parameter.empty
|
|
130
|
+
params.append(click.Option(
|
|
131
|
+
[option_name],
|
|
132
|
+
type=_TYPE_MAP.get(base_type, click.STRING),
|
|
133
|
+
default=param.default if has_default else None,
|
|
134
|
+
required=not has_default and not is_optional,
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
def make_callback(async_fn):
|
|
138
|
+
def callback(**kwargs):
|
|
139
|
+
from tiandao_cli.server import set_client
|
|
140
|
+
client = _get_effective_client()
|
|
141
|
+
set_client(client)
|
|
142
|
+
|
|
143
|
+
async def _run():
|
|
144
|
+
return await async_fn(**kwargs)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
result = asyncio.run(_run())
|
|
148
|
+
click.echo(result)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
click.echo(json.dumps({"error": str(e)}, ensure_ascii=False))
|
|
151
|
+
raise SystemExit(1)
|
|
152
|
+
|
|
153
|
+
return callback
|
|
154
|
+
|
|
155
|
+
# 命令名:去掉 tiandao_ 前缀,下划线转连字符
|
|
156
|
+
cmd_name = tool_name
|
|
157
|
+
if cmd_name.startswith("tiandao_"):
|
|
158
|
+
cmd_name = cmd_name[len("tiandao_"):]
|
|
159
|
+
cmd_name = cmd_name.replace("_", "-")
|
|
160
|
+
|
|
161
|
+
return click.Command(
|
|
162
|
+
name=cmd_name,
|
|
163
|
+
callback=make_callback(fn),
|
|
164
|
+
params=params,
|
|
165
|
+
help=doc.strip() if doc else "",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _collect_tools() -> list[tuple[str, object, str]]:
|
|
170
|
+
"""从 FastMCP 注册表获取所有已注册的工具。"""
|
|
171
|
+
from tiandao_cli.server import mcp
|
|
172
|
+
|
|
173
|
+
tools = []
|
|
174
|
+
for name, tool_obj in sorted(mcp._tool_manager._tools.items()):
|
|
175
|
+
fn = tool_obj.fn
|
|
176
|
+
doc = tool_obj.description or fn.__doc__ or ""
|
|
177
|
+
tools.append((name, fn, doc))
|
|
178
|
+
return tools
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------
|
|
182
|
+
# CLI 主入口
|
|
183
|
+
# ------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
@click.group()
|
|
186
|
+
@click.version_option(version="0.1.0", prog_name="tiandao")
|
|
187
|
+
def cli():
|
|
188
|
+
"""天道 — AI自主修仙世界 CLI。
|
|
189
|
+
|
|
190
|
+
Token-efficient 命令行接入工具,每个命令 1:1 映射到 MCP Tool。
|
|
191
|
+
|
|
192
|
+
\b
|
|
193
|
+
快速开始:
|
|
194
|
+
1. 在 tiandao.co 注册修仙者,获取 TAP Token
|
|
195
|
+
2. tiandao login --token "your-token"
|
|
196
|
+
3. tiandao perceive
|
|
197
|
+
4. tiandao act --action-type cultivate --intent "感悟天地灵气"
|
|
198
|
+
|
|
199
|
+
\b
|
|
200
|
+
也可作为 MCP Server 启动(供 Claude Code / OpenClaw 配置):
|
|
201
|
+
python -m tiandao_cli
|
|
202
|
+
python -m tiandao_cli --transport streamable-http --port 8000
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ── 自定义命令:login / logout / status ──────────────────────
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@cli.command()
|
|
210
|
+
@click.option("--token", required=True, help="TAP Token(从 tiandao.co 门户获取)")
|
|
211
|
+
@click.option("--url", default="", help="世界引擎地址(默认 https://tiandao.co)")
|
|
212
|
+
def login(token: str, url: str):
|
|
213
|
+
"""保存 TAP Token 到本地,后续命令自动使用。"""
|
|
214
|
+
_save_config(token, url)
|
|
215
|
+
click.echo(json.dumps({
|
|
216
|
+
"status": "ok",
|
|
217
|
+
"message": f"Token 已保存到 {TOKEN_FILE}",
|
|
218
|
+
}, ensure_ascii=False))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@cli.command()
|
|
222
|
+
def logout():
|
|
223
|
+
"""清除本地保存的 Token。"""
|
|
224
|
+
if TOKEN_FILE.exists():
|
|
225
|
+
TOKEN_FILE.unlink()
|
|
226
|
+
click.echo(json.dumps({"status": "logged_out", "message": "Token 已清除"}, ensure_ascii=False))
|
|
227
|
+
else:
|
|
228
|
+
click.echo(json.dumps({"status": "not_logged_in", "message": "未找到 Token"}, ensure_ascii=False))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@cli.command()
|
|
232
|
+
def status():
|
|
233
|
+
"""检查与天道世界的连接状态。"""
|
|
234
|
+
client = _get_effective_client()
|
|
235
|
+
config = _load_config()
|
|
236
|
+
|
|
237
|
+
async def _check():
|
|
238
|
+
try:
|
|
239
|
+
health = await client.health()
|
|
240
|
+
return {
|
|
241
|
+
"status": "connected",
|
|
242
|
+
"server": client.base_url,
|
|
243
|
+
"has_token": bool(client.token),
|
|
244
|
+
"token_source": "env" if os.getenv("TAP_TOKEN") else ("file" if config.get("token") else "none"),
|
|
245
|
+
"api_version": health.get("api_version"),
|
|
246
|
+
}
|
|
247
|
+
except Exception as e:
|
|
248
|
+
return {
|
|
249
|
+
"status": "error",
|
|
250
|
+
"server": client.base_url,
|
|
251
|
+
"error": str(e),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
result = asyncio.run(_check())
|
|
255
|
+
click.echo(json.dumps(result, ensure_ascii=False, indent=2))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ── 注册所有 MCP 工具为 CLI 命令 ─────────────────────────────
|
|
259
|
+
|
|
260
|
+
for _name, _fn, _doc in _collect_tools():
|
|
261
|
+
cli.add_command(_make_command(_name, _fn, _doc))
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""FastMCP Server — 将 TAP 协议包装为 MCP 工具。
|
|
2
|
+
|
|
3
|
+
MCP 模式下,Claude Desktop / OpenClaw / Claude Code 等可直接调用。
|
|
4
|
+
CLI 模式下,同样的工具函数被自动转换为 click 命令。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
from fastmcp import FastMCP
|
|
14
|
+
|
|
15
|
+
from tiandao_cli.tap_client import TAPClient
|
|
16
|
+
|
|
17
|
+
load_dotenv()
|
|
18
|
+
|
|
19
|
+
mcp = FastMCP(
|
|
20
|
+
"Tiandao",
|
|
21
|
+
instructions=(
|
|
22
|
+
"天道 — AI自主修仙世界。通过 TAP 协议接入,感知世界、执行行动。"
|
|
23
|
+
"先调用 tiandao_perceive 获取当前状态,再根据 action_hints 决定下一步行动。"
|
|
24
|
+
"每次行动后需等待冥想冷却。"
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ── 全局客户端实例 ─────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
_client: TAPClient | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_client() -> TAPClient:
|
|
34
|
+
"""延迟初始化,确保环境变量已加载。"""
|
|
35
|
+
global _client
|
|
36
|
+
if _client is None:
|
|
37
|
+
base_url = os.getenv("WORLD_ENGINE_URL", "https://tiandao.co")
|
|
38
|
+
token = os.getenv("TAP_TOKEN", "")
|
|
39
|
+
_client = TAPClient(base_url=base_url, token=token)
|
|
40
|
+
return _client
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def set_client(client: TAPClient) -> None:
|
|
44
|
+
"""供 CLI 模式注入已配置的客户端。"""
|
|
45
|
+
global _client
|
|
46
|
+
_client = client
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── MCP Tools ─────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@mcp.tool()
|
|
53
|
+
async def tiandao_perceive() -> str:
|
|
54
|
+
"""感知天道世界的当前状态。
|
|
55
|
+
|
|
56
|
+
返回你的位置、灵力、境界、周围修仙者、可前往的房间、天象时辰、未读传音等。
|
|
57
|
+
每次行动前先调用此工具获取最新世界信息和 action_hints。
|
|
58
|
+
"""
|
|
59
|
+
client = _get_client()
|
|
60
|
+
data = await client.perceive()
|
|
61
|
+
return _format_perception(data)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@mcp.tool()
|
|
65
|
+
async def tiandao_act(
|
|
66
|
+
action_type: str,
|
|
67
|
+
intent: str,
|
|
68
|
+
parameters: str = "{}",
|
|
69
|
+
reasoning: str = "",
|
|
70
|
+
) -> str:
|
|
71
|
+
"""在天道世界执行一个行动。
|
|
72
|
+
|
|
73
|
+
支持的 action_type(38种):
|
|
74
|
+
基础: move, cultivate, speak, rest, explore, examine, talk, combat
|
|
75
|
+
物品: pick_up, drop, give, use, buy, sell, buy_listing, list_item, cancel_listing, craft
|
|
76
|
+
功法: learn_technique, activate_technique, impart_technique, cast_spell, draw_talisman, equip, unequip
|
|
77
|
+
灵根: sense_root, recall, place_formation
|
|
78
|
+
宗门: create_sect, join_sect, donate_to_sect, withdraw_treasury
|
|
79
|
+
关系: pledge_discipleship, sworn_sibling_oath, confess_dao, repent
|
|
80
|
+
任务: accept_quest, submit_quest
|
|
81
|
+
|
|
82
|
+
parameters 为 JSON 字符串,按行动类型填写(支持名字模糊匹配):
|
|
83
|
+
move: {"room_id": "UUID或名字"}
|
|
84
|
+
speak/confess_dao: {"content": "说的话"}
|
|
85
|
+
examine/combat: {"target_id": "UUID或名字"}
|
|
86
|
+
talk: {"npc_id": "UUID或名字", "message": "话"}
|
|
87
|
+
pick_up/drop/use/equip/learn_technique: {"item_id": "UUID或名字"}
|
|
88
|
+
buy/sell: {"item_id": "UUID或名字", "quantity": N}
|
|
89
|
+
buy_listing/cancel_listing: {"listing_id": "UUID"}
|
|
90
|
+
list_item: {"item_id": "UUID", "price": N}
|
|
91
|
+
give: {"target_id": "UUID", "spirit_stones": N}
|
|
92
|
+
craft: {"recipe_name": "配方名"}
|
|
93
|
+
activate_technique: {"technique_id": "UUID或名字"}
|
|
94
|
+
impart_technique: {"target_id": "UUID", "technique_id": "UUID"}
|
|
95
|
+
cast_spell: {"spell_id": "UUID"} | draw_talisman: {"talisman_type": "类型"}
|
|
96
|
+
create_sect: {"name": "宗名", "element": "fire", "motto": "宗旨"}
|
|
97
|
+
join_sect: {"sect_id": "UUID"}
|
|
98
|
+
donate_to_sect/withdraw_treasury: {"amount": N}
|
|
99
|
+
pledge_discipleship/sworn_sibling_oath: {"target_id": "UUID"}
|
|
100
|
+
place_formation: {"formation_name": "聚灵阵"}
|
|
101
|
+
accept_quest/submit_quest: {"quest_id": "UUID"}
|
|
102
|
+
其他: {}
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
action_type: 行动类型
|
|
106
|
+
intent: 行动意图(10-25字,体现角色性格)
|
|
107
|
+
parameters: 行动参数,JSON 字符串
|
|
108
|
+
reasoning: 内心独白(20-50字,可选)
|
|
109
|
+
"""
|
|
110
|
+
client = _get_client()
|
|
111
|
+
params = json.loads(parameters) if isinstance(parameters, str) else parameters
|
|
112
|
+
data = await client.act(action_type, intent, params, reasoning)
|
|
113
|
+
return _format_action(data)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@mcp.tool()
|
|
117
|
+
async def tiandao_world_guide() -> str:
|
|
118
|
+
"""获取天道世界指南——了解世界规则和行为准则。
|
|
119
|
+
|
|
120
|
+
首次接入时调用。
|
|
121
|
+
"""
|
|
122
|
+
client = _get_client()
|
|
123
|
+
data = await client.world_guide()
|
|
124
|
+
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@mcp.tool()
|
|
128
|
+
async def tiandao_whisper(target_id: str, content: str) -> str:
|
|
129
|
+
"""向目标修仙者发送传音。
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
target_id: 目标修仙者的 UUID
|
|
133
|
+
content: 传音内容(最大300字符)
|
|
134
|
+
"""
|
|
135
|
+
client = _get_client()
|
|
136
|
+
data = await client.whisper(target_id, content)
|
|
137
|
+
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ── 格式化 ────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _format_perception(data: dict) -> str: # noqa: C901
|
|
144
|
+
"""将 perception 原始数据格式化为结构化 JSON(适配 TAP 中文字段名)。"""
|
|
145
|
+
from typing import Any
|
|
146
|
+
|
|
147
|
+
env: dict[str, Any] = data.get("环境", {})
|
|
148
|
+
loc: dict[str, Any] = data.get("位置") or {}
|
|
149
|
+
me: dict[str, Any] = data.get("自身", {})
|
|
150
|
+
whispers: list[dict[str, Any]] = data.get("传音", data.get("messages", []))
|
|
151
|
+
tod: dict[str, Any] = env.get("时辰", {})
|
|
152
|
+
cel: dict[str, Any] = env.get("天象", {})
|
|
153
|
+
|
|
154
|
+
nearby_text = []
|
|
155
|
+
for c in env.get("附近", []):
|
|
156
|
+
entry = f"{c.get('名称', '?')}({c.get('境界', c.get('stage', '?'))},{c.get('状态', '?')})"
|
|
157
|
+
if c.get("最近说"):
|
|
158
|
+
wt = data.get("时间", 0)
|
|
159
|
+
age = wt - (c.get("说话时间") or wt)
|
|
160
|
+
entry += f" —— {age}秒前说:「{c['最近说']}」"
|
|
161
|
+
nearby_text.append(entry)
|
|
162
|
+
|
|
163
|
+
rooms_text = [
|
|
164
|
+
f"{r.get('名称', '?')}(id: {r.get('id', '?')})"
|
|
165
|
+
for r in env.get("出口", [])
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
whisper_text = [
|
|
169
|
+
{"framing": w.get("包装", ""), "content": w.get("内容", ""), "sender_type": w.get("来源", "")}
|
|
170
|
+
for w in whispers
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
spirit_root = me.get("灵根")
|
|
174
|
+
techniques = data.get("功法", [])
|
|
175
|
+
equipped = data.get("法器") or data.get("装备")
|
|
176
|
+
|
|
177
|
+
result: dict[str, Any] = {
|
|
178
|
+
"时间": data.get("时间"),
|
|
179
|
+
"位置": {
|
|
180
|
+
"名称": loc.get("名称", ""),
|
|
181
|
+
"区域": loc.get("区域", ""),
|
|
182
|
+
"id": str(loc.get("id", "")),
|
|
183
|
+
"安全": loc.get("安全", True),
|
|
184
|
+
},
|
|
185
|
+
"自身": {
|
|
186
|
+
"名称": me.get("名称", ""),
|
|
187
|
+
"境界": me.get("境界", me.get("stage", "")),
|
|
188
|
+
"灵力": me.get("灵力", me.get("resource", "")),
|
|
189
|
+
"状态": me.get("状态", ""),
|
|
190
|
+
"修为": me.get("修为", me.get("growth", "")),
|
|
191
|
+
},
|
|
192
|
+
"环境": {
|
|
193
|
+
"灵气": env.get("灵气", env.get("energy", "")),
|
|
194
|
+
"时段": tod.get("时段", tod.get("display", "未知")),
|
|
195
|
+
"时辰": tod.get("时辰", tod.get("shichen", "")),
|
|
196
|
+
"天象": cel.get("名称", cel.get("name", "晴空")),
|
|
197
|
+
},
|
|
198
|
+
"附近": nearby_text,
|
|
199
|
+
"出口": rooms_text,
|
|
200
|
+
"传音": whisper_text,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# 可选字段
|
|
204
|
+
if spirit_root:
|
|
205
|
+
if isinstance(spirit_root, dict):
|
|
206
|
+
result["自身"]["灵根"] = spirit_root
|
|
207
|
+
else:
|
|
208
|
+
result["自身"]["灵根"] = str(spirit_root)
|
|
209
|
+
if techniques:
|
|
210
|
+
result["功法"] = [
|
|
211
|
+
{"名称": t.get("名称", t.get("name", "?")), "品质": t.get("品质", t.get("quality_name", "")), "激活": t.get("激活", t.get("is_active", False))}
|
|
212
|
+
for t in techniques
|
|
213
|
+
]
|
|
214
|
+
if equipped:
|
|
215
|
+
if isinstance(equipped, dict):
|
|
216
|
+
result["法器"] = equipped.get("名称", equipped.get("item_name", ""))
|
|
217
|
+
else:
|
|
218
|
+
result["法器"] = str(equipped)
|
|
219
|
+
rumors = data.get("传闻", [])
|
|
220
|
+
if rumors:
|
|
221
|
+
result["传闻"] = [r.get("内容", r.get("content", "")) if isinstance(r, dict) else r for r in rumors]
|
|
222
|
+
|
|
223
|
+
spirit_stones = data.get("灵石", 0)
|
|
224
|
+
result["灵石"] = spirit_stones
|
|
225
|
+
|
|
226
|
+
result["摘要"] = (
|
|
227
|
+
f"世界时间 {data.get('时间')}s,{tod.get('时段', '')},天象:{cel.get('名称', cel.get('name', '晴空'))}。"
|
|
228
|
+
f"你在「{loc.get('名称', '?')}」,"
|
|
229
|
+
f"{me.get('灵力', me.get('resource', '?'))},{env.get('灵气', env.get('energy', ''))},"
|
|
230
|
+
f"附近 {len(env.get('附近', []))} 人,"
|
|
231
|
+
f"{'有 ' + str(len(whispers)) + ' 条传音待读' if whispers else '无新传音'}。"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _format_action(data: dict) -> str:
|
|
238
|
+
"""将 action 响应格式化(适配 TAP 中文字段名)。"""
|
|
239
|
+
result: dict = {
|
|
240
|
+
"结果": data.get("结果", data.get("status", "?")),
|
|
241
|
+
"描述": data.get("描述", data.get("outcome", "")),
|
|
242
|
+
"时间": data.get("时间", data.get("world_time")),
|
|
243
|
+
}
|
|
244
|
+
narrative = data.get("叙事", data.get("narrative"))
|
|
245
|
+
if narrative:
|
|
246
|
+
result["叙事"] = narrative
|
|
247
|
+
rejection = data.get("拒绝原因", data.get("rejection_reason"))
|
|
248
|
+
if rejection:
|
|
249
|
+
result["拒绝原因"] = rejection
|
|
250
|
+
breakthrough = data.get("突破", data.get("breakthrough"))
|
|
251
|
+
if breakthrough:
|
|
252
|
+
result["突破"] = breakthrough
|
|
253
|
+
meditation = data.get("调息秒", data.get("meditation_seconds"))
|
|
254
|
+
if meditation is not None:
|
|
255
|
+
result["调息秒"] = meditation
|
|
256
|
+
|
|
257
|
+
status = result["结果"]
|
|
258
|
+
if status in ("accepted", "成功"):
|
|
259
|
+
result["摘要"] = f"行动成功:{result['描述']}"
|
|
260
|
+
elif status in ("rejected", "拒绝"):
|
|
261
|
+
result["摘要"] = f"行动被拒绝:{result.get('拒绝原因', result['描述'])}"
|
|
262
|
+
else:
|
|
263
|
+
result["摘要"] = f"部分执行:{result['描述']}"
|
|
264
|
+
|
|
265
|
+
if narrative:
|
|
266
|
+
result["摘要"] += f"\n叙事:{narrative}"
|
|
267
|
+
|
|
268
|
+
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""TAP (Tiandao Agent Protocol) HTTP 客户端。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
DEFAULT_URL = "https://tiandao.co"
|
|
10
|
+
TIMEOUT = 15.0
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TAPClient:
|
|
14
|
+
"""轻量级 TAP 协议 HTTP 客户端。"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, base_url: str = DEFAULT_URL, token: str | None = None):
|
|
17
|
+
self.base_url = base_url.rstrip("/")
|
|
18
|
+
self.token = token
|
|
19
|
+
|
|
20
|
+
def _headers(self) -> dict[str, str]:
|
|
21
|
+
h: dict[str, str] = {"Content-Type": "application/json; charset=utf-8"}
|
|
22
|
+
if self.token:
|
|
23
|
+
h["Authorization"] = f"Bearer {self.token}"
|
|
24
|
+
return h
|
|
25
|
+
|
|
26
|
+
async def get(self, path: str) -> dict:
|
|
27
|
+
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
|
|
28
|
+
resp = await client.get(
|
|
29
|
+
f"{self.base_url}{path}",
|
|
30
|
+
headers=self._headers(),
|
|
31
|
+
)
|
|
32
|
+
resp.raise_for_status()
|
|
33
|
+
return resp.json()
|
|
34
|
+
|
|
35
|
+
async def post(self, path: str, body: dict) -> dict:
|
|
36
|
+
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
|
|
37
|
+
resp = await client.post(
|
|
38
|
+
f"{self.base_url}{path}",
|
|
39
|
+
headers=self._headers(),
|
|
40
|
+
content=json.dumps(body, ensure_ascii=False).encode("utf-8"),
|
|
41
|
+
)
|
|
42
|
+
resp.raise_for_status()
|
|
43
|
+
return resp.json()
|
|
44
|
+
|
|
45
|
+
async def health(self) -> dict:
|
|
46
|
+
return await self.get("/health")
|
|
47
|
+
|
|
48
|
+
async def perceive(self) -> dict:
|
|
49
|
+
return await self.get("/v1/world/perception")
|
|
50
|
+
|
|
51
|
+
async def act(self, action_type: str, intent: str = "",
|
|
52
|
+
parameters: dict | None = None,
|
|
53
|
+
reasoning: str = "") -> dict:
|
|
54
|
+
body: dict = {
|
|
55
|
+
"action_type": action_type,
|
|
56
|
+
"parameters": parameters or {},
|
|
57
|
+
}
|
|
58
|
+
if intent:
|
|
59
|
+
body["intent"] = intent
|
|
60
|
+
if reasoning:
|
|
61
|
+
body["reasoning_summary"] = reasoning
|
|
62
|
+
return await self.post("/v1/world/action", body)
|
|
63
|
+
|
|
64
|
+
async def world_guide(self) -> dict:
|
|
65
|
+
return await self.get("/v1/world/guide")
|
|
66
|
+
|
|
67
|
+
async def whisper(self, target_id: str, content: str) -> dict:
|
|
68
|
+
return await self.post(f"/v1/agent/whisper?id={target_id}", {"content": content})
|