tytcli 0.2.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.
@@ -0,0 +1,437 @@
1
+ Metadata-Version: 2.5
2
+ Name: tytcli
3
+ Version: 0.2.0
4
+ Summary: 投研通 (TYT) HTTP API CLI, designed for agent + skill driven use
5
+ Project-URL: Homepage, https://tyt.1234567.com.cn/
6
+ License-Expression: LicenseRef-Proprietary
7
+ License-File: LICENSE.txt
8
+ Keywords: tyt,天天基金投研通,投研通
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Programming Language :: Python :: 3.15
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: click>=8.1
16
+ Requires-Dist: httpx>=0.27
17
+ Description-Content-Type: text/markdown
18
+
19
+ # tytcli
20
+
21
+ 投研通 (TYT) HTTP API 的命令行客户端,为 **agent + skill** 场景设计:默认输出 JSON、退出码稳定、绝不交互式提问,便于 agent 解析结果并自主扩展源码。
22
+
23
+ ## 安装与运行
24
+
25
+ ```bash
26
+ uv tool install tytcli # 推荐:隔离安装,自带 `tytcli upgrade` 自升级
27
+ # 或
28
+ pip install tytcli
29
+ ```
30
+
31
+ 开发期直接从源码运行:
32
+
33
+ ```bash
34
+ uv sync
35
+ uv run tytcli --help
36
+ uv run tytcli whoami
37
+ ```
38
+
39
+ ## 环境
40
+
41
+ 面向使用者的环境是 `prod`,也是默认值:
42
+
43
+ | 环境 | base URL |
44
+ |---|---|
45
+ | `prod`(默认) | `https://tytapi.1234567.com.cn` |
46
+
47
+ 端点在代码里一律写成相对于 base URL 的形式(如 `Login/GetLoginInfoV2`)。
48
+
49
+ ```bash
50
+ tytcli config show # 当前生效配置(Token 脱敏)
51
+ tytcli config path # 配置文件在哪、是否存在
52
+ tytcli whoami # Token 是否有效(成功即返回登录身份摘要)
53
+ ```
54
+
55
+ > 除 `prod` 外还存在一个**仅供开发**使用的环境,它不会出现在 `--help` 或常规输出里,
56
+ > 文档也不介绍它。能用 `TYTCLI_ENV` 显式指定它是给开发者留的口子,不是常规用法。
57
+
58
+ ## 配置
59
+
60
+ 默认路径:`$XDG_CONFIG_HOME/tytcli/config.toml`,未设置 `XDG_CONFIG_HOME` 时为 `~/.config/tytcli/config.toml`。
61
+
62
+ ```bash
63
+ tytcli config path # 打印路径与是否存在
64
+ tytcli config init # 创建 0600 权限的模板
65
+ tytcli config init --force # 覆盖
66
+ ```
67
+
68
+ 模板(**注意里面没有 `base_url`**):
69
+
70
+ ```toml
71
+ active_env = "prod"
72
+ timeout_seconds = 15.0
73
+
74
+ [env.prod]
75
+ tyt_token = "…你的 token…"
76
+ ```
77
+
78
+ **Token 与环境绑定**:一个环境的 Token 拿到另一个环境会直接返回 `AUTH_EXPIRED`。
79
+
80
+ 写 Token 最简单的方式是 `auth` 子命令,而不是手改文件:
81
+
82
+ ```bash
83
+ tytcli auth --token <TOKEN> # 写入当前生效环境
84
+ tytcli auth --token <TOKEN> --verify # 写入后立即联网校验
85
+ echo "$TOK" | tytcli auth --token - # 从 stdin 读,不进 shell 历史
86
+ tytcli auth --env <ENV> --token <TOKEN> # 写入指定环境(默认:当前生效环境)
87
+ ```
88
+
89
+ `auth` 会按需从模板创建配置文件(权限 `0600`),写入后**回读校验**确认落盘无误。
90
+
91
+ ### 环境地址固定在代码中
92
+
93
+ 两个 base URL 是 `src/tytcli/config.py` 里的 `BASE_URLS` 常量,**不可通过配置文件或环境变量覆盖**:
94
+
95
+ - 配置文件里的 `base_url` 如果与内置地址**不一致** → 直接报错 `CONFIG_BASE_URL_NOT_CONFIGURABLE`(而不是静默忽略——被忽略的覆盖项极难排查)。若只是**复述**内置值(旧版模板生成的配置就是这样)则容忍并忽略,以免老配置无法加载。
96
+ - `TYTCLI_BASE_URL` 已移除,设了也无效。
97
+ - 因为地址不可配,自定义环境名会被 `CONFIG_UNKNOWN_ENV` 拒绝。
98
+
99
+ ### 覆盖优先级
100
+
101
+ ```
102
+ 环境变量 > 配置文件 > 内置默认值
103
+ ```
104
+
105
+ | 环境变量 | 覆盖内容 |
106
+ |---|---|
107
+ | `TYTCLI_CONFIG` | 配置文件路径(文件不存在直接报错,不回退) |
108
+ | `TYTCLI_ENV` | 当前环境 |
109
+ | `TYTCLI_TOKEN` | 当前环境的 Token |
110
+
111
+ 刻意的设计:
112
+
113
+ - **不自动加载 CWD 下的配置或 `.env`**,避免 agent 换目录时误用凭证。
114
+ - **显式设置为空的 `TYTCLI_TOKEN` 会报错**,不会静默回退到文件里的值。
115
+ - **`auth --env X` 只写 X 的 section,不会顺手切换 `active_env`。** 如果你给非当前环境写了 Token,下一次调用会明确告诉你“哪个环境已有 Token、怎么切过去”,而不是含糊地报“缺少 Token”。
116
+
117
+ > ⚠️ **`auth --token` 的安全权衡**:Token 作为命令行参数会进入 shell 历史,并短暂出现在 `ps` 进程列表里。交互式使用请优先用 `--token -` 从 stdin 管道传入。agent 调用时 Token 本来就在上下文里,风险相对可控,但仍应避免把它写进会被持久化的日志。
118
+
119
+ ## 输出与退出码
120
+
121
+ **stdout 恰好一个 JSON 文档,命令不往 stderr 写任何东西**——结果不会在两个通道里各出现一份。
122
+ (唯一例外:隐式自更新钩子的警告,以及裸调 `tytcli` 时路由到 stderr 的 help 文本。
123
+ `--help` / `--version` 按通用惯例输出纯文本。)
124
+
125
+ ```bash
126
+ tytcli whoami | jq .
127
+ tytcli --pretty whoami # 缩进输出,全局参数需放在子命令前
128
+ ```
129
+
130
+ 成功(`whoami`):
131
+
132
+ ```json
133
+ {"ok":true,"env":"prod","base_url":"https://tytapi.1234567.com.cn","http_status":200,"identity":{"UserId":"9000001","UserName":"…","CompanyName":"…","MenuCount":6},"data":{"Success":true,"ErrorCode":0,"Error":"","Data":{}}}
134
+ ```
135
+
136
+ 失败:
137
+
138
+ ```json
139
+ {"ok":false,"error":{"code":"AUTH_EXPIRED","message":"登录超时,请重新登录!","details":{"env":"prod","error_code":-100,"hint":"…"}}}
140
+ ```
141
+
142
+ - `data` 是服务端响应的**原样透传**(PascalCase),不重命名、不裁剪。`whoami` 的 `Data.Menus` 是完整权限菜单树(包含各模块路由),体积较大,也是后续加子命令时的接口地图。
143
+ - `identity` 是关键字段的**摘要**,方便 agent 不必吞下整棵 `Menus` 树。**它刻意不包含 `Phone`**(手机号是 PII,而摘要是最容易被记进日志的部分);需要时仍可从 `data` 里取。
144
+
145
+ > ⚠️ `whoami` 的响应包含个人信息:真实姓名、手机号、工号、公司/部门。转发输出或写日志时注意脱敏。
146
+
147
+ | 退出码 | 含义 | 典型 code |
148
+ |---|---|---|
149
+ | 0 | 成功 | — |
150
+ | 1 | API / 业务错误 | `API_BUSINESS_ERROR`, `API_BAD_REQUEST`, `HTTP_ERROR`, `NON_JSON_RESPONSE`, `UNEXPECTED_SHAPE`, `INDEX_HTTP_ERROR`, `UPDATE_NOT_PUBLISHED`, `UPDATE_FAILED`, `UPDATE_TIMEOUT`, `UPDATE_UNSUPPORTED`, `INTERNAL_ERROR` |
151
+ | 2 | 用法或配置错误 | `USAGE`, `USAGE_STDIN_REQUIRED`, `CONFIG_NOT_FOUND`, `CONFIG_PARSE_ERROR`, `CONFIG_MISSING_TOKEN`, `CONFIG_UNKNOWN_ENV`, `CONFIG_EMPTY_TOKEN`, `CONFIG_BASE_URL_NOT_CONFIGURABLE`, `AUTH_EMPTY_TOKEN`, `AUTH_MALFORMED_TOKEN`, `CONFIG_EXISTS`, `CONFIG_WRITE_VERIFY_FAILED` |
152
+ | 3 | 认证失败 | `AUTH_EXPIRED` |
153
+ | 4 | 网络错误或超时 | `NETWORK_TIMEOUT`, `NETWORK_ERROR` |
154
+
155
+ Agent 应按 `error.code` 分支,而不是解析 `message`。
156
+
157
+ ## 命令
158
+
159
+ ```
160
+ tytcli whoami 查询当前 Token 对应的登录身份
161
+ tytcli auth 把 Token 写入配置文件
162
+ --token TOKEN|- TOKEN 或 `-`(从 stdin 读)
163
+ --env ENV 写入哪个环境(默认:当前生效环境)
164
+ --verify 写入后立即调用 whoami 校验
165
+ tytcli config show 显示当前生效配置(Token 脱敏)
166
+ tytcli config path 显示配置文件路径
167
+ tytcli config init 创建配置模板
168
+ tytcli upgrade 检查包索引并升级到新版
169
+ --check 只检查,不升级
170
+ --force 忽略版本比较,强制重装
171
+ tytcli wechat articles 公众号文章列表
172
+ --view ALL|SUBSCRIBED|FAVORITE 默认 ALL
173
+ --fake-id ID 只看某个公众号
174
+ --page N 页码,从 1 开始
175
+ --page-size N 1–100,默认 20
176
+ --fields A,B,C 只输出这些字段(与 --full 互斥)
177
+ --full 输出服务端全量字段(与 --fields 互斥)
178
+ tytcli wechat article 单篇文章详情
179
+ --article-id HEX64 列表返回的 articleId(必填)
180
+ tytcli sentiment dates 列出有日报的日期
181
+ tytcli sentiment image 下载某天的日报图片
182
+ --date YYYY-MM-DD 日报日期(必填)
183
+ --output PATH 保存路径(默认当前目录 tytcli-sentiment-<日期>.png)
184
+ tytcli roadshow search 按时间/行业/机构/主讲人查路演
185
+ --title T --institution I --speaker S
186
+ --theme CODE|NAME 1权益 2固收+ 3纯债 4互认 5策略 6热点
187
+ --start YYYY-MM-DD --end YYYY-MM-DD 注意 --end 不含当天
188
+ --page N --page-size N --fields A,B,C --full
189
+ tytcli roadshow status 某一场我能否预约/取消
190
+ --app-id ID 必填,来自 search
191
+ tytcli roadshow summary 公开 AI 纪要
192
+ --app-id ID 必填
193
+ --fields A,B,C / --full 默认简略版,--full 详细版
194
+ tytcli roadshow book 预约(写操作)
195
+ --app-id ID --yes 不带 --yes 只做只读预检,不写入
196
+ tytcli roadshow cancel 取消预约(写操作,同上)
197
+ --app-id ID --yes
198
+ tytcli roadshow bookings 我的预约
199
+ --state BOOKED|ENDED 不传为全部未取消的预约
200
+ --meeting-start/--meeting-end YYYY-MM-DD 含结束日全天
201
+ --book-start/--book-end "YYYY-MM-DD HH:MM:SS"
202
+ --page N --page-size N --fields A,B,C --full
203
+ tytcli help [TOPIC] 打印命令用法 + 随包发布的 skill 文档
204
+ ```
205
+
206
+ ### 文档随包发布:`tytcli help`
207
+
208
+ skill 文档(`SKILL.md` + `references/`)随 wheel 一起安装(`tytcli/skill/`),
209
+ 与仓库里的 `skills/tytcli/` **同源**(构建时复制,不是两份维护)。
210
+ CLI 自己就能打印它们:
211
+
212
+ ```
213
+ tytcli help 命令摘要 + SKILL.md(路由文档)
214
+ tytcli help wechat wechat 用法 + references/wechat.md
215
+ tytcli help output 输出契约 / 退出码 / 错误码
216
+ ```
217
+
218
+ 每个命令的 `--help` 末尾都有一行 `完整文档:tytcli help <topic>` 指针。
219
+ `--help` 保持精简(只指路),全文走 `help`——这样用法摘要不会被文档淹没,
220
+ 也不会让 14 个 help 面随文档改动而漂移。
221
+
222
+ 不用 `tytcli <cmd> help` 形式:它会和位置参数冲突——任何接受位置参数的命令都会
223
+ 把 `help` 当成参数值处理,而不是打印文档。
224
+
225
+ ### 路演中心:写操作默认只预检
226
+
227
+ `roadshow book` / `roadshow cancel` 会改变服务端状态,而且**这些写入是真实生效的**:
228
+ 一次误调用就会在账号里留下一条真实预约记录。
229
+
230
+ 所以两条写命令**不带 `--yes` 时不发写请求**,只调一次状态查询做预检,报告
231
+ `would_write` 与 `next_step`。带 `--yes` 时还有三层保护:
232
+
233
+ - 已处于目标状态 → `already_booked` / `already_cancelled`,跳过写入
234
+ - 资格不允许 → `ROADSHOW_NOT_BOOKABLE` / `ROADSHOW_NOT_CANCELLABLE`(exit 1),
235
+ 带服务端原文 `reason`,跳过写入
236
+ - **写入超时 → 回读状态再决定结果**。超时不等于失败,请求可能已经落地;直接报错会让
237
+ 调用方重试,而重试就是重复预约。已生效则报 `recovered_after_timeout:true` + exit 0,
238
+ 未生效则如实报超时并附对账后的状态
239
+
240
+ 服务端本身也是幂等的(重复预约不会改变已存在的 `bookTime`,重复取消同样成功),但能
241
+ 不发写请求就不发。
242
+
243
+ ### 两个列表接口的日期语义相反
244
+
245
+ | 命令 | 入参 | 服务端格式 | `--end` 语义 |
246
+ |---|---|---|---|
247
+ | `roadshow search` | `YYYY-MM-DD` | `yyyy/M/d` | 截止该日 **00:00:00**,不含当天 |
248
+ | `roadshow bookings` | `YYYY-MM-DD` | `yyyy-MM-dd` | **含**当天全天 |
249
+
250
+ CLI 统一收 ISO 日期并在内部转译,因为让调用方记两种格式必然出错。**转译不是装饰**:
251
+ 服务端对**无法解析**的日期是**静默忽略**——筛选不生效、照旧返回全量结果,看起来像
252
+ "这个区间有这么多场路演"。所以 `search` 的输出会回显 `filters.start_sent_as`
253
+ 与 `end_includes_that_day`,让调用方能确认筛选真的被应用了。
254
+
255
+ ### 情绪日报:图片走文件,不走 stdout
256
+
257
+ `sentiment image` 的响应是**图片本身**(数 MiB 的二进制),不是 JSON 信封。为守住
258
+ "stdout 恰好一个 JSON 文档"的契约,图片写入文件,stdout 只输出元数据:
259
+ `path` / `bytes` / `format` / `sha256` / `content_type` / `overwritten`。
260
+
261
+ 两条会影响调用方的行为:
262
+
263
+ * `-101`(服务器繁忙)在该模块是**兜底码**:日期格式错、当天没有日报、**以及有效日期
264
+ 但图片尚未渲染完成**都会返回它,前两种重试无用、最后一种重试能成功。所以命令在
265
+ 客户端先校验日期格式与真实性(`USAGE_DATE_INVALID`,exit 2),并对 `-101` 做有限
266
+ 重试(2 次,间隔 2s);重试耗尽时 `details.retries_exhausted` 说明已尽力。
267
+ * 图片的扩展名按**魔数**判定而不是固定 `.png`;魔数不认识的字节**拒绝落盘**
268
+ (`UNEXPECTED_SHAPE`),以免产生一个撒谎的 `.png` 文件。
269
+
270
+ ### 公众号模块:三件必须先知道的事
271
+
272
+ 1. **该模块并非在所有环境都已部署。** 命令不会预先拦截:未部署时服务端返回
273
+ HTTP 404(`exit 1`),错误的 `details.hint` 会说明原因——那不是路由拼错,
274
+ 重试也没用。部署上线后命令自动可用,无需改代码。
275
+ 2. **列表默认是精简索引。** 20 条原样是 66.8 KiB,其中 `cleanBody`(正文)一个字段
276
+ 就占 74%——而正文是详情接口的职责。所以默认只给
277
+ `title / nickname / summary / articleId / publishTime`(约 5 KiB),并在输出里放一个
278
+ `__tytcli_hint__` 说明本次还返回了哪些字段、怎么用 `--fields` / `--full` 取到。
279
+ 详情则默认全量原样(单篇约 14 KiB)。
280
+ 3. **`data` 是服务端的 `Data` 成员,不是整个信封。** 与 `whoami` 不同(后者的 `data`
281
+ 是整个响应原样)。因为 `Success`/`ErrorCode`/`Error` 已由 `ok` 和错误文档表达,
282
+ 而 `data` 在 `--fields` / `--full` 下保持稳定形状对 agent 更有用。
283
+
284
+ ## 自动更新
285
+
286
+ **每条命令启动时会自动做一次版本检查**:若包索引上有更新版本,先升级再重新执行原命令(re-exec),所以你拿到的总是新版结果。
287
+
288
+ 检查哪个索引,按这个顺序取第一个非空值:
289
+
290
+ 1. `TYTCLI_UPDATE_INDEX_URL`(tytcli 自己的覆盖)
291
+ 2. `PIP_INDEX_URL`——你已经为 pip 配好的镜像(**只看环境变量**,不读 `pip config` 文件)
292
+ 3. `https://pypi.org/simple/`
293
+
294
+ 继承 `PIP_INDEX_URL` 的意义是:本地镜像通常比 pypi.org 快得多,而"快"正是这个检查
295
+ 唯一的开销。两者都是 PEP 503 simple 索引。`PIP_INDEX_URL` 只是**默认值的来源**,
296
+ 不是 tytcli 的开关;要指定一个只用于更新检查的索引,用 `TYTCLI_UPDATE_INDEX_URL`。
297
+ 注意:如果那个镜像上没有 tytcli(只代理公网包的内网镜像常见),检查会安静地
298
+ 变成 no-op——它把"索引上没这个包"当成"不需要升级"。
299
+
300
+ 具体行为:
301
+
302
+ - **默认 10 分钟最多查一次**,结果缓存在 `$XDG_CACHE_HOME/tytcli/update-check.json`(未设 `XDG_CACHE_HOME` 时为 `~/.cache/…`)。量级:首次调用约 1s(含一次网络往返),缓存命中时可忽略不计。
303
+ - **失败开放**:索引不可达、超时、权限不足、升级命令失败 —— 全部只在 stderr 警告,然后用当前版本继续执行,退出码仍由业务结果决定。离线时也能正常用。
304
+ - **失败也会被缓存**,所以离线时不会每条命令都卡在超时上;一个 TTL 周期内只警告一次。
305
+ - **只接受纯数字版本**(`0.2.0`)。`1.0rc1` 之类的预发布版本绝不会被自动安装。
306
+ - **自动探测安装方式**:uv tool 安装 → `uv tool upgrade`;带 pip 的 venv → `pip install -U`;uv 创建的不带 pip 的 venv → `uv pip install`。**系统/共享 site-packages 拒绝自动升级**(需要提权或会污染环境),只提示手动命令。
307
+
308
+ 相关环境变量:
309
+
310
+ | 变量 | 作用 |
311
+ |---|---|
312
+ | `TYTCLI_UPDATE_CHECK_TIMEOUT` | 单次检查超时(默认 5s) |
313
+ | `TYTCLI_UPDATE_INDEX_URL` | 覆盖包索引地址(优先级最高) |
314
+ | `PIP_INDEX_URL` | 未设上一项时,自动用它作为索引默认值 |
315
+
316
+ **没有关闭开关。**检查间隔固定 10 分钟,不可配置;产品契约就是“始终运行最新版”。
317
+ 曾经存在的 `TYTCLI_NO_UPDATE_CHECK` / `TYTCLI_UPDATE_CHECK_INTERVAL` 已删除——
318
+ 一个可调的间隔只会让安装悄悄过期而看起来健康。需要知道版本情况时用
319
+ `tytcli upgrade --check`(它绕过缓存直接联网,且不受任何开关影响)。
320
+
321
+ 对自动化的含义:命令**可能**在中途升级并 re-exec,并往 stderr 写一行警告
322
+ (这是 stderr 唯一合法的写入者)。**看到警告不等于命令失败**;stdout 仍是
323
+ 恰好一个 JSON 文档,退出码仍由业务结果决定。
324
+
325
+ 安全约束:版本检查**绝不跟随重定向**,只信任来自包索引的 HTTP 200,3xx 一律当作
326
+ “这个索引上没有该包”。跟随重定向等于把版本判断交给索引之外的第三方:一旦有人在
327
+ 别处抢注了同名包,任何基于它做的“自动升级”就会拉取并执行来路不明的代码。
328
+
329
+ ## API 契约
330
+
331
+ 以下是客户端依赖的服务端约定(`src/tytcli/client.py` 顶部注释是同源的一份,更详细):
332
+
333
+ 1. 只接受 `POST`;`GET` 返回 `ErrorCode -1`。
334
+ 2. **必须携带 JSON body,但 `{}` 就足够**;完全不带 body 返回 `ErrorCode -1`。不需要 `userId` / `passportId` / `deviceid` / `version` / `product` / `plat`。
335
+ 3. 认证只需要 `TYT_Token` Cookie;`Origin` / `Referer` / `User-Agent` / `sec-ch-ua*` / `Sec-Fetch-*` 都不需要。
336
+ 4. **失败时 HTTP 状态码仍是 200**,必须读 body 里的 `Success` 字段判断成败。
337
+ 5. 响应信封为 PascalCase:`Success` / `ErrorCode` / `Error`,prod 可能多出 `TotalCount` 等字段。
338
+ 6. **成功时 `ErrorCode` 为 `0`、`Error` 为空字符串**(不是 `null`),业务数据在 `Data` 下。
339
+ 7. **Token 与环境绑定**:同一个 Token 换一个环境会直接返回 `-100`,绝不跨环境携带。
340
+
341
+ `GetLoginInfoV2` 成功响应的 `Data` 结构:
342
+
343
+ ```text
344
+ UserId, UserName, JjtId, CompanyId, CompanyName, Department, Duty,
345
+ Salesman, Phone, AccountType, HasPassword, VersionId, EmpNo,
346
+ EstPerm, EmotionPerm, Menus[]
347
+ ```
348
+
349
+ `Menus` 是权限菜单树,每项带 `MenuName` / `Url` / `SubMenuList`,当前有 6 个一级菜单:基金筛选、基金研究、研报中心、路演中心、组合管理、基金工具。**它是拓展子命令时的路由地图**(如 `/fundSelect/fundFilter`、`/fundResearch/fundDetail`、`/ManageGroup/storeAsserts`),但那些是前端路由,只能作为端点线索。
350
+
351
+ 已知 `ErrorCode`:
352
+
353
+ | 值 | 含义 | CLI 处理 |
354
+ |---|---|---|
355
+ | `0` | 成功(配合 `Success: true`) | 退出码 0 |
356
+ | `-100` | 登录超时,请重新登录 | `AUTH_EXPIRED`,退出码 3 |
357
+ | `-1` | 地址错误或未按规定访问 | `API_BAD_REQUEST`,退出码 1 |
358
+
359
+ ## 静态检查
360
+
361
+ 代码必须同时通过 **ruff** 和 **basedpyright**,两者都要 0 error:
362
+
363
+ ```bash
364
+ uv run ruff check src tests scripts
365
+ uv run basedpyright
366
+ ```
367
+
368
+ basedpyright 基线是 `typeCheckingMode = "strict"`,`pythonVersion = "3.11"`,覆盖 `src` 和 `tests`。
369
+
370
+ **刻意不用 basedpyright 默认的 `recommended` 档**:它会全面禁用 `Any`(`reportAny` / `reportExplicitAny`)。TYT 返回的是任意 PascalCase JSON 并且原样透传,`dict[str, Any]` 才是诚实的类型;强行改成 `object` 只会到处塞 cast,把真 bug 埋进噪音里。
371
+
372
+ `tests/` 通过 `executionEnvironments` 放宽了与 pytest fixture 惯用法冲突的规则(fixture 参数按名字注入,逐个标注毫无收益),但**仍然是 strict 级别**——`typeCheckingMode` 不是 `executionEnvironments` 的合法键,basedpyright 会警告并忽略它。
373
+
374
+ `tests/test_compat.py` 会校验这些配置没被降级(仍是 strict、仍覆盖 src+tests、dev group 里确实有两个工具)。
375
+
376
+ 具体的类型写法约定:对 `Any` 做 `isinstance` 窄化后**必须紧跟一次 `cast`** 把类型
377
+ 确定下来,否则 `Unknown` 会一路扩散出大量 `reportUnknown*` 噪声;**不要用
378
+ `# type: ignore` 批量压制**,多余到可以忽略的运行时检查应该被删掉,而不是用 ignore
379
+ 盖住(basedpyright 的 `reportUnnecessaryIsInstance` 会告诉你哪些是死代码)。
380
+
381
+ ## 测试
382
+
383
+ ```bash
384
+ uv run pytest # 当前解释器,全程离线(respx mock),< 1s
385
+ ./scripts/test-matrix.sh # 静态检查 + 3.11~3.15 全版本,收工前必跑
386
+ ```
387
+
388
+ ## Python 兼容性
389
+
390
+ 支持 **Python 3.11 – 3.15**。3.11 是硬下限,因为配置解析直接用标准库 `tomllib`(3.11 才引入),不引第三方 TOML 依赖。
391
+
392
+ 跑全矩阵:
393
+
394
+ ```bash
395
+ ./scripts/test-matrix.sh # 3.11 3.12 3.13 3.14 3.15
396
+ ./scripts/test-matrix.sh 3.11 3.15 # 只跑指定版本
397
+ PY_VERSIONS="3.12 3.13" ./scripts/test-matrix.sh
398
+ ./scripts/test-matrix.sh -- -k whoami # `--` 之后的参数透传给 pytest
399
+ ```
400
+
401
+ 每个版本都跑在 `uv run --isolated` 的**一次性环境**里,不会动项目的 `.venv`;解释器缺失时 uv 会自动下载。环境有缓存,重跑约 0.3s/版本。
402
+
403
+ 矩阵开头会先跑一次 ruff 和 basedpyright——两者都与宿主解释器无关(ruff 是独立二进制,basedpyright 按配置里的 `pythonVersion` 分析,固定钉在 3.11 下限),所以没必要每版重复。ruff 的 `target-version = "py311"` 同样钉住了语法下限。
404
+
405
+ ### 多处版本声明必须一致
406
+
407
+ | 位置 | 作用 |
408
+ |---|---|
409
+ | `pyproject.toml` `requires-python` + `classifiers` | 对外声明的支持范围 |
410
+ | `pyproject.toml` `[tool.basedpyright] pythonVersion` | 静态分析针对的语言版本 |
411
+ | `scripts/test-matrix.sh` `DEFAULT_VERSIONS` | 实际跑过的范围 |
412
+ | `scripts/test-matrix.sh` `STATIC_FLOOR` | basedpyright 解析依赖用的解释器 |
413
+ | `tests/test_compat.py` `SUPPORTED` + `FLOOR` | 上述所有的校验基准 |
414
+ | `.github/workflows/*.yml` 的 `python-version` 矩阵 | CI 实际跑的版本 |
415
+
416
+ `tests/test_compat.py` 会把这几处交叉校验,任何一处漂移都会让测试失败——避免"声明支持 3.11 但没人真的在 3.11 上跑过"。增删受支持版本时,**以上全部都要改**。
417
+
418
+ CI 在 GitHub Actions:`ci.yml` 在每次 push 上跑静态检查 + 3.11~3.15 矩阵 + 制品审计;
419
+ `release.yml` 在推送 `v*.*.*` tag 时先校验 tag / `pyproject.toml` / `__init__.py` 三处
420
+ 版本号一致,再复跑审计与矩阵,最后经 Trusted Publishing(OIDC,**没有长期 token**)
421
+ 发布到 pypi.org。
422
+
423
+ CI 只在推送之后给答案;**本地矩阵仍是提交前的门禁**——`./scripts/test-matrix.sh` 能
424
+ 提前给出同样的结论。
425
+
426
+ 该文件还会用 `ast.parse(..., feature_version=(3, 11))` 编译所有源码,拒绝 3.12+ 才有的语法(如 PEP 695 的 `type X = ...`)。**但它管不了 API 层面的兼容性**——某个标准库函数或第三方库在旧版本不存在,只有真的在那个解释器上跑一遍才能发现,所以矩阵脚本不可省。
427
+
428
+ ### Windows
429
+
430
+ 未测试。矩阵目前只在 macOS (arm64) 上跑过,Linux 也未单独验证。已知两处平台差异已在测试中做了 guard:POSIX 权限位(`0600`/`0700`)在 Windows 上语义不同,以及 `Path.home()` 在 Windows 读 `USERPROFILE` 而非 `HOME`。运行时代码本身用 `contextlib.suppress(OSError)` 包住 `chmod`,在 Windows 上不会崩,但**不保证配置文件真的被收紧到 0600**——在 Windows 上存放 Token 需要另行注意。
431
+
432
+ ## 安全约定
433
+
434
+ - Token 不出现在源码、仓库、日志、错误详情或 stdout/stderr 中;`auth` 的输出只显示首尾各 4 位的掩码(`token_hint`)。
435
+ - 始终校验 TLS 证书,不提供跳过校验的开关。
436
+ - 不自动跟随重定向,避免凭证被带到非预期主机。
437
+ - 真实配置文件权限 `0600`,目录 `0700`。
@@ -0,0 +1,32 @@
1
+ tytcli/__init__.py,sha256=SQ0VQ3USlVFv_0ZNK9cj8fFKdJiDfXwgN_3uSod5QRs,166
2
+ tytcli/cli.py,sha256=EXRmBJ8cIwTNuK2LBg_dB-9TBqQir6vnEpXnJYmofbU,6545
3
+ tytcli/client.py,sha256=rEIzC1elbiy3gNg5_wOHsuzOT1rjAih3HJvM9ynYsT8,38496
4
+ tytcli/config.py,sha256=hCzal6WcRUo-jVQNpncJbpPdqBu6575gY0bgS7kvyqk,22829
5
+ tytcli/errors.py,sha256=97GIYgtFl8yIzIGTqV_gJoeLHcNXc7oAUCIPmtPq4Zw,1810
6
+ tytcli/output.py,sha256=YMjeyrfuzEmoah5aw5AXgZL047kpTB-k0_XlXqc_2EM,2828
7
+ tytcli/selfupdate.py,sha256=ROX24oNJkKZHF_1dOhhMvEceT58rlcBSmjNQcy3lkr4,25219
8
+ tytcli/skill.py,sha256=aP-uyeQO_qj9eaAfv-9Cy6S30pHYRyvf_csFJ3maK6c,4378
9
+ tytcli/commands/__init__.py,sha256=uDnsa05S-I6_O1oSX47u1FZrXV5F01roelbVjVo9V7w,1198
10
+ tytcli/commands/auth.py,sha256=G6QhBAtuFWH2_I7qAHaVVXP3cWA1whJFkCtMhI67DKE,5599
11
+ tytcli/commands/config.py,sha256=ifenhQIDdUQZC31PWuhPbEkj_enFzUPtIQojVpbcC_M,1947
12
+ tytcli/commands/env.py,sha256=bnh9786jO9G-aqlOEHHAJ9W5HHurrwi2RCuCpiB1rmI,3643
13
+ tytcli/commands/help.py,sha256=hGQxE2SN8Gb8RJ0E1jHWfgZvWYxyYrVy5kLwMBJZUX4,4383
14
+ tytcli/commands/roadshow.py,sha256=Sq_6K8mQAH_eOAAglZ8rH9-781xlAHODHs2ShDqvpLs,34424
15
+ tytcli/commands/sentiment.py,sha256=IzIoVQ5z2dZ--mzcjegoSQJL1crgRSZF4fJfsHqQZO0,6602
16
+ tytcli/commands/shared.py,sha256=5qsASKH5snX3ZT1S9qJZI3jwcTIFNHngiUbBQRqilZE,8786
17
+ tytcli/commands/upgrade.py,sha256=q36BmPt_lo2tcZqgsOE30FbjZU4FOlk11F9nzT5-eK0,3960
18
+ tytcli/commands/wechat.py,sha256=yQ5uZuO0ynI6VxlT160h6E_uClf9AXkiIrPcSU14pVc,8058
19
+ tytcli/commands/whoami.py,sha256=dMDPX2JreEIz2Tmby2zTEkcp6-bKB_QS-VByz-kWOeQ,2435
20
+ tytcli/skill/SKILL.md,sha256=24RgpAvZY3EU8JdT-KQkEXYmYga-od1Lol0b-BbhP8c,6441
21
+ tytcli/skill/references/output-contract.md,sha256=bgQJb24FZttVMzwqVi3SfMRkn-LIqoBdos7AM8BDat8,5889
22
+ tytcli/skill/references/roadshow.md,sha256=Qv2jzMnkVdyoYimsxflqG_LZ7-ps8jp5kajAo2V_fHE,9425
23
+ tytcli/skill/references/selfupdate.md,sha256=jzvMfzYgTVZg-KvCXu_IwpQPqUHy2ooGJ1nT8E8ez2E,3736
24
+ tytcli/skill/references/sentiment.md,sha256=PuEZwgFoQujOTrHzaqLQzs19leKqPwQLj5MXaKwmk80,2688
25
+ tytcli/skill/references/setup.md,sha256=Lo6yDmw8WXjJIQHh5OG_kMw0fWH3440I3PRg8pwoSnw,5206
26
+ tytcli/skill/references/wechat.md,sha256=QBkofLJG6xCeyqmAsPQhsBxTTxpHIFWtWvmI-ljaB-8,3414
27
+ tytcli/skill/references/whoami.md,sha256=5TmqDCDWVcwTqVqS6WdDoWh10-ui6hUrG-f4ka6Arfc,1057
28
+ tytcli-0.2.0.dist-info/METADATA,sha256=_4G5TrwfViW8pAkO18f_cJNWYCHjx-UFk6zJsgQrGFM,24051
29
+ tytcli-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
30
+ tytcli-0.2.0.dist-info/entry_points.txt,sha256=-OQJKfvIHZ3CUmwwrqiVmSDZV8XMYiFbIDZXIx7NTLs,49
31
+ tytcli-0.2.0.dist-info/licenses/LICENSE.txt,sha256=MlgLnVhPCjJIZ6aJznanwQk0upQNFi6quw4xYVbMNKs,992
32
+ tytcli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tytcli = tytcli.cli:entrypoint
@@ -0,0 +1,16 @@
1
+ tytcli — 投研通 (TYT) HTTP API 命令行工具
2
+
3
+ Copyright (c) 2026 上海天天基金销售有限公司 版权所有。
4
+
5
+ 本软件及其附带的文档、技能文档与图片(以下统称“本软件”)是专有软件,
6
+ 版权归权利人所有,保留所有权利。
7
+
8
+ 未经权利人书面许可,任何人不得复制、修改、再发布、再许可、出售、出租、
9
+ 逆向工程本软件,或将其用于商业目的。通过包索引获取、安装或以其他方式
10
+ 使用本软件,不构成任何明示或默示的许可、授权或权利转让。
11
+
12
+ 本软件按“现状”提供,不附带任何明示或默示的担保,包括但不限于对适销性、
13
+ 特定用途适用性和不侵权的担保。在任何情况下,权利人均不对因使用或无法
14
+ 使用本软件而产生的任何直接、间接、附带、特殊或后果性损失承担责任。
15
+
16
+ 权利人可随时更新本条款;对某一份发布制品而言,以其随附的本文件为准。