feat: 元点流商 OriginFlow 二次开发 + 与 originflow 统一 web/src 前端 - #6690
feat: 元点流商 OriginFlow 二次开发 + 与 originflow 统一 web/src 前端#6690peidunyue wants to merge 18 commits into
Conversation
- 前端营销站:首页/定价/模型/方案/联系销售,双语 i18n,
按 host 切换营销(www)/控制台(app) 路由(ADR-1 域名隔离)
- 后端公开接口:/api/public/{site-config,pricing,model-catalog,contact-sales},
含销售线索/定价/模型目录模型与 AutoMigrate,common.IsSafeRedirect 防开放重定向
- 品牌替换:默认站点名 OriginFlow、logo.svg、favicon;控制台布局不变
- 部署:nginx www/app/api 三 server block + HTTPS;
__root.tsx 跨域 host 重定向(营销专属路径 <-> 控制台)
Co-Authored-By: CodeBuddy Code <https://cnb.cool>
- 新增 QuickStart 与 Usage 双语文档页,接入 PublicLayout + useSeo - 新增 useSeo hook(title/description/og/JSON-LD)并应用到所有营销页 - 新增 AnalyticsEvent 模型与 /api/public/track 埋点接口(事件白名单 + 字段截断 + 限速) - 销售线索后台管理:/admin/sales-leads 列表/详情/状态流转(白名单),提交后异步邮件通知 - 营销组件埋点:访问(visit)、定价点击(pricing_click)、线索提交(lead_submit) - 修正 .gitignore 误吞 marketing/data 源码目录(data/ 例外放行) Co-Authored-By: CodeBuddy <codebuddy@tencent.com>
- 通过 bun patchDependencies 对 date-fns-tz@1.3.8 打补丁:将已被 date-fns@4 移除的内部子路径导入(_lib/cloneObject、_lib/toInteger、_lib/getTimezoneOffsetInMilliseconds、 fp/_lib/convertToFP/index.js、format/index.js)改为等价的内联实现或 v4 暴露的子路径。 - 该补丁仅影响 classic 主题(semi-ui 依赖),不影响 default 主题(使用 date-fns@4 直连)。 - 修复后 web/classic 可正常构建,后端二进制可同时嵌入 default 与 classic 两个前端产物。 Co-Authored-By: CodeBuddy <codebuddy@tencent.com>
实现面向客户的单模型商业上架(Model Market)后端 v1: - model/market_model.go: MarketModel 表(Model 唯一不可变,关联 Pricing.ModelName 与 PublicModelCategory.Category)。含状态(available/coming_soon/disabled)、计价单位(token/image/second/char)、Currency(CNY/USD)、Metadata(JSON 多语言展示覆盖)、Featured/Sort 等字段与 CRUD。 - model/main.go: 将 MarketModel 纳入 AutoMigrate。 - controller/market.go: 管理员 CRUD(AdminAuth 保护)+ 公开读取 GetPublicMarketModels。校验长度/白名单/非负/Metadata 为合法 JSON;新建做唯一性校验。公开接口支持 ?locale=zh|en,从 Metadata 解析并叠加 i18n(name/description)。 - router/api-router.go: 注册 /admin/market-models (GET/POST/GET :id/PUT :id/DELETE :id,AdminAuth) 与公开 GET /market-models。 v1 为展示型目录,不含 UserMarketModel 激活/开通与计费门禁。
新增 Model Market 后台管理 UI(web/default,React 19 + TanStack Router/Query/Table + shadcn Base UI),镜像 redemption-codes CRUD 范式: - 路由 /_authenticated/market-models/(ROLE.ADMIN 守卫,自动注册于 routeTree);侧边栏新增「Model Market」入口(Store 图标)。 - 列表页:分页表格、状态筛选、关键字搜索(model/provider);状态/精选徽标、价格按币种格式化(¥/$ + 最小货币单位/1M)。 - 新建/编辑抽屉(Sheet):Model(编辑时只读,唯一不可变)、Provider、Category、Tags、Input/Output 价格、Currency(CNY/USD)、Unit(token/image/second/char)、Status、TrialQuota、Sort、Featured 开关、Metadata(JSON,i18n 多语言展示覆盖,前端校验合法 JSON)。 - 删除确认弹窗(AlertDialog);新建做唯一性校验、编辑回显并重置表单。 - 配套后端:ListMarketModels 支持 p/page_size 分页并返回 total(SearchMarketModelsPaginated),供管理后台表格使用。 - 类型检查(本 feature 无错误)、oxlint 全绿;构建通过。
- 新增公开门店页 GET /api/market-models 的前端展示(storefront): 仅展示已上架模型,按分类/排序渲染卡片网格。 - 服务端 i18n 覆盖(name/description)通过 ?locale=zh|en 解析, 匿名访客请求错误静默降级为空列表,不弹错误提示。 - 复用 Card / StatusBadge / formatMarketPrice 等现有组件与常量; 新增 /market 路由并在营销导航中加入入口。 - i18n 资源补充 Featured / Model Market / View pricing / Trial quota 等键值。 Co-Authored-By: CodeBuddy Code <noreply@codebuddy.ai>
Base UI 的 Select onValueChange 回调值为 string | null,而 FormState.region / use_case 为 string;以 v ?? '' 兜底避免 TS2345。 Co-Authored-By: CodeBuddy Code <noreply@codebuddy.ai>
TanStack Router 的 location.search 为已解析对象({})而非字符串,
函数期望 string。改用 location.searchStr 取原始查询串,并修正
import { type QueryClient } 为顶层 import type 以通过 oxlint。
Co-Authored-By: CodeBuddy Code <noreply@codebuddy.ai>
P2 平台化四项能力的后端实现: - P2-02 企业团队空间:Team / TeamMember / TeamProject 模型与管理端 CRUD、部门账单聚合 - P2-03 SLA 与状态页:SlaIncident 模型与事件 CRUD,公开 /api/sla/status 与 /api/sla/incidents(聚合 Channel 与 PerfMetric) - P2-04 区域路由:RegionRoute 配置 CRUD,并接入真实选渠道链路——按请求头 X-Region 收窄候选渠道,按 latency / availability / cost / fixed 策略排序,热路径 60s TTL 缓存且写操作即时失效;未命中区域、无策略或白名单无交集时无损回退原有逻辑 - P2-05 分销商控制台:Distributor / DistributorPrice 模型、下级用户查询与账单 附带:users.team_id 列、GetUsersByInviterId、AutoMigrate 新增 7 张表、router/p2-router.go 接线;修复 RegionRoute.Enabled 因 GORM 忽略零值导致显式禁用被写成启用的问题。
…umNous#1) Squash merge of the Model Market (模型商店) feature for the OriginFlow / 元点流商 platform. ## Backend (admin CRUD + public read) - `model/market_model.go`: `MarketModel` table — immutable unique `Model`, links to `Pricing.ModelName` and `PublicModelCategory.Category`. Fields for status (available/coming_soon/disabled), billing unit (token/image/second/char), currency (CNY/USD), i18n `Metadata` (JSON), `Featured`/`Sort`. - `model/main.go`: register `MarketModel` in AutoMigrate. - `controller/market.go`: AdminAuth-protected CRUD + public `GetPublicMarketModels` (validates length/whitelist/non-negative, parses Metadata JSON, unique-check on create). Public endpoint supports `?locale=zh|en` multi-language overlay. - `router/api-router.go`: register `/admin/market-models` (CRUD, AdminAuth) and public `GET /market-models`. ## Frontend (web/src) - `_authenticated/market-models/` admin route (ROLE.ADMIN guard) + sidebar entry (Store icon). - List/create/edit/delete pages mirroring the redemption-codes CRUD pattern (pagination, status filter, search, price/currency formatting). Reuses existing Card / StatusBadge / Select components. - `routeTree.gen.ts` regenerated; added "Model Market" i18n key. - Public `/market` storefront page — directory display, multi-language + price. v1 is a display catalog; does not yet include UserMarketModel activation/provisioning or billing gating.
* feat(p2): P2 前端管理页面(团队空间/区域路由/SLA/分销商)+ 团队详情路由
- 补全 teams 功能缺失的路由层:列表路由、$teamId 详情路由与 TeamDetail 页(复用 TeamMembersTab)
- 前端生产构建与 tsgo 类型检查均零错误
- routeTree.gen.ts 由构建重新生成,纳入 teams/region-routes/sla 新路由
- 后端 router/p2-router.go 已注册对应 /api/admin/{teams,sla-incidents,region-routes,distributors} 及公开 /api/sla/{incidents,status} 端点
* feat(p2): 用户区域偏好 + 团队真实用量聚合
- User.RegionPreference:用户级区域路由偏好,detectRegion 降级链
X-Region 头 → 请求上下文区域 → 用户偏好;经 UserBase 缓存写入上下文
- Log.TeamId:消耗日志打标团队归属,供团队级计费聚合
- TeamBilling 扩展真实用量(消耗配额/tokens/请求数),按 team_id 聚合 logs 表
- 新增 service/channel_select_test.go 覆盖 detectRegion 降级与归一化
- AutoMigrate 自动加列;UserBase 缓存 schema 版本 2→3 使旧缓存失效
* fix(teams): align TeamBilling type with backend and add billing tab
- Add usage_quota/prompt_tokens/completion_tokens/request_count to
the frontend TeamBilling interface to match the backend deferred
team real-usage aggregation.
- Add TeamBillingTab component surfacing both allocated/used quota and
real usage aggregated from consume logs (by team_id).
- Wire the new Billing tab into team detail, reusing the existing
/api/admin/teams/{id}/billing endpoint (previously unused).
---------
Co-authored-by: 丁帅帅(丁帅帅) <dingshsh@yoozoo.com>
- Add TeamProjectsTab component with list/add/remove CRUD, reusing the
existing listTeamProjects/addTeamProject/removeTeamProject APIs, the
team project form schema, and project success/error messages.
- Wire the Projects tab into team detail alongside Members and Billing,
closing the previously-unused /api/admin/teams/{id}/projects endpoint.
Co-authored-by: 丁帅帅(丁帅帅) <dingshsh@yoozoo.com>
- Dockerfile: 在 bun install 前注入 .npmrc,新增 NPM_REGISTRY 构建参数与重试逻辑 (Bun 默认并发在大依赖上偶发 integrity 校验失败,降级并发+重试规避) - docker-compose.yml: 由上游发行镜像改为 build 本地源码,确保二次开发功能被打入镜像 - .dockerignore: 忽略各级 node_modules 与 web/dist,运行时目录改由 compose bind mount
- 新增 web/public/logo.webp 与更新 favicon.ico(取自历史品牌资产,重映射到当前 V-2.0 默认前端) - constants.ts: DEFAULT_SYSTEM_NAME 改为「元点流商 OriginFlow」,DEFAULT_LOGO 改为 /logo.webp - web/index.html: 标题/og 与 favicon 链接同步为 OriginFlow - footer.tsx / system-info-section.tsx 默认 logo 回退统一为 logo.webp (注:V-2.0 仅保留单一默认前端,classic 目录为空未跟踪;V-1.0 的品牌提交落到了已改名的项目目录,本次重新落地到正确仓库)
后端(QuantumNous#6 公开接口): - 新增模型 SalesLead / PublicPricing / PublicModelCategory 并注册 AutoMigrate - 默认种子数据(中英双语定价方案与模型目录) - controller/public_site.go:GET /api/public/site-config、/pricing、/model-categories, POST /api/public/sales-lead(校验 + 关键限流 + 开放重定向防护) - router:新增 /api/public 分组,复用 GlobalAPIRateLimit / CriticalRateLimit 前端(QuantumNous#5 营销站): - web/src/features/marketing:Hero/TrustBar/ModelGateway/ChinaGlobalFlow/UseCases/ PricingPreview/Faq/FooterCta、PricingCard/ModelCategoryCard、ContactForm、双语文案 - 营销路由 /、/pricing、/models、/solutions、/contact-sales(solutions/contact-sales 为新增页) - lib/marketing-mode.ts:www 域名(或本地 ?marketing=1)进入营销前台, __root 增加 www↔app 跨域重定向守卫;控制台 /、/pricing、/_authenticated/models 在营销模式下切换为公开营销版,app 域名行为不变(不改控制台布局)
根据验收反馈,将营销站中文首屏标题由「中国与世界大模型的统一 API 网关」 简化为「大模型统一API网关」,更聚焦于核心价值主张。
营销站新增 /quick-start 页面,提供 Base URL、API Key 鉴权说明、 四步快速接入指引与 cURL / Python / Node.js 代码示例(OpenAI 兼容)。 - 中英双语内容,代码示例语言无关共用一份 - 新增 CodeBlock 组件(带复制按钮) - 将 /quick-start 纳入营销站 host 隔离与跨域重定向 - 导航新增 Quick Start / 快速开始 入口 - 同步生成 routeTree.gen.ts
- Resolve the web directory conflict by keeping web/src as the single canonical frontend (matches fork/upstream-main); drop originflow's divergent web/src additions and the unused date-fns-tz patch. - Keep originflow's self-contained backend additions: common/redirect.go (IsSafeRedirect), model/analytics_event.go, deploy/nginx/originflow.conf, and the OriginFlow SystemName branding in common/constants.go. - Drop service/public_site.go: it referenced model helpers not present in our tree (GetEnabledPublicPricings/GetEnabledPublicModelCategories/NormalizeLocale) and was not wired into any router, so it did not compile.
WalkthroughOriginFlow adds regional channel routing, administration APIs and models, public marketing pages, model catalogs, SLA status, teams, distributors, deployment configuration, and frontend management pages. ChangesPlatform foundation
Regional routing
Backend domains and APIs
Frontend features
Routing and host isolation
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Warning |
There was a problem hiding this comment.
Actionable comments posted: 2
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/routes/_authenticated/route.tsx (1)
25-40: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winLimit the authentication bypass to the public models route.
isMarketingMode()is true when the request includesmarketing=1. Lines 25-40 then bypass authentication for every_authenticatedchild route, not only/models.Use
location.pathnameto allow the bypass only for/modelsand/models/. Keep authentication required for all other child routes. The root host redirect can remain an additional safeguard.As per coding guidelines, “认证与权限必须在路由和接口层校验”。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/routes/_authenticated/route.tsx` around lines 25 - 40, Restrict the isMarketingMode() authentication bypass in Route.beforeLoad to pathname values exactly equal to /models or /models/. For all other _authenticated child routes, continue through the existing auth.user and auth.accessToken check and redirect to /sign-in when unauthenticated.Source: Coding guidelines
🟠 Major comments (34)
model/analytics_event.go-13-15 (1)
13-15: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRemove query data before persisting URLs.
PathandReferrercan contain email addresses, invitation tokens, or reset tokens in query parameters. Normalize these values server-side and discard query and fragment data before storage.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model/analytics_event.go` around lines 13 - 15, Normalize URL values assigned to the Path and Referrer fields before persisting analytics events, removing all query parameters and fragments while preserving the URL path. Apply this server-side at the event handling or model persistence boundary, using the existing URL parsing utilities if available.Dockerfile-14-14 (1)
14-14: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winPreserve the existing
.npmrcsettings.Line 14 replaces the copied
.npmrcfile. A build that usesNPM_REGISTRYthen loses scoped registry and authentication configuration.Append the registry override instead of replacing the file.
Proposed fix
-RUN if [ -n "$NPM_REGISTRY" ]; then echo "registry=$NPM_REGISTRY" > .npmrc; fi \ +RUN if [ -n "$NPM_REGISTRY" ]; then printf '\nregistry=%s\n' "$NPM_REGISTRY" >> .npmrc; fi \🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Dockerfile` at line 14, Update the Dockerfile RUN command that handles NPM_REGISTRY to append the registry override to the existing .npmrc instead of overwriting it, preserving copied scoped registry and authentication settings while retaining the conditional behavior.model/main.go-295-313 (1)
295-313: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winMigrate
AnalyticsEventbefore accepting analytics writes.
CreateAnalyticsEventwrites to theAnalyticsEventmodel, butmigrateDB()’sAutoMigrate()list omits&AnalyticsEvent{}. Add it so fresh deployments createanalytics_eventsbefore analytics writes run.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model/main.go` around lines 295 - 313, Add &AnalyticsEvent{} to the AutoMigrate model list in migrateDB alongside the other persisted models, ensuring the analytics_events table is created before CreateAnalyticsEvent accepts writes.model/region_route.go-171-181 (1)
171-181: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftBound and actively evict routing-cache entries.
Line 171 includes
modelNamein the cache key. Lines 172-181 never delete expired entries. Requests with unique model names can growregionRoutingCachewithout bound, even though entries have a TTL.Use a size-bounded cache with expiry eviction. Do not retain unlimited request-derived keys.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model/region_route.go` around lines 171 - 181, The region routing cache around regionRoutingCache.Load and Store must enforce both a maximum size and expiry eviction, rather than retaining expired request-derived modelName keys indefinitely. Update the cache implementation and its use in the routing-resolution flow to remove expired entries and evict entries when the configured bound is reached, while preserving valid cached routing results.model/public_site_seed.go-11-18 (1)
11-18: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftMake default pricing initialization safe for concurrent startup.
Two instances can both read
pricingCount == 0and insert the defaults.PublicPricinghas separate indexes onplan_keyandlocale, but no composite uniqueness constraint. This can create duplicate public plans.Add a unique
(plan_key, locale)constraint and insert through an idempotent conflict-safe operation. Deduplicate existing rows in the migration path before enforcing the constraint.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model/public_site_seed.go` around lines 11 - 18, Update defaultPublicPricings initialization and its migration path: deduplicate existing PublicPricing rows by (plan_key, locale), add a composite unique constraint for those columns, and replace the unconditional DB.Create call with an idempotent conflict-safe insert. Preserve existing pricing data while ensuring concurrent startups cannot create duplicate plans.model/region_route.go-21-22 (1)
21-22: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDefine or remove
RegionRoute.Weight.
Weightis accepted by the region route API and stored inRegionRoute, butRegionRoutingdoes not carry it and channel selection only uses theRegionRouting.AllowedIds,Strategy, andPriority. Users can save a routeweightwith no routing effect; add and test the intended weighting behavior, or remove it from the model and API.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model/region_route.go` around lines 21 - 22, Remove RegionRoute.Weight from the model and API unless weighting is implemented end to end; otherwise propagate it through RegionRouting and channel selection with tests covering its effect. Ensure RegionRoute no longer accepts or stores an ignored weight value.deploy/nginx/originflow.conf-143-145 (1)
143-145: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAllow the regional routing request header in CORS.
Third-party browser clients that send
X-Regiontrigger a preflight request. The currentAccess-Control-Allow-Headersvalue rejects that request. AddX-Regionto the allowlist.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deploy/nginx/originflow.conf` around lines 143 - 145, Update the Access-Control-Allow-Headers directive to include X-Region alongside the existing Authorization and Content-Type headers, preserving the current CORS configuration and always behavior.model/user.go-1435-1437 (1)
1435-1437: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winUse
commonGroupColfor the reservedgroupcolumn.
groupis a reserved SQL keyword in MySQL. This raw select list can fail on a required MySQL deployment. Build the select list withcommonGroupColinstead of the bare identifier.As per coding guidelines, “When raw SQL is unavoidable, account for dialect-specific quoting and values; use commonGroupCol ... for reserved columns.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model/user.go` around lines 1435 - 1437, Update the raw select list in the query building the user items to use the existing commonGroupCol symbol for the reserved group column instead of the bare group identifier, while preserving the other selected columns and query behavior.Source: Coding guidelines
deploy/nginx/originflow.conf-19-22 (1)
19-22: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winUse certificate paths that match the documented Certbot command.
A fresh deployment creates one certificate lineage for this multi-domain command, normally named
www.91flow.com. It does not create theapp.91flow.comandapi.91flow.comlineages referenced at Lines 102-103 and Lines 131-132.Use the
www.91flow.comcertificate path in all three TLS blocks, or issue three explicitly named certificates. Otherwise Nginx cannot load the app and API server blocks.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deploy/nginx/originflow.conf` around lines 19 - 22, Update the certificate paths in all three TLS server blocks to use the single Certbot lineage `/etc/letsencrypt/live/www.91flow.com/` for both fullchain.pem and privkey.pem, matching the documented multi-domain command and replacing the app- and api-specific lineage references.model/channel_cache.go-268-270 (1)
268-270: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winEnforce active regional allowlists consistently.
Both selection paths restore excluded channels when an active route has no eligible allowlisted candidate. This bypasses the regional policy. Return no candidate instead. Configure an explicit fallback route when availability fallback is required.
model/channel_cache.go#L268-L270: return the emptyfilteredset instead ofchannels.model/ability.go#L171-L173: preserve the empty allowlist result instead of restoringabilities.model/region_route_test.go#L131-L133: expect no cached candidate for an empty allowlist intersection.model/region_route_test.go#L154-L156: expect no database-backed ability for an empty allowlist intersection.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model/channel_cache.go` around lines 268 - 270, Enforce active regional allowlists by preserving empty results instead of restoring excluded candidates: in model/channel_cache.go lines 268-270, return filtered; in model/ability.go lines 171-173, preserve the empty allowlist result rather than abilities; update model/region_route_test.go lines 131-133 to expect no cached candidate and lines 154-156 to expect no database-backed ability for empty allowlist intersections. Configure an explicit fallback route where availability fallback is required.controller/distributor.go-230-235 (1)
230-235: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winBind the price update to the parent distributor.
The handler reads
price_idonly.model.UpdateDistributorPricefilters byidalone. A request to/admin/distributors/1/prices/99therefore updates a price row that belongs to distributor 2. The same applies toDeleteDistributorPrice. Parse the:idpath parameter and pass it as an additional filter.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@controller/distributor.go` around lines 230 - 235, Update UpdateDistributorPrice and DeleteDistributorPrice to parse the parent distributor path parameter (:id) and pass it alongside priceId to model.UpdateDistributorPrice or the corresponding delete operation. Ensure both operations filter by distributor ID and price ID so prices belonging to another distributor cannot be modified.router/p2-router.go-41-43 (1)
41-43: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAdd rate limiting to the public SLA endpoints.
Both routes are anonymous.
GetPublicSlaStatusruns an aggregation over a window of up to 720 hours, so repeated anonymous calls can load the database. The comparable public endpoints inrouter/api-router.goapplymiddleware.CriticalRateLimit(). Apply it here, or cache the summary.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@router/p2-router.go` around lines 41 - 43, Apply middleware.CriticalRateLimit() to both anonymous SLA routes, /sla/incidents and /sla/status, in the router registration alongside controller.GetPublicSlaIncidents and controller.GetPublicSlaStatus. Match the middleware usage in router/api-router.go and preserve the existing handlers.controller/public_site.go-34-38 (1)
34-38: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winDo not return raw database errors on public endpoints.
err.Error()exposes driver and schema details to anonymous callers. Log the error and return a generic message, or usecommon.ApiError, which the other new controllers use.Also applies to: 46-51
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@controller/public_site.go` around lines 34 - 38, Update the database error handling in the controller methods containing the model.DB queries to avoid exposing err.Error() through public API responses. Log the underlying error for server-side diagnostics and return a generic client-safe message, applying the same behavior to both referenced error branches.model/distributor.go-95-105 (1)
95-105: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReused GORM query chains between
CountandFind. Each listing builds one*gorm.DB, calls theCountfinisher, then reuses the same chain forFind. GORM treats chain reuse after a finisher as unsafe, because clauses from the first statement can leak into the second. Create a new session for the second statement in each listing.
model/distributor.go#L95-L105: inSearchDistributors, derive the paged query fromq.Session(&gorm.Session{})beforeOrder/Offset/Limit/Find.model/team.go#L67-L77: inListTeams, apply the same session split before the pagedFind.model/team.go#L114-L121: inListTeamMembers, apply the same session split before the pagedFind.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model/distributor.go` around lines 95 - 105, Split the GORM query session after counting to prevent finisher-state reuse: in SearchDistributors (model/distributor.go:95-105), ListTeams (model/team.go:67-77), and ListTeamMembers (model/team.go:114-121), derive the paginated query from q.Session(&gorm.Session{}) before applying ordering, pagination, and Find; leave the count query unchanged.model/distributor.go-175-182 (1)
175-182: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
DistributorBilling.DistributorIdreceives a user id.
GetDistributorBillingtakesdistributorUserIdand assigns it toDistributorId. The controller passesd.UserId, so the field returned to the frontend holds the owner account id, not the distributor id. Any client that correlates billing with a distributor row will match the wrong record. Pass the distributor id explicitly, or rename the response field.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model/distributor.go` around lines 175 - 182, Update GetDistributorBilling to distinguish the distributor identifier from distributorUserId: obtain or pass the actual distributor id explicitly and assign that value to DistributorBilling.DistributorId, while retaining distributorUserId for querying sub-users. Update its callers, including the controller using d.UserId, so the response field identifies the distributor record rather than the owner account.controller/distributor.go-247-254 (1)
247-254: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winValidate
modeland prices inUpdateDistributorPrice.
CreateDistributorPricerejects an emptymodeland negative prices.UpdateDistributorPricedoes not.model.UpdateDistributorPricewritesmodel,input_price, andoutput_priceunconditionally, so a request can clear the model name or store a negative price on a billing record.🛡️ Proposed fix
+ if req.Model == "" { + common.ApiErrorMsg(c, "model is required") + return + } + if req.InputPrice < 0 || req.OutputPrice < 0 { + common.ApiErrorMsg(c, "prices must be >= 0") + return + } if !model.AllowedDistributorPriceCurrencies[req.Currency] { common.ApiErrorMsg(c, "invalid currency (expected CNY|USD)") return }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@controller/distributor.go` around lines 247 - 254, Update UpdateDistributorPrice to apply the same validation as CreateDistributorPrice: reject an empty req.Model and any negative input or output price before calling model.UpdateDistributorPrice. Preserve the existing currency and unit validation, and return through common.ApiErrorMsg for invalid requests.controller/region_route.go-112-130 (1)
112-130: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winApply the create-time normalization in
UpdateRegionRoute.
CreateRegionRouterequires a non-emptyregionand converts an emptymodelto"*". The update handler does neither, so an update can store an emptyregionor an emptymodel. Routes stored that way no longer match the wildcard convention used at creation. Also note that an omittedenabledfield resets the route to enabled, which silently re-enables a disabled route.🐛 Proposed fix
+ if req.Region == "" { + common.ApiErrorMsg(c, "region is required") + return + } + if req.Model == "" { + req.Model = "*" + } if !model.AllowedRegionRouteStrategies[req.Strategy] {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@controller/region_route.go` around lines 112 - 130, Apply the same normalization used by CreateRegionRoute in UpdateRegionRoute: reject an empty req.Region and convert an empty req.Model to the wildcard value "*" before constructing model.RegionRoute. Preserve the existing Enabled value when req.Enabled is omitted instead of defaulting it to true, while still applying an explicitly provided enabled value.controller/public_site.go-102-108 (1)
102-108: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winReuse
common.IsSafeRedirect(req.Redirect).The local check keeps the local
safeRedirectstring and rejects valid relative redirects containing:, while bypassing the shared parsed-URL whitelist forhttp/httpstargets. Use the shared helper instead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@controller/public_site.go` around lines 102 - 108, Replace the local redirect validation in the surrounding handler with common.IsSafeRedirect(req.Redirect), and assign its result to safeRedirect while preserving the existing empty/default behavior. Remove the duplicated strings-based checks so redirects follow the shared helper’s parsed-URL whitelist, including valid relative paths and permitted http/https targets.web/src/features/region-routes/components/channel-multi-select.tsx-45-50 (1)
45-50: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftDo not limit selectable channels to the first 1,000 records.
This query loads only one page. Channels after that page cannot be selected for a region route. Add server-side search or pagination, and resolve already-selected channel IDs separately.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/region-routes/components/channel-multi-select.tsx` around lines 45 - 50, Update the channel-loading query in the channel multi-select component so it does not assume the first 1,000 channels are sufficient. Add server-side search or pagination for selectable channels, and separately resolve already-selected channel IDs so existing selections remain available even when outside the current result page.controller/team.go-47-55 (1)
47-55: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winValidate team and project names by character count.
len(req.Name)counts UTF-8 bytes. It rejects valid non-ASCII names before the stated 64-character limit.
controller/team.go#L47-L55: use a character-aware count before creating a team.controller/team.go#L199-L203: use the same character-aware count before creating a team project.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@controller/team.go` around lines 47 - 55, Replace the byte-based len(req.Name) validation with a character-aware count in the team creation validation at controller/team.go lines 47-55 and the team project creation validation at controller/team.go lines 199-203. Apply the same character-counting approach at both sites while preserving the existing required-name and 64-character limit behavior.web/src/features/teams/components/teams-mutate-drawer.tsx-94-117 (1)
94-117: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winHandle unsuccessful API responses as failures.
A
success: falsemutation leaves the drawer open without feedback. Asuccess: falsebilling response becomesnull, then renders as zero quota and usage.
web/src/features/teams/components/teams-mutate-drawer.tsx#L94-L117: show the localized server error when the mutation response is unsuccessful.web/src/features/teams/components/team-billing-tab.tsx#L35-L67: propagate the failed response as a query error and render an error state instead of zero billing values.As per coding guidelines, “服务端错误统一使用
handleServerError;错误提示使用 i18n,统一通过toast.error等方式展示。”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/teams/components/teams-mutate-drawer.tsx` around lines 94 - 117, Handle unsuccessful mutation responses in teams-mutate-drawer.tsx within onSubmit by passing the server error through handleServerError and displaying the localized message with toast.error, while preserving the submitting-state cleanup. In team-billing-tab.tsx, propagate unsuccessful billing responses as query errors rather than converting them to null, and render the query error state instead of zero quota and usage; apply the changes at web/src/features/teams/components/teams-mutate-drawer.tsx:94-117 and web/src/features/teams/components/team-billing-tab.tsx:35-67.Source: Coding guidelines
web/src/features/sla-status/index.tsx-54-68 (1)
54-68: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDo not render failed SLA status responses as healthy data.
getPublicSlaStatus()currently acceptssuccess: falseresponses whendataexists, so lines 113, 124, 136, and 146 can show availability, incident/node counts, and window metadata from a failed request. Throw the business-failure response and handle SLA query errors instead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/sla-status/index.tsx` around lines 54 - 68, Update the statusData queryFn around getPublicSlaStatus to detect unsuccessful responses and throw the business-failure response before returning data, so failed responses never render as healthy SLA data. Add error handling for the SLA status query and preserve the existing successful data path; ensure the rendering logic at the status display points handles the query error state appropriately.web/src/features/distributors/components/distributor-billing-tab.tsx-35-42 (1)
35-42: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDo not convert failed API responses into valid empty data.
These query functions mark an unsuccessful API response as a successful query result. Billing then displays zero values. The tables display an empty state. The detail page loses the distributor identity. Throw or propagate the failure, then render an explicit error state. Reserve empty data for successful responses.
web/src/features/distributors/components/distributor-billing-tab.tsx#L35-L42: propagate the failed billing request instead of returningnull.web/src/features/distributors/components/distributor-detail.tsx#L42-L45: propagate the failed detail request instead of returningnull.web/src/features/distributors/components/distributor-sub-users-tab.tsx#L48-L60: do not return an empty page for a failed request.web/src/features/teams/components/teams-table.tsx#L64-L77: do not return an empty page for a failed request.As per coding guidelines, “服务端错误统一使用
handleServerError;错误提示使用 i18n,统一通过toast.error等方式展示。”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/distributors/components/distributor-billing-tab.tsx` around lines 35 - 42, Update the query functions to propagate unsuccessful API responses instead of returning null or empty pages, so only successful responses produce data. Apply this in web/src/features/distributors/components/distributor-billing-tab.tsx lines 35-42, web/src/features/distributors/components/distributor-detail.tsx lines 42-45, web/src/features/distributors/components/distributor-sub-users-tab.tsx lines 48-60, and web/src/features/teams/components/teams-table.tsx lines 64-77; use handleServerError for server failures and preserve i18n-based toast.error messaging, with each query rendering its explicit error state.Source: Coding guidelines
web/src/features/distributors/components/distributors-mutate-drawer.tsx-105-129 (1)
105-129: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winHandle failed API operations in every mutation and query path.
Each path only handles successful responses. A
success: falseresponse gives no feedback for several mutations. A rejectedapirequest also bypasses feedback. Show a localized error for unsuccessful responses. Catch rejected requests withhandleServerError. Keep destructive dialogs open when the operation fails.
web/src/features/distributors/components/distributors-mutate-drawer.tsx#L105-L129: Handle unsuccessful create and update responses and rejected requests.web/src/features/sla-incidents/components/sla-incidents-table.tsx#L74-L88: Handle rejected list requests through the standard server-error path.web/src/features/sla-incidents/components/data-table-row-actions.tsx#L53-L66: Handle unsuccessful resolve responses and rejected requests.web/src/features/teams/components/team-projects-tab.tsx#L116-L150: Handle unsuccessful add and remove responses and rejected requests.web/src/features/teams/components/team-members-tab.tsx#L135-L169: Handle unsuccessful add and remove responses and rejected requests.As per coding guidelines, “服务端错误统一使用
handleServerError;错误提示使用 i18n,统一通过toast.error等方式展示”.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/distributors/components/distributors-mutate-drawer.tsx` around lines 105 - 129, Handle both unsuccessful responses and rejected requests across all mutation and query paths: in web/src/features/distributors/components/distributors-mutate-drawer.tsx:105-129, update onSubmit for create and update failures; in web/src/features/sla-incidents/components/sla-incidents-table.tsx:74-88, catch list-request errors; in web/src/features/sla-incidents/components/data-table-row-actions.tsx:53-66, handle failed resolve operations; and in web/src/features/teams/components/team-projects-tab.tsx:116-150 and web/src/features/teams/components/team-members-tab.tsx:135-169, handle failed add/remove operations. Use localized toast.error messages for success:false responses, route rejected requests through handleServerError, and only close destructive dialogs after successful operations.Source: Coding guidelines
web/src/features/sla-incidents/lib/sla-incidents-form.ts-49-56 (1)
49-56: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winCreate the default start time when the form resets.
Line 54 runs once when this module loads.
SlaIncidentsMutateDrawerreuses this value foruseFormand create resets. An incident created later can persist the application-start time instead of its creation time.Replace the constant date with a default-values factory. Call that factory for form initialization and every create reset.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/sla-incidents/lib/sla-incidents-form.ts` around lines 49 - 56, Replace the static started_at value in SLA_INCIDENT_FORM_DEFAULT_VALUES with a default-values factory that creates a fresh Date on each invocation. Update SlaIncidentsMutateDrawer to call the factory when initializing useForm and for every create reset, while preserving the existing field defaults.web/src/features/market-models/components/market-models-mutate-drawer.tsx-101-137 (1)
101-137: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReset the form and report failed operations.
A failed
getMarketModelrequest leaves the previous form values in place because thecatchblock is empty. A later submit can updatecurrentRow.idwith stale values. Failed create or update responses also provide no feedback.Reset to defaults before loading the selected model. Handle unsuccessful responses and rejected requests with
handleServerError.As per coding guidelines, “服务端错误统一使用
handleServerError;错误提示使用 i18n,统一通过toast.error等方式展示”。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/market-models/components/market-models-mutate-drawer.tsx` around lines 101 - 137, Update the useEffect around getMarketModel to reset the form to MARKET_MODEL_FORM_DEFAULT_VALUES before loading the selected model, and call handleServerError for unsuccessful or rejected requests instead of swallowing errors. In onSubmit, handle failed createMarketModel and updateMarketModel results and rejected requests with handleServerError, displaying errors through the existing i18n-backed toast.error flow while preserving success behavior.Source: Coding guidelines
web/src/features/region-routes/components/region-routes-table.tsx-49-76 (1)
49-76: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winDo not render load failures as an empty list.
listRegionRoutesfailures returnitems: []fromqueryFn, androutes = data?.items ?? []passes that through toDataTablePage. The component also destructures onlydata,isLoading, andisFetching, so query errors do not render an error state.Propagate failures from
queryFn; catch rejected requests and show the translated server error viahandleServerErrorand/ortoast.errorinstead ofNo Region Routes Found.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/region-routes/components/region-routes-table.tsx` around lines 49 - 76, Update the region-routes query around listRegionRoutes and its consuming state so failures are not converted into an empty items array. Propagate unsuccessful or rejected requests as query errors, capture the query error state, and render the translated server error through handleServerError and/or toast.error instead of allowing DataTablePage to show “No Region Routes Found.”Source: Coding guidelines
web/src/features/market-models/components/market-models-table.tsx-68-95 (1)
68-95: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winWire global search into the backend list call.
globalFilteronly triggers a refetch and a local filter for the current page.manualFiltering: truedisablesglobalFilterFnfor pagination and row models, whileListMarketModelsonly readsstatusandcategory. Add keyword support to the backend path and forward it viagetMarketModels; otherwise do not expose this search input.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/market-models/components/market-models-table.tsx` around lines 68 - 95, Add backend keyword-search support to the market-models query by extending the getMarketModels request in the queryFn to forward globalFilter, and update the corresponding list API parameter handling to apply it. Ensure globalFilter is not exposed unless it is actually sent to and processed by the backend; preserve the existing pagination and status filtering behavior.web/src/features/marketing/components/Cards.tsx-4-9 (1)
4-9: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winValidate parsed JSON shapes before rendering.
JSON.parseaccepts valid non-array JSON such asnullor{}. Line 25 then callsfeatures.map, and Line 64 accesses fields on eachmodelsentry. A malformed persisted public record can therefore crash the marketing page. Validate that each value is an array and filter entries to the expected element shape before rendering.Also applies to: 51-56
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/marketing/components/Cards.tsx` around lines 4 - 9, Validate the parsed values in the Cards component before rendering: ensure plan.features is an array, and filter its entries to the expected feature shape before the features.map call. Apply the same array and element-shape validation to the models parsing block around the models rendering path, so malformed records cannot cause field-access or map errors.web/src/lib/marketing-mode.ts-27-32 (1)
27-32: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winNormalize trailing slashes before classifying paths.
isMarketingPath('/pricing/')andisMarketingPath('/models/')returnfalse.isConsolePaththen returnstrue, so a public URL such aswww.example.com/pricing/redirects to the console host. Normalize the pathname before both marketing-path checks.Also applies to: 45-53
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/lib/marketing-mode.ts` around lines 27 - 32, Normalize the pathname by removing its trailing slash before the path-membership checks in both isMarketingPath and isMarketingOnlyPath. Ensure normalized public paths such as “/pricing/” and “/models/” classify identically to their slashless forms while preserving the root path behavior.web/src/features/marketing/hooks/usePublicData.ts-38-40 (1)
38-40: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winUse the shared
apiclient for public API requests.Direct
fetchbypasses the project interceptor path. It also does not reject non-2xx responses, so an error payload can be converted into an empty result instead of setting the query error state. Use the sharedapiinstance and propagate failed responses.Also applies to: 50-52
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/marketing/hooks/usePublicData.ts` around lines 38 - 40, Replace the direct fetch calls in the public pricing and corresponding second request within the hook’s query functions with the shared api client. Ensure non-2xx responses are rejected and propagated to the query error state, while preserving the existing locale parameters and successful data extraction behavior.Source: Coding guidelines
web/src/features/marketing/components/Cards.tsx-32-41 (1)
32-41: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRoute custom plans to the sales page.
Line 40 shows
Contact Salesfor custom billing, but Line 33 always links to/sign-up. Send custom plans to/contact-sales.Proposed fix
- href='/sign-up' + href={plan.billing_mode === 'custom' ? '/contact-sales' : '/sign-up'}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/marketing/components/Cards.tsx` around lines 32 - 41, Update the plan CTA anchor in Cards so its href routes custom billing plans to /contact-sales and all other plans to /sign-up. Preserve the existing Contact Sales and Get Started labels and styling.web/src/features/marketing/hooks/useSiteContent.ts-8-13 (1)
8-13: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftUse the shared i18next catalogs for all marketing text.
useSiteContentbypassest()and selects a separate catalog that supports onlyenandzh. Users on the other supported locales receive English fallback text. The copy button and region labels also bypass translation.
web/src/features/marketing/hooks/useSiteContent.ts#L8-L13: replace the custom locale switch witht()calls.web/src/features/marketing/types.ts#L1-L1: remove the two-locale marketing contract.web/src/features/marketing/data/content.ts#L3-L3: migrate marketing strings to the shared locale JSON catalogs.web/src/features/marketing/components/CodeBlock.tsx#L20-L20: translateCopyandCopied.web/src/features/marketing/components/ContactForm.tsx#L5-L5: store region translation keys instead of display strings.web/src/features/marketing/components/ContactForm.tsx#L97-L101: render each region witht().As per coding guidelines, “Frontend user-facing text must use i18next/react-i18next” and supported locales include en, zh, zh-TW, fr, ru, ja, and vi.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/marketing/hooks/useSiteContent.ts` around lines 8 - 13, Replace the two-locale catalog selection in useSiteContent with react-i18next t() lookups, and migrate the marketing content contract and strings in web/src/features/marketing/types.ts#L1-L1 and web/src/features/marketing/data/content.ts#L3-L3 into the shared locale JSON catalogs for all supported locales. In web/src/features/marketing/components/CodeBlock.tsx#L20-L20, translate Copy and Copied with t(); in web/src/features/marketing/components/ContactForm.tsx#L5-L5, store region translation keys, and at web/src/features/marketing/components/ContactForm.tsx#L97-L101 render labels through t().Source: Coding guidelines
web/src/features/marketing/data/content.ts-141-169 (1)
141-169: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDo not hardcode the public API endpoint.
The quick-start copy and all examples embed
https://api.91flow.com/v1. This makes the generated documentation incorrect for regional or alternate deployments. Define aVITE_public API base URL and build the displayed base URL and examples from that value.
web/src/features/marketing/data/content.ts#L141-L169: use the environment-backed value in English quick-start text.web/src/features/marketing/data/content.ts#L309-L336: use the same value in Chinese quick-start text.web/src/features/marketing/data/content.ts#L345-L383: generate each example from the same value.As per coding guidelines, “环境变量使用
.env并以VITE_为前缀,代码中不得硬编码配置”.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/features/marketing/data/content.ts` around lines 141 - 169, Replace the hardcoded public API endpoint with a shared VITE_-prefixed environment-backed base URL in content.ts. Update the English quick-start text at web/src/features/marketing/data/content.ts:141-169 and Chinese quick-start text at :309-336 to use that value, and generate all examples at :345-383 from the same value rather than embedding the endpoint; ensure the corresponding .env configuration is used.Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 32ba0ca5-9556-498e-86e7-72e1a3e117ea
⛔ Files ignored due to path filters (1)
web/public/favicon.icois excluded by!**/*.ico
📒 Files selected for processing (162)
.dockerignoreDockerfilecommon/constants.gocommon/redirect.goconstant/context_key.gocontroller/distributor.gocontroller/market.gocontroller/public_site.gocontroller/region_route.gocontroller/sla.gocontroller/team.godeploy/nginx/originflow.confdocker-compose.ymlmodel/ability.gomodel/analytics_event.gomodel/channel_cache.gomodel/distributor.gomodel/log.gomodel/main.gomodel/market_model.gomodel/public_model_category.gomodel/public_pricing.gomodel/public_site_seed.gomodel/region_route.gomodel/region_route_test.gomodel/sales_lead.gomodel/sla.gomodel/team.gomodel/user.gomodel/user_cache.gorouter/api-router.gorouter/p2-router.goservice/channel_select.goservice/channel_select_test.goweb/index.htmlweb/public/logo.webpweb/src/components/layout/components/footer.tsxweb/src/features/distributors/api.tsweb/src/features/distributors/components/data-table-row-actions.tsxweb/src/features/distributors/components/distributor-billing-tab.tsxweb/src/features/distributors/components/distributor-detail.tsxweb/src/features/distributors/components/distributor-price-mutate-drawer.tsxweb/src/features/distributors/components/distributor-prices-tab.tsxweb/src/features/distributors/components/distributor-sub-users-tab.tsxweb/src/features/distributors/components/distributors-columns.tsxweb/src/features/distributors/components/distributors-delete-dialog.tsxweb/src/features/distributors/components/distributors-dialogs.tsxweb/src/features/distributors/components/distributors-mobile-list.tsxweb/src/features/distributors/components/distributors-mutate-drawer.tsxweb/src/features/distributors/components/distributors-primary-buttons.tsxweb/src/features/distributors/components/distributors-provider.tsxweb/src/features/distributors/components/distributors-table.tsxweb/src/features/distributors/constants.tsweb/src/features/distributors/index.tsxweb/src/features/distributors/lib/distributors-form.tsweb/src/features/distributors/lib/index.tsweb/src/features/distributors/types.tsweb/src/features/market-models/api.tsweb/src/features/market-models/components/market-models-columns.tsxweb/src/features/market-models/components/market-models-delete-dialog.tsxweb/src/features/market-models/components/market-models-dialogs.tsxweb/src/features/market-models/components/market-models-mutate-drawer.tsxweb/src/features/market-models/components/market-models-primary-buttons.tsxweb/src/features/market-models/components/market-models-provider.tsxweb/src/features/market-models/components/market-models-row-actions.tsxweb/src/features/market-models/components/market-models-table.tsxweb/src/features/market-models/constants.tsweb/src/features/market-models/index.tsxweb/src/features/market-models/lib/market-model-form.tsweb/src/features/market-models/storefront.tsxweb/src/features/market-models/types.tsweb/src/features/marketing/components/Cards.tsxweb/src/features/marketing/components/CodeBlock.tsxweb/src/features/marketing/components/ContactForm.tsxweb/src/features/marketing/components/MarketingSections.tsxweb/src/features/marketing/data/content.tsweb/src/features/marketing/hooks/usePublicData.tsweb/src/features/marketing/hooks/useSiteConfig.tsweb/src/features/marketing/hooks/useSiteContent.tsweb/src/features/marketing/pages/ContactSales.tsxweb/src/features/marketing/pages/Home.tsxweb/src/features/marketing/pages/Models.tsxweb/src/features/marketing/pages/Pricing.tsxweb/src/features/marketing/pages/QuickStart.tsxweb/src/features/marketing/pages/Solutions.tsxweb/src/features/marketing/types.tsweb/src/features/region-routes/api.tsweb/src/features/region-routes/components/channel-multi-select.tsxweb/src/features/region-routes/components/data-table-row-actions.tsxweb/src/features/region-routes/components/region-routes-columns.tsxweb/src/features/region-routes/components/region-routes-delete-dialog.tsxweb/src/features/region-routes/components/region-routes-dialogs.tsxweb/src/features/region-routes/components/region-routes-mobile-list.tsxweb/src/features/region-routes/components/region-routes-mutate-drawer.tsxweb/src/features/region-routes/components/region-routes-primary-buttons.tsxweb/src/features/region-routes/components/region-routes-provider.tsxweb/src/features/region-routes/components/region-routes-table.tsxweb/src/features/region-routes/constants.tsweb/src/features/region-routes/index.tsxweb/src/features/region-routes/lib/index.tsweb/src/features/region-routes/lib/region-routes-form.tsweb/src/features/region-routes/lib/utils.tsweb/src/features/region-routes/types.tsweb/src/features/sla-incidents/api.tsweb/src/features/sla-incidents/components/data-table-row-actions.tsxweb/src/features/sla-incidents/components/sla-incidents-columns.tsxweb/src/features/sla-incidents/components/sla-incidents-delete-dialog.tsxweb/src/features/sla-incidents/components/sla-incidents-dialogs.tsxweb/src/features/sla-incidents/components/sla-incidents-mobile-list.tsxweb/src/features/sla-incidents/components/sla-incidents-mutate-drawer.tsxweb/src/features/sla-incidents/components/sla-incidents-primary-buttons.tsxweb/src/features/sla-incidents/components/sla-incidents-provider.tsxweb/src/features/sla-incidents/components/sla-incidents-table.tsxweb/src/features/sla-incidents/constants.tsweb/src/features/sla-incidents/index.tsxweb/src/features/sla-incidents/lib/index.tsweb/src/features/sla-incidents/lib/sla-incidents-form.tsweb/src/features/sla-incidents/types.tsweb/src/features/sla-status/index.tsxweb/src/features/system-settings/general/system-info-section.tsxweb/src/features/teams/api.tsweb/src/features/teams/components/data-table-row-actions.tsxweb/src/features/teams/components/team-billing-tab.tsxweb/src/features/teams/components/team-detail.tsxweb/src/features/teams/components/team-members-tab.tsxweb/src/features/teams/components/team-projects-tab.tsxweb/src/features/teams/components/teams-columns.tsxweb/src/features/teams/components/teams-delete-dialog.tsxweb/src/features/teams/components/teams-dialogs.tsxweb/src/features/teams/components/teams-mobile-list.tsxweb/src/features/teams/components/teams-mutate-drawer.tsxweb/src/features/teams/components/teams-primary-buttons.tsxweb/src/features/teams/components/teams-provider.tsxweb/src/features/teams/components/teams-table.tsxweb/src/features/teams/constants.tsweb/src/features/teams/index.tsxweb/src/features/teams/lib/index.tsweb/src/features/teams/lib/teams-form.tsweb/src/features/teams/types.tsweb/src/hooks/use-sidebar-data.tsweb/src/i18n/locales/en.jsonweb/src/i18n/locales/zh.jsonweb/src/lib/constants.tsweb/src/lib/marketing-mode.tsweb/src/routeTree.gen.tsweb/src/routes/__root.tsxweb/src/routes/_authenticated/distributors/$distributorId/index.tsxweb/src/routes/_authenticated/distributors/index.tsxweb/src/routes/_authenticated/market-models/index.tsxweb/src/routes/_authenticated/models/index.tsxweb/src/routes/_authenticated/region-routes/index.tsxweb/src/routes/_authenticated/route.tsxweb/src/routes/_authenticated/sla-incidents/index.tsxweb/src/routes/_authenticated/teams/$teamId/index.tsxweb/src/routes/_authenticated/teams/index.tsxweb/src/routes/contact-sales.tsxweb/src/routes/index.tsxweb/src/routes/market/index.tsxweb/src/routes/pricing/index.tsxweb/src/routes/quick-start.tsxweb/src/routes/sla-status/index.tsxweb/src/routes/solutions.tsx
| // 相对路径,但排除协议相对地址(//evil.com) | ||
| if strings.HasPrefix(u, "/") { | ||
| return !strings.HasPrefix(u, "//") | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
Reject backslash-based protocol-relative paths.
Line 16 accepts /\evil.example because it starts with / but not //. Browser URL parsing treats backslashes as path separators for special URLs. The destination can become //evil.example and bypass the origin restriction.
Normalize backslashes before the protocol-relative URL check.
Proposed fix
if strings.HasPrefix(u, "/") {
- return !strings.HasPrefix(u, "//")
+ return !strings.HasPrefix(strings.ReplaceAll(u, "\\", "/"), "//")
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 相对路径,但排除协议相对地址(//evil.com) | |
| if strings.HasPrefix(u, "/") { | |
| return !strings.HasPrefix(u, "//") | |
| } | |
| // 相对路径,但排除协议相对地址(//evil.com) | |
| if strings.HasPrefix(u, "/") { | |
| return !strings.HasPrefix(strings.ReplaceAll(u, "\\", "/"), "//") | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@common/redirect.go` around lines 15 - 18, Update the relative-path validation
around the strings.HasPrefix check in the redirect helper to normalize
backslashes to forward slashes before testing for protocol-relative paths.
Ensure inputs such as `/\evil.example` are rejected like `//evil.example`, while
preserving acceptance of valid single-slash relative paths.
| m := &model.MarketModel{ | ||
| Model: req.Model, | ||
| Provider: req.Provider, | ||
| Category: req.Category, | ||
| Tags: req.Tags, | ||
| InputPrice: req.InputPrice, | ||
| OutputPrice: req.OutputPrice, | ||
| Unit: orDefault(req.Unit, "token"), | ||
| TrialQuota: req.TrialQuota, | ||
| Status: req.Status, | ||
| Featured: req.Featured, | ||
| Sort: req.Sort, | ||
| } | ||
| if m.Status == 0 { | ||
| m.Status = MarketModelStatusAvailable | ||
| } | ||
| if m.Currency == "" { | ||
| m.Currency = "CNY" | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
CreateMarketModel drops Currency and Metadata.
The struct literal never assigns req.Currency or req.Metadata. m.Currency is therefore always empty at Line 137 and gets forced to "CNY", so a request with "currency": "USD" is stored as CNY. The validated Metadata is discarded, so locale overrides read back by resolveMarketModelI18n are always absent for newly created records. UpdateMarketModel sets both fields, which confirms the omission.
The m.Status == 0 branch is also unreachable, because validateMarketModelRequest rejects status 0.
🐛 Proposed fix
m := &model.MarketModel{
Model: req.Model,
Provider: req.Provider,
Category: req.Category,
Tags: req.Tags,
InputPrice: req.InputPrice,
OutputPrice: req.OutputPrice,
+ Currency: orDefault(req.Currency, "CNY"),
Unit: orDefault(req.Unit, "token"),
+ Metadata: req.Metadata,
TrialQuota: req.TrialQuota,
Status: req.Status,
Featured: req.Featured,
Sort: req.Sort,
}
- if m.Status == 0 {
- m.Status = MarketModelStatusAvailable
- }
- if m.Currency == "" {
- m.Currency = "CNY"
- }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| m := &model.MarketModel{ | |
| Model: req.Model, | |
| Provider: req.Provider, | |
| Category: req.Category, | |
| Tags: req.Tags, | |
| InputPrice: req.InputPrice, | |
| OutputPrice: req.OutputPrice, | |
| Unit: orDefault(req.Unit, "token"), | |
| TrialQuota: req.TrialQuota, | |
| Status: req.Status, | |
| Featured: req.Featured, | |
| Sort: req.Sort, | |
| } | |
| if m.Status == 0 { | |
| m.Status = MarketModelStatusAvailable | |
| } | |
| if m.Currency == "" { | |
| m.Currency = "CNY" | |
| } | |
| m := &model.MarketModel{ | |
| Model: req.Model, | |
| Provider: req.Provider, | |
| Category: req.Category, | |
| Tags: req.Tags, | |
| InputPrice: req.InputPrice, | |
| OutputPrice: req.OutputPrice, | |
| Currency: orDefault(req.Currency, "CNY"), | |
| Unit: orDefault(req.Unit, "token"), | |
| Metadata: req.Metadata, | |
| TrialQuota: req.TrialQuota, | |
| Status: req.Status, | |
| Featured: req.Featured, | |
| Sort: req.Sort, | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@controller/market.go` around lines 121 - 139, Update CreateMarketModel’s
model construction to assign req.Currency and req.Metadata, preserving requested
currency values and validated locale metadata for newly created records; retain
the existing default currency behavior only when the request currency is empty.
Remove the unreachable m.Status == 0 defaulting branch, since
validateMarketModelRequest rejects status 0.
|
@Calcium-Ion 你好,这是 OriginFlow(元点流商)二次开发 + 与 originflow 分支统一 web/src 前端的 PR,CI 已通过且可合并。麻烦抽空评审,谢谢! |
|
这个pr改的太多了,并且我们对 originflow 并不熟悉,如果您是 originflow 相关负责人,麻烦先和 newapi@quantumnous.com 联系,我们会在一周内回复您 |
|
Superseded by a split for easier review: this combined PR (originflow merge + OriginFlow secondary-dev) was broken into two independent, build-passing PRs:
Together they reconstruct this change. Please review #6738 and #6739 instead. Note: the frontend PR (#6739) currently drops upstream's auto-group frontend feature (our branch predates it) — flagged there; can be restored as a follow-up if desired. |
Summary
This PR delivers the OriginFlow (元点流商) secondary-development work and resolves the frontend directory conflict with the
originflowbranch by unifying onweb/srcas the single canonical frontend.web/src unification (conflict resolution)
web/srcis now the single canonical frontend (matches ourfork/upstream-main); originflow's divergentweb/srcadditions and the unuseddate-fns-tzpatch are dropped.bun run build+bun x tsgo -b).Backend
common/redirect.go(IsSafeRedirect),model/analytics_event.go,deploy/nginx/originflow.conf, and theOriginFlowSystemNamebranding incommon/constants.go.service/public_site.go(referenced model helpers absent from our tree and was not wired into any router, so it did not compile).Feature content included
Verification
go build(CGO_ENABLED=0) passesbun run buildpassesbun x tsgo -bpassesSummary by CodeRabbit