--- name: thfund-holdings description: 查询天弘基金网上交易持仓或交易记录时使用。通过 etrade.thfund.com.cn/webtrade 只读登录,复用私有 Cookie 会话;图形验证码先读取一次,读不出再问人。不申购、赎回或改账户。 --- # 天弘基金网上交易查询 只通过 [scripts/query.py](scripts/query.py) 的独立 HTTP 会话完成「复用会话,或图形验证码登录一次 → 用户信息 → 持仓和交易记录」。站点是 `https://etrade.thfund.com.cn`,只做登录、会话检查和只读查询,不申购、赎回、转换、撤单、改密或修改资料。没有保活循环。 用户要求查询天弘网上交易账户时,这些步骤已经包含在请求里:优先复用已保存会话;会话缺失或明确过期时,用已指定的证件号、证件类型和密码向官网提交一次登录。不要再单独询问是否允许提交账号密码或更新本地会话。仍缺的是验证码文本、配置路径或状态目录,只问这些。 “优先复用”是执行顺序,不是“禁止密码登录”。只有用户明确要求只复用、不登录时才按该限制执行。沿用已经确定的配置和状态目录,不要搜索其他文件找证件号或密码。 浏览器里查看天弘官网页面时用 `query-tianhong-fund-holdings`(优先云电脑,不保存浏览器 Cookie)。本技能不打开浏览器,也不读取浏览器登录态。 仅安装、维护或离线测试时,不登录真实账户,不拉取验证码。会话里已经授权的查询不需要再次确认。 ## 范围与输入 - 依赖 macOS/Linux 和 Python 3.9+,保持 HTTPS 证书校验。验证码接口返回 JPEG;保存 PNG 需要 Pillow、ffmpeg、macOS `sips` 或 ImageMagick 之一。不需要 Node.js。 - 证件号来自 `THFUND_ID` 或配置文件的 `certificate_no`。密码来自 `THFUND_PASSWORD` 或 `password`。证件类型来自 `THFUND_CERT_TYPE` 或 `certificate_type`,默认 `"0"`(身份证;须为 15 或 18 位)。环境变量覆盖配置里的同名字段。不要在聊天中索要证件号或密码,也不要从对账单、PDF 或其他目录推断证件号。 - 需要模板时只写空 JSON,由用户填写。配置文件必须是权限 `600` 的普通文件。状态目录用 `--state-dir` 指定,位于技能目录之外,属主为当前用户且权限 `700`;脚本会创建它。任务中使用已忽略的 `work/thfund/`。`session.json`、`pending_login.json`、验证码 PNG 和查询结果权限为 `600`。同目录 `.lock` 被占用时直接停止,不登录。这些文件不进入技能、插件缓存或版本库。 ## 启动查询 把下面的路径换成实际绝对路径。`--as-of` 使用用户时区的当天或用户指定的截止日期。先检查会话;退出码 0 再查询,退出码 3 再走验证码。`query` 不会在会话失效后自动登录。 ```bash python3 /absolute/skill/scripts/query.py \ --state-dir /absolute/task/work/thfund \ --config /absolute/private/thfund-config.json \ check python3 /absolute/skill/scripts/query.py \ --state-dir /absolute/task/work/thfund \ --config /absolute/private/thfund-config.json \ query --as-of YYYY-MM-DD ``` 已有同账号会话时,`check` 和 `query` 可以不带 `--config`。带了配置或环境变量里的证件号时,必须和 `session.json` 的账号哈希一致,否则停止且不覆盖会话。 脚本输出 JSON 行。`session_reuse.success=true` 才表示这次查询复用了会话。退出码:`0` 成功,`2` 用法、配置、权限或账号不一致,`3` 无会话或会话过期,`4` 登录被拒绝,`5` 网络或接口错误。 ## 图形验证码 `captcha` 建立新的网页会话,把验证码存成 PNG,并写入 `pending_login.json`。进度行 `captcha_ready` 的 `file` 是 PNG 绝对路径,`expires_in_s` 是客户端保留上限(20 分钟),不是测得的服务端有效期。过期后重新运行 `captcha`。 1. 用当前客户端的读图能力读取这张 PNG,只读一次。登录页验证码框最多 4 个字符;按读到的原文使用,保留大小写,只去掉首尾空白。 2. 读不到、结果含糊,或当前环境不能看图片时,把 PNG 路径给用户,请用户读出验证码。不要再猜一次,也不要把图片送到其他服务。 3. 用标准输入提交,避免验证码出现在命令参数里。一张验证码只提交一次: ```bash printf '%s\n' "$CODE" | python3 /absolute/skill/scripts/query.py \ --state-dir /absolute/task/work/thfund \ --config /absolute/private/thfund-config.json \ login --captcha - ``` 4. 加密密钥从当次公开的 `globalConfig.js` 读取,不写进仓库,也不沿用上一次的密钥。密钥读不到或加密失败时不提交密码,并保留这张验证码。 5. 脚本在发出登录 POST 之前删除 pending。提交前的网络失败不消耗验证码;POST 一旦发出,这张图就不能再交。不要用同一个验证码启动第二次 `login`。 6. `login_rejected.kind` 为 `password_failed` 或 `account_locked` 时立即停止。不要再运行 `captcha`,不要再登录。再试可能锁定账户。只向用户说明 kind,不复述密码。 7. kind 为 `captcha_wrong` 时可以再取一张新图,并按上面的规则再读一次。不要重交旧图。新图仍读错时请用户看图,不要循环试密码。 8. 只有 `login_ok` 之后才运行 `query`。未知拒绝同样不自动重试。 ## 会话与查询 `session.json` 保存官方 Cookie(含 `JSESSIONID`、`WEBTRADEID`、`acw_tc`)、账号哈希、证件类型和登录 token,不保存密码、证件号或验证码。每次成功的检查或接口请求之后都会原子重写该文件,以保留服务端新下发的 Cookie。不要另写刷新循环,也不要为了“保活”反复打开资产页。 `check` 请求受保护资产页且不跟随跳转。回到登录地址或响应仍是登录表单时退出码为 3。损坏的会话文件、错误权限和账号不一致退出码为 2,不删除文件,也不改走登录。 `query` 在确认会话有效后先调用用户信息接口,再读持仓和交易。用户信息里的身份字段不写入结果。持仓来自 `crm_Query.do` 的 `commonassetdetail`,按基金代码合并渠道;2026-10-21 的 my-funds 接口返回空列表,本技能不调用它。交易默认是截止日前 365 天,来自 `searchHistoryApp.do`,并一并读取代销确认 `crm_HistoryTrades.do`。服务端大约只返回一年。响应里没有总条数时,只报告实际读到的记录和请求区间,不把这说成完整历史。 持仓或交易列表的字段缺失时退出码为 5,不报告零资产。列表存在且为空,才是该范围内的零记录。只有 `complete.success=true` 才报告查询完成。 ## 结果 默认结果是状态目录中的 `thfund_YYYY-MM-DD.json`,也可用 `--out` 指定已有目录下的新路径。权限 `600`。渠道只保留渠道名和份额,不保留银行卡号或卡号后四位。原始响应会去掉产品字典和证件、手机、邮箱、地址、姓名、银行卡字段。 汇报时分开写查询时间、各基金净值日期和交易区间。`total_market_value` 是持仓市值合计;`total_ex_money_market` 已排除货币基金和余额宝,不要再加一次。同一基金的多个渠道已经合并,渠道份额写在 `channel` 里。缺少的收益或净值留空,不填零。份额、净值和市值口径不一致时不要强行用乘积改写市值。 接口字段和 2026-10-21 的边界见 [references/web-api.md](references/web-api.md)。页面脚本变化时先核对官方实现,不猜测参数,也不反复提交密码。