Commit Graph

20 Commits

Author SHA1 Message Date
Blizzard 466959d3c3 fix: 静默吞错横扫 —— 数据落库/配置广播/审计失败不再无声
把检索链路那次的做法推到同类位置。判据是"丢了这个 error 的代价",只改代价
高的,不动刻意的 fire-and-forget。

改(丢了就是数据永久丢失或行为不可解释):
  - task_handler: 任务**输出**与**执行轨迹**的收尾落库。这是唯一的持久副本
    (Redis 流 10min TTL),失败则永远无法复盘,界面上只显示"这次运行没有
    轨迹"。轨迹的 json.Marshal 失败分支同样是静默跳过,一并补上。
  - admin.broadcastActive: 模型配置热更新广播。失败 = dispatcher/mcp-go 仍用
    旧配置,症状是"控制台改了模型却不生效",而操作者这边一切正常——正是今天
    排查半天的那类问题。
  - middleware/audit: 审计留痕写入。不阻断主流程是对的(业务已完成),但静默
    失败意味着敏感操作没有记录,且没人知道记录缺了,这是合规缺口。

不改(确认过是合理的):
  - dispatcher 的 PublishToken/PublishExec:往前端推流,丢一帧是 UI 瑕疵,
    且按 token 打日志会刷屏;
  - 计量回写 PublishUsage:本来就检查 error 并打日志,钱的路径是干净的。

另修一处误导文案:任务下钻的轨迹空态原本把原因说死为"早于该功能上线",
现在"落库失败"也是已知原因,文案改为并列并指向日志里的 [task] 告警。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:49:13 +08:00
Blizzard 518cddbd1c feat(admin): 检索试验台分路卡片直接说明"为什么空"
此前四张卡片一律写"这一路没召回",而人的视线落在卡片上、不在顶部状态行。
现在按该路诊断分别显示:未启用给出缺什么配置(琥珀)、报错给出错误原文(红)、
真没匹配才说没召回。

真环境验证(截图为证):向量="该知识库在 Milvus 中没有向量(未入库或集合被
重建过)"、图谱="未启用:Neo4j 未连接或未配置"、全文="索引里确实没有匹配内容"。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:29:04 +08:00
Blizzard b26fe21408 fix(rag): 检索三路不再静默吞错 —— 逐路诊断 + 一路挂不拖垮全部
排查"向量路为什么是空的"花了半小时,因为空就是空,没有任何线索。这次把
整条检索链上的静默降级一次清掉。

真 bug(不只是可观测性):
  - kb_search 与 Search() 都拿 rag.Ready() 当总闸,而 Ready() 只代表"向量路
    可用"(embedding + Milvus)。全文(bleve)与图谱(Neo4j)根本不依赖它们,却
    被一并毙掉 → "模型配置没下发"表现为"整个知识库什么都搜不到",还不报错。
    改为逐路判定,任一路可用就仍有召回。

不再吞错:
  - milvus.search 原先把 error 转成 nil,nil —— 检索失败与无召回彻底无法区分;
  - bleve.search / graph.search 出错直接回 nil,连日志都没有;
  - searchPaths 丢掉 embedding 的 error。
    三处改为如实返回,错误统一打日志。

逐路诊断(RouteDiag):每路上报 ok/empty/disabled/error + 耗时 + 原因,经
kb_search 的 diag 参数(仅试验台传,生产调用返回值不变)→ gateway → 检索
试验台。界面上现在能直接看出"这一路没配置/报错了/确实没匹配",不必翻日志。
内存兜底索引也会在 note 里点明"重启即清零"。

测试:3 组,覆盖"无 embedding 时全文仍可召回"、三种空的区分、内存索引提示。
把总闸加回去验证过第一条确实会红——测试能抓到这个回归,不是摆设。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:10:03 +08:00
Blizzard 00ad6c8ac0 fix(admin): 任务下钻按真实轨迹字段渲染 + 修评测页白屏
docker 起来后拿真库验证,上一版有两处只有真数据才暴露的问题:

1. 轨迹字段名猜错了。实际是 seq/ts/node/kind/phase/label/detail,我写的是
   type/msg/at。更糟的是兜底判断用了 !e.msg && !e.node —— node 恰好存在,
   兜底不触发,结果会渲染出一列几乎空白的行。现按真实字段渲染:label 作
   标题、detail 作正文、kind 上色(system/model/tool/agent/plan/section/
   render/approval 八种)、phase=error 整行标红、ts 转本地时间。

2. 评测页签点开直接白屏。库里 flags 存的是字符串 "null"(不是 "[]"),
   JSON.parse 出来是 null,flags.length 抛 TypeError 把整个控制台打崩。
   `as string[]` 的类型断言骗过了 tsc,单测也覆盖不到——只有点开真数据的
   页面才会炸。改为 Array.isArray 校验。

验证方式:本地起 gateway + 真 Postgres,浏览器里逐个页签点过(轨迹 15 条
真实事件、最终输出、评测 100/100/100、DSL 格式化),并在真库上确认跨租户
读取、jsonb ->> 取 topic、cast(jsonb as text) 取 graph 均正常。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 14:20:58 +08:00
Blizzard e4ec74893f feat(admin): 任务观测支持下钻 —— 轨迹/输出/评测/DSL
全平台任务页此前只能看列表,点进去什么都没有。现在点一行开抽屉,四个页签:
执行轨迹、最终输出、评测明细、提交时的 DSL。不含审批操作——审批是客户端
用户的行为(桌面端 ApprovalBar),管理端只做观测。

不复用用户面的 /tasks/:id/replay:Task/Eval 都在租户插件作用域内,用请求 ctx
查别的租户的任务不会报错,而是静默返回空输出/空轨迹,UI 上表现为"这任务没
产出",比报错难查得多。新增 admin 端点走 WithoutTenant。

数据取自 sundynix_task 收尾落库的 output/trace 列,不依赖 Redis 流(10min TTL)。
所以这是复盘视图,运行中的任务轨迹为空——UI 里明确写出来,免得被当成轨迹丢了。

修的两处与测试环境失真有关(写测试时暴露的):
  - 测试库没配 NamingStrategy,与 OpenPostgres 不一致:多数模型有显式
    TableName() 碰巧对得上,但 Task 这类没有的会退化成 "tasks",导致写裸
    SQL 的查询在测试里查无此表。现已对齐 sundynix_ 前缀 + 单数表名。
  - graph 的 ::text 换成标准 cast(... as text):前者是 Postgres 专有,
    换掉后这条查询才能被内存库覆盖。

新增 5 组测试,其中一组专门先证明"租户过滤在测试环境里确实开着"——否则
"跨租户能读到"的断言可能只是因为插件没装,属于假过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 14:07:02 +08:00
Blizzard 84463394d4 fix(admin): 审计筛选下沉 SQL + 工具数不再写死 + 模型删除加确认
清单里那批小毛病,逐条复核后修(「审计详情列缺失」那条已不成立,早补上了)。

1. 审计筛选只在当前页生效 —— 影响最大的一条。分页是服务端的,筛选却在
   前端对已取回的 50 条做,于是搜一个用户 ID 显示"无结果"时,后面几页
   可能还有几百条。审计的用途就是查证,"搜不到"会被读成"没发生过"。
   改为 action/path/q 三个条件全部落到 SQL,前端只管发条件(防抖 300ms)。

2. 服务状态页把 mcp-go/mcp-py 的工具数写死成 23/4 —— 增删工具后一直骗人,
   且服务离线时照样显示,看不出工具其实一个都没注册上。改取实际上报值。

3. 模型删除一点即删,无任何确认。补二次确认,并对"正在使用中"的模型
   单独说明后果(删掉会立刻打断线上对话/向量能力)。

审计筛选补了 4 组回归测试,两条是踩出来的坑:
  - q 的 OR 组必须带括号:gorm 以 AND 拼接各 Where,裸 OR 会让 action
    条件被绕过(测试里用"同 IP 不同方法"两行钉死这个语义);
  - LIKE 必须显式写 ESCAPE '\':Postgres 默认拿反斜杠当转义符,SQLite
    不写就没有转义符——原来的写法在单测里静默失效,搜 "100%" 命中 0 条。
    顺带把 ILIKE 换成 LOWER()+LIKE,这段才能被内存库覆盖。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 13:51:59 +08:00
Blizzard 2c9ecc833e feat(admin): 积分包支持就地生成付款码,验证支付链路
管理端「支付 · 配置 → 积分包」每行加「生成付款码」:下单 → 二维码 → 盯到终态,
用来确认「渠道配置 → 下单 → 扫码 → 回调/查单 → 幂等入账 → 积分到账」在当前
环境真的通。

刻意走用户面的 /api/v1/billing/*,不做 admin 专用旁路 —— 造条测试专线的话,
验过了也不能说明线上用户那条路通。租户由服务端按登录用户解析,无需带租户头。

代价是这会真实扣款,所以:
- 弹窗顶部显著提示是真单、会真扣钱,并指向「订单与对账」的退款入口;
- 已下架的包按钮禁用(服务端本就会拒,别让人白扫)。

弹窗展示订单号/状态/轮询次数/到账积分,出问题时能直接判断卡在哪一环,
比只说一句"成功/失败"有用。轮询策略与 sundynix-web 支付弹窗保持一致:
递归 setTimeout、切后台暂停、失败指数退避、倒计时以服务端 expires_at 为准。

依赖:admin 新增 qrcode(懒加载进 PaymentConfigPage 分块,不进主包)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 12:52:24 +08:00
Blizzard 79b110afd0 feat(admin): 数据源页转 RAG 运维台 + 支付/模型菜单重组 + 概览升级为仪表盘
后端:
- 新增 POST /admin/kb/search:管理端跨租户检索,支持 mode 指定单路
  (vector/fulltext/graph/hybrid),不走 scopedKB(否则会被强制锁到调用者
  自己的 space,跨租户排障就没法做了)
- KB 清单补 space_id(检索键是 <space_id>/<name>,缺它前端拼不出 key)

前端:
- 数据源&RAG 页补「检索试验台」:同一 query 并排跑生产链路 + 四路诊断,
  召回不准时能直接定位是向量/分词/图谱哪一环挂了
- 支付拆成「配置 / 订单与对账」两个子页,挂到运维 > 支付 下;
  导航支持二级菜单(NavParent 命中子路由自动展开)
- SettingsPage → ModelConfigPage「模型配置」,模型参数与计费规则合一
- 概览 → 仪表盘:并入计费与用量(UsagePage → UsageSection),
  去掉系统健康拓扑(与服务状态页重复,同一份 /admin/status 数据)
- 全局隐藏滚动条(保留滚动)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:17:54 +08:00
Blizzard 0d2929931f feat(admin): upgrade admin console UI widgets, add plan & status management, and redesign system status dashboard 2026-07-18 22:18:12 +08:00
Blizzard 5e5f6e9610 feat(billing): 人工退款入口 —— paid 单冲销积分+置 refunded(幂等)
PAYMENT_DESIGN §5 承诺却只留了 OrderRefunded 常量、无入口。补齐全链路:

- store.RefundOrder:订单 CAS(paid→refunded) 为主闸(重复退 changed=false 幂等),
  同事务记 adjust 负分录(ref=订单号)+ 回退物化余额。照抄 MarkOrderPaid 双闸范式;
  新增 idx_ledger_refund_ref 部分唯一索引(kind='adjust' AND ref<>'')做账本级兜底,
  与 grant 索引对称、不与 admin 手工校正(ref 空)冲突。
- 积分若已消费,回退后余额可为负(人工退款预期,账本仍自洽,后续消费被硬拦截)。
- handler AdminRefundOrder + POST /admin/orders/:id/refund(admin 组已挂 Audit 留痕);
  真渠道钱款原路退回需 admin 另在商户后台操作,本地仅冲销积分与订单态(不接自动退款 API)。
- admin 订单流加「退款」按钮(仅 paid 单可见,二次确认+填原因)。
- 测试:RefundOrder 冲销+幂等、只退 paid 两个不变量测试(sqlite 真 DB,余额=账本之和)。
  gateway build/vet/test 全绿,admin tsc 干净。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 13:35:31 +08:00
Blizzard a1c35852ef chore: 删死代码 —— OpenReport/ReadLocalFile/isDesktop/openReport + admin Soon (P2)
完成度审计确认全链零生产调用:
- app.go OpenReport(仅被死包装引用)、ReadLocalFile(仅测试引用);
  desktop.ts openReport 包装、isDesktop 导出(实际用 isMacDesktop)——全删。
  绑定重新生成(OpenReport/ReadLocalFile 归零,PrintReportPage/SaveReportAs/
  Notify 保留)。删对应的 app_test TestReadLocalFile。
- admin Soon.tsx(规划中占位组件)已成未引用死组件——routes.tsx 9 条路由全
  ready:true,import 未用。删组件+import。

openInSystem/filepath/os 仍被 PrintReportPage/download 使用,不孤立。
desktop go test + tsc + 68 vitest、admin tsc + 41 vitest 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 11:56:51 +08:00
Blizzard 3db2de1ef6 feat(billing): 支付 P5.3 —— 掉单补偿定时器 + admin 订单流 + 日终对账
支付线封口。此前 pending 单只在「用户开着账单页轮询」时才查单确认——用户扫完码
关页面,钱付了、积分永不到账。

- 掉单补偿定时器(payment_reconcile.go):gateway 内每分钟扫 pending 微信单,
  逐单 reconcileOrder 主动查单落态。把「用户在不在场」从入账链路摘掉。
  reconcileOrder 从 BillingOrderStatus 抽出、前端轮询与定时器共用一份幂等
  落态逻辑(不重蹈 GenerateReport/SubmitTask 的漂移)。渠道未配置时空转不炸。
- admin 订单流 GET /admin/orders(状态计数+全平台订单,可筛)。
- 日终对账 GET /admin/orders/reconcile:paid 单 ↔ 账本 grant 分录逐单比对,
  抓 order_without_ledger(钱到了积分没给,最严重)/ ledger_without_paid_order。
- admin 计费页「充值订单与对账」块:计数卡片+订单流+一键对账。

⚠️ live 抓到并修掉一个真 bug:OrderStats 复用同一个 gorm.DB 链式 Count 三次,
WHERE 累加成 status=A AND status=B → 恒 0(订单流显示 2 单但计数全 0)。
改成每次起新 query builder。—— 又一次只有 live 才暴露的。

验证:go 6 包测试+tsc+41 vitest 全绿;live 造差异单对账正确抓出
order_without_ledger、清账后回零差异;补偿器启动日志+渠道未配置空转不炸;
浏览器验订单流卡片+一键对账绿条。TTL 过期路径需真渠道触发,部署后自然覆盖。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 11:25:37 +08:00
Blizzard 5d7eca5de3 fix(billing): 微信支付钉死「微信支付公钥」验签体系 —— 商户 2025-09 开户没有平台证书
用户拿旧项目代码对出来的真问题:我此前用 WithWechatPayAutoAuthCipher(平台证书
模式,APIv3 密钥自动下载平台证书验签),但 2024 起新注册商户只发「微信支付公钥」
(PUB_KEY_ID_ 开头)、没有平台证书——在该商户号上初始化/回调验签都会挂。

- 改 WithWechatPayPublicKeyAuthCipher(商户私钥+公钥ID+公钥文件);回调验签用
  NewSHA256WithRSAPubkeyVerifier;平台证书模式不留双模式赘肉(YAGNI)。
- Config 增 public_key_path/public_key_id(必填,公钥文件同样只存路径);
  admin 卡片补两字段;env 兜底加 WECHAT_PUBLIC_KEY(_ID)。
- 顺手修 live 撞出的真 bug:sundynix_setting.value 是 varchar(255),
  支付配置 JSON(含加密密钥)一条就超(SQLSTATE 22001)→ 改 text。
live:列类型已迁 text;缺公钥两项报「配置不全,缺: public_key_path,
public_key_id」;GET 回显含新字段。go 6 包测试+tsc+41 vitest 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:12:26 +08:00
Blizzard d1e1e0fc4a feat(billing): 微信支付配置进 DB —— admin 控制面保存即热生效
用户定的形态:配置存数据库、密钥文件放服务器磁盘(库里只存路径)。
env 降级为兜底(DB 优先 → env → 隐藏,与 tokens_per_credit 同一约定)。

- payment 包重构:Config(6 字段)+ Manager(RWMutex 热重载,学 prompt 控制面
  改完即生效不重启);未启用原因人话化(未配置/缺哪些字段/初始化失败具体错)。
- APIv3 密钥入库前 AES-GCM 加密(shared/secrets,与模型 API Key 同一把
  SUNDYNIX_SECRET_KEY);GET 只回 has_apiv3_key 不回显;PUT 留空=沿用旧密钥
  (只写不回显语义,同模型 Key)。
- admin GET/PUT /admin/payment/wechat;业务路径全部改经 Manager.Current()
  取快照(BillingPacks/下单/查单/回调)。
- admin 计费页「微信支付配置」卡片:状态徽章(已启用/未启用+原因)+六字段
  +保存并热生效。
live:无配置→「未配置」;存假配置→热重载报「私钥加载失败:decode err」;
去掉 appid→「配置不全,缺: appid」;密钥留空沿用(has_apiv3_key 保持 true);
psql 复核库内密文 enc:1: 前缀、不含明文子串。go/tsc/41 vitest 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 10:55:30 +08:00
Blizzard 929bbf334b feat(admin): 计费页长出「充值渠道」块 —— 兑换码生成/台账 + 积分包定价
P5.1 收尾:此前生成码只有 API。计费页现在从上到下 = 计费规则(积分→token
汇率) → 充值渠道(钱→积分:兑换码 + 微信定价用的积分包) → 用量观测,
两层汇率在同一页可见、各管各的。

- 兑换码:面额/张数/备注生成;**明文码只在生成响应显示一次**(等同现金,
  台账 GET /admin/redeem-codes 服务端脱敏只露首尾,丢码重生成、不提供找回
  ——顺手把接口这个第二明文出口堵了);台账含核销状态。
- 积分包:新增/上下架(微信 P5.2 上线前把定价面备好);admin api.ts 补
  packs/redeem-codes 四个函数。
- launch.json 加 admin-console-alt(:5176)——5174 被用户自己的 sundynix-site
  占着,不动别人端口。
live:5176 登录→生成 5 张(绿色一次性面板+复制全部)→配「入门包 1000分/¥9.9」
在售可下架→台账脱敏 curl 复核;tsc+41 vitest 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 10:28:03 +08:00
Blizzard b46382817c feat(admin): 计费页加「余额硬拦截」开关 + 各租户「充值」按钮
闭合积分环的配置/操作端,接到 P4 后端:
- 计费规则区:加「余额硬拦截」开关(credit_enforce),与汇率合成一个「保存规则」。
- 用量观测·租户排行:每行加「充值」按钮(正=充值、负=校正),调 /admin/credits/grant,
  充值后即时刷新余额。
- api:getBillingConfig/saveBillingConfig 带 credit_enforce;新增 grantCredits。

live 验证(preview):开关渲染+可存;点公司A「充值 10」→余额 5.92→15.92、账本落对租户;
tsc + 41 vitest 全过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:53:52 +08:00
Blizzard 11d321f218 feat(admin): 计费 & 用量合成一页 —— 顶部配规则、下方看结果(闭环)
按"配置和观测放一个页面"收口:删掉重复的「计价 & 预算」页(其预算区块是 mock),
把真·计价配置并入「计费 & 用量」,形成 规则→扣费→观测 一页闭环。
- 新组件 BillingRules:全局 token→积分汇率 + 每模型 单价/币种/积分权重(读写
  /admin/pricing + /admin/billing-config),改完对后续任务实时生效。
- UsagePage:顶部「计费规则」(配置端) + 下方「用量观测」(按规则折算后的实际消耗)。
- 删 PricingPage + /pricing 路由;api 加 credit_weight / getBillingConfig / saveBillingConfig。

live 验证(preview):改汇率保存→持久化→新任务按新汇率扣→用量观测反映;
tsc + 41 vitest 全过;无 PricingPage 残引。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:26:33 +08:00
Blizzard 075d41f5b3 feat(llm): 本地模型接入 vLLM / Ollama + reasoning 思考过程入轨迹
vLLM 与 Ollama 都暴露 OpenAI 兼容 API(底层 go-openai 请求 {base}/chat/completions),
故统一走 openai 客户端,按 provider 归一化连接参数:
- Ollama/vLLM 的 OpenAI 端点固定在 /v1,BaseURL 漏写自动补全(否则打到 /chat/completions 404)
- 本地后端默认不校验 api_key → 缺省补占位(ollama→"ollama",vllm→"EMPTY";openai 客户端要求非空)
- 显式 key 一律尊重(vLLM --api-key 启动);在线 provider 原样不动(DeepSeek 两种都收)

reasoning_content 适配:ChatStream 增 onReasoning 回调,捕获 reasoning 模型
(本地 Qwen3 思考 / DeepSeek-R1 / QwQ、在线 deepseek-v4-pro)的思考分片。思考阶段分片
Content 为空本就不污染答案;runAgent 把思考累计后 surface 到 exec 轨迹「推理过程」事件。

控制台 ModelManager 加 ollama 选项;llm 包补 normalizeBaseURL / apiKeyOrPlaceholder 单测。
四模块全绿。live:经 admin 配 ollama qwen2.5:0.5b → dispatcher 热切(日志 base 自动 .../v1)
→ POST /v1/chat/completions 200 端到端出答案;切回 deepseek-v4-pro → exec 轨迹现「推理过程:思考26字…」。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:10:18 +08:00
Blizzard 3b54e59ecf feat: embedding 配置搬上控制面 — 数据源页可视化配置 + 热更新
embedding 从 env 改为控制面驱动(持久化+可视化),复用 chat 模型同套范式:
配置控制面泛化为按 kind(chat/embedding),加 embedding kind。

- shared: 配置 subjects 泛化 sundynix.config.<kind>.get/.updated;bus 方法改 kind 参数
  (RequestConfig/ServeConfig/PublishConfigUpdated/SubscribeConfigUpdated)
- gateway: sundynix_model 加 kind 列(每 kind 唯一激活)+旧行回填 chat;admin 按 kind
  增删改/激活/列表,测试连接 embedding 走 POST /embeddings;main 按 kind ServeConfig;
  变更广播各 kind
- dispatcher: 取 chat 配置(kind 化)
- mcp-go: rag.Engine.SetEmbedding 热更新(RWMutex);main 取/订阅 embedding 控制面配置
  (覆盖 env)
- admin 控制台: api 按 kind;抽出复用 ModelManager;ModelsPage(chat)+新 DatasourcesPage
  (embedding + 向量/图库占位);routes 数据源页就绪
- 验证: 全模块 build✓ + e2e PASS + 控制台 npm build✓;live 全跑通——chat(DeepSeek 回填
  kind 仍工作);mcp-go 不带 EMBED env 启动→控制台配 embedding(百炼)→测试连接✓→激活
  →NATS 热更新 mcp-go→入库+语义检索'存向量的数据库'→Milvus;浏览器数据源页拉到激活配置

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 17:25:54 +08:00
Blizzard f6a669070d refactor(admin): 控制台改为路由表驱动的动态路由 (react-router)
控制台从 useState 切 tab + 硬编码条件渲染,改为路由注册表驱动 + 真实 URL 路由,
加页面只需在 routes.tsx 加一条,不动外壳。

- 依赖 react-router-dom v7;App=HashRouter(静态托管/桌面内嵌都能深链)
- routes.tsx:路由注册表(单一事实源,导航+内容都派生);real 页面 lazy 懒加载(代码分割)
- shell/AppShell:NavLink 分组导航(配置/平台) + Routes + Suspense + 健康灯,全从注册表派生
- 页面归入 pages/(ModelsPage 移入),components/Soon 占位复用
- 验证:npm build✓(ModelsPage 独立 chunk=懒加载生效);真实浏览器——默认重定向 #/models、
  nav 切换改 URL hash、深链 #/guardrails 直达、浏览器后退回 #/datasources、active 高亮

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:09:07 +08:00