Files
Jyotisha/frontend
Jesse_ChenandClaude Opus 5 a1a78c7ebc
Independent Staging Quality Gate / validate (push) Canceled after 3m14s
Independent Staging Quality Gate / publish (push) Canceled after 0s
feat(ui): 报告中心改行式列表,阅读页并入外壳并把目录挪到右侧常驻
报告中心原来是卡片方阵,状态只靠三块底色区分;报告一多,扫读成本
按卡片数线性涨。现在一列一行:带色点的状态 chip、标题、创建时间 ·
深度 · 主题,操作靠右。失败原因从右边一小块挪进行内,能完整读到。

chip 里原来的 StatusIcon(generating 转圈、ready 打勾、failed 警告)
换成 5px 色点——原型如此,且列表是轮询不是演出。失败行不加「重新
生成」,顶栏已经有唯一的生成入口。

行内小字只写接口真给的东西。GET /api/reports 不返回节数和盘数,所以
不写「9 节 · 22 张盘」(VOICE.md 第 2 条),原型图上那行是 mock。

/reports/[reportId] 是最后一个脱离外壳的全屏路由,九个 phase 全部并进
SecondaryShell,根节点从 <main> 改成 <div>(外壳自己就是 main)。

任务书 E10「没有目录」已过期:目录在 cfcd369d 就存在。本轮把它从左栏
挪到正文右侧并定稿视觉,位置用 grid-column 显式指定而不是靠 DOM 次序,
这样窄屏抽屉仍能排在源码最前面,不会掉到全文末尾。860px 以下常驻栏
消失、折叠抽屉保留——删掉抽屉等于窄屏彻底失去章节定位。

挂外壳带出一个真实风险:window.print() 打的是整篇,而 .chat-app /
.chat-panel 是 height:100%;overflow:hidden,会把九节报告裁成一页。阅读
页因此多挂一条 media="print" 样式,把外壳既隐藏又解锁。放组件里而不是
globals.css:它只在阅读页挂载期存在,对话页的打印不受影响,也不用
:has() 去够祖先,更不越界到并行轮次的 CSS 区段。

纸面三个 token 一个没动,--report-accent 仍是 #85432f;目录栏用应用
调色板,因为它是 chrome 不是纸。report-chart-grid-rehype.ts 一行未动,
新增断言守住它仍然接线(BUG-616/617)。

tsc 0 错;lint 0 error / 118 warning(未增);3350→3355 条,0 条既有
断言被改,31 条无 Docker 失败与基线逐条一致;四个路由渲染标记不变,
/ 首屏 gzip −0.58%。

「20 张盘以上不重叠、滚动不卡」与「挂外壳后的打印真实输出」无真机做
不了,已写成环境缺口,清单在 docs/testing/cend-report-20260916.md。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-16 06:41:39 +00:00
..

Jyotisha Web

Jyotisha 是 yinduzhanxing 的聊天式消费者 Web 前端。它使用 Next.js App Router 提供用户与 Mastra Agent 的连续问答,同时继续复用仓库现有的 Python 印度占星计算工作流。它不生成固定报告。

架构

Browser
  -> Next.js /api/consult
    -> Mastra agent
      -> 绑定 live jyotish-vedic-astrology Skill 方法摘录
      -> consultation tool
        -> Python /api/consultation_workflow
  • Next.js 只负责产品界面、输入校验和结果呈现。
  • Python 服务仍是星盘、分盘、时序与证据计算的事实来源。
  • Skill 提供方法、路由和真实性边界;它不会替代 Python 的实际排盘计算。
  • Mastra 只能基于工具返回的数据组织语言;工具失败时会明确降级,不生成虚构星位。
  • Agent 回答通过 /api/consult 以纯文本流返回;前端在收到分片时增量渲染 Markdown,而不是等待整段回答完成。

环境要求

  • Node.js 20+
  • Python 3.11 或 3.12(主项目代码不兼容系统自带的 Python 3.9)
  • OpenAI 或兼容 OpenAI Chat Completions 的第三方模型 Key。至少要配置一个可用模型;未配置时咨询入口会明确提示模型服务不可用,不会扣点或返回伪造摘要。
  • Supabase 项目,用于邮箱 OTP 登录、咨询点数、一次性兑换码和账务流水。

配置

cd /Users/jesse/Downloads/Copse/astrology/yinduzhanxing/frontend
# 仓库不提供含占位密钥的 .env.example;请新建仅供本机使用的 .env.local
$EDITOR .env.local

.env.local

# Python 占星计算服务
JYOTISH_API_BASE=http://127.0.0.1:5200

# 模型供应商配置加密主密钥:严格 Base64 编码的 32 字节随机值
MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY=<strict-base64-32-byte-key>

# OpenAI-compatible 自定义端点可使用任意公网 HTTPS origin;无需配置域名白名单

模型目录、供应商地址及 API key 均通过管理端配置;API key 使用上述主密钥 AES-256-GCM 加密后存入数据库。运行时不读取 LLM_MODELS_JSONLLM_BASE_URLLLM_MODEL 或供应商 API-key 环境变量。 供应商地址仍受服务端 SSRF 防护:仅允许 HTTPS、禁止用户名/密码、拒绝 localhost、内网和保留地址;域名必须解析到公网地址,请求会固定到已验证的 DNS 地址并拒绝重定向。

Skill 如何触发

不需要输入 /skill 命令。解盘 Agent 在启动时从 live skill 树绑定运行时方法摘录,再调用计算工具;模型没有「先激活 skill」这一步。

例如,先在“个人资料”中保存出生信息,然后直接发送:

请根据我的出生资料,分析未来一年事业发展和适合跳槽的时间窗口。

完整链路是:

用户问题
  -> 已绑定的 live jyotish-vedic-astrology 方法
  -> Agent 依据 Skill 选择工作流
  -> run-jyotish-consultation
  -> Python consultation_workflow
  -> Agent 流式组织聊天回答

必须配置可用的模型 Key 才会进入这条链路。没有配置模型时,/api/models 返回 503,聊天框会停止发送;/api/consult 也会在预扣点数前拒绝未知或不可用模型。成功咨询始终由 Mastra Agent 生成流式回答。

仓库根目录的主 SKILL.md 通过 skills/jyotish-vedic-astrology/ 加载。该目录名必须与 Skill frontmatter 中的 name 一致。咨询读这份 live 目录,不校验 sha256、不钉版本;更新 skill 后重启 web 即可。部署时需确保 SKILL.mdreferences/scripts/assets/ 一起存在;如果目录结构不同,请设置 JYOTISH_SKILL_PATH。生时纠正和个人报告仍使用哈希锁定的版本包。

本地启动

先创建并使用项目自己的 Python 3.11/3.12 虚拟环境。这样 pyswisseph 会安装在 API 实际使用的解释器中,避免星盘工具因缺少 swisseph 降级或报错:

cd /Users/jesse/Downloads/Copse/astrology/yinduzhanxing
# 首次执行;把 python3.12 换成你机器上可用的 Python 3.11/3.12
python3.12 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt

# 每次启动 API 都使用同一个虚拟环境
.venv/bin/python scripts/jyotish_api_server.py --host 127.0.0.1 --port 5200

如果你的终端没有 python3.12,请先安装或定位一个 Python 3.11+ 解释器后再创建 .venv;不要使用 macOS/Xcode 自带的 Python 3.9。

再启动 Web

cd /Users/jesse/Downloads/Copse/astrology/yinduzhanxing/frontend
npm install
npm run dev

默认访问 http://localhost:3000。如果该端口已被占用,Next.js 会自动选择下一个可用端口。

验证

npm run lint
npm run build

当前产品能力

  • Supabase 邮箱 OTP 登录;未登录用户不能调用咨询接口。
  • 左侧聊天 Session、每个 Session 的消息与更新时间均存储在 Supabase;切换 Session 会将该会话的最近上下文提交给 Agent。
  • 出生资料与称呼位于“账户与出生资料”面板,保存后可在同一账号的所有 Session 与其他设备间复用。
  • 账户、出生档案、聊天历史、点数和兑换记录均存储在 Supabase,并通过 RLS 限制为用户只能读取和修改自己的数据。
  • 出生地点支持中国的国家 / 省级 / 市级 / 县区四级选择,按行政区中心坐标进行星盘计算。
  • 没有每日提问次数限制。点击发送后有 2.5 秒免费撤回窗口,此时尚未调用模型或预扣点数。窗口结束后咨询开始并预扣 1 点;首个输出分片前取消会幂等退款,已经收到输出后停止会保留现有内容并正常计费,避免部分回答被无限免费获取。
  • Agent 流式回答支持 Markdown 与 GFM 表格。
  • 首次进入空 Session 时,Agent 会在聊天区引导填写出生资料。保存后,Mastra 中的 onboarding Agent 会按照 jyotish-vedic-astrology Skill 生成欢迎语和事业、关系、时运三个入门问题;结果按版本缓存到 Supabase,同一用户不会在每次刷新时重复消耗模型。每次正式回答则在同一次咨询 Agent 调用中生成三个与当前解读相关的后续问题,并随 Session 保存到 Supabase。

中国出生地点数据

个人资料中的出生地点目前覆盖中国的省级、市级与县区级行政区。位置数据为行政区中心点,不是地址级定位;如需更新本地快照:

cd /Users/jesse/Downloads/Copse/astrology/yinduzhanxing/frontend
PATH="/opt/homebrew/bin:$PATH" npm run data:china

数据来源与许可证说明见 src/data/CHINA_LOCATION_DATA.md

Supabase 账户、点数与兑换码

1. 创建项目并配置环境变量

在 Supabase 创建项目后,将 Project Settings / API 中的值写入 .env.local

NEXT_PUBLIC_SUPABASE_URL=https://PROJECT_REF.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
ADMIN_EMAILS=admin@example.com,ops@example.com
  • NEXT_PUBLIC_* 只包含允许浏览器使用的项目 URL 和 anon key。
  • SUPABASE_SERVICE_ROLE_KEY 可以绕过 RLS,只能放在服务端环境变量中,绝不能加 NEXT_PUBLIC_、提交到 Git 或展示给前端。
  • ADMIN_EMAILS 是逗号分隔的管理员邮箱白名单。没有配置时普通账户仍可登录,但 /admin/codes 不可用。

2. 执行数据库迁移

使用 Supabase CLI

npx supabase login
npx supabase link --project-ref PROJECT_REF
npx supabase db push

也可以在 Supabase SQL Editor 中执行:

supabase/migrations/20260715000000_account_credits.sql
supabase/migrations/20260715010000_harden_credit_rpcs.sql
supabase/migrations/20260715020000_service_role_table_grants.sql
supabase/migrations/20260715030000_user_profiles_chat_sessions.sql
supabase/migrations/20260715040000_agent_onboarding_cache.sql
supabase/migrations/20260717000000_consultation_request_lifecycle.sql
supabase/migrations/20260717010000_chat_session_model.sql

迁移会创建:

  • profiles:用户点数余额、称呼与出生档案。
  • chat_sessions:用户的聊天 Session、消息、每个 Session 选用的模型和最近更新时间。
  • redemption_codes:只保存兑换码 SHA-256 与掩码,不保存完整码。
  • credit_transactions:兑换、预扣、退款和模型 Token 用量流水。
  • redeem_code:一次性兑换,使用行锁保证同一码全局只成功一次,并记录兑换账户。
  • consultation_requests:保存每次咨询的 reserved / completed / cancelled 结算状态。
  • begin_consultation_credit / complete_consultation_credit / cancel_consultation_credit:仅允许服务端 service_role 调用,通过请求级事务锁保证预扣、完成与退款互斥且幂等。

3. 配置邮箱验证码模板

在 Supabase Dashboard 的 Authentication / Email Templates 中编辑 Magic Link 模板,使用验证码而不是登录链接:

<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;color:#1d1d1f">
  <h2 style="font-size:22px">登录 Stellara</h2>

  <p>你的登录验证码是:</p>

  <div style="margin:24px 0;font-size:32px;font-weight:600;letter-spacing:8px">
    {{ .Token }}
  </div>

  <p style="color:#6e6e73;font-size:14px">
    验证码仅用于本次登录。如果不是你本人操作,请忽略这封邮件。
  </p>
</div>

登录页会调用 verifyOtp({ email, token, type: "email" }) 校验验证码;不要把模板改回 {{ .ConfirmationURL }}。还应在 Authentication / URL Configuration 中配置本地和生产站点 URL。

4. 创建第一批兑换码

  1. 使用 ADMIN_EMAILS 中的邮箱登录。
  2. 打开 /admin/codes
  3. 选择每个兑换码包含的点数、数量、有效期和备注。
  4. 点击生成后立即复制完整码;刷新或离开页面后只会保留掩码。

相同兑换码只能兑换一次;首次兑换后会永久记录兑换账户、邮箱和时间。新注册用户默认 0 点,因此即使 demo URL 被转发,也不能在没有兑换码的情况下调用 Agent。

当前线上部署

当前生产 Demo 使用 https://jyotisha.chatNext.js、Mastra 和 Python API 通过 Docker Compose 部署在香港 VPSSupabase 与模型 API 继续使用云服务。服务器、DNS、环境变量、更新和验收命令统一以 ../deploy/README.md 为准。

Vercel + Supabase 备选部署

以下方案只作为无服务器备选,不是当前线上拓扑。

Web 部署到 Vercel

frontend 作为 Vercel Root Directory,并在 Vercel Project Settings / Environment Variables 配置:

NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
ADMIN_EMAILS=...
MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY=<strict-base64-32-byte-key>
JYOTISH_API_BASE=https://your-python-api.example.com

模型目录及供应商 API key 仅由管理端数据库配置提供;环境只保留 MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY 主密钥。不要把任何密钥写进浏览器代码。

Python 服务必须单独部署

Vercel 上的 Next.js 不能访问你电脑的 127.0.0.1:5200。需要把仓库根目录的 Python API 部署到可从公网 HTTPS 访问的服务,例如独立 VM、Railway、Render 或 Fly.io,然后把公开地址写入 JYOTISH_API_BASE

部署完成后依次验证:

1. Python 健康检查和 consultation_workflow 可访问
2. 邮箱 OTP 可以登录
3. 管理员可以生成兑换码
4. 普通账户只能兑换一次
5. 余额为 0 时不能咨询
6. 成功回答扣 1 点
7. Agent/服务端在首个输出前异常时点数退回;已有输出后异常会保留现有内容并正常计费
8. 用户在 2.5 秒撤回窗口内停止时不调用模型、不扣点
9. 撤回窗口结束后、首个输出分片前取消时点数退回
10. 用户已经收到输出后停止时保留已有内容并正常计费
11. 登录后 `/api/models` 只返回模型 ID、名称、说明、点数和默认状态,不包含 Key、端点或环境变量名
12. 不同 Session 能保存各自的模型选择;已下线模型会回退到默认模型

Demo 防滥用边界

当前版本采用“登录 + 兑换码点数”而不是每日次数限制:

  • 所有咨询请求必须有有效 Supabase 登录态。
  • 余额不足时服务端不会调用模型或 Python 工具。
  • 兑换码一次性使用并绑定账号,数据库不保存完整码。
  • 预扣和退款只允许服务端 service role 调用,且以 request_id 幂等。
  • 服务端拦截系统提示词、Skill 原文和密钥提取请求;Agent 也被限制不得给出医疗、法律、投资等安全关键指令或确定性死亡/诊断预测。
  • 这不是完整内容审核平台。正式公开投放前,可再增加登录/兑换接口速率限制、验证码防机器人和运营后台封禁能力,但不需要把产品改成“每天最多问几次”。

尚未实现

  • 微信/支付宝/Stripe 等真实支付与自动发码。
  • 管理员禁用尚未兑换的兑换码。
  • 完整的风控、审计后台和退款工单。