Files
Jyotisha/frontend
Jesse_Chen 6a44c778c3 fix(web): show live agent work progress and fail truncated rectification answers
Rectification dropped tool.activity started events and treated length finishes as completed. Share generation settings with consultation, keep the activity line through streaming, and name multi-domain chart calculation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 12:39:09 +08: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 等真实支付与自动发码。
  • 管理员禁用尚未兑换的兑换码。
  • 完整的风控、审计后台和退款工单。