sciverse 0.3.0__py3-none-any.whl
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.
- sciverse/__init__.py +25 -0
- sciverse/cli.py +142 -0
- sciverse/client.py +177 -0
- sciverse/credentials.py +107 -0
- sciverse/py.typed +0 -0
- sciverse/tools.py +437 -0
- sciverse/types.py +191 -0
- sciverse-0.3.0.dist-info/METADATA +354 -0
- sciverse-0.3.0.dist-info/RECORD +11 -0
- sciverse-0.3.0.dist-info/WHEEL +4 -0
- sciverse-0.3.0.dist-info/entry_points.txt +2 -0
sciverse/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""SciVerse Agent Tools - Python SDK."""
|
|
2
|
+
from sciverse.client import AgentToolsClient
|
|
3
|
+
from sciverse.credentials import (
|
|
4
|
+
credentials_path,
|
|
5
|
+
delete_credentials,
|
|
6
|
+
load_credentials,
|
|
7
|
+
resolve_endpoint,
|
|
8
|
+
resolve_token,
|
|
9
|
+
save_credentials,
|
|
10
|
+
)
|
|
11
|
+
from sciverse.tools import OPENAI_TOOLS, ANTHROPIC_TOOLS, TOOLS_VERSION
|
|
12
|
+
|
|
13
|
+
__version__ = "0.3.0"
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AgentToolsClient",
|
|
16
|
+
"OPENAI_TOOLS",
|
|
17
|
+
"ANTHROPIC_TOOLS",
|
|
18
|
+
"TOOLS_VERSION",
|
|
19
|
+
"credentials_path",
|
|
20
|
+
"delete_credentials",
|
|
21
|
+
"load_credentials",
|
|
22
|
+
"resolve_endpoint",
|
|
23
|
+
"resolve_token",
|
|
24
|
+
"save_credentials",
|
|
25
|
+
]
|
sciverse/cli.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""`sciverse` CLI —— 主要为用户提供本地凭据管理。
|
|
2
|
+
|
|
3
|
+
设计选择:当前不实现完整 OAuth device-code flow(需要后端 + 控制台前端配套)。
|
|
4
|
+
走"token paste"模式 —— 体验类似 `aws configure` / `gh auth login (--with-token)`,
|
|
5
|
+
用户从控制台复制 token 粘贴,CLI 保存到 ~/.sciverse/credentials.json (0600)。
|
|
6
|
+
后续后端就绪后可无痛升级到 device-code(保留同样命令接口)。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
import webbrowser
|
|
13
|
+
from typing import Sequence
|
|
14
|
+
|
|
15
|
+
from .credentials import (
|
|
16
|
+
DEFAULT_ENDPOINT,
|
|
17
|
+
credentials_path,
|
|
18
|
+
delete_credentials,
|
|
19
|
+
load_credentials,
|
|
20
|
+
save_credentials,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
CONSOLE_TOKEN_URL = "https://sciverse.space/tokens"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _cmd_login(args: argparse.Namespace) -> int:
|
|
27
|
+
print("SciVerse 登录")
|
|
28
|
+
print("-" * 40)
|
|
29
|
+
print(f"1. 浏览器将打开控制台 token 页:{CONSOLE_TOKEN_URL}")
|
|
30
|
+
print("2. 登录后创建 / 复制一个 API Token(形如 sv-xxx)")
|
|
31
|
+
print("3. 把 token 粘贴回这里")
|
|
32
|
+
print()
|
|
33
|
+
|
|
34
|
+
if not args.no_browser:
|
|
35
|
+
try:
|
|
36
|
+
webbrowser.open(CONSOLE_TOKEN_URL)
|
|
37
|
+
except Exception:
|
|
38
|
+
print(f"[warn] 无法自动打开浏览器,请手动访问 {CONSOLE_TOKEN_URL}")
|
|
39
|
+
|
|
40
|
+
# 用 getpass 避免 token 在 shell history 留痕
|
|
41
|
+
if args.token:
|
|
42
|
+
token = args.token.strip()
|
|
43
|
+
else:
|
|
44
|
+
try:
|
|
45
|
+
from getpass import getpass
|
|
46
|
+
token = getpass("Token: ").strip()
|
|
47
|
+
except (EOFError, KeyboardInterrupt):
|
|
48
|
+
print("\n已取消", file=sys.stderr)
|
|
49
|
+
return 1
|
|
50
|
+
|
|
51
|
+
if not token:
|
|
52
|
+
print("错误:token 为空", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
if not token.startswith("sv-"):
|
|
55
|
+
print(
|
|
56
|
+
"[warn] token 看起来不像 SciVerse 格式(应以 sv- 开头),"
|
|
57
|
+
"但仍按用户输入保存。",
|
|
58
|
+
file=sys.stderr,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
endpoint = args.endpoint or DEFAULT_ENDPOINT
|
|
62
|
+
path = save_credentials(token, endpoint)
|
|
63
|
+
print(f"✓ 凭据已保存到 {path}(权限 0600)")
|
|
64
|
+
print(f" endpoint: {endpoint}")
|
|
65
|
+
print()
|
|
66
|
+
print("现在 Python / TS SDK / MCP server 不需要再传 token —— 会自动读取本文件。")
|
|
67
|
+
print("更换 token:再跑一次 `sciverse auth login`。")
|
|
68
|
+
print("删除:`sciverse auth logout`。")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _cmd_logout(_: argparse.Namespace) -> int:
|
|
73
|
+
if delete_credentials():
|
|
74
|
+
print(f"✓ 已删除 {credentials_path()}")
|
|
75
|
+
return 0
|
|
76
|
+
print(f"凭据文件不存在:{credentials_path()}", file=sys.stderr)
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cmd_status(_: argparse.Namespace) -> int:
|
|
81
|
+
path = credentials_path()
|
|
82
|
+
creds = load_credentials()
|
|
83
|
+
if creds is None:
|
|
84
|
+
print(f"未登录。运行 `sciverse auth login` 完成登录。")
|
|
85
|
+
print(f"凭据文件路径(未创建):{path}")
|
|
86
|
+
return 1
|
|
87
|
+
token = creds.get("token", "")
|
|
88
|
+
masked = (token[:6] + "…" + token[-4:]) if len(token) > 12 else "***"
|
|
89
|
+
print(f"已登录")
|
|
90
|
+
print(f" 凭据文件: {path}")
|
|
91
|
+
print(f" token: {masked}")
|
|
92
|
+
print(f" endpoint: {creds.get('endpoint', DEFAULT_ENDPOINT)}")
|
|
93
|
+
print(f" 保存时间: {creds.get('saved_at', '?')}")
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
98
|
+
parser = argparse.ArgumentParser(
|
|
99
|
+
prog="sciverse",
|
|
100
|
+
description="SciVerse 开发者工具 CLI。",
|
|
101
|
+
)
|
|
102
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
103
|
+
|
|
104
|
+
auth = sub.add_parser("auth", help="本地凭据管理")
|
|
105
|
+
auth_sub = auth.add_subparsers(dest="auth_command", required=True)
|
|
106
|
+
|
|
107
|
+
login = auth_sub.add_parser(
|
|
108
|
+
"login",
|
|
109
|
+
help="保存 API Token 到 ~/.sciverse/credentials.json",
|
|
110
|
+
)
|
|
111
|
+
login.add_argument(
|
|
112
|
+
"--token",
|
|
113
|
+
help="直接传入 token(跳过交互式粘贴;适合脚本场景)",
|
|
114
|
+
)
|
|
115
|
+
login.add_argument(
|
|
116
|
+
"--endpoint",
|
|
117
|
+
help=f"API endpoint,默认 {DEFAULT_ENDPOINT}",
|
|
118
|
+
)
|
|
119
|
+
login.add_argument(
|
|
120
|
+
"--no-browser",
|
|
121
|
+
action="store_true",
|
|
122
|
+
help="不自动打开浏览器(远程 / 无桌面环境)",
|
|
123
|
+
)
|
|
124
|
+
login.set_defaults(func=_cmd_login)
|
|
125
|
+
|
|
126
|
+
logout = auth_sub.add_parser("logout", help="删除本地凭据文件")
|
|
127
|
+
logout.set_defaults(func=_cmd_logout)
|
|
128
|
+
|
|
129
|
+
status = auth_sub.add_parser("status", help="查看当前凭据状态")
|
|
130
|
+
status.set_defaults(func=_cmd_status)
|
|
131
|
+
|
|
132
|
+
return parser
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
136
|
+
parser = _build_parser()
|
|
137
|
+
args = parser.parse_args(argv)
|
|
138
|
+
return args.func(args)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
raise SystemExit(main())
|
sciverse/client.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""SciVerse Agent Tools 异步 HTTP client。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import platform
|
|
5
|
+
import uuid
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
_SKILL_NAME = "sciverse"
|
|
12
|
+
_CHANNEL = "python-sdk"
|
|
13
|
+
_PLATFORM = platform.system().lower() # "linux" | "darwin" | "windows"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_PASSTHROUGH = ("query", "page", "page_size", "fields")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _to_backend_payload(kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
20
|
+
"""把 search_papers 高级参数映射为 platform-console MetaSearchBody 接受的
|
|
21
|
+
canonical 格式(query/filters/sort/fields/page/page_size)。"""
|
|
22
|
+
out: dict[str, Any] = {}
|
|
23
|
+
filters: list[dict[str, Any]] = []
|
|
24
|
+
sort: list[dict[str, str]] = []
|
|
25
|
+
|
|
26
|
+
for k in _PASSTHROUGH:
|
|
27
|
+
if k in kwargs and kwargs[k] is not None:
|
|
28
|
+
out[k] = kwargs[k]
|
|
29
|
+
|
|
30
|
+
def _filter(field: str, op: str, value: Any) -> None:
|
|
31
|
+
filters.append({"field": field, "operator": op, "value": value})
|
|
32
|
+
|
|
33
|
+
if (v := kwargs.get("title_contains")) is not None:
|
|
34
|
+
_filter("title", "FILTER_OP_CONTAINS", v)
|
|
35
|
+
if (v := kwargs.get("abstract_contains")) is not None:
|
|
36
|
+
_filter("abstract", "FILTER_OP_CONTAINS", v)
|
|
37
|
+
if (v := kwargs.get("authors")) is not None and len(v) > 0:
|
|
38
|
+
_filter("author", "FILTER_OP_IN", list(v))
|
|
39
|
+
if (v := kwargs.get("year_from")) is not None:
|
|
40
|
+
_filter("publication_published_year", "FILTER_OP_GTE", v)
|
|
41
|
+
if (v := kwargs.get("year_to")) is not None:
|
|
42
|
+
_filter("publication_published_year", "FILTER_OP_LTE", v)
|
|
43
|
+
if (v := kwargs.get("journals")) is not None and len(v) > 0:
|
|
44
|
+
_filter("publication_venue_name", "FILTER_OP_IN", list(v))
|
|
45
|
+
if (v := kwargs.get("subjects")) is not None and len(v) > 0:
|
|
46
|
+
_filter("subjects", "FILTER_OP_IN", list(v))
|
|
47
|
+
if (v := kwargs.get("filters_advanced")) is not None:
|
|
48
|
+
for item in v:
|
|
49
|
+
entry = dict(item)
|
|
50
|
+
entry.setdefault("operator", "FILTER_OP_EQ")
|
|
51
|
+
filters.append(entry)
|
|
52
|
+
|
|
53
|
+
sort_by_year = kwargs.get("sort_by_year")
|
|
54
|
+
if sort_by_year and sort_by_year != "none":
|
|
55
|
+
sort.append({
|
|
56
|
+
"field": "publication_published_year",
|
|
57
|
+
"order": "SORT_ORDER_DESC" if sort_by_year == "desc" else "SORT_ORDER_ASC",
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if filters:
|
|
61
|
+
out["filters"] = filters
|
|
62
|
+
if sort:
|
|
63
|
+
out["sort"] = sort
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AgentToolsClient:
|
|
68
|
+
"""封装 SciVerse 三个对外检索接口的 Bearer-authenticated 异步 client。
|
|
69
|
+
|
|
70
|
+
用法:
|
|
71
|
+
async with AgentToolsClient(base_url=..., token=...) as c:
|
|
72
|
+
r = await c.semantic_search(query="...")
|
|
73
|
+
|
|
74
|
+
token / base_url 都可省略 —— 省略时按以下顺序 fallback:
|
|
75
|
+
1. 显式参数
|
|
76
|
+
2. 环境变量 SCIVERSE_API_TOKEN / SCIVERSE_BASE_URL
|
|
77
|
+
3. ~/.sciverse/credentials.json(由 `sciverse auth login` 写入)
|
|
78
|
+
4. base_url 默认值 https://api.sciverse.space;token 找不到则抛 ValueError
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
base_url: str | None = None,
|
|
85
|
+
token: str | None = None,
|
|
86
|
+
timeout: float = 30.0,
|
|
87
|
+
) -> None:
|
|
88
|
+
from sciverse.credentials import resolve_endpoint, resolve_token
|
|
89
|
+
resolved_token = resolve_token(token)
|
|
90
|
+
if not resolved_token:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"未找到 SciVerse API Token。请显式传 token、或设 SCIVERSE_API_TOKEN 环境变量、"
|
|
93
|
+
"或运行 `sciverse auth login` 保存凭据到 ~/.sciverse/credentials.json。"
|
|
94
|
+
)
|
|
95
|
+
self._base_url = resolve_endpoint(base_url).rstrip("/")
|
|
96
|
+
self._token = resolved_token
|
|
97
|
+
self._client = httpx.AsyncClient(
|
|
98
|
+
base_url=self._base_url,
|
|
99
|
+
timeout=timeout,
|
|
100
|
+
headers={"Authorization": f"Bearer {resolved_token}"},
|
|
101
|
+
event_hooks={"request": [self._inject_request_id]},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
async def _inject_request_id(request: httpx.Request) -> None:
|
|
106
|
+
request.headers["X-Request-Id"] = f"{_SKILL_NAME}-{_PLATFORM}-{_CHANNEL}-{uuid.uuid4()}"
|
|
107
|
+
|
|
108
|
+
async def __aenter__(self) -> "AgentToolsClient":
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
async def __aexit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None:
|
|
112
|
+
await self.aclose()
|
|
113
|
+
|
|
114
|
+
async def aclose(self) -> None:
|
|
115
|
+
await self._client.aclose()
|
|
116
|
+
|
|
117
|
+
async def search_papers(self, **kwargs: Any) -> dict[str, Any]:
|
|
118
|
+
"""对应 POST /meta-search。
|
|
119
|
+
|
|
120
|
+
参数会被转换为后端 canonical 格式。Agent 友好参数:
|
|
121
|
+
- query, page, page_size, fields (透传)
|
|
122
|
+
- title_contains, abstract_contains → filter CONTAINS
|
|
123
|
+
- authors → filter author IN
|
|
124
|
+
- year_from, year_to → filter publication_published_year GTE/LTE
|
|
125
|
+
- journals → filter publication_venue_name IN
|
|
126
|
+
- subjects → filter subjects IN
|
|
127
|
+
- filters_advanced → 直接拼到 filters 列表
|
|
128
|
+
- sort_by_year → sort publication_published_year DESC/ASC
|
|
129
|
+
"""
|
|
130
|
+
body = _to_backend_payload(kwargs)
|
|
131
|
+
resp = await self._client.post("/meta-search", json=body)
|
|
132
|
+
resp.raise_for_status()
|
|
133
|
+
return resp.json()
|
|
134
|
+
|
|
135
|
+
async def semantic_search(self, *, query: str, **kwargs: Any) -> dict[str, Any]:
|
|
136
|
+
"""对应 POST /agentic-search。"""
|
|
137
|
+
body = {"query": query, **{k: v for k, v in kwargs.items() if v is not None}}
|
|
138
|
+
resp = await self._client.post("/agentic-search", json=body)
|
|
139
|
+
resp.raise_for_status()
|
|
140
|
+
return resp.json()
|
|
141
|
+
|
|
142
|
+
async def list_catalog(self, *, include_sample_values: bool = False) -> dict[str, Any]:
|
|
143
|
+
"""对应 GET /meta-catalog。
|
|
144
|
+
|
|
145
|
+
返回字段 catalog:每个字段的名称 / 类型 / filterable / sortable / default_returned /
|
|
146
|
+
描述 / 适用 FilterOperator,外加 enum-like 字段的样本值(include_sample_values=True 时)。
|
|
147
|
+
Agent 第一次接触 SciVerse 或碰到字段不确定时建议先调一次再构造 search_papers。
|
|
148
|
+
"""
|
|
149
|
+
params = {"include_sample_values": str(include_sample_values).lower()}
|
|
150
|
+
resp = await self._client.get("/meta-catalog", params=params)
|
|
151
|
+
resp.raise_for_status()
|
|
152
|
+
return resp.json()
|
|
153
|
+
|
|
154
|
+
async def read_content(self, *, doc_id: str, offset: int = 0, limit: int = 4096) -> dict[str, Any]:
|
|
155
|
+
"""对应 GET /content。"""
|
|
156
|
+
params = {"doc_id": doc_id, "offset": offset, "limit": limit}
|
|
157
|
+
resp = await self._client.get("/content", params=params)
|
|
158
|
+
resp.raise_for_status()
|
|
159
|
+
return resp.json()
|
|
160
|
+
|
|
161
|
+
async def get_resource(self, *, file_name: str) -> tuple[bytes, str]:
|
|
162
|
+
"""对应 GET /resource。
|
|
163
|
+
|
|
164
|
+
取文献附属图片字节流。触发场景:read_content 返回的 Markdown 中含
|
|
165
|
+
`` 图片占位时,调本接口拿图片 binary。
|
|
166
|
+
|
|
167
|
+
返回 (bytes, mime_type)。mime_type 来自响应头 content-type,如
|
|
168
|
+
"image/png" / "image/jpeg" / "application/octet-stream"。
|
|
169
|
+
"""
|
|
170
|
+
resp = await self._client.get(
|
|
171
|
+
"/resource",
|
|
172
|
+
params={"file_name": file_name},
|
|
173
|
+
headers={"accept": "image/*"},
|
|
174
|
+
)
|
|
175
|
+
resp.raise_for_status()
|
|
176
|
+
mime = (resp.headers.get("content-type") or "application/octet-stream").split(";")[0].strip()
|
|
177
|
+
return resp.content, mime
|
sciverse/credentials.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""共享凭据文件读写。
|
|
2
|
+
|
|
3
|
+
约定(与 MCP server / TS SDK 一致):
|
|
4
|
+
- 路径:`~/.sciverse/credentials.json`
|
|
5
|
+
- 文件权限:0600(仅当前用户可读写)
|
|
6
|
+
- 内容:
|
|
7
|
+
{
|
|
8
|
+
"token": "sv-xxx",
|
|
9
|
+
"endpoint": "https://api.sciverse.space",
|
|
10
|
+
"saved_at": "2026-05-14T15:30:00Z"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
读取顺序(client.py / cli.py 共用):
|
|
14
|
+
1. 显式构造 client 传 token
|
|
15
|
+
2. 环境变量 SCIVERSE_API_TOKEN
|
|
16
|
+
3. 凭据文件
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import stat
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import TypedDict
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Credentials(TypedDict, total=False):
|
|
29
|
+
token: str
|
|
30
|
+
endpoint: str
|
|
31
|
+
saved_at: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DEFAULT_ENDPOINT = "https://api.sciverse.space"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def credentials_path() -> Path:
|
|
38
|
+
return Path.home() / ".sciverse" / "credentials.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_credentials() -> Credentials | None:
|
|
42
|
+
"""读凭据文件。文件不存在 / 解析失败 / 不是 dict 时返回 None(不抛错)。"""
|
|
43
|
+
path = credentials_path()
|
|
44
|
+
if not path.exists():
|
|
45
|
+
return None
|
|
46
|
+
try:
|
|
47
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
48
|
+
except (OSError, json.JSONDecodeError):
|
|
49
|
+
return None
|
|
50
|
+
if not isinstance(data, dict):
|
|
51
|
+
return None
|
|
52
|
+
return data # type: ignore[return-value]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def save_credentials(token: str, endpoint: str = DEFAULT_ENDPOINT) -> Path:
|
|
56
|
+
"""保存凭据到 ~/.sciverse/credentials.json,文件权限设为 0600。返回 path。"""
|
|
57
|
+
path = credentials_path()
|
|
58
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
payload: Credentials = {
|
|
60
|
+
"token": token,
|
|
61
|
+
"endpoint": endpoint,
|
|
62
|
+
"saved_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
63
|
+
}
|
|
64
|
+
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
65
|
+
# 0600
|
|
66
|
+
try:
|
|
67
|
+
path.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
68
|
+
except OSError:
|
|
69
|
+
# Windows / 某些 FS 不支持 chmod;保留文件,不抛错
|
|
70
|
+
pass
|
|
71
|
+
return path
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def delete_credentials() -> bool:
|
|
75
|
+
"""删凭据文件。文件不存在时返回 False,删除成功返回 True。"""
|
|
76
|
+
path = credentials_path()
|
|
77
|
+
if not path.exists():
|
|
78
|
+
return False
|
|
79
|
+
path.unlink()
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_token(explicit: str | None = None) -> str | None:
|
|
84
|
+
"""按 [显式参数 → SCIVERSE_API_TOKEN → 凭据文件] 顺序返回 token,
|
|
85
|
+
都没有时返回 None(不抛错;让调用方决定怎么提示用户)。"""
|
|
86
|
+
if explicit:
|
|
87
|
+
return explicit
|
|
88
|
+
env_token = os.environ.get("SCIVERSE_API_TOKEN")
|
|
89
|
+
if env_token:
|
|
90
|
+
return env_token
|
|
91
|
+
creds = load_credentials()
|
|
92
|
+
if creds and creds.get("token"):
|
|
93
|
+
return creds["token"]
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def resolve_endpoint(explicit: str | None = None) -> str:
|
|
98
|
+
"""按 [显式参数 → SCIVERSE_BASE_URL → 凭据文件 → 默认值] 顺序返回 endpoint。"""
|
|
99
|
+
if explicit:
|
|
100
|
+
return explicit
|
|
101
|
+
env_url = os.environ.get("SCIVERSE_BASE_URL")
|
|
102
|
+
if env_url:
|
|
103
|
+
return env_url
|
|
104
|
+
creds = load_credentials()
|
|
105
|
+
if creds and creds.get("endpoint"):
|
|
106
|
+
return creds["endpoint"]
|
|
107
|
+
return DEFAULT_ENDPOINT
|
sciverse/py.typed
ADDED
|
File without changes
|