Skip to content

✨ feat(layout): refine footer visibility logic to target CardPro component pages - #1890

Merged
Calcium-Ion merged 158 commits into
mainfrom
pr/console-footer
Oct 2, 2025
Merged

✨ feat(layout): refine footer visibility logic to target CardPro component pages#1890
Calcium-Ion merged 158 commits into
mainfrom
pr/console-footer

Conversation

@t0ng7u

@t0ng7u t0ng7u commented Sep 27, 2025

Copy link
Copy Markdown
Collaborator
  • Replace blanket console route footer hiding with specific page targeting
  • Only hide footer on pages that use CardPro component:
    • /console/channel (channels management)
    • /console/log (usage logs)
    • /console/redemption (redemption codes)
    • /console/user (user management)
    • /console/token (token management)
    • /console/midjourney (midjourney logs)
    • /console/task (task logs)
    • /console/models (model management)
    • /pricing (pricing page)
  • Footer now displays on other console pages (dashboard, settings, topup, etc.)
  • Improves UI consistency by showing footer where CardPro's internal pagination isn't used

This change ensures footer is only hidden when CardPro component provides its own pagination/footer functionality, while preserving footer visibility on other pages that benefit from the global footer navigation.

Summary by CodeRabbit

  • New Features

    • Passkey (WebAuthn): register/login/verify flows, settings UI, account management, admin reset, and universal secure verification modal.
    • Gotify: user notification option with configuration.
    • New channels: SubModel and Doubao Video support; model test endpoint selector and video task handling.
    • Stripe: option to enable promotion codes.
    • Français: French translations and README; language selector and footer updates.
  • Style

    • UI polish and localization refinements across settings, logs, and top-up pages.

wzxjohn and others added 30 commits May 16, 2025 16:44
# Conflicts:
#	common/api_type.go
#	constant/api_type.go
#	constant/channel.go
#	relay/relay_adaptor.go
#	web/src/constants/channel.constants.js
增加并发支持
将连接延迟关闭逻辑调整到协程中执行,防止在完全接收到所有数据前提前关闭
seefs001 and others added 25 commits October 2, 2025 00:36
fix(openai): add nil checks for web_search streaming to prevent panic
fix: missing field & field control
feat: Add Gotify Notification Channel for Quota Alerts
feat: Add visual editing mode for chat configurations
feat: Add navigation buttons for channel edit form sections
fix: get video task err when Content-Type=json
feat(i18n): add and update French translations
…onent pages

- Replace blanket console route footer hiding with specific page targeting
- Only hide footer on pages that use CardPro component:
  * /console/channel (channels management)
  * /console/log (usage logs)
  * /console/redemption (redemption codes)
  * /console/user (user management)
  * /console/token (token management)
  * /console/midjourney (midjourney logs)
  * /console/task (task logs)
  * /console/models (model management)
  * /pricing (pricing page)
- Footer now displays on other console pages (dashboard, settings, topup, etc.)
- Improves UI consistency by showing footer where CardPro's internal pagination isn't used

This change ensures footer is only hidden when CardPro component provides its own
pagination/footer functionality, while preserving footer visibility on other pages
that benefit from the global footer navigation.
…le-footer

# Conflicts:
#	web/src/components/table/channels/modals/EditChannelModal.jsx
#	web/src/hooks/common/useSidebar.js
- Removed the 'chatnio' link from the footer.
- Added new links for 'CoAI' and 'GPT-Load' in the footer.
- Updated the localization key for '基于New API的项目' to '友情链接' for better clarity.
- Adjusted the design of the footer to improve layout and visibility of the developer credit.
@Calcium-Ion
Calcium-Ion merged commit 19bbb7d into main Oct 2, 2025

@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: 34

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web/src/components/settings/PersonalSetting.jsx (2)

335-341: Fix: wrong modal closed after password change

Should close the Change Password modal, not WeChat bind modal.

-      showSuccess(t('密码修改成功!'));
-      setShowWeChatBindModal(false);
+      showSuccess(t('密码修改成功!'));
+      setShowChangePasswordModal(false);

349-356: Bug: early return leaves button disabled when Turnstile token missing

Re-enable the button (and clear loading if set) before returning.

-    setDisableButton(true);
-    if (turnstileEnabled && turnstileToken === '') {
-      showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
-      return;
-    }
+    setDisableButton(true);
+    if (turnstileEnabled && turnstileToken === '') {
+      showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
+      setDisableButton(false);
+      setLoading(false);
+      return;
+    }
🧹 Nitpick comments (52)
README.fr.md (1)

35-41: Remove the blank line inside the blockquote.

Line 37 introduces an empty line within the blockquote, triggering markdownlint rule MD028. Delete that blank line so the rendered admonitions stay contiguous and the docs lint passes. Based on static analysis hints.

docker-compose.yml (1)

7-11: Update MySQL switch instructions.

The guidance here still references the old line numbers (15, 16, 28, 64), so anyone following it will be hunting in the wrong spots. Please rephrase it to reference the actual keys/sections instead of hard-coded line numbers. For example:

-# Using MySQL instead of PostgreSQL:
-#   1. Comment out the postgres service and SQL_DSN line 15
-#   2. Uncomment the mysql service and SQL_DSN line 16
-#   3. Uncomment mysql in depends_on (line 28)
-#   4. Uncomment mysql_data in volumes section (line 64)
+# Using MySQL instead of PostgreSQL:
+#   1. Comment out the postgres service block and the PostgreSQL SQL_DSN entry.
+#   2. Uncomment the mysql service block and the MySQL SQL_DSN entry.
+#   3. Swap the dependency in the depends_on list from postgres to mysql.
+#   4. Swap the volume binding to use mysql_data instead of pg_data.
web/src/components/table/task-logs/modals/ContentModal.jsx (1)

117-126: Add accessibility attributes to video element.

The video element lacks accessibility attributes for screen readers. Adding aria-label improves the experience for users with assistive technologies.

Apply this diff:

 <video 
   src={modalContent} 
   controls 
   style={{ width: '100%' }} 
   autoPlay
   crossOrigin="anonymous"
+  aria-label="Video content"
   onError={handleVideoError}
   onLoadedData={handleVideoLoaded}
   onLoadStart={() => setIsLoading(true)}
 />
go.mod (1)

24-24: Two jwt majors in use (v3 and v5). Consider consolidating to v5.

Mixed majors increase binary size and confusion; migrate imports to github.com/golang-jwt/jwt/v5 where feasible.

Also applies to: 71-71

relay/channel/ollama/adaptor.go (1)

44-47: URL selection logic LGTM; optional readability tweak.

Consider a switch on RelayMode with a fallback to path sniffing for maintainability.

dto/openai_request.go (1)

793-804: Confirm intended type of prompt_cache_key across APIs

GeneralOpenAIRequest uses string for prompt_cache_key, while OpenAIResponsesRequest uses json.RawMessage. If both should be strings, align types; if Responses allows structured values, add a brief comment to avoid confusion.

web/src/components/table/channels/modals/EditChannelModal.jsx (2)

676-701: Rename for clarity and remove legacy 2FA remnants

handleShow2FAModal now drives the unified verification flow. Rename to handleShowVerificationModal and clean up unused 2FA state to avoid confusion.

I can generate a small patch to rename the handler and update callers if you want.


2063-2086: Optional: prefer initValue over defaultValue in controlled Form

Since this field is managed by Form via field='base_url', defaultValue can be redundant. Consider using initValue or relying on values set in loadChannel/handleInputChange.

dto/gemini.go (1)

254-254: Consider documenting the expected structure for URLContext.

The URLContext field uses type any, which provides maximum flexibility but no type safety or documentation about the expected structure. Based on the AI summary mentioning make(map[string]string) in the relay flow, consider:

  1. Adding a code comment documenting the expected type and purpose
  2. Using a more specific type like map[string]string if the structure is known

Example documentation:

 type GeminiChatTool struct {
 	GoogleSearch          any `json:"googleSearch,omitempty"`
 	GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"`
 	CodeExecution         any `json:"codeExecution,omitempty"`
 	FunctionDeclarations  any `json:"functionDeclarations,omitempty"`
+	// URLContext carries optional URL context data for Gemini tool calls.
+	// Expected type: map[string]string
 	URLContext            any `json:"urlContext,omitempty"`
 }
web/src/pages/Setting/Model/SettingClaudeModel.jsx (1)

201-214: Consider adding an upper bound or validation for the budget percentage.

The removal of the max={1} constraint allows users to enter budget percentages greater than 100%, which could lead to:

  1. Budget tokens significantly exceeding max tokens (e.g., 5x multiplier)
  2. Unexpected behavior in downstream Claude API calls
  3. Potential resource exhaustion or cost issues

Unless there's a specific use case for >100% budget percentages, consider either:

  1. Re-adding a reasonable maximum (e.g., max={2} for 200%)
  2. Adding form validation to warn users about large values
  3. Documenting the expected range and implications
 <Form.InputNumber
   label={t('思考适配 BudgetTokens 百分比')}
   field={'claude.thinking_adapter_budget_tokens_percentage'}
   initValue={''}
   extraText={t('0.1以上的小数')}
   min={0.1}
+  max={2}  // or another reasonable upper bound
   onChange={(value) =>
     setInputs({
       ...inputs,
       'claude.thinking_adapter_budget_tokens_percentage': value,
     })
   }
 />
web/src/components/table/users/modals/ResetTwoFAModal.jsx (1)

23-36: Consider adding PropTypes validation.

The component would benefit from PropTypes validation to ensure type safety and improve maintainability.

Apply this diff to add PropTypes:

 import React from 'react';
 import { Modal } from '@douyinfe/semi-ui';
+import PropTypes from 'prop-types';

 const ResetTwoFAModal = ({ visible, onCancel, onConfirm, user, t }) => {
   return (
     <Modal
       title={t('确认重置两步验证')}
       visible={visible}
       onCancel={onCancel}
       onOk={onConfirm}
       type='warning'
     >
       {t('此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。')}{' '}
       {user?.username ? t('目标用户:{{username}}', { username: user.username }) : ''}
     </Modal>
   );
 };

+ResetTwoFAModal.propTypes = {
+  visible: PropTypes.bool.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onConfirm: PropTypes.func.isRequired,
+  user: PropTypes.shape({
+    username: PropTypes.string,
+  }),
+  t: PropTypes.func.isRequired,
+};
+
 export default ResetTwoFAModal;
relay/channel/openai/relay-openai.go (1)

189-209: LGTM with minor suggestion.

The enterprise pre-processing logic for OpenRouter is well-structured with proper error handling. The conditional check, unmarshaling, and data replacement flow are correct.

Consider improving the error log at line 200. Currently, enterpriseResponse.Data is logged as raw JSON bytes, which may not be human-readable:

-		logger.LogError(c, fmt.Sprintf("openrouter enterprise response success=false, data: %s", enterpriseResponse.Data))
+		logger.LogError(c, fmt.Sprintf("openrouter enterprise response success=false, data: %s", string(enterpriseResponse.Data)))

This ensures the log message is readable when debugging enterprise response failures.

web/src/components/table/users/modals/ResetPasskeyModal.jsx (1)

23-36: Consider adding PropTypes validation.

Similar to ResetTwoFAModal, this component would benefit from PropTypes validation for type safety and maintainability.

Apply this diff to add PropTypes:

 import React from 'react';
 import { Modal } from '@douyinfe/semi-ui';
+import PropTypes from 'prop-types';

 const ResetPasskeyModal = ({ visible, onCancel, onConfirm, user, t }) => {
   return (
     <Modal
       title={t('确认重置 Passkey')}
       visible={visible}
       onCancel={onCancel}
       onOk={onConfirm}
       type='warning'
     >
       {t('此操作将解绑用户当前的 Passkey,下次登录需要重新注册。')}{' '}
       {user?.username ? t('目标用户:{{username}}', { username: user.username }) : ''}
     </Modal>
   );
 };

+ResetPasskeyModal.propTypes = {
+  visible: PropTypes.bool.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onConfirm: PropTypes.func.isRequired,
+  user: PropTypes.shape({
+    username: PropTypes.string,
+  }),
+  t: PropTypes.func.isRequired,
+};
+
 export default ResetPasskeyModal;
web/src/hooks/users/useUsersData.jsx (2)

157-172: Consider logging the error for debugging.

The catch block shows a generic error message to the user, which is appropriate for UX. However, the actual error object is lost and not logged for debugging purposes.

Apply this diff to preserve error information:

     } catch (error) {
+      console.error('Failed to reset passkey:', error);
       showError(t('操作失败,请重试'));
     }

174-189: Consider logging the error for debugging.

The catch block shows a generic error message to the user, which is appropriate for UX. However, the actual error object is lost and not logged for debugging purposes.

Apply this diff to preserve error information:

     } catch (error) {
+      console.error('Failed to reset 2FA:', error);
       showError(t('操作失败,请重试'));
     }
router/api-router.go (1)

68-73: Consider rate limiting on sensitive passkey operations.

The passkey registration, verification, and deletion endpoints are under UserAuth but lack explicit rate limiting. Consider whether these operations should have CriticalRateLimit or require SecureVerificationRequired middleware (similar to the channel key endpoint on line 131) to prevent abuse.

For example, if deletion should require additional verification:

-				selfRoute.DELETE("/passkey", controller.PasskeyDelete)
+				selfRoute.DELETE("/passkey", middleware.SecureVerificationRequired(), controller.PasskeyDelete)
web/src/components/table/channels/modals/ModelTestModal.jsx (3)

65-74: Localize all option labels and confirm value tokens match backend

  • Labels mix localized and hardcoded English. Wrap all labels with t() for consistency.
  • Verify that endpoint_type values (e.g., "openai-response", "jina-rerank") match backend expectations.

Apply this diff to localize labels:

-  const endpointTypeOptions = [
-    { value: '', label: t('自动检测') },
-    { value: 'openai', label: 'OpenAI (/v1/chat/completions)' },
-    { value: 'openai-response', label: 'OpenAI Response (/v1/responses)' },
-    { value: 'anthropic', label: 'Anthropic (/v1/messages)' },
-    { value: 'gemini', label: 'Gemini (/v1beta/models/{model}:generateContent)' },
-    { value: 'jina-rerank', label: 'Jina Rerank (/rerank)' },
-    { value: 'image-generation', label: t('图像生成') + ' (/v1/images/generations)' },
-    { value: 'embeddings', label: 'Embeddings (/v1/embeddings)' },
-  ];
+  const endpointTypeOptions = [
+    { value: '', label: t('自动检测') },
+    { value: 'openai', label: t('OpenAI (/v1/chat/completions)') },
+    { value: 'openai-response', label: t('OpenAI Response (/v1/responses)') },
+    { value: 'anthropic', label: t('Anthropic (/v1/messages)') },
+    { value: 'gemini', label: t('Gemini (/v1beta/models/{model}:generateContent)') },
+    { value: 'jina-rerank', label: t('Jina Rerank (/rerank)') },
+    { value: 'image-generation', label: t('图像生成 (/v1/images/generations)') },
+    { value: 'embeddings', label: t('Embeddings (/v1/embeddings)') },
+  ];

169-171: Result key may conflate tests across endpoint types

modelTestResults are keyed by channelId-model. If you test the same model with different endpoint types, results will overwrite. Consider including endpointType in the key to disambiguate, or clarify that overwriting is intended.


246-255: Optional: prevent re-creation of options each render

Memoize endpointTypeOptions via useMemo to avoid unnecessary re-allocations on rerenders. Low impact, optional.

web/src/components/common/examples/ChannelKeyViewExample.jsx (1)

58-67: Consider handling startVerification return value

startVerification returns false when no methods are available. Optionally check the return and early-bail to avoid misleading UI flows (though your fallback modal already covers this).

service/passkey/session.go (2)

12-12: Export not-found error for callers to type-check

Expose ErrSessionNotFound so handlers can use errors.Is(err, ErrSessionNotFound).

Apply this diff:

- var errSessionNotFound = errors.New("Passkey 会话不存在或已过期")
+ var ErrSessionNotFound = errors.New("Passkey 会话不存在或已过期")

And update usage:

- if raw == nil {
-   return nil, errSessionNotFound
- }
+ if raw == nil {
+   return nil, ErrSessionNotFound
+ }

Also applies to: 32-33


20-25: Prefer project JSON helpers for consistency

The repo exposes common/json wrappers. Consider switching to them for uniform behavior (e.g., UseNumber, HTML escaping config).

Based on learnings

Also applies to: 39-44

web/src/components/common/modals/SecureVerificationModal.jsx (2)

116-123: Prevent closing via mask during verification

Add maskClosable={!loading} to avoid accidental close while verifying.

Apply this diff:

 <Modal
   title={title || t('安全验证')}
   visible={visible}
   onCancel={loading ? undefined : onCancel}
   closeOnEsc={!loading}
+  maskClosable={!loading}
   footer={null}
   width={460}
   centered

50-52: Remove unused local state

isAnimating and verifySuccess are set but never used. Safe to remove.

Also applies to: 56-63

web/src/components/settings/SystemSetting.jsx (2)

80-87: Initialize passkey.origins as a string, not array

getOptions treats passkey.origins as a comma-separated string. Initialize consistently to avoid type mismatches.

Apply this diff:

-    'passkey.origins': [],
+    'passkey.origins': '',

610-639: Normalize and validate Passkey settings before save

  • Trim and sanitize rp_id (strip scheme/path).
  • Normalize origins: split by comma, trim, remove trailing slashes, ensure https://, dedupe, and re-join. Show a clear error if invalid.

Apply this diff:

-  const submitPasskeySettings = async () => {
-    // 使用formApi直接获取当前表单值
-    const formValues = formApiRef.current?.getValues() || {};
-
-    const options = [];
-
-    options.push({
-      key: 'passkey.rp_display_name',
-      value: formValues['passkey.rp_display_name'] || inputs['passkey.rp_display_name'] || '',
-    });
-    options.push({
-      key: 'passkey.rp_id',
-      value: formValues['passkey.rp_id'] || inputs['passkey.rp_id'] || '',
-    });
-    options.push({
-      key: 'passkey.user_verification',
-      value: formValues['passkey.user_verification'] || inputs['passkey.user_verification'] || 'preferred',
-    });
-    options.push({
-      key: 'passkey.attachment_preference',
-      value: formValues['passkey.attachment_preference'] || inputs['passkey.attachment_preference'] || '',
-    });
-    options.push({
-      key: 'passkey.origins',
-      value: formValues['passkey.origins'] || inputs['passkey.origins'] || '',
-    });
-
-    await updateOptions(options);
-  };
+  const submitPasskeySettings = async () => {
+    const formValues = formApiRef.current?.getValues() || {};
+
+    // rp_id: strip scheme and path, keep hostname-ish string
+    let rpId = (formValues['passkey.rp_id'] ?? inputs['passkey.rp_id'] ?? '').trim();
+    rpId = rpId.replace(/^https?:\/\//i, '').replace(/\/.*$/, '');
+
+    // origins: normalize list
+    const rawOrigins = (formValues['passkey.origins'] ?? inputs['passkey.origins'] ?? '').trim();
+    const origins = Array.from(
+      new Set(
+        rawOrigins
+          .split(',')
+          .map(s => s.trim())
+          .filter(Boolean)
+          .map(o => removeTrailingSlash(o))
+      )
+    );
+    const invalid = origins.filter(o => !/^https:\/\//i.test(o));
+    if (invalid.length > 0) {
+      showError(t('允许的 Origins 必须以 https:// 开头:') + invalid.join(', '));
+      return;
+    }
+
+    const options = [
+      { key: 'passkey.rp_display_name', value: (formValues['passkey.rp_display_name'] ?? inputs['passkey.rp_display_name'] ?? '').trim() },
+      { key: 'passkey.rp_id', value: rpId },
+      { key: 'passkey.user_verification', value: (formValues['passkey.user_verification'] ?? inputs['passkey.user_verification'] ?? 'preferred') },
+      { key: 'passkey.attachment_preference', value: (formValues['passkey.attachment_preference'] ?? inputs['passkey.attachment_preference'] ?? '') },
+      { key: 'passkey.origins', value: origins.join(',') },
+    ];
+
+    await updateOptions(options);
+  };
relay/common/relay_info.go (1)

108-110: Consider surfacing IsClaudeBetaQuery in logs.

Including IsClaudeBetaQuery in ToString aids debugging production issues tied to beta behavior.

web/src/components/auth/LoginForm.jsx (1)

280-327: Enable conditional mediation for smoother UX where supported.

Pass { mediation: 'conditional' } to credentials.get for auto‑fill sign‑in on Chrome/Android.

- const assertion = await navigator.credentials.get({ publicKey: publicKeyOptions });
+ const assertion = await navigator.credentials.get({
+   publicKey: publicKeyOptions,
+   // ignored by non‑supporting browsers
+   mediation: 'conditional',
+ });
service/http_client.go (2)

66-74: Harden transport defaults (timeouts, pooling).

Set sensible Transport fields to avoid connection leaks and long stalls.

- client := &http.Client{
-   Transport: &http.Transport{
-     Proxy: http.ProxyURL(parsedURL),
-   },
- }
+ tr := &http.Transport{
+   Proxy: http.ProxyURL(parsedURL),
+   MaxIdleConns:        100,
+   MaxIdleConnsPerHost: 10,
+   IdleConnTimeout:     90 * time.Second,
+   TLSHandshakeTimeout: 10 * time.Second,
+   ExpectContinueTimeout: 1 * time.Second,
+ }
+ client := &http.Client{ Transport: tr }
  client.Timeout = time.Duration(common.RelayTimeout) * time.Second

99-110: SOCKS5: honor timeouts and dialing context.

Provide a net.Dialer with timeouts to SOCKS5 and reuse it in Transport.

- dialer, err := proxy.SOCKS5("tcp", parsedURL.Host, auth, proxy.Direct)
+ base := &net.Dialer{
+   Timeout:   30 * time.Second,
+   KeepAlive: 30 * time.Second,
+ }
+ dialer, err := proxy.SOCKS5("tcp", parsedURL.Host, auth, base)
  if err != nil { return nil, err }
- client := &http.Client{
-   Transport: &http.Transport{
-     DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
-       return dialer.Dial(network, addr)
-     },
-   },
- }
+ tr := &http.Transport{
+   DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+     // proxy.Dialer lacks DialContext; use base timeouts and honor client.Timeout
+     return dialer.Dial(network, addr)
+   },
+   MaxIdleConns:        100,
+   MaxIdleConnsPerHost: 10,
+   IdleConnTimeout:     90 * time.Second,
+   TLSHandshakeTimeout: 10 * time.Second,
+}
+ client := &http.Client{ Transport: tr }
  client.Timeout = time.Duration(common.RelayTimeout) * time.Second
web/src/pages/Setting/Chat/SettingsChats.jsx (2)

226-236: Stronger duplicate check (trim + case‑insensitive).

Avoid accidental duplicates caused by casing or whitespace.

- const isDuplicate = chatConfigs.some(
-   (config) =>
-     config.name === values.name &&
-     (!isEdit || config.id !== editingConfig.id)
- );
+ const newName = (values.name || '').trim().toLowerCase();
+ const isDuplicate = chatConfigs.some((config) => {
+   const existing = (config.name || '').trim().toLowerCase();
+   return existing === newName && (!isEdit || config.id !== editingConfig.id);
+ });

490-495: Validate URL format (permit placeholders).

Guard against invalid URLs while allowing {address}/{key}.

- rules={[{ required: true, message: t('请输入URL链接') }]}
+ rules={[
+   { required: true, message: t('请输入URL链接') },
+   {
+     validator: (_, value) => {
+       const v = (value || '').trim()
+         .replace('{address}', 'https://example.com')
+         .replace('{key}', 'sk-xxxx');
+       try { new URL(v); return true; } catch { return false; }
+     },
+     message: t('请输入有效的 URL'),
+   },
+ ]}
web/src/components/settings/personal/cards/AccountManagement.jsx (1)

527-551: Optional: add aria-labels for accessibility.

Improve SR UX on security‑critical actions.

Example:

<Button aria-label={passkeyEnabled ? t('解绑 Passkey') : t('注册 Passkey')} ...>
service/passkey/service.go (2)

104-109: Allow IPv6 loopback for HTTP (when insecure origins are disallowed, treat ::1 as local).

Currently only localhost/127.0.0.1 are exempt. Include [::1] variants.

-	if scheme == "http" && !settings.AllowInsecureOrigin && r.Host != "localhost" && r.Host != "127.0.0.1" && !strings.HasPrefix(r.Host, "127.0.0.1:") && !strings.HasPrefix(r.Host, "localhost:") {
+	if scheme == "http" && !settings.AllowInsecureOrigin &&
+		r.Host != "localhost" && r.Host != "127.0.0.1" && r.Host != "[::1]" &&
+		!strings.HasPrefix(r.Host, "127.0.0.1:") &&
+		!strings.HasPrefix(r.Host, "localhost:") &&
+		!strings.HasPrefix(strings.ToLower(r.Host), "[::1]:") {

47-51: Resident Key set to “required” by default may reduce compatibility.

Consider making ResidentKey/RequireResidentKey configurable via settings, defaulting to “preferred” to avoid excluding authenticators.

dto/claude.go (2)

198-206: Tools typed as ‘any’ may break downstream processing and token counting.

JSON decoding will yield []map[...] rather than []Tool/[]ClaudeWebSearchTool; GetTools/ProcessTools won’t match, so counts may be off.

  • Change Tools to []any for clearer intent and compatibility with AddTool/GetTools.
  • Extend ProcessTools to accept map-shaped entries (convert to Tool/ClaudeWebSearchTool before counting).

Example change to field type:

-	Tools             any             `json:"tools,omitempty"`
+	Tools             []any           `json:"tools,omitempty"`

And in ProcessTools, detect map[string]any and coerce to the expected struct fields before counting (can be added outside this hunk).


205-207: ServiceTier passthrough can escalate costs. Gate and sanitize.

If upstream accepts “service_tier”, enforce allowlist and strip disallowed values at handler layer (e.g., in relay/claude_handler.go) or default to safest tier.

web/src/components/table/users/UsersColumnDefs.jsx (1)

282-293: Add accessible label to the “more” icon button.

Improve a11y with aria-label/title.

-        <Button
+        <Button
           type='tertiary'
           size='small'
           icon={<IconMore />}
+          aria-label={t('更多操作')}
+          title={t('更多操作')}
         />
web/src/hooks/channels/useChannelsData.jsx (2)

807-813: results array is unused. Remove to reduce noise.

-      const results = [];
+      // no need to collect results; we update state per test

695-715: Short-circuiting when stopped should also clear per-test “in-progress” state.

If early-returning due to stop, ensure you don’t leave stale testing flags.

-    if (shouldStopBatchTestingRef.current && isBatchTesting) {
-      return Promise.resolve();
-    }
+    if (shouldStopBatchTestingRef.current && isBatchTesting) {
+      return Promise.resolve();
+    }

Consider moving the testingModels add/remove strictly around the actual request to avoid adding when already stopping, or add a guard to skip adding when stop is set.

relay/channel/ollama/relay-ollama.go (1)

79-89: SSRF protections are already applied in DoDownloadRequest via common.ValidateURLWithFetchSetting (enforcing HTTP(S), domain/IP allowlists) and oversized downloads are prevented with io.LimitReader. Ensure your FetchSetting has EnableSSRFProtection=true and AllowPrivateIp=false. For improved resilience, switch from http.Get to http_client.GetHttpClient() (with Timeout) and add a CheckRedirect handler to re-validate redirected URLs.

middleware/secure_verification.go (2)

20-21: Doc/status mismatch: comment says 401 but code returns 403

Update the comment to reflect the actual behavior (403 on missing/expired verification).

-// 如果未验证或验证已过期,返回 401 错误
+// 如果未验证或验证已过期,返回 403 错误

51-53: Handle session.Save() errors (log at least)

Swallowing Save errors hides state drift. Log failures to aid debugging.

-            _ = session.Save()
+            if err := session.Save(); err != nil {
+                // TODO: inject logger and log save failure
+            }

Also applies to: 66-68, 130-131

web/src/services/secureVerification.js (1)

120-124: Guard against missing WebAuthn APIs before calling navigator.credentials.get

Avoid TypeError in unsupported environments even if called accidentally.

-      const credential = await navigator.credentials.get({ publicKey });
+      if (
+        !window?.PublicKeyCredential ||
+        !navigator?.credentials?.get
+      ) {
+        throw new Error('当前环境不支持 Passkey/WebAuthn');
+      }
+      const credential = await navigator.credentials.get({ publicKey });
relay/channel/ollama/stream.go (1)

75-80: Optionally tolerate occasional malformed lines instead of aborting stream

Consider logging and continuing on JSON decode errors to avoid tearing down client streams when a single line is malformed.

web/src/components/settings/PersonalSetting.jsx (1)

146-166: Harden parsing of user settings JSON

JSON.parse can throw and break the page. Add try/catch fallback.

-    if (userState?.user?.setting) {
-      const settings = JSON.parse(userState.user.setting);
+    if (userState?.user?.setting) {
+      let settings = {};
+      try {
+        settings = JSON.parse(userState.user.setting);
+      } catch (e) {
+        settings = {};
+      }
       setNotificationSettings({
         warningType: settings.notify_type || 'email',
         warningThreshold: settings.quota_warning_threshold || 500000,
         webhookUrl: settings.webhook_url || '',
         webhookSecret: settings.webhook_secret || '',
         notificationEmail: settings.notification_email || '',
         barkUrl: settings.bark_url || '',
         gotifyUrl: settings.gotify_url || '',
         gotifyToken: settings.gotify_token || '',
         gotifyPriority:
           settings.gotify_priority !== undefined
             ? settings.gotify_priority
             : 5,
         acceptUnsetModelRatioModel:
           settings.accept_unset_model_ratio_model || false,
         recordIpLog: settings.record_ip_log || false,
       });
     }
web/src/helpers/passkey.js (1)

1-26: Looks good; consider SSR-safe atob/btoa guards (optional)

Helpers are correct for browsers. If ever imported server‑side, gate window.atob/btoa via globalThis detection.

relay/channel/task/doubao/adaptor.go (5)

143-145: Avoid writing HTTP response inside adaptor; let the caller/controller respond

Writing c.JSON here can cause double-writes or break response composition. Prefer returning taskID/taskData and let the higher layer send the response.

Would you confirm the project’s adaptor contract? If the controller is responsible for responding, please remove the c.JSON call (as in the diff in the previous comment).


86-88: Make URL construction robust to trailing slash in baseURL

Prevent accidental double slashes by trimming the base URL.

Apply this diff (also adds strings import in the imports block):

-  return fmt.Sprintf("%s/api/v3/contents/generations/tasks", a.baseURL), nil
+  base := strings.TrimRight(a.baseURL, "/")
+  return base + "/api/v3/contents/generations/tasks", nil

And add import:

 import (
   "bytes"
   "encoding/json"
   "fmt"
   "io"
   "net/http"
   "one-api/constant"
   "one-api/dto"
   "one-api/model"
   "one-api/relay/channel"
   relaycommon "one-api/relay/common"
   "one-api/service"
+  "strings"

161-164: GET requests don’t need Content-Type

Setting Content-Type on a GET is unnecessary; some servers treat it oddly. Keep Accept only.

Apply this diff:

   req.Header.Set("Accept", "application/json")
-  req.Header.Set("Content-Type", "application/json")
   req.Header.Set("Authorization", "Bearer "+key)

176-181: Model may be required by upstream; validate early

If Doubao requires a model, fail fast with a clear error instead of letting upstream 4xx bubble up generically.

Apply this diff (requires strings import already added above):

 func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*requestPayload, error) {
-  r := requestPayload{
+  r := requestPayload{
     Model:   req.Model,
     Content: []ContentItem{},
   }
+  if strings.TrimSpace(r.Model) == "" {
+      return nil, fmt.Errorf("model is required")
+  }

117-121: Upstream request retries may reuse body incorrectly

channel.DoTaskApiRequest sets req.GetBody = io.NopCloser(requestBody), which returns the same reader, not a fresh one. Retries/redirects may fail to resend the body.

Consider updating DoTaskApiRequest (relay/channel/api_request.go) to buffer the payload and return a new bytes.Reader in GetBody, e.g., cache body bytes once and set:

  • req.Body = io.NopCloser(bytes.NewReader(buf))
  • req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(buf)), nil }

Comment on lines +256 to +305
switch info.RelayMode {
case relayconstant.RelayModeEmbeddings:
// Embedding 请求 - request 已经是正确的类型
if embeddingReq, ok := request.(*dto.EmbeddingRequest); ok {
convertedRequest, err = adaptor.ConvertEmbeddingRequest(c, info, *embeddingReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid embedding request type"),
newAPIError: types.NewError(errors.New("invalid embedding request type"), types.ErrorCodeConvertRequestFailed),
}
}
imageRequest := dto.ImageRequest{
Prompt: prompt,
Model: request.Model,
N: uint(request.N),
Size: request.Size,
case relayconstant.RelayModeImagesGenerations:
// 图像生成请求 - request 已经是正确的类型
if imageReq, ok := request.(*dto.ImageRequest); ok {
convertedRequest, err = adaptor.ConvertImageRequest(c, info, *imageReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid image request type"),
newAPIError: types.NewError(errors.New("invalid image request type"), types.ErrorCodeConvertRequestFailed),
}
}
case relayconstant.RelayModeRerank:
// Rerank 请求 - request 已经是正确的类型
if rerankReq, ok := request.(*dto.RerankRequest); ok {
convertedRequest, err = adaptor.ConvertRerankRequest(c, info.RelayMode, *rerankReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid rerank request type"),
newAPIError: types.NewError(errors.New("invalid rerank request type"), types.ErrorCodeConvertRequestFailed),
}
}
case relayconstant.RelayModeResponses:
// Response 请求 - request 已经是正确的类型
if responseReq, ok := request.(*dto.OpenAIResponsesRequest); ok {
convertedRequest, err = adaptor.ConvertOpenAIResponsesRequest(c, info, *responseReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid response request type"),
newAPIError: types.NewError(errors.New("invalid response request type"), types.ErrorCodeConvertRequestFailed),
}
}
default:
// Chat/Completion 等其他请求类型
if generalReq, ok := request.(*dto.GeneralOpenAIRequest); ok {
convertedRequest, err = adaptor.ConvertOpenAIRequest(c, info, generalReq)
} else {

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

Respect adaptor Convert signatures (pass pointers).*

ConvertEmbeddingRequest, ConvertImageRequest, ConvertRerankRequest, and ConvertOpenAIResponsesRequest all expect pointer arguments. Dereferencing the typed requests here hands over copies, so any adaptor mutating the request or relying on pointer semantics will panic or misbehave (and today it’s a compile error). Pass the pointers directly.

-		if embeddingReq, ok := request.(*dto.EmbeddingRequest); ok {
-			convertedRequest, err = adaptor.ConvertEmbeddingRequest(c, info, *embeddingReq)
+		if embeddingReq, ok := request.(*dto.EmbeddingRequest); ok {
+			convertedRequest, err = adaptor.ConvertEmbeddingRequest(c, info, embeddingReq)-		if imageReq, ok := request.(*dto.ImageRequest); ok {
-			convertedRequest, err = adaptor.ConvertImageRequest(c, info, *imageReq)
+		if imageReq, ok := request.(*dto.ImageRequest); ok {
+			convertedRequest, err = adaptor.ConvertImageRequest(c, info, imageReq)-		if rerankReq, ok := request.(*dto.RerankRequest); ok {
-			convertedRequest, err = adaptor.ConvertRerankRequest(c, info.RelayMode, *rerankReq)
+		if rerankReq, ok := request.(*dto.RerankRequest); ok {
+			convertedRequest, err = adaptor.ConvertRerankRequest(c, info.RelayMode, rerankReq)-		if responseReq, ok := request.(*dto.OpenAIResponsesRequest); ok {
-			convertedRequest, err = adaptor.ConvertOpenAIResponsesRequest(c, info, *responseReq)
+		if responseReq, ok := request.(*dto.OpenAIResponsesRequest); ok {
+			convertedRequest, err = adaptor.ConvertOpenAIResponsesRequest(c, info, responseReq)
📝 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.

Suggested change
switch info.RelayMode {
case relayconstant.RelayModeEmbeddings:
// Embedding 请求 - request 已经是正确的类型
if embeddingReq, ok := request.(*dto.EmbeddingRequest); ok {
convertedRequest, err = adaptor.ConvertEmbeddingRequest(c, info, *embeddingReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid embedding request type"),
newAPIError: types.NewError(errors.New("invalid embedding request type"), types.ErrorCodeConvertRequestFailed),
}
}
imageRequest := dto.ImageRequest{
Prompt: prompt,
Model: request.Model,
N: uint(request.N),
Size: request.Size,
case relayconstant.RelayModeImagesGenerations:
// 图像生成请求 - request 已经是正确的类型
if imageReq, ok := request.(*dto.ImageRequest); ok {
convertedRequest, err = adaptor.ConvertImageRequest(c, info, *imageReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid image request type"),
newAPIError: types.NewError(errors.New("invalid image request type"), types.ErrorCodeConvertRequestFailed),
}
}
case relayconstant.RelayModeRerank:
// Rerank 请求 - request 已经是正确的类型
if rerankReq, ok := request.(*dto.RerankRequest); ok {
convertedRequest, err = adaptor.ConvertRerankRequest(c, info.RelayMode, *rerankReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid rerank request type"),
newAPIError: types.NewError(errors.New("invalid rerank request type"), types.ErrorCodeConvertRequestFailed),
}
}
case relayconstant.RelayModeResponses:
// Response 请求 - request 已经是正确的类型
if responseReq, ok := request.(*dto.OpenAIResponsesRequest); ok {
convertedRequest, err = adaptor.ConvertOpenAIResponsesRequest(c, info, *responseReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid response request type"),
newAPIError: types.NewError(errors.New("invalid response request type"), types.ErrorCodeConvertRequestFailed),
}
}
default:
// Chat/Completion 等其他请求类型
if generalReq, ok := request.(*dto.GeneralOpenAIRequest); ok {
convertedRequest, err = adaptor.ConvertOpenAIRequest(c, info, generalReq)
} else {
switch info.RelayMode {
case relayconstant.RelayModeEmbeddings:
// Embedding 请求 - request 已经是正确的类型
if embeddingReq, ok := request.(*dto.EmbeddingRequest); ok {
convertedRequest, err = adaptor.ConvertEmbeddingRequest(c, info, embeddingReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid embedding request type"),
newAPIError: types.NewError(errors.New("invalid embedding request type"), types.ErrorCodeConvertRequestFailed),
}
}
case relayconstant.RelayModeImagesGenerations:
// 图像生成请求 - request 已经是正确的类型
if imageReq, ok := request.(*dto.ImageRequest); ok {
convertedRequest, err = adaptor.ConvertImageRequest(c, info, imageReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid image request type"),
newAPIError: types.NewError(errors.New("invalid image request type"), types.ErrorCodeConvertRequestFailed),
}
}
case relayconstant.RelayModeRerank:
// Rerank 请求 - request 已经是正确的类型
if rerankReq, ok := request.(*dto.RerankRequest); ok {
convertedRequest, err = adaptor.ConvertRerankRequest(c, info.RelayMode, rerankReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid rerank request type"),
newAPIError: types.NewError(errors.New("invalid rerank request type"), types.ErrorCodeConvertRequestFailed),
}
}
case relayconstant.RelayModeResponses:
// Response 请求 - request 已经是正确的类型
if responseReq, ok := request.(*dto.OpenAIResponsesRequest); ok {
convertedRequest, err = adaptor.ConvertOpenAIResponsesRequest(c, info, responseReq)
} else {
return testResult{
context: c,
localErr: errors.New("invalid response request type"),
newAPIError: types.NewError(errors.New("invalid response request type"), types.ErrorCodeConvertRequestFailed),
}
}
default:
// Chat/Completion 等其他请求类型
if generalReq, ok := request.(*dto.GeneralOpenAIRequest); ok {
convertedRequest, err = adaptor.ConvertOpenAIRequest(c, info, generalReq)
} else {
// ...
}
}
🤖 Prompt for AI Agents
In controller/channel-test.go around lines 256 to 305, the adaptor Convert*
calls are passing dereferenced request values (copies) which violate the adaptor
signatures that expect pointers; update the calls to pass the typed request
pointers (e.g., pass embeddingReq, imageReq, rerankReq, responseReq) instead of
*embeddingReq, *imageReq, *rerankReq, *responseReq so the adaptor receives the
correct pointer types and pointer semantics are preserved.

Comment thread controller/passkey.go
Comment on lines +419 to +477
func PasskeyVerifyFinish(c *gin.Context) {
if !system_setting.GetPasskeySettings().Enabled {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员未启用 Passkey 登录",
})
return
}

user, err := getSessionUser(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": err.Error(),
})
return
}

wa, err := passkeysvc.BuildWebAuthn(c.Request)
if err != nil {
common.ApiError(c, err)
return
}

credential, err := model.GetPasskeyByUserID(user.Id)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该用户尚未绑定 Passkey",
})
return
}

sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.VerifySessionKey)
if err != nil {
common.ApiError(c, err)
return
}

waUser := passkeysvc.NewWebAuthnUser(user, credential)
_, err = wa.FinishLogin(waUser, *sessionData, c.Request)
if err != nil {
common.ApiError(c, err)
return
}

// 更新凭证的最后使用时间
now := time.Now()
credential.LastUsedAt = &now
if err := model.UpsertPasskeyCredential(credential); err != nil {
common.ApiError(c, err)
return
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Passkey 验证成功",
})
}

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 | 🟠 Major

Secure verification never marked

PasskeyVerifyFinish completes WebAuthn but does not call PasskeyVerifyAndSetSession (or otherwise persist the secure-verification timestamp). Yet the frontend expects /api/user/passkey/verify/finish to mark the session verified before posting /api/verify. Without setting the session, the follow-up /api/verify {method:"passkey"} currently passes because of the earlier bug; once we fix that, this controller must store the secure-verification timestamp at the end of PasskeyVerifyFinish.

🤖 Prompt for AI Agents
In controller/passkey.go around lines 419 to 477, after successful
wa.FinishLogin and before sending the final JSON response, call the existing
PasskeyVerifyAndSetSession helper to persist the secure-verification timestamp
(and handle its error like other ops using common.ApiError); ensure the call
runs only on successful verification and return on error so the session is
marked as verified before responding.

Comment on lines +92 to +111
case "passkey":
if !hasPasskey {
common.ApiError(c, fmt.Errorf("用户未启用Passkey"))
return
}
// Passkey 验证需要先调用 PasskeyVerifyBegin 和 PasskeyVerifyFinish
// 这里只是验证 Passkey 验证流程是否已经完成
// 实际上,前端应该先调用这两个接口,然后再调用本接口
verified = true // Passkey 验证逻辑已在 PasskeyVerifyFinish 中完成
verifyMethod = "Passkey"

default:
common.ApiError(c, fmt.Errorf("不支持的验证方式: %s", req.Method))
return
}

if !verified {
common.ApiError(c, fmt.Errorf("验证失败,请检查验证码"))
return
}

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

Passkey branch allows bypass without proof

UniversalVerify marks Passkey users as verified without confirming PasskeyVerifyFinish actually ran. If a user has a passkey bound, posting {"method":"passkey"} sets the session success flag even when no WebAuthn ceremony happened, so any client can bypass secure verification. We need a server-side attestation (e.g., session flag set by PasskeyVerifyForSecure) before trusting the passkey method.

Comment on lines +146 to +170
session := sessions.Default(c)
verifiedAtRaw := session.Get(SecureVerificationSessionKey)

if verifiedAtRaw == nil {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": VerificationStatusResponse{
Verified: false,
},
})
return
}

verifiedAt, ok := verifiedAtRaw.(int64)
if !ok {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": VerificationStatusResponse{
Verified: false,
},
})
return
}

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 | 🟠 Major

Session type mismatch breaks status check

GetVerificationStatus assumes verified_at is stored as int64, but gin-contrib/sessions JSON store typically deserializes integers as float64. In that case verifiedAtRaw.(int64) fails, every status request reports unverified, and expiry never works. We should handle numeric types robustly (e.g., accept int64, int, float64) before clearing state.

🤖 Prompt for AI Agents
In controller/secure_verification.go around lines 146 to 170, the status check
extracts session.Get(SecureVerificationSessionKey) and only type-asserts to
int64; modify this to accept and normalize numeric types commonly returned by
the JSON store (float64, int, int64, uint) — detect the concrete type, convert
the value to int64 (e.g., if float64 use int64(v), if int/uint cast to int64),
then continue using that normalized int64 for expiry checks and clearing the
session; if the value is not a numeric type, treat it as missing/unverified as
before.

Comment on lines +225 to +230
func PasskeyVerifyAndSetSession(c *gin.Context) {
session := sessions.Default(c)
now := time.Now().Unix()
session.Set(SecureVerificationSessionKey, now)
_ = session.Save()
}

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 | 🟠 Major

Persist session errors

PasskeyVerifyAndSetSession ignores session.Save() failures. If persistence fails we still treat the user as verified, but subsequent checks will fail. Bubble the error so callers can surface it, same as we do in UniversalVerify.

🤖 Prompt for AI Agents
In controller/secure_verification.go around lines 225-230,
PasskeyVerifyAndSetSession currently ignores the error from session.Save(),
causing callers to believe verification succeeded when persistence failed;
change the function signature to return error, call session.Save() and if it
returns non-nil return that error (or wrap with context), and update all callers
to handle/propagate the returned error the same way UniversalVerify does so
failures are surfaced to the client.

Comment on lines +62 to +74
<div style={{ textAlign: 'center', padding: '40px' }}>
<Text type="tertiary" style={{ display: 'block', marginBottom: '16px' }}>
视频无法在当前浏览器中播放,这可能是由于:
</Text>
<Text type="tertiary" style={{ display: 'block', marginBottom: '8px', fontSize: '12px' }}>
• 视频服务商的跨域限制
</Text>
<Text type="tertiary" style={{ display: 'block', marginBottom: '8px', fontSize: '12px' }}>
• 需要特定的请求头或认证
</Text>
<Text type="tertiary" style={{ display: 'block', marginBottom: '16px', fontSize: '12px' }}>
• 防盗链保护机制
</Text>

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 | 🟡 Minor

Hard-coded Chinese text should be internationalized.

The error message text is hard-coded in Chinese, which prevents localization for users in other languages. The codebase appears to use react-i18next based on the retrieved learnings.

Consider using translation keys:

<Text type="tertiary" style={{ display: 'block', marginBottom: '16px' }}>
  {t('video.playback_error.description')}
</Text>
<Text type="tertiary" style={{ display: 'block', marginBottom: '8px', fontSize: '12px' }}>
   {t('video.playback_error.cors_restriction')}
</Text>
<Text type="tertiary" style={{ display: 'block', marginBottom: '8px', fontSize: '12px' }}>{t('video.playback_error.auth_required')}
</Text>
<Text type="tertiary" style={{ display: 'block', marginBottom: '16px', fontSize: '12px' }}>
   {t('video.playback_error.hotlink_protection')}
</Text>
🤖 Prompt for AI Agents
In web/src/components/table/task-logs/modals/ContentModal.jsx around lines 62 to
74, the user-facing Chinese strings are hard-coded; replace them with
react-i18next translation keys (e.g. video.playback_error.description,
video.playback_error.cors_restriction, video.playback_error.auth_required,
video.playback_error.hotlink_protection), import and use the useTranslation hook
(const { t } = useTranslation()) at the top of the component, call t(...) for
each text node while preserving existing styling and bullet characters, and add
the corresponding keys/translated values to the locale JSON files so the
messages are localizable.

Comment on lines +695 to +706
const testChannel = async (record, model, endpointType = '') => {
const testKey = `${record.id}-${model}`;

// 检查是否应该停止批量测试
if (shouldStopBatchTestingRef.current && isBatchTesting) {
return Promise.resolve();
}
};

// Process test queue
const processTestQueue = async () => {
if (!isProcessingQueue || testQueue.length === 0) return;
// 添加到正在测试的模型集合
setTestingModels(prev => new Set([...prev, model]));

const { channel, model, indexInFiltered } = testQueue[0];
try {

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 | 🟠 Major

Avoid collisions in testingModels: key by channel+model, not just model.

Using only model causes cross-channel interference (same model tested on multiple channels toggles each other’s state).

Apply this diff:

-  const testKey = `${record.id}-${model}`;
+  const testKey = `${record.id}-${model}`;
   // ...
-  setTestingModels(prev => new Set([...prev, model]));
+  setTestingModels(prev => new Set([...prev, testKey]));

Also update removal below.

📝 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.

Suggested change
const testChannel = async (record, model, endpointType = '') => {
const testKey = `${record.id}-${model}`;
// 检查是否应该停止批量测试
if (shouldStopBatchTestingRef.current && isBatchTesting) {
return Promise.resolve();
}
};
// Process test queue
const processTestQueue = async () => {
if (!isProcessingQueue || testQueue.length === 0) return;
// 添加到正在测试的模型集合
setTestingModels(prev => new Set([...prev, model]));
const { channel, model, indexInFiltered } = testQueue[0];
try {
const testChannel = async (record, model, endpointType = '') => {
const testKey = `${record.id}-${model}`;
// 检查是否应该停止批量测试
if (shouldStopBatchTestingRef.current && isBatchTesting) {
return Promise.resolve();
}
// 添加到正在测试的模型集合
setTestingModels(prev => new Set([...prev, testKey]));
try {
🤖 Prompt for AI Agents
In web/src/hooks/channels/useChannelsData.jsx around lines 695 to 706,
testingModels currently uses only the model as the set key which causes
cross-channel collisions; switch to using the testKey (channelId-record.id +
model) so entries are unique per channel+model: when adding replace
setTestingModels(prev => new Set([...prev, model])) with adding testKey, and
update the removal logic later to delete by testKey instead of model; ensure any
checks that read testingModels use the testKey as well.

Comment on lines 768 to 775
} finally {
setTestingModels((prev) => {
// 从正在测试的模型集合中移除
setTestingModels(prev => {
const newSet = new Set(prev);
newSet.delete(model);
return newSet;
});
}

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 | 🟠 Major

Update removal from testingModels to match new key.

-      setTestingModels(prev => {
-        const newSet = new Set(prev);
-        newSet.delete(model);
-        return newSet;
-      });
+      setTestingModels(prev => {
+        const newSet = new Set(prev);
+        newSet.delete(`${record.id}-${model}`);
+        return newSet;
+      });
📝 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.

Suggested change
} finally {
setTestingModels((prev) => {
// 从正在测试的模型集合中移除
setTestingModels(prev => {
const newSet = new Set(prev);
newSet.delete(model);
return newSet;
});
}
} finally {
// 从正在测试的模型集合中移除
setTestingModels(prev => {
const newSet = new Set(prev);
newSet.delete(`${record.id}-${model}`);
return newSet;
});
}
🤖 Prompt for AI Agents
In web/src/hooks/channels/useChannelsData.jsx around lines 768 to 775, the
finally block removes the model from testingModels using newSet.delete(model)
but the set uses the model's key (e.g. model.id or another identifier) when
items are added; update the delete call to remove by the same key used when
inserting (for example newSet.delete(model.id) or newSet.delete(modelKey)) and
ensure the type/shape matches (string vs object) so the item is actually removed
from the Set.

Comment on lines +44 to +64
console.log('=== DEBUGGING VERIFICATION METHODS ===');
console.log('2FA Response:', JSON.stringify(twoFAResponse, null, 2));
console.log('Passkey Response:', JSON.stringify(passkeyResponse, null, 2));

const has2FA = twoFAResponse.data?.success && twoFAResponse.data?.data?.enabled === true;
const hasPasskey = passkeyResponse.data?.success && passkeyResponse.data?.data?.enabled === true;

console.log('has2FA calculation:', {
success: twoFAResponse.data?.success,
dataExists: !!twoFAResponse.data?.data,
enabled: twoFAResponse.data?.data?.enabled,
result: has2FA
});

console.log('hasPasskey calculation:', {
success: passkeyResponse.data?.success,
dataExists: !!passkeyResponse.data?.data,
enabled: passkeyResponse.data?.data?.enabled,
result: hasPasskey
});

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 | 🟠 Major

Remove debugging logs leaking sensitive data

These console logs may expose 2FA/passkey status and server payloads in production.

-      console.log('=== DEBUGGING VERIFICATION METHODS ===');
-      console.log('2FA Response:', JSON.stringify(twoFAResponse, null, 2));
-      console.log('Passkey Response:', JSON.stringify(passkeyResponse, null, 2));
-      
-      console.log('has2FA calculation:', {
-        success: twoFAResponse.data?.success,
-        dataExists: !!twoFAResponse.data?.data,
-        enabled: twoFAResponse.data?.data?.enabled,
-        result: has2FA
-      });
-      
-      console.log('hasPasskey calculation:', {
-        success: passkeyResponse.data?.success,
-        dataExists: !!passkeyResponse.data?.data,
-        enabled: passkeyResponse.data?.data?.enabled,
-        result: hasPasskey
-      });
📝 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.

Suggested change
console.log('=== DEBUGGING VERIFICATION METHODS ===');
console.log('2FA Response:', JSON.stringify(twoFAResponse, null, 2));
console.log('Passkey Response:', JSON.stringify(passkeyResponse, null, 2));
const has2FA = twoFAResponse.data?.success && twoFAResponse.data?.data?.enabled === true;
const hasPasskey = passkeyResponse.data?.success && passkeyResponse.data?.data?.enabled === true;
console.log('has2FA calculation:', {
success: twoFAResponse.data?.success,
dataExists: !!twoFAResponse.data?.data,
enabled: twoFAResponse.data?.data?.enabled,
result: has2FA
});
console.log('hasPasskey calculation:', {
success: passkeyResponse.data?.success,
dataExists: !!passkeyResponse.data?.data,
enabled: passkeyResponse.data?.data?.enabled,
result: hasPasskey
});
const has2FA = twoFAResponse.data?.success && twoFAResponse.data?.data?.enabled === true;
const hasPasskey = passkeyResponse.data?.success && passkeyResponse.data?.data?.enabled === true;
🤖 Prompt for AI Agents
In web/src/services/secureVerification.js around lines 44 to 64, remove the
verbose console.log calls that print full 2FA/passkey responses and derived
booleans; instead either delete these debug logs or guard them behind a
non-production check (e.g. only log when NODE_ENV !== 'production') and ensure
any remaining logs redact sensitive payload fields (do not stringify full
response objects or include enabled/status values for production). Replace with
a single minimal, non-sensitive log or a metric/event emit that indicates
verification was checked without leaking payload details.

Comment on lines +117 to +121
// 准备WebAuthn选项
const publicKey = prepareCredentialRequestOptions(beginResponse.data.data.options);

// 执行WebAuthn验证
const credential = await navigator.credentials.get({ publicKey });

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.

🛠️ Refactor suggestion | 🟠 Major

Make request options normalization resilient

Passing .options directly may fail if backend already returns a bare PublicKeyCredentialRequestOptions or nests differently. Let the helper normalize the full payload.

-      const publicKey = prepareCredentialRequestOptions(beginResponse.data.data.options);
+      const publicKey = prepareCredentialRequestOptions(
+        beginResponse?.data?.data ?? beginResponse?.data
+      );
📝 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.

Suggested change
// 准备WebAuthn选项
const publicKey = prepareCredentialRequestOptions(beginResponse.data.data.options);
// 执行WebAuthn验证
const credential = await navigator.credentials.get({ publicKey });
// 准备WebAuthn选项
const publicKey = prepareCredentialRequestOptions(
beginResponse?.data?.data ?? beginResponse?.data
);
// 执行WebAuthn验证
const credential = await navigator.credentials.get({ publicKey });
🤖 Prompt for AI Agents
In web/src/services/secureVerification.js around lines 117 to 121, the code
currently passes beginResponse.data.data.options directly to
prepareCredentialRequestOptions which can break if the backend returns a bare
PublicKeyCredentialRequestOptions or uses a different nesting; update the call
to pass the whole payload (e.g.,
prepareCredentialRequestOptions(beginResponse.data.data)) and modify the
prepareCredentialRequestOptions helper to normalize input by accepting either
the full response object or the nested .options, extracting and validating the
PublicKeyCredentialRequestOptions shape (or using the input as-is when it
already matches) and returning a consistent publicKey object for
navigator.credentials.get.

@Calcium-Ion
Calcium-Ion deleted the pr/console-footer branch October 3, 2025 13:52
x22x22 pushed a commit to x22x22/new-api that referenced this pull request Apr 24, 2026
✨ feat(layout): refine footer visibility logic to target CardPro component pages
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.