Skip to content

Codex/new api skill market - #4357

Closed
Vinoxcn wants to merge 6 commits into
QuantumNous:mainfrom
bulaya:codex/new-api-skill-market
Closed

Codex/new api skill market#4357
Vinoxcn wants to merge 6 commits into
QuantumNous:mainfrom
bulaya:codex/new-api-skill-market

Conversation

@Vinoxcn

@Vinoxcn Vinoxcn commented Apr 20, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

  • Closes # (如有)

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

(请在此粘贴截图、关键日志或测试报告,以证明变更生效)

Summary by CodeRabbit

  • New Features

    • SMS-based login and phone verification with support for Aliyun, Aliyun PNVS, and Tencent SMS providers.
    • Client API for third-party integrations with SMS authentication and token management.
    • Admin skill market management interface for publishing and managing reusable skills.
    • Popup login mode for embedded authentication flows with secure callback messaging.
    • User phone number field for account management.
  • Bug Fixes

    • CORS origin validation now restricts to localhost and HTTPS origins only.
    • Default currency setting changed from USD to CNY.
  • Documentation

    • Added fork changelog documenting customizations and migration notes.

didi and others added 6 commits April 3, 2026 11:45
Record SMS verification code login feature added in this fork:
- Support Aliyun, Aliyun PNVS, Tencent Cloud SMS providers
- Rate limiting: 1/60s per phone, 5/hour per IP
- Auto-register new users with phone number
- Support 2FA and Turnstile verification

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Support Aliyun, Aliyun PNVS, Tencent Cloud SMS providers
- Add rate limiting: 1/60s per phone, 5/hour per IP
- Auto-register new users with phone number
- Support 2FA and Turnstile verification
- Add Phone field to User model

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add /api/client/login_sms: SMS code login with auto-register and
  per-user reusable API token (returns full key, no cookie session)
- Add /api/client/self: API key authenticated user info endpoint
- Add /api/client/subscription/plans: public subscription plans list
- Add /api/client/subscription/epay/pay: subscription order via API key

Currency defaults switched from USD to CNY:
- SubscriptionPlan.Currency default
- subscription_plans table DDL default
- AdminCreate/UpdateSubscriptionPlan force CNY
- General quota_display_type default
- sync-db-pull.sh: download server one-api.db via SSH, auto-backup local
- sync-db-push.sh: upload local db to server with confirmation + rollback
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR adds SMS-based authentication with configurable providers (Aliyun, Tencent), introduces client-specific API endpoints for SMS login and token management, and implements a skill market admin interface for managing downloadable skills. Supporting changes include SMS rate limiting, CORS refinement, user phone field storage, database migrations, UI components for SMS login forms, internationalization keys, and database sync utility scripts.

Changes

Cohort / File(s) Summary
SMS Provider Implementations
common/sms/aliyun.go, common/sms/aliyun_pnvs.go, common/sms/tencent.go, common/sms/sms.go
Implemented SMS sending via Aliyun (standard and PNVS), Tencent Cloud, with HMAC-SHA1/TC3-HMAC-SHA256 signing and factory pattern for provider selection.
SMS Verification & Login
controller/sms_login.go
Added SMS code generation, verification, and user login with auto-registration, 2FA gating, and phone masking.
Client API Authentication
controller/client_auth.go, controller/client_skillhub.go
Introduced client SMS login, self-info endpoint, dedicated token management, and SkillHub download proxy.
Skill Market Management
model/client_skill_market.go, controller/client_skills.go
Added client skill marketplace model, CRUD endpoints (admin + public), download tracking, and filtering logic.
User & Model Updates
model/user.go, model/main.go, common/verification.go
Extended User model with phone field, added phone lookup/availability checks, registered skill market model for migration.
Authentication Middleware & Rate Limiting
middleware/auth.go, middleware/sms_rate_limit.go, middleware/rate-limit.go, middleware/cors.go, middleware/cache.go
Updated session/token auth to tolerate missing headers for session mode, added SMS-specific rate limiting (phone + IP), refined CORS origin checking, adjusted asset caching strategy.
Settings & Configuration
setting/system_setting/sms.go, controller/option.go, controller/misc.go, model/option.go, setting/operation_setting/general_setting.go, model/subscription.go
Added SMS settings (provider, credentials), default currency change USD → CNY, SMS toggle validation, status endpoint SMS flag.
Internationalization
i18n/keys.go, i18n/locales/en.yaml, i18n/locales/zh-CN.yaml, i18n/locales/zh-TW.yaml
Added SMS message keys and translations (8 keys across 3 locales) for login disabled, send success/failure, validation, and configuration guidance.
API Routing
router/api-router.go
Registered SMS verification/login endpoints, client API group with SMS auth, skill CRUD (public + admin), SkillHub proxy, subscription endpoints.
Frontend SMS Login UI
web/src/components/auth/SmsLoginForm.jsx, web/src/components/auth/LoginForm.jsx, web/src/components/common/logo/PhoneIcon.jsx
Introduced SmsLoginForm component with Turnstile/2FA/agreement gating, updated LoginForm to support SMS toggle and popup-mode callback, added phone icon.
Frontend Skill Market Admin
web/src/pages/SkillMarket/index.jsx, web/src/pages/SkillMarket/Editor.jsx, web/src/pages/SkillMarket/EditSkillModal.jsx
Implemented skill listing with filtering/pagination, editor form for create/edit, modal for inline editing, status transitions, preview links, and preview URL generation.
Frontend Navigation & Auth
web/src/components/layout/SiderBar.jsx, web/src/hooks/common/useSidebar.js, web/src/helpers/auth.jsx, web/src/helpers/render.jsx
Added skill-market sidebar menu item, updated admin section visibility, added popup-mode auth bypass, extended icon support with Store icon for skill market.
Frontend Localization
web/src/i18n/locales/en.json, web/src/i18n/locales/zh-CN.json
Added 39 new SMS/phone login and SMS configuration labels/prompts (phone input, code input, resend countdown, success/failure, provider selection, etc.).
Database Sync Utilities
scripts/sync-db-pull.sh, scripts/sync-db-push.sh
Added production database pull (with backup/pruning) and push (with rollback on restart failure) scripts with table row-count reporting.
Fork Documentation
CHANGELOG_FORK.md
Documented fork-specific modifications (SMS login, CORS fixes, popup mode) with detailed feature chapters, file references, and upstream merge guidance.

Sequence Diagram(s)

sequenceDiagram
    participant User as User Client
    participant Server as New-API Server
    participant SmsProvider as SMS Provider<br/>(Aliyun/Tencent)
    participant DB as Database

    User->>Server: POST /api/sms/send<br/>{phone}
    activate Server
    Server->>Server: Validate phone
    Server->>Server: Generate 6-digit code
    Server->>DB: Store verification code<br/>(phone, code, purpose)
    DB-->>Server: ✓
    Server->>SmsProvider: SendCode(phone, code)
    activate SmsProvider
    SmsProvider-->>Server: ✓ Success
    deactivate SmsProvider
    Server-->>User: {success: true}
    deactivate Server

    User->>Server: POST /api/user/login/sms<br/>{phone, code}
    activate Server
    Server->>DB: Fetch verification code
    DB-->>Server: {code, timestamp}
    Server->>Server: Verify code match
    Server->>DB: Delete verification record
    Server->>DB: Load user by phone
    alt User not found
        Server->>Server: Auto-register enabled?
        Server->>DB: Create new user<br/>(phone, username, etc.)
        DB-->>Server: {userId}
    else User exists
        DB-->>Server: {userId, status}
    end
    Server->>Server: Check 2FA required?
    alt 2FA needed
        Server->>Server: Store pending login in session
        Server-->>User: {require_2fa: true}
    else No 2FA
        Server->>DB: Create/fetch dedicated token
        DB-->>Server: {apiKey}
        Server->>DB: Log login event
        Server-->>User: {success: true, user, apiKey}
    end
    deactivate Server
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Suggested labels

enhancement, feature/sms-login, feature/skill-market, frontend, backend

Suggested reviewers

  • Calcium-Ion
  • seefs001

Poem

🐰 A rabbit hops through SMS gates so fine,
With provider choices, credentials align!
Skills now marketplace in admin sight,
Auth flows popup-bound, reaching new height!
Phone-verified logins, the future's delight! 📱✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.80% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Codex/new api skill market' is vague and uses branch/namespace naming rather than a clear description of the main change. Clarify the title to reflect the primary feature (e.g., 'Add SMS login and skill market admin management' or similar) to make the changeset's main purpose immediately clear.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch codex/new-api-skill-market

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

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 (2)
model/option.go (1)

267-276: ⚠️ Potential issue | 🟡 Minor

Stale comment and loss of USD path via legacy toggle.

Two concerns in this block:

  1. The comment on line 269 still says true -> USD, false -> TOKENS, but the code now maps true to "CNY". Please update the comment to match reality.
  2. The legacy DisplayInCurrencyEnabled=true path previously let operators choose USD display; after this change they can no longer reach QuotaDisplayTypeUSD through this compatibility branch. Any client or admin UI still toggling DisplayInCurrencyEnabled will now silently coerce display to CNY. If non-CNY deployments are still supported, consider either (a) preserving the previous USD mapping here and relying on quota_display_type setters for the CNY default, or (b) documenting clearly that legacy toggling now forces CNY.
✏️ Minimal comment fix
 		case "DisplayInCurrencyEnabled":
 			// 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效)
-			// true -> USD, false -> TOKENS
+			// true -> CNY, false -> TOKENS
 			newVal := "CNY"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/option.go` around lines 267 - 276, The comment in the switch case for
"DisplayInCurrencyEnabled" is stale and the compatibility mapping changed:
update the inline comment in the case to reflect that true now maps to "CNY"
(not USD), and address the behavior change so legacy toggles don't silently
force CNY — either restore the previous USD mapping for backward compatibility
(set newVal to "USD" when boolValue is true) or add a clear comment and/or
documentation note that this branch now coerces legacy true to "CNY"; modify the
code around the DisplayInCurrencyEnabled case (variable newVal, the boolValue
check, and the call to config.UpdateConfigFromMap via
config.GlobalConfig.Get("general_setting")) to implement your chosen approach
and ensure QuotaDisplayTypeUSD remains reachable if you opt to preserve USD
mapping.
web/src/components/settings/SystemSetting.jsx (1)

182-199: ⚠️ Potential issue | 🟠 Major

Normalize sms.enabled before binding it to the checkbox.

Line 1128 binds sms.enabled to a Form.Checkbox, but this switch does not convert the option value from "true" / "false" to a boolean. A stored "false" string can be treated as truthy by the form control and show SMS login enabled when it is not.

Proposed fix
           case 'LinuxDOOAuthEnabled':
           case 'discord.enabled':
           case 'oidc.enabled':
+          case 'sms.enabled':
           case 'passkey.enabled':
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/SystemSetting.jsx` around lines 182 - 199, The
sms.enabled option isn't normalized to a boolean before binding to the
Form.Checkbox, so add the 'sms.enabled' case to the switch that converts
item.value via toBoolean; specifically update the block handling option keys
(cases like 'PasswordLoginEnabled', 'discord.enabled', etc.) to include
'sms.enabled' so item.value = toBoolean(item.value) runs for it, ensuring the
Form.Checkbox receives a real boolean rather than the string "true"/"false".
🟠 Major comments (24)
scripts/sync-db-push.sh-7-9 (1)

7-9: ⚠️ Potential issue | 🟠 Major

Default service name myclaw-newapi does not match the systemd unit shipped in this repo.

The repository ships new-api.service (unit name new-api), but this script defaults REMOTE_SERVICE to myclaw-newapi. Any contributor who runs this script without setting NEWAPI_REMOTE_SERVICE will get a systemctl restart myclaw-newapi that fails on a fresh install — and because the failure happens after the DB has already been overwritten (line 61), the rollback branch at line 66‑70 will also run against the wrong service and leave the server in a broken state.

Either default to new-api (matching the shipped unit) or make NEWAPI_REMOTE_SERVICE mandatory with :?.

-REMOTE_SERVICE="${NEWAPI_REMOTE_SERVICE:-myclaw-newapi}"
+REMOTE_SERVICE="${NEWAPI_REMOTE_SERVICE:-new-api}"

Same concern about REMOTE_HOST="001" applies as in sync-db-pull.sh — it's a personal SSH alias.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync-db-push.sh` around lines 7 - 9, Change the unsafe defaults:
replace the current fallback for REMOTE_SERVICE (NEWAPI_REMOTE_SERVICE) so it
either defaults to "new-api" (the shipped systemd unit) or is made mandatory
with the parameter expansion form that fails if unset (use :?); likewise remove
the personal SSH alias default for REMOTE_HOST ("001") by either defaulting to
empty or making NEWAPI_REMOTE_HOST mandatory with :? so contributors cannot run
the script against the wrong host/service; update references to REMOTE_SERVICE
and REMOTE_HOST in this script (the REMOTE_SERVICE variable and systemctl
restart/rollback logic) to rely on the new behavior.
model/client_skill_market.go-1-63 (1)

1-63: 🛠️ Refactor suggestion | 🟠 Major

Coding guideline violation: direct encoding/json usage.

Per repository guidelines, all JSON marshal/unmarshal operations in Go business code must go through the wrappers in common/json.go (common.Marshal, common.Unmarshal, common.UnmarshalJsonStr, common.DecodeJson, common.GetJsonType). This file imports encoding/json (line 4) and calls json.Unmarshal in TagList (line 59).

As per coding guidelines: "All JSON marshal/unmarshal operations MUST use wrapper functions in common/json.go... Do NOT directly import or call encoding/json in business code."

♻️ Proposed fix
 import (
-	"encoding/json"
-
 	"github.com/QuantumNous/new-api/common"
 	"gorm.io/gorm"
 )
@@
 func (s *ClientSkillMarketItem) TagList() []string {
 	if len(s.Tags) == 0 {
 		return []string{}
 	}
 	var tags []string
-	if err := json.Unmarshal(s.Tags, &tags); err != nil {
+	if err := common.Unmarshal(s.Tags, &tags); err != nil {
 		return []string{}
 	}
 	return tags
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/client_skill_market.go` around lines 1 - 63, The file imports
encoding/json and calls json.Unmarshal in ClientSkillMarketItem.TagList,
violating the guideline to use common/json.go wrappers; remove the encoding/json
import and replace json.Unmarshal(s.Tags, &tags) with the repository wrapper
(e.g. common.Unmarshal or common.UnmarshalJsonStr as appropriate) so TagList
uses common.Unmarshal to decode s.Tags into []string, returning an empty slice
on error as before; update imports accordingly and keep the function name
TagList and type JSONValue unchanged.
controller/client_skillhub.go-26-33 (1)

26-33: ⚠️ Potential issue | 🟠 Major

slug is concatenated into the upstream URL without URL-encoding.

slug comes straight from c.Query("slug") and is appended to skillHubDownloadBaseURL + slug. A slug containing &, #, ?, a space, or non-ASCII bytes will change the upstream query semantics or break the request (e.g. slug=foo&admin=1 would inject a second query parameter into the SkillHub call). Percent-encode it before concatenation.

🛡️ Proposed fix
-	"net/http"
-	"path"
-	"strings"
+	"net/http"
+	"net/url"
+	"path"
+	"strings"
@@
-	upstreamURL := skillHubDownloadBaseURL + slug
+	upstreamURL := skillHubDownloadBaseURL + url.QueryEscape(slug)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/client_skillhub.go` around lines 26 - 33, The slug from
c.Query("slug") is used raw when building upstreamURL (skillHubDownloadBaseURL +
slug) which allows injection of special chars; before concatenation,
percent-encode the slug (e.g. use url.PathEscape or equivalent) and assign the
escaped value back to slug (or a new variable) so upstreamURL is built as
skillHubDownloadBaseURL + escapedSlug; keep the rest of the flow (resp, err :=
service.DoDownloadRequest(upstreamURL, "skillhub skill download proxy", slug))
but pass the original slug where semantic logging/identification is needed and
the escapedSlug into upstreamURL construction.
controller/subscription.go-129-132 (1)

129-132: ⚠️ Potential issue | 🟠 Major

Don’t overwrite explicit plan currencies.

These unconditional assignments force every created or updated plan to CNY, discarding admin-provided values. Keep CNY as the default only when the request leaves currency empty.

🐛 Proposed fix
 	if req.Plan.Currency == "" {
 		req.Plan.Currency = "CNY"
 	}
-	req.Plan.Currency = "CNY"
 	if req.Plan.Currency == "" {
 		req.Plan.Currency = "CNY"
 	}
-	req.Plan.Currency = "CNY"

Also applies to: 192-195

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/subscription.go` around lines 129 - 132, The code currently
unconditionally sets req.Plan.Currency = "CNY" (lines around the first block and
again around lines 192-195), which overwrites admin-provided currencies; change
both places to only assign the default when the incoming value is empty (e.g.,
if req.Plan.Currency == "" { req.Plan.Currency = "CNY" }) and remove the extra
unconditional assignment so explicit currencies are preserved while still
defaulting to CNY when not provided.
middleware/cors.go-18-28 (1)

18-28: ⚠️ Potential issue | 🟠 Major

Tighten the credentialed CORS origin check.

With AllowCredentials = true, strings.HasPrefix(origin, "https://") still allows any HTTPS site to issue credentialed cross-origin requests. The localhost prefix checks also match hostnames like http://localhost.evil.example. Parse the origin and allow only exact dev hosts plus a configured production allowlist.

🔒 Proposed direction
+	allowedOrigins := common.GetEnvOrDefaultString("CORS_ALLOWED_ORIGINS", "")
+	allowed := map[string]struct{}{}
+	for _, item := range strings.Split(allowedOrigins, ",") {
+		if origin := strings.TrimSpace(item); origin != "" {
+			allowed[origin] = struct{}{}
+		}
+	}
 	config.AllowOriginFunc = func(origin string) bool {
-		// Allow localhost for development
-		if strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") {
+		u, err := url.Parse(origin)
+		if err != nil {
+			return false
+		}
+		if u.Scheme == "http" && (u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1") {
 			return true
 		}
-		// Allow all HTTPS origins (production)
-		if strings.HasPrefix(origin, "https://") {
+		if _, ok := allowed[origin]; ok {
 			return true
 		}
 		return false
 	}

This also requires adding net/url to imports and wiring the production origins through configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware/cors.go` around lines 18 - 28, The current AllowOriginFunc
(config.AllowOriginFunc) is too permissive for credentialed CORS and matches
substrings like "http://localhost.evil.example"; change it to parse the origin
with net/url (url.Parse) and then validate scheme == "http" or "https" and exact
host equality (e.g., "localhost", "127.0.0.1" with optional port) for dev, and
check the parsed origin (Scheme + "://" + Host) against a configured production
allowlist (passed via config) for production; use the parsed Host (not
HasPrefix) to avoid subdomain spoofing and ensure AllowCredentials remains safe
by only returning true when the origin exactly matches an allowed entry.
web/src/i18n/locales/en.json-3384-3390 (1)

3384-3390: ⚠️ Potential issue | 🟠 Major

Remove duplicate locale keys before they override existing translations.

The keys 请输入验证码, 获取验证码, and 两步验证 already exist earlier in this translation object. JSON object keys must be unique; the last duplicate value overwrites earlier ones, silently changing existing UI copy globally, including the 2FA label. Use distinct Chinese source strings such as 获取短信验证码 or 短信两步验证 instead.

🌐 Suggested cleanup
-    "请输入验证码": "Please enter verification code",
     "请输入手机号和验证码": "Please enter phone number and verification code",
-    "获取验证码": "Get Code",
+    "获取短信验证码": "Get SMS Code",
     "秒后重新获取": "s to resend",
     "验证码发送成功": "Verification code sent",
     "验证码发送失败": "Failed to send verification code",
-    "两步验证": "Two-Step Verification",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/i18n/locales/en.json` around lines 3384 - 3390, The JSON contains
duplicate Chinese keys "请输入验证码", "获取验证码", and "两步验证" which override earlier
translations; remove or rename these duplicates (for example rename to
"请输入短信验证码", "获取短信验证码", or "短信两步验证") and update their English values accordingly
so each source key is unique; ensure you keep the original earlier entries
intact (or merge meanings consistently) and adjust any code references if they
rely on the renamed keys.
web/src/hooks/common/useSidebar.js-51-51 (1)

51-51: ⚠️ Potential issue | 🟠 Major

Remove skill-market from DEFAULT_ADMIN_CONFIG or gate it properly via isModuleVisible().

The sidebar explicitly bypasses the isModuleVisible() check for skill-market, only checking isAdmin() (line 204 in SiderBar.jsx). Adding this entry to the default config is misleading because administrators cannot disable it through SidebarModulesAdmin—the sidebar will always show it for all admins regardless of configuration. Either:

  • Remove the config entry and document why skill-market is intentionally outside the permission system, or
  • Update the sidebar to use isModuleVisible('admin', 'skill-market') like other admin modules.

Per the sidebar architecture, admin access should be governed by the SidebarModulesAdmin configuration, not hardcoded allowlists. The current implementation bypasses that system.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/hooks/common/useSidebar.js` at line 51, DEFAULT_ADMIN_CONFIG
currently contains 'skill-market' but SiderBar.jsx bypasses isModuleVisible()
for that module (it only checks isAdmin()), so remove 'skill-market' from
DEFAULT_ADMIN_CONFIG or make the sidebar obey the admin config; specifically
either delete the 'skill-market' entry from DEFAULT_ADMIN_CONFIG (and add a
comment explaining it's intentionally outside the permission system) or modify
SiderBar.jsx to call isModuleVisible('admin', 'skill-market') where other admin
modules are checked so SidebarModulesAdmin controls its visibility instead of
the hardcoded allowlist.
middleware/rate-limit.go-91-101 (1)

91-101: ⚠️ Potential issue | 🟠 Major

Do not bypass the global web limiter based on loopback ClientIP().

In typical reverse-proxy deployments where the proxy connects from 127.0.0.1, this bypass disables rate limiting for all traffic. The localhost prefix check is ineffective dead code (IP addresses never match this pattern). Remove the bypass or gate it behind an explicit development-only configuration flag.

🛡️ Safer default
 func GlobalWebRateLimit() func(c *gin.Context) {
 	if common.GlobalWebRateLimitEnable {
-		limiter := rateLimitFactory(common.GlobalWebRateLimitNum, common.GlobalWebRateLimitDuration, "GW")
-		return func(c *gin.Context) {
-			clientIP := c.ClientIP()
-			if clientIP == "127.0.0.1" || clientIP == "::1" || strings.HasPrefix(clientIP, "localhost") {
-				c.Next()
-				return
-			}
-			limiter(c)
-		}
+		return rateLimitFactory(common.GlobalWebRateLimitNum, common.GlobalWebRateLimitDuration, "GW")
 	}
 	return defNext
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware/rate-limit.go` around lines 91 - 101, The GlobalWebRateLimit
middleware currently bypasses the limiter for loopback IPs using ClientIP()
(checks for "127.0.0.1", "::1", "localhost"), which is unsafe in reverse-proxy
deployments; remove that bypass so the limiter(c) is always invoked, or if you
need a bypass for local development, replace the IP checks with an explicit
development-only feature flag (e.g., common.AllowLocalhostBypass) and gate the
bypass on that flag only; update the function GlobalWebRateLimit and its use of
rateLimitFactory to either delete the conditional IP-check block or change it to
check the new explicit flag (and document that the flag must be true only in
dev).
model/user.go-630-635 (1)

630-635: ⚠️ Potential issue | 🟠 Major

Return the phone lookup error.

FillUserByPhone currently returns nil even when no user exists or the query fails, which can make SMS login paths treat an unfilled User as valid.

Proposed fix
 func (user *User) FillUserByPhone() error {
-	if user.Phone == "" {
+	user.Phone = strings.TrimSpace(user.Phone)
+	if user.Phone == "" {
 		return errors.New("phone 为空!")
 	}
-	DB.Where("phone = ?", user.Phone).First(user)
-	return nil
+	return DB.Where("phone = ?", user.Phone).First(user).Error
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/user.go` around lines 630 - 635, The FillUserByPhone function currently
ignores the DB query result and always returns nil; change it to capture the
GORM result (e.g. r := DB.Where("phone = ?", user.Phone).First(user)) and return
r.Error so callers receive the lookup error (including not-found or other DB
errors); keep the existing empty-phone check and return early if user.Phone ==
"".
web/src/components/layout/SiderBar.jsx-203-205 (1)

203-205: ⚠️ Potential issue | 🟠 Major

Respect sidebar module visibility for the admin section.

skill-market is already part of the admin sidebar config, so hardcoding it to show for every admin bypasses the configured module visibility. The section render should also keep the hasSectionVisibleModules('admin') gate to avoid showing an admin group when all admin modules are disabled.

Proposed fix
-    // 根据配置过滤项目
+    // 根据配置过滤项目
     const filteredItems = items.filter((item) => {
-      // 技能管理是本地扩展核心入口,管理员下始终显示,避免被配置误隐藏。
-      if (item.itemKey === 'skill-market') return isAdmin();
       const configVisible = isModuleVisible('admin', item.itemKey);
       return configVisible;
     });
-          {/* 管理员区域 - 管理员始终显示,避免配置误隐藏 */}
-          {isAdmin() && (
+          {/* 管理员区域 */}
+          {isAdmin() && hasSectionVisibleModules('admin') && (

Based on learnings, the sidebar management system uses SidebarModulesAdmin configuration to control admin user permissions, and admin access to console modules should not be bypassed with hardcoded allowlists.

Also applies to: 487-488

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/layout/SiderBar.jsx` around lines 203 - 205, Remove the
hardcoded always-show for the 'skill-market' item and instead use the normal
visibility checks: rely on isModuleVisible('admin', item.itemKey) to decide
per-item visibility (including 'skill-market') and ensure the admin group
rendering is guarded by hasSectionVisibleModules('admin') so the entire admin
section is hidden when no admin modules are enabled; update the logic around
item.itemKey === 'skill-market' and the admin group render to remove the
special-case and use isModuleVisible and hasSectionVisibleModules respectively.
model/user.go-37-37 (1)

37-37: ⚠️ Potential issue | 🟠 Major

Make phone ownership unambiguous.

phone is only indexed, and IsPhoneAlreadyTaken returns false when duplicate rows already exist because RowsAffected would be greater than 1. SMS login should treat any existing phone as taken and the write path should enforce one active owner per phone.

Minimum local fix
 func IsPhoneAlreadyTaken(phone string) bool {
-	return DB.Unscoped().Where("phone = ?", phone).Find(&User{}).RowsAffected == 1
+	phone = strings.TrimSpace(phone)
+	if phone == "" {
+		return false
+	}
+	return DB.Unscoped().Where("phone = ?", phone).Find(&User{}).RowsAffected > 0
 }

Also add a cross-database-safe uniqueness strategy for non-empty phone bindings during registration/binding; avoid a plain unique index while unbound users are stored as "", because that would block multiple unbound users.

Also applies to: 638-640

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/user.go` at line 37, The Phone field and IsPhoneAlreadyTaken logic make
ownership ambiguous—update IsPhoneAlreadyTaken to treat any existing non-empty
phone row as taken (RowsAffected > 0), and in all registration/binding write
paths (the functions that assign/validate Phone during registration/binding)
enforce one active owner by performing a transactional check-and-set (e.g.,
SELECT ... FOR UPDATE or an equivalent DB lock) before inserting/updating; also
change the storage/uniqueness strategy so unbound users do not use the empty
string (use NULL or a marker) and add a cross-database-safe uniqueness
constraint for non-empty phones (e.g., partial/filtered unique index where phone
IS NOT NULL / phone <> '' or enforce uniqueness at the application level within
the same transaction) to ensure no two active owners can exist for the same
phone.
web/src/components/auth/SmsLoginForm.jsx-85-87 (1)

85-87: ⚠️ Potential issue | 🟠 Major

Send the Turnstile token under the parameter name the middleware reads.

The routes use TurnstileCheck(), and the provided middleware reads turnstile_token, but this component sends turnstile. SMS login will fail whenever Turnstile is enabled.

Proposed fix
       const res = await API.post(
-        `/api/sms/send?turnstile=${turnstileToken}`,
+        `/api/sms/send?turnstile_token=${turnstileToken}`,
         { phone },
       );
@@
       const res = await API.post(
-        `/api/user/login/sms?turnstile=${turnstileToken}`,
+        `/api/user/login/sms?turnstile_token=${turnstileToken}`,
         { phone, code },
       );

Also applies to: 119-121

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/auth/SmsLoginForm.jsx` around lines 85 - 87, In
SmsLoginForm.jsx update the API.post calls that currently send the Turnstile
token as turnstile to use the parameter name the middleware expects
(turnstile_token); locate the two occurrences around the API.post calls in the
SmsLoginForm component and change the query param from
turnstile=${turnstileToken} to turnstile_token=${turnstileToken} so
TurnstileCheck() can read the token correctly.
common/sms/tencent.go-118-126 (1)

118-126: ⚠️ Potential issue | 🟠 Major

Treat an empty Tencent send status as a failed send.

If Tencent returns no top-level Error but also no SendStatusSet, this returns nil even though no phone delivery status was confirmed.

Proposed fix
 	if result.Response.Error != nil {
 		return fmt.Errorf("tencent SMS error: %s - %s", result.Response.Error.Code, result.Response.Error.Message)
 	}
 
-	if len(result.Response.SendStatusSet) > 0 && result.Response.SendStatusSet[0].Code != "Ok" {
+	if len(result.Response.SendStatusSet) == 0 {
+		return fmt.Errorf("tencent SMS send error: empty SendStatusSet")
+	}
+	if result.Response.SendStatusSet[0].Code != "Ok" {
 		return fmt.Errorf("tencent SMS send error: %s - %s", result.Response.SendStatusSet[0].Code, result.Response.SendStatusSet[0].Message)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/sms/tencent.go` around lines 118 - 126, The current return path treats
a response with no top-level Error and an empty SendStatusSet as success; update
the check around result.Response.SendStatusSet so that if
len(result.Response.SendStatusSet) == 0 you return an error indicating "tencent
SMS send error: empty SendStatusSet" (or similar) before the existing check that
inspects SendStatusSet[0].Code, ensuring you still handle result.Response.Error
first and then treat missing send status as a failed send (references:
result.Response.Error and result.Response.SendStatusSet).
common/sms/aliyun.go-69-73 (1)

69-73: ⚠️ Potential issue | 🟠 Major

Set a timeout for Aliyun SMS requests.

Both http.Get calls in the SMS sender modules use the default client with no timeout, allowing stalled Aliyun connections to block login/send-code requests indefinitely. Apply the same fix to both common/sms/aliyun.go (line 69) and common/sms/aliyun_pnvs.go (line 82).

Proposed fix
-	resp, err := http.Get(reqURL)
+	client := &http.Client{Timeout: 10 * time.Second}
+	resp, err := client.Get(reqURL)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/sms/aliyun.go` around lines 69 - 73, Replace use of the default
http.Get (which has no timeout) with an HTTP request that enforces a timeout:
create a local http.Client with a reasonable Timeout (e.g., a few seconds) and
call client.Get instead of http.Get, or perform the request with a context with
deadline and http.NewRequest; update the call in common/sms/aliyun.go (the resp,
err := http.Get(reqURL) site) and apply the identical change to
common/sms/aliyun_pnvs.go (the similar http.Get usage) so stalled Aliyun
connections cannot block processing indefinitely.
common/sms/tencent.go-7-7 (1)

7-7: ⚠️ Potential issue | 🟠 Major

Use common.Marshal / common.Unmarshal here.

This file directly imports and calls encoding/json in business code. Use the project wrappers instead. As per coding guidelines, "All JSON marshal/unmarshal operations MUST use wrapper functions from common/json.go (common.Marshal, common.Unmarshal, common.UnmarshalJsonStr, common.DecodeJson, common.GetJsonType). Do NOT directly import or call encoding/json in business code."

Proposed fix
-	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
@@
 	"time"
+
+	"github.com/QuantumNous/new-api/common"
 )
@@
-	payloadBytes, err := json.Marshal(payload)
+	payloadBytes, err := common.Marshal(payload)
@@
-	if err := json.Unmarshal(body, &result); err != nil {
+	if err := common.Unmarshal(body, &result); err != nil {
 		return fmt.Errorf("tencent SMS parse response failed: %w", err)
 	}

Also applies to: 45-48, 114-116

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/sms/tencent.go` at line 7, Replace direct use of encoding/json in this
file: remove the "encoding/json" import and replace all
json.Marshal/json.Unmarshal calls used when building the SMS request payload and
when parsing Tencent API responses with the project wrappers common.Marshal and
common.Unmarshal; specifically update the code paths that serialize the request
body (where the payload is constructed) and the code that decodes the response
into structs to call common.Marshal(...) and common.Unmarshal(..., &target)
respectively, and ensure the import for the common package is added.
common/sms/aliyun.go-7-7 (1)

7-7: ⚠️ Potential issue | 🟠 Major

Replace direct encoding/json with wrapper function from common/json.go.

This file imports encoding/json directly and calls json.Unmarshal at line 84. Use common.Unmarshal instead to maintain consistency with the project's JSON handling. The coding guidelines require all JSON operations to use wrapper functions from common/json.go.

Proposed fix
-	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
@@
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
 	"github.com/google/uuid"
@@
-	if err := json.Unmarshal(body, &result); err != nil {
+	if err := common.Unmarshal(body, &result); err != nil {
 		return fmt.Errorf("aliyun SMS parse response failed: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/sms/aliyun.go` at line 7, The file currently imports "encoding/json"
and calls json.Unmarshal (the call at json.Unmarshal around line 84); replace
that usage with the project's JSON wrapper by removing the "encoding/json"
import and importing the project's common package, then call common.Unmarshal
instead of json.Unmarshal (preserving the same arguments and error handling).
Ensure any variable names and error checks around the Unmarshal call remain
unchanged and that import grouping/aliases are adjusted so the file compiles
with common.Unmarshal from common/json.go.
controller/client_skills.go-202-205 (1)

202-205: ⚠️ Potential issue | 🟠 Major

Check RowsAffected before reporting a download as recorded.

The model helper returns only error, so GORM can update zero rows for a missing/disabled/non-public skill and this handler still returns recorded: true. Return rows affected from the model helper or perform the update here and require RowsAffected > 0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/client_skills.go` around lines 202 - 205, The handler currently
treats any nil error from model.IncrementClientSkillMarketDownload(id) as a
successful recorded download even if GORM updated zero rows; change the model
helper IncrementClientSkillMarketDownload to return (int64, error) (or return
the gorm.Result), or perform the DB update in this handler, and check the
returned RowsAffected > 0 before calling common.ApiSuccess(c, gin.H{"recorded":
true}); if RowsAffected == 0 return recorded: false or an appropriate error
response instead. Ensure you adjust the call site in this handler (the code
using IncrementClientSkillMarketDownload) to handle the new return signature.
middleware/sms_rate_limit.go-74-87 (1)

74-87: ⚠️ Potential issue | 🟠 Major

Keep the phone-based SMS limit in the in-memory fallback too.

When Redis is disabled, this path only limits by IP, so a single phone can receive up to the per-IP burst and distributed IPs bypass the per-phone protection entirely. Mirror the phone key limit here as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware/sms_rate_limit.go` around lines 74 - 87, memorySmsRateLimiter
currently only enforces IP-based in-memory limits; also enforce the per-phone
in-memory limit by reading the phone number from the request (e.g., the same
param used in your send-SMS handler), build a phone key using SmsRateLimitMark +
":phone:" + phone, and call inMemoryRateLimiter.Request with the phone-specific
limits (SmsMaxRequestsPerPhone, SmsPhoneDuration); if that request returns
false, respond with the same 429 JSON and abort just like the IP branch. Ensure
you perform the phone check before c.Next() and keep the same error response
shape.
controller/sms_login.go-119-143 (1)

119-143: ⚠️ Potential issue | 🟠 Major

Avoid deriving usernames from GetMaxUserId()+1.

Concurrent SMS auto-registrations can compute the same nextId, causing duplicate sms_<id> usernames and failed registration. Use a collision-resistant username derived from the phone hash/random suffix, or retry on duplicate-key errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/sms_login.go` around lines 119 - 143, The current registration
derives username from GetMaxUserId()+1 which can collide under concurrent SMS
auto-registrations; change User creation to generate a collision-resistant
username (e.g., hash of req.Phone plus short random suffix or a UUID) instead of
fmt.Sprintf("sms_%d", nextId), or implement a retry-on-duplicate-key loop around
newUser.Insert that regenerates the username and retries until Insert succeeds;
update references in this block (GetMaxUserId usage, the username creation
logic, and the call to newUser.Insert) and ensure GenerateKey/DisplayName logic
remains unchanged while properly handling and logging duplicate-key errors
returned by model.User.Insert.
controller/client_auth.go-104-122 (1)

104-122: ⚠️ Potential issue | 🟠 Major

Avoid GetMaxUserId()+1 for auto-generated usernames.

Concurrent client SMS registrations can compute the same next id and collide on sms_<id>. Generate a unique phone-derived/random username or retry on duplicate-key errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/client_auth.go` around lines 104 - 122, The current username
generation using GetMaxUserId()+1 (username := fmt.Sprintf("sms_%d", nextId))
can collide under concurrent SMS registrations; change username creation to
produce a unique value (for example derive from the phone plus a random/UUID
suffix or use a secure short random token from common.GenerateKey instead of
nextId) and update the flow around model.User.Insert to handle duplicate-key
errors by retrying username generation a few times (identify duplicate-key error
returned by newUser.Insert and regenerate username and call Insert again,
failing after N attempts). Ensure you reference and update the GetMaxUserId
usage, the username construction, common.GenerateKey usage, and the
newUser.Insert error handling to implement the retry/unique-username strategy.
middleware/sms_rate_limit.go-26-52 (1)

26-52: ⚠️ Potential issue | 🟠 Major

Parse the JSON body before applying the phone limiter.

SendSmsVerification accepts JSON, but this middleware only reads c.PostForm("phone"), so normal JSON requests skip the SmsMaxRequestsPerPhone limit and only hit the coarser IP limit. Read and restore the request body, then decode phone, or make the client submit form data consistently.

🛡️ Proposed direction
+// Read JSON body without consuming it for the controller, then apply the same
+// phone limiter to both JSON and form requests.
 phone := c.PostForm("phone")
+if phone == "" && strings.Contains(c.GetHeader("Content-Type"), "application/json") {
+    body, _ := io.ReadAll(c.Request.Body)
+    c.Request.Body = io.NopCloser(bytes.NewReader(body))
+    var req struct {
+        Phone string `json:"phone"`
+    }
+    if err := common.Unmarshal(body, &req); err == nil {
+        phone = req.Phone
+    }
+    c.Request.Body = io.NopCloser(bytes.NewReader(body))
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware/sms_rate_limit.go` around lines 26 - 52, The phone-based limiter
in middleware/sms_rate_limit.go currently only reads c.PostForm("phone") so JSON
requests bypass SmsMaxRequestsPerPhone; before using phone in the phone limiter
block, read and buffer c.Request.Body, decode the JSON body to extract the
"phone" field (falling back to c.PostForm("phone") if not present), then restore
the request body for downstream handlers and continue using the existing
phoneKey/Incr/Expire/TTL logic with SmsRateLimitMark, SmsPhoneDuration and
SmsMaxRequestsPerPhone; ensure any body read errors fall back to IP-only
limiting and do not disrupt the request flow.
controller/sms_login.go-54-66 (1)

54-66: ⚠️ Potential issue | 🟠 Major

Only register the verification code after the SMS is accepted for sending.

The code is stored before sms.NewSender() and sender.SendCode(). If provider config or delivery fails, an unsent code remains valid and may also replace a previously deliverable code.

🛡️ Proposed fix
 code := common.GenerateVerificationCode(6)
-common.RegisterVerificationCodeWithKey(req.Phone, code, common.SmsVerificationPurpose)

 sender, err := sms.NewSender()
 if err != nil {
   common.ApiErrorI18n(c, i18n.MsgSmsProviderNotConfig)
   return
 }

 if err := sender.SendCode(req.Phone, code); err != nil {
   common.SysLog("SMS send failed: " + err.Error())
   common.ApiErrorI18n(c, i18n.MsgSmsSendFailed)
   return
 }
+common.RegisterVerificationCodeWithKey(req.Phone, code, common.SmsVerificationPurpose)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/sms_login.go` around lines 54 - 66, The verification code is
registered before ensuring the SMS provider is configured and the message is
accepted; move the call to common.RegisterVerificationCodeWithKey so it only
happens after sms.NewSender() succeeds and sender.SendCode(req.Phone, code)
returns nil. In other words: call common.GenerateVerificationCode(6), create the
sender via sms.NewSender(), call sender.SendCode(...), and only when SendCode
returns no error invoke common.RegisterVerificationCodeWithKey(req.Phone, code,
common.SmsVerificationPurpose); keep existing error handling for sms.NewSender
and sender.SendCode unchanged.
controller/client_skills.go-148-192 (1)

148-192: ⚠️ Potential issue | 🟠 Major

Do not fall back to bundled public skills after the DB returns empty or not-found.

If an admin disables/removes all public skills, loadClientPublicSkills() still returns defaultClientPublicSkills; similarly, a disabled/missing id can be served from the fallback. That makes admin visibility controls ineffective. Prefer seeding defaults into the DB, or only use fallback on actual DB errors.

🛡️ Proposed fix
 if err != nil {
   common.SysError("load client public skills from db failed: " + err.Error())
   return defaultClientPublicSkills
 }
-if len(items) == 0 {
-  return defaultClientPublicSkills
-}

@@
-if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
   common.SysError("get client public skill from db failed: " + err.Error())
+  for _, skill := range defaultClientPublicSkills {
+    if skill.ID == id {
+      common.ApiSuccess(c, skill)
+      return
+    }
+  }
 }
-
-for _, skill := range defaultClientPublicSkills {
-  if skill.ID == id {
-    common.ApiSuccess(c, skill)
-    return
-  }
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/client_skills.go` around lines 148 - 192, loadClientPublicSkills
and ClientGetPublicSkill currently fall back to defaultClientPublicSkills when
the DB returns an empty list or a not-found id, which bypasses admin removals;
change logic so that loadClientPublicSkills returns an empty slice when
model.ListPublicClientSkillMarketItems succeeds but returns zero items (only use
defaultClientPublicSkills when ListPublicClientSkillMarketItems returns an
actual error), and change ClientGetPublicSkill to only use
defaultClientPublicSkills when model.GetPublicClientSkillMarketItemByID returns
an error indicative of a DB failure (not when it returns
gorm.ErrRecordNotFound); reference functions/vars: loadClientPublicSkills,
ClientGetPublicSkill, defaultClientPublicSkills,
model.ListPublicClientSkillMarketItems,
model.GetPublicClientSkillMarketItemByID.
common/sms/aliyun_pnvs.go-82-86 (1)

82-86: ⚠️ Potential issue | 🟠 Major

Add HTTP timeout and status code validation to the Aliyun PNVS request.

The http.Get() call uses the default client which has no timeout, allowing a stalled provider to block the request handler indefinitely. Additionally, the response body is parsed without validating the HTTP status code, so 4xx/5xx responses with invalid JSON will produce misleading error messages.

Add a timeout to the client and validate the status code before decoding the response:

Proposed fix
- resp, err := http.Get(reqURL)
+ client := &http.Client{Timeout: 10 * time.Second}
+ resp, err := client.Get(reqURL)
  if err != nil {
    return fmt.Errorf("aliyun PNVS SMS request failed: %w", err)
  }
  defer resp.Body.Close()
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+   return fmt.Errorf("aliyun PNVS SMS HTTP error: %d", resp.StatusCode)
+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/sms/aliyun_pnvs.go` around lines 82 - 86, Replace the use of the
default http.Get with an explicit http.Client that has a sensible Timeout (e.g.,
5-10s) and use client.Get(reqURL) instead of http.Get(reqURL); after receiving
resp check resp.StatusCode and if it is not in the 2xx range read/limit the
resp.Body to capture a short error snippet and return a clear error including
the status code and snippet, then only proceed to decode the body when the
status is OK; ensure resp.Body.Close() is still deferred.
🟡 Minor comments (11)
scripts/sync-db-push.sh-47-51 (1)

47-51: ⚠️ Potential issue | 🟡 Minor

Use read -r to avoid backslash mangling.

Shellcheck SC2162. Trivial fix:

-read -p "确认上传并重启服务?(输入 yes 继续): " answer
+read -r -p "确认上传并重启服务?(输入 yes 继续): " answer
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync-db-push.sh` around lines 47 - 51, The prompt uses plain read
which allows backslash interpretation; change the interactive prompt to use read
-r (i.e., replace the read invocation shown in the prompt line with read -r) so
backslashes are not mangled and satisfy ShellCheck SC2162 while keeping the
existing variable name (answer) and prompt text unchanged.
setting/operation_setting/general_setting.go-30-30 (1)

30-30: ⚠️ Potential issue | 🟡 Minor

Default currency flip (USD → CNY) is an observable behavior change for fresh installs.

For brand-new deployments and for any installation that hasn't persisted quota_display_type explicitly, the admin panel, billing UI, logs, and API responses will now render quotas in CNY and apply ConvertQuotaToCNY. For non-Chinese operators this will be surprising. Consider documenting this flip in the changelog/migration notes (and in CHANGELOG_FORK.md which is part of this PR), and double-check that controller/topup.go, controller/billing.go, and logger/logger.go all produce sensible output under the new default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setting/operation_setting/general_setting.go` at line 30, The default
QuotaDisplayType is being set to QuotaDisplayTypeCNY which causes a behavioral
change for fresh installs; revert the default back to QuotaDisplayTypeUSD in the
general setting (replace QuotaDisplayTypeCNY with QuotaDisplayTypeUSD in the
QuotaDisplayType default), and add a clear migration/changelog entry in
CHANGELOG_FORK.md documenting the intentional currency default change if you
decide to keep CNY; also review and update usages in controller/topup.go,
controller/billing.go, and logger/logger.go to ensure they format and convert
quotas correctly (run a quick test of API responses, admin UI, and logs to
confirm no regressions).
CHANGELOG_FORK.md-85-92 (1)

85-92: ⚠️ Potential issue | 🟡 Minor

Add languages to the fenced code blocks.

markdownlint flags these fences; marking the HTTP examples and flow block keeps the new changelog lint-clean.

🧹 Proposed documentation fix
-```
+```http
 POST /api/sms/send?turnstile={token}
 Content-Type: application/json
 
 {
   "phone": "+8613800138000"
 }

...
- +http
POST /api/user/login/sms?turnstile={token}
Content-Type: application/json

{
"phone": "+8613800138000",
"code": "123456"
}

...
-```
+```text
1. AionUi 打开弹窗:
   https://new-api.example.com/login?mode=popup&callback_origin=https://aionui.example.com

Also applies to: 104-112, 226-240

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG_FORK.md` around lines 85 - 92, The fenced code blocks in
CHANGELOG_FORK.md are missing language specifiers; update each fence shown (the
HTTP example starting with "POST /api/sms/send?turnstile={token}", the second
block starting "POST /api/user/login/sms?turnstile={token}", and the flow block
beginning "1. AionUi 打开弹窗:") to include appropriate languages (e.g., ```http for
HTTP requests and ```text for the numbered flow) so markdownlint no longer flags
them; keep the original block contents unchanged and only add the language
tokens to the opening backtick fences.
controller/option.go-191-198 (1)

191-198: ⚠️ Potential issue | 🟡 Minor

Use the existing SMS i18n key for this response.

This new SMS-specific message is hardcoded, while the SMS controllers already use i18n.MsgSmsProviderNotConfig (controller/sms_login.go:59-71, i18n/keys.go:320-327). Keeping the same key avoids returning Chinese text in non-Chinese admin sessions.

🌐 Proposed fix
+	"github.com/QuantumNous/new-api/i18n"
 	"github.com/QuantumNous/new-api/model"
 	case "sms.enabled":
 		if option.Value == "true" && system_setting.GetSmsSettings().Provider == "" {
 			c.JSON(http.StatusOK, gin.H{
 				"success": false,
-				"message": "无法启用短信登录,请先配置短信服务商!",
+				"message": i18n.T(c, i18n.MsgSmsProviderNotConfig),
 			})
 			return
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/option.go` around lines 191 - 198, Replace the hardcoded Chinese
message in the "sms.enabled" case with the existing SMS i18n key: use
i18n.MsgSmsProviderNotConfig for the "message" field so the response follows the
same localization used in controller/sms_login.go; update the block that checks
option.Value == "true" and system_setting.GetSmsSettings().Provider == "" to
return the i18n key rather than the literal string.
middleware/cache.go-12-21 (1)

12-21: ⚠️ Potential issue | 🟡 Minor

Don’t attach no-cache legacy headers to immutable assets.

The /assets/ branch advertises long immutable caching, but lines 20-21 still add Pragma: no-cache and Expires: 0 to those same responses. Keep those headers only on the non-asset branch.

Proposed fix
 		if strings.HasPrefix(uri, "/assets/") {
 			// 构建产物文件名带 hash,允许长缓存。
 			c.Header("Cache-Control", "public, max-age=604800, immutable")
 		} else {
 			// 页面路由(如 /console/skill-market)和其他入口统一禁用缓存,
 			// 避免 IAB 持续命中旧前端代码导致“编辑空白”。
 			c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
+			c.Header("Pragma", "no-cache")
+			c.Header("Expires", "0")
 		}
-		c.Header("Pragma", "no-cache")
-		c.Header("Expires", "0")
 		c.Header("Cache-Version", "skill-market-hotfix-20260419")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware/cache.go` around lines 12 - 21, The current middleware sets long
immutable caching for asset paths (strings.HasPrefix(uri, "/assets/")) but still
unconditionally adds legacy no-cache headers via c.Header("Pragma", "no-cache")
and c.Header("Expires", "0"); move those two header calls into the non-asset
branch (the else block) so that only non-asset responses get "Pragma: no-cache"
and "Expires: 0" while the assets branch only sets the immutable Cache-Control
header using c.Header("Cache-Control", "public, max-age=604800, immutable").
web/src/i18n/locales/zh-CN.json-2988-2994 (1)

2988-2994: ⚠️ Potential issue | 🟡 Minor

Remove duplicate locale keys.

请输入验证码, 获取验证码, and 两步验证 already exist earlier in this locale file, so re-adding them creates duplicate JSON members.

Proposed fix
-    "请输入验证码": "请输入验证码",
     "请输入手机号和验证码": "请输入手机号和验证码",
-    "获取验证码": "获取验证码",
     "秒后重新获取": "秒后重新获取",
     "验证码发送成功": "验证码发送成功",
     "验证码发送失败": "验证码发送失败",
-    "两步验证": "两步验证",
     "允许通过短信验证码登录": "允许通过短信验证码登录",

As per coding guidelines, use CLI tools: bun run i18n:extract, bun run i18n:sync, bun run i18n:lint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/i18n/locales/zh-CN.json` around lines 2988 - 2994, Remove the
duplicate JSON members by deleting the repeated locale keys "请输入验证码", "获取验证码",
and "两步验证" from the added block so each key appears only once in the zh-CN
locale; after removing those entries, run the i18n tooling to
re-extract/sync/lint (bun run i18n:extract, bun run i18n:sync, bun run
i18n:lint) to ensure no other duplicates or extraction issues remain.
web/src/pages/SkillMarket/index.jsx-365-623 (1)

365-623: ⚠️ Potential issue | 🟡 Minor

Wrap the new Skill Market UI strings with t(...).

This new page hard-codes table titles, button labels, filter labels, status text, and empty-state text, but it does not use useTranslation(). These strings will be skipped by the i18n flow. As per coding guidelines, “Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/SkillMarket/index.jsx` around lines 365 - 623, The page
hard-codes UI text in the columns constant and JSX (e.g., table column titles in
columns, Tag labels, Button text, select option labels, Input placeholder,
statusSummary/filteredSkills labels, Empty description) and must use i18n:
import and call useTranslation() in this component and wrap each user-visible
Chinese string with t('中文原文') (use the Chinese text itself as the key per locale
guidelines). Update occurrences in the columns renderers (columns const), Button
labels (openCreateEditor, loadSkills buttons, action Buttons), Switch labels,
select option texts (status/category/page size options), Tag texts
(statusSummary tags and status render), Empty.description, and any hard-coded
tooltips or Popconfirm titles/content to use t('...'); keep dynamic values
(numbers) unchanged and ensure keys match the flat locales JSON convention.
web/src/pages/SkillMarket/EditSkillModal.jsx-13-289 (1)

13-289: ⚠️ Potential issue | 🟡 Minor

Wrap user-facing copy with useTranslation() and t(...).

This component hardcodes all labels, buttons, status text, and error copy. Please use react-i18next so the skill market admin UI remains localizable. As per coding guidelines, web/src/**/*.{ts,tsx,js,jsx} must use useTranslation() and call t('中文key') in components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/SkillMarket/EditSkillModal.jsx` around lines 13 - 289, The
component EditSkillModal currently hardcodes all user-facing strings; update it
to use react-i18next by calling useTranslation() inside EditSkillModal and
replacing each literal label/button/error text with t('key') calls (e.g. title
tags, Tag text, Typography.Title, all Input labels like "原始名称", "分类",
placeholders, debug panel strings, buttons "保存"/"取消"/"预览技能页"/"立即上架"/"立即下架",
error message wrapping editError, and the getMylclawNavLabel display). Ensure
you import useTranslation from 'react-i18next', create meaningful i18n keys for
these strings, and keep existing handlers (setEditingSkill, onSubmit, onCancel,
updateSkillStatus, getPreviewUrl, statusLoadingMap) unchanged while swapping
literals for t('...') calls so the UI remains localizable.
web/src/pages/SkillMarket/Editor.jsx-92-490 (1)

92-490: ⚠️ Potential issue | 🟡 Minor

Localize the editor page copy via react-i18next.

All labels, button text, loading/error messages, and toasts are hardcoded in Chinese. Please add useTranslation() and wrap Chinese source strings with t(...). As per coding guidelines, web/src/**/*.{ts,tsx,js,jsx} must use useTranslation() and call t('中文key') in components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/SkillMarket/Editor.jsx` around lines 92 - 490, The component
SkillMarketEditor currently contains hardcoded Chinese UI strings; import and
use useTranslation from 'react-i18next' (e.g. const { t } = useTranslation()) at
the top of the SkillMarketEditor component and replace all hardcoded Chinese
literals in JSX, button labels, placeholders, status/loading/error messages,
toast calls (showError/showSuccess), and the static build banner with t('...')
keys; keep existing logic and identifiers (SkillMarketEditor, previewUrl,
submitSkill, loadSkill, updateSkillStatus, updateField, isCreate, editingSkill)
and use interpolation for dynamic strings (e.g.
t('skillMarket.editor.titleWithId', { id: editingSkill.id || skillId })) and
consistent key names like 'skillMarket.editor.save',
'skillMarket.editor.loading', 'skillMarket.editor.pageError', etc., so the UI
strings are localized via react-i18next.
common/sms/aliyun_pnvs.go-7-7 (1)

7-7: ⚠️ Potential issue | 🟡 Minor

Use the project JSON wrappers instead of encoding/json.

Replace the direct import and json.Unmarshal call with common.Unmarshal to keep behavior consistent with the rest of the codebase. All Go JSON marshal/unmarshal operations must use wrapper functions from common/json.go.

♻️ Proposed fix
 import (
   "crypto/hmac"
   "crypto/sha1"
   "encoding/base64"
-  "encoding/json"
   "fmt"
   "io"
   "net/http"
@@
+  "github.com/QuantumNous/new-api/common"
   "github.com/google/uuid"
 )
@@
- if err := json.Unmarshal(body, &result); err != nil {
+ if err := common.Unmarshal(body, &result); err != nil {
   return fmt.Errorf("aliyun PNVS SMS parse response failed: %w", err)
 }

Also applies to: 101-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/sms/aliyun_pnvs.go` at line 7, Replace direct use of the standard
library JSON package with the project's JSON wrapper: remove the "encoding/json"
import and import the project's common JSON wrapper package (the package that
exposes Unmarshal), then replace calls to json.Unmarshal (and any json.Marshal
usages) with common.Unmarshal (and common.Marshal) in this file—specifically
update the json.Unmarshal usages around the current occurrences (including the
ones referenced at lines ~101-103) so they call common.Unmarshal and match the
wrapper's signature.
controller/client_skills.go-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor

Replace json.Marshal with common.Marshal wrapper function.

Line 96 in tagsToJSON directly calls json.Marshal. Per project guidelines, all JSON marshal/unmarshal operations in Go files must use wrapper functions from common/json.go. Remove the direct encoding/json import at line 4 and use common.Marshal instead.

Proposed fix
 import (
-	"encoding/json"
 	"errors"
 	"strconv"
 	"strings"

 	"github.com/QuantumNous/new-api/common"
 	"github.com/QuantumNous/new-api/model"
 	"github.com/gin-gonic/gin"
 	"gorm.io/gorm"
 )
 func tagsToJSON(tags []string) (model.JSONValue, error) {
 	if tags == nil {
 		tags = []string{}
 	}
-	tagBytes, err := json.Marshal(tags)
+	tagBytes, err := common.Marshal(tags)
 	if err != nil {
 		return nil, err
 	}
 	return model.JSONValue(tagBytes), nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/client_skills.go` at line 4, The function tagsToJSON currently
uses json.Marshal directly and imports "encoding/json"; replace that call with
the project wrapper common.Marshal and remove the direct "encoding/json" import.
Update tagsToJSON to call common.Marshal(tags) (handle the returned ([]byte,
error) exactly as before) and import the package that provides common.Marshal
(or qualify it with the existing common package) instead of using json.Marshal;
ensure error handling remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 15532ba0-f599-43ae-9073-67f368e8fdf2

📥 Commits

Reviewing files that changed from the base of the PR and between f995a86 and 7f4e07e.

⛔ Files ignored due to path filters (1)
  • web/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (47)
  • CHANGELOG_FORK.md
  • common/sms/aliyun.go
  • common/sms/aliyun_pnvs.go
  • common/sms/sms.go
  • common/sms/tencent.go
  • common/verification.go
  • controller/client_auth.go
  • controller/client_skillhub.go
  • controller/client_skills.go
  • controller/misc.go
  • controller/option.go
  • controller/sms_login.go
  • controller/subscription.go
  • controller/user.go
  • i18n/keys.go
  • i18n/locales/en.yaml
  • i18n/locales/zh-CN.yaml
  • i18n/locales/zh-TW.yaml
  • middleware/auth.go
  • middleware/cache.go
  • middleware/cors.go
  • middleware/rate-limit.go
  • middleware/sms_rate_limit.go
  • model/client_skill_market.go
  • model/main.go
  • model/option.go
  • model/subscription.go
  • model/user.go
  • router/api-router.go
  • scripts/sync-db-pull.sh
  • scripts/sync-db-push.sh
  • setting/operation_setting/general_setting.go
  • setting/system_setting/sms.go
  • web/src/App.jsx
  • web/src/components/auth/LoginForm.jsx
  • web/src/components/auth/SmsLoginForm.jsx
  • web/src/components/common/logo/PhoneIcon.jsx
  • web/src/components/layout/SiderBar.jsx
  • web/src/components/settings/SystemSetting.jsx
  • web/src/helpers/auth.jsx
  • web/src/helpers/render.jsx
  • web/src/hooks/common/useSidebar.js
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/zh-CN.json
  • web/src/pages/SkillMarket/EditSkillModal.jsx
  • web/src/pages/SkillMarket/Editor.jsx
  • web/src/pages/SkillMarket/index.jsx

Comment thread controller/client_auth.go
Comment on lines +55 to +59
// 与 SmsLogin 的区别:
// - 不创建 cookie session(客户端不需要)
// - 自动确保用户拥有一个名为 "MyClaw Client" 的 Token,并返回完整 Key
// - 不支持 2FA 流程(客户端登录场景简化)
func ClientSmsLogin(c *gin.Context) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not bypass 2FA when issuing a client API key.

This endpoint skips the 2FA flow but returns a long-lived API key. Accounts with 2FA enabled can therefore authenticate with only SMS here, unlike SmsLogin, which checks model.IsTwoFAEnabled(user.Id) before completing login.

🛡️ Proposed direction
 if user.Status != common.UserStatusEnabled {
   common.ApiErrorI18n(c, i18n.MsgUserDisabled)
   return
 }
+
+if model.IsTwoFAEnabled(user.Id) {
+  common.ApiErrorI18n(c, i18n.MsgUserRequire2FA)
+  return
+}

 // 6. 查找或创建客户端专用 Token
 apiKey, err := ensureClientToken(user.Id, user.Username)

Also applies to: 135-147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/client_auth.go` around lines 55 - 59, ClientSmsLogin currently
skips the 2FA check and issues a long‑lived API key; modify ClientSmsLogin (and
the similar logic around lines 135-147) to call model.IsTwoFAEnabled(user.Id)
after verifying the SMS code and before creating or returning the "MyClaw
Client" token, and if 2FA is enabled reject the flow (return an appropriate
2FA-required error) instead of issuing the API key; mirror the same 2FA gate
used in SmsLogin to ensure users with 2FA enabled cannot obtain the client API
key via SMS-only login.

Comment thread scripts/sync-db-push.sh
Comment on lines +59 to +70
# 2. 上传
echo "==> 上传中..."
scp -q "$LOCAL_PATH" "${REMOTE_HOST}:${REMOTE_PATH}"
echo "✓ 上传完成"

# 3. 重启服务
echo "==> 重启服务 ${REMOTE_SERVICE}..."
ssh "$REMOTE_HOST" "systemctl restart ${REMOTE_SERVICE} && sleep 2 && systemctl is-active ${REMOTE_SERVICE}" || {
echo "✗ 服务重启失败,尝试回滚..."
ssh "$REMOTE_HOST" "cp ${REMOTE_PATH}.bak.${TIMESTAMP} ${REMOTE_PATH} && systemctl restart ${REMOTE_SERVICE}"
exit 1
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

SQLite WAL mode corruption when database file is replaced while process is open

💡 Result:

Replacing the main SQLite database file (e.g., db.sqlite) while a process has it open in WAL mode will likely cause corruption or data loss. SQLite connections maintain open file handles to the main database, -wal, and -shm files, using a shared memory region (-shm) and reading changes from the WAL file. Directly overwriting the main file bypasses these mechanisms, leading to inconsistencies between the process's in-memory state, WAL content, and the new main file. Key risks from official documentation: - WAL is part of the database's persistent state; separating the main file from its WAL can cause committed transactions to be lost or the database to corrupt. - File overwrite by external processes is a primary corruption vector. - Processes detect WAL mode via the database header (version bytes 18-19 set to 2); a mismatched replacement file may trigger errors like "file is encrypted or is not a database" or malformed database. Supported safe alternatives: - Use the Online Backup API (sqlite3_backup_init etc.) to create a consistent snapshot without locking the source for extended periods. This copies main + WAL content atomically into a new file. - VACUUM INTO 'newfile'; (SQLite 3.27+) for a vacuumed live copy. - Close all connections, replace files (main + -wal + -shm if present), then reopen. Unsupported hacks (e.g., overwriting -wal/-shm or PRAGMA journal_mode=DELETE tricks) risk corruption and are not recommended, as confirmed in SQLite forums. To replace while processes are open, signal them to close/reopen connections after preparing a consistent replacement via backup API.

Citations:


🏁 Script executed:

# Verify the file exists and check the relevant section
if [ -f scripts/sync-db-push.sh ]; then
  echo "File exists. Showing lines 1-80 with line numbers:"
  head -80 scripts/sync-db-push.sh | cat -n
else
  echo "File not found at scripts/sync-db-push.sh"
  # Try to find it
  fd sync-db-push.sh
fi

Repository: QuantumNous/new-api

Length of output: 2998


🏁 Script executed:

# Search for SQLite configuration or WAL mode settings in the codebase
rg -i "pragma.*journal_mode|wal|synchronous" --type-add 'config:*.{toml,yaml,yml,conf,ini}' -t go -t js -t python -t config

Repository: QuantumNous/new-api

Length of output: 93


🏁 Script executed:

# Search for SQLite configuration or WAL mode settings
rg -i "pragma.*journal_mode|wal|synchronous" --type go --type js

Repository: QuantumNous/new-api

Length of output: 6030


🏁 Script executed:

# Search for database initialization or SQLite pragma settings
rg "gorm\.|db\.|sqlite" --type go -A 3 -B 1 | grep -i "pragma\|journal\|wal\|gorm.Open\|sqlite" | head -40

Repository: QuantumNous/new-api

Length of output: 1257


🏁 Script executed:

# Check the database initialization file for SQLite pragma settings
sed -n '1,100p' model/main.go | cat -n

Repository: QuantumNous/new-api

Length of output: 2976


🏁 Script executed:

# Search more thoroughly for SQLite pragma settings or DSN configuration
rg "sqlite.Open|DSN|ParseTime|pragma|journal_mode" model/main.go -A 5 -B 2

Repository: QuantumNous/new-api

Length of output: 1637


🏁 Script executed:

# Search for SQLite connection string or initialization with parameters
rg "SQLitePath|cache=|mode=" --type go -A 1 -B 1 | head -40

Repository: QuantumNous/new-api

Length of output: 842


🏁 Script executed:

# Check the common/database.go file to see the full SQLite configuration
cat -n common/database.go | grep -A 5 -B 5 SQLitePath

Repository: QuantumNous/new-api

Length of output: 315


Critical data-corruption risk: Database is overwritten while the service is still running.

The script uploads one-api.db (line 61) while the remote service continues running, which is unsafe for SQLite:

  • If WAL mode is enabled (indicated by .db-shm/.db-wal references in the codebase), the running process maintains open file handles to those WAL/SHM files. Overwriting the main DB file creates a mismatch between the file and the WAL, leading to corruption or silent data loss on the next write.
  • scp truncates-and-writes; concurrent writes from the running process during upload create indeterminate file state.
  • Line 39's remote query against a running server may also hold locks and read inconsistent state.

The service must be stopped before upload to release all DB locks and flush any WAL data:

Recommended fix
+# Stop service first to release DB locks and flush WAL
+ssh "$REMOTE_HOST" "systemctl stop ${REMOTE_SERVICE}"
+
 # 1. 在服务器上备份
 TIMESTAMP=$(date +%Y%m%d-%H%M%S)
 echo "==> 服务器端备份现有 db..."
-ssh "$REMOTE_HOST" "cp ${REMOTE_PATH} ${REMOTE_PATH}.bak.${TIMESTAMP}"
+ssh "$REMOTE_HOST" "cp '${REMOTE_PATH}' '${REMOTE_PATH}.bak.${TIMESTAMP}' \
+  && rm -f '${REMOTE_PATH}-wal' '${REMOTE_PATH}-shm'"
 echo "✓ 备份完成: ${REMOTE_PATH}.bak.${TIMESTAMP}"

 # 2. 上传
 echo "==> 上传中..."
 scp -q "$LOCAL_PATH" "${REMOTE_HOST}:${REMOTE_PATH}"
 echo "✓ 上传完成"

 # 3. 重启服务
-echo "==> 重启服务 ${REMOTE_SERVICE}..."
-ssh "$REMOTE_HOST" "systemctl restart ${REMOTE_SERVICE} && sleep 2 && systemctl is-active ${REMOTE_SERVICE}" || {
+echo "==> 启动服务 ${REMOTE_SERVICE}..."
+ssh "$REMOTE_HOST" "systemctl start ${REMOTE_SERVICE} && sleep 2 && systemctl is-active ${REMOTE_SERVICE}" || {
   echo "✗ 服务重启失败,尝试回滚..."
-  ssh "$REMOTE_HOST" "cp ${REMOTE_PATH}.bak.${TIMESTAMP} ${REMOTE_PATH} && systemctl restart ${REMOTE_SERVICE}"
+  ssh "$REMOTE_HOST" "cp '${REMOTE_PATH}.bak.${TIMESTAMP}' '${REMOTE_PATH}' && systemctl start ${REMOTE_SERVICE}"
   exit 1
 }

Also add a trap to handle Ctrl-C between stop and start, which would otherwise leave the service down.

🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 66-66: Note that, unescaped, this expands on the client side.

(SC2029)


[info] 68-68: Note that, unescaped, this expands on the client side.

(SC2029)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync-db-push.sh` around lines 59 - 70, The script uploads the
database while the remote service is still running, risking SQLite WAL/SHM
corruption; modify the flow in the block using REMOTE_SERVICE, REMOTE_HOST,
REMOTE_PATH and scp so you first stop the remote service (systemctl stop
${REMOTE_SERVICE}), verify it is inactive (systemctl is-active), create a backup
of the remote DB (e.g. cp ${REMOTE_PATH} ${REMOTE_PATH}.bak.${TIMESTAMP}), then
perform scp of LOCAL_PATH to REMOTE_PATH, and only then start the service
(systemctl start/restart ${REMOTE_SERVICE}) and verify it became active; add a
trap on the local script to handle interrupts between stop and start that will
attempt to restore the backup and restart the service on failure or Ctrl-C, and
in the existing ssh failure branch ensure you restore the .bak.${TIMESTAMP}
backup before attempting to restart.

Comment on lines +523 to +566
// 弹窗模式:登录成功后获取 token 并回传给 opener
const handlePopupCallback = async (userData) => {
if (!window.opener || !callbackOrigin) {
showSuccess('登录成功!');
navigate('/console');
return;
}
try {
// 1. 获取 Access Token
const tokenRes = await API.get('/api/user/token');
const accessToken = tokenRes.data.success ? tokenRes.data.data : '';

// 2. 获取 API Token 列表
const tokensRes = await API.get('/api/token/');
let apiTokenKey = '';
let tokens = tokensRes.data?.data?.items || tokensRes.data?.data || [];
if (tokens.length === 0) {
// 没有 Token,创建一个
const createRes = await API.post('/api/token/', {
name: 'AionUi Default',
remain_quota: 0,
expired_time: -1,
unlimited_quota: true,
});
if (createRes.data.success) {
const newTokensRes = await API.get('/api/token/');
tokens = newTokensRes.data?.data?.items || newTokensRes.data?.data || [];
}
}
if (tokens.length > 0) {
// 获取第一个 token 的 key
const keyRes = await API.post(`/api/token/${tokens[0].id}/key`);
if (keyRes.data.success) {
apiTokenKey = keyRes.data.data;
}
}

// 3. postMessage 回传
window.opener.postMessage({
type: 'aionui-auth',
accessToken,
apiToken: apiTokenKey,
user: userData,
}, callbackOrigin);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not send tokens to an untrusted callback_origin, and fix token response parsing.

Line 525 trusts callback_origin from the URL, so any site can open the login popup with its own origin and receive accessToken / apiToken after the user logs in. Validate the target origin against a server-controlled allowlist or a signed login state before calling postMessage.

Also, lines 547 and 555 expect { success, data }, but the provided token handlers return { id, key, data } for token creation and the token object directly for /api/token/:id/key; this can send an empty apiToken to the opener.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/auth/LoginForm.jsx` around lines 523 - 566, The
handlePopupCallback flow currently trusts callbackOrigin and mis-parses token
responses; before calling window.opener.postMessage validate callbackOrigin
against a server-controlled allowlist or a server-provided signed state (e.g.,
compare callbackOrigin to a validated origin returned from your login/session
endpoint or verify a signed callback token stored in state) and abort sending
any tokens if validation fails, and only send a minimal user payload when
untrusted. Also fix token parsing: treat the /api/token/ POST response as
returning {id, key, data} (use the returned key if present) and treat the
/api/token/:id/key response as returning the token string directly (or check
both shapes), so set apiTokenKey from keyRes.data.key or keyRes.data.data or
keyRes.data as appropriate, and similarly extract tokens from
tokensRes.data?.data?.items or tokensRes.data?.data ensuring you handle both
array and object shapes; only call /api/token/:id/key when you have a valid
token id and include robust checks before attaching apiToken to the postMessage.

Comment on lines +612 to +618
// Secret field: only send if non-empty
if (inputs['sms.access_key_secret'] !== '') {
options.push({
key: 'sms.access_key_secret',
value: inputs['sms.access_key_secret'],
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Avoid sending an undefined SMS secret.

Because setInputs(newInputs) replaces the initial defaults, inputs['sms.access_key_secret'] can be undefined when the backend does not return secrets. Line 613 then evaluates undefined !== '' as true and sends the secret option with an undefined/missing value; the option endpoint persists whatever is sent, so this can overwrite the existing SMS credential.

Proposed fix
     }
     // Secret field: only send if non-empty
-    if (inputs['sms.access_key_secret'] !== '') {
+    const accessKeySecret = inputs['sms.access_key_secret'] ?? '';
+    if (
+      accessKeySecret !== '' &&
+      originInputs['sms.access_key_secret'] !== accessKeySecret
+    ) {
       options.push({
         key: 'sms.access_key_secret',
-        value: inputs['sms.access_key_secret'],
+        value: accessKeySecret,
       });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/SystemSetting.jsx` around lines 612 - 618, The
current check only compares inputs['sms.access_key_secret'] !== '' which treats
undefined as "not empty" and causes sending an undefined secret; update the
condition in the block that pushes into options (the code that references inputs
and options in SystemSetting.jsx) to only push when the value is both defined
(not null/undefined) and not an empty string (e.g., check
inputs['sms.access_key_secret'] != null && inputs['sms.access_key_secret'] !==
''), so the secret option is only sent when a real value exists.

@Vinoxcn Vinoxcn closed this Apr 20, 2026
@Vinoxcn
Vinoxcn deleted the codex/new-api-skill-market branch April 20, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant