Dev#1699
Conversation
fix: 修复快速审核结构化错误导致的 React #31
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
Walkthrough本PR新增通用ID工具与错误格式化、为插件统计添加本地缓存与用户状态接口、引入插件市场排序、更新主题色与CSS变量,并在若干前端页面(Header、Prompts、Plugins、ReasoningProcess)做布局与交互调整;同时同步后端路由与配置默认值更新。 Changes跨层级功能增强与优化
Estimated code review effort: Possibly related PRs:
总体概览本PR涵盖前后端多个层面的协调更新:工具函数统一(ID生成)、错误处理标准化、插件统计缓存机制与排序功能、主题色系更新、推理过程API扩展,以及UI组件与配置的微调。共影响30+个文件,核心是围绕插件统计体验优化与界面紧凑化。 变更详情插件统计与缓存
基础设施与主题
UI与页面调整
推理过程与配置
相关PR
🎯 4 (Complex) | ⏱️ ~50 minutes ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/config/official_configs.py (1)
2338-2351:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win配置默认值变更已通过升级钩子进行向后兼容性处理
enable_precise_expression_selection的默认值从True改为False,但config_upgrade_hooks.py第 268-272 行的升级钩子提供了向后兼容性保障:现有部署升级时,若该字段缺失会自动设置为True,确保现有用户的行为不变。这不是破坏性变更,而是新装系统的配置优化:
- 新装部署默认禁用精细表达选择以降低资源消耗
- 现有部署通过升级钩子自动保持原有行为
advanced: False的变更使该配置在 WebUI 中更易访问建议在发布说明中说明此变更及其设计意图,便于用户理解为何新装默认禁用该功能。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/config/official_configs.py` around lines 2338 - 2351, The default for enable_precise_expression_selection was changed from True to False but an upgrade hook in config_upgrade_hooks.py (around the upgrade hook handling at/near the logic that sets this field when missing) preserves existing deployments; update the release notes and deployment documentation to explicitly state that enable_precise_expression_selection now defaults to False for fresh installs, that existing installs are preserved by the upgrade hook in config_upgrade_hooks.py (refer to the hook that sets the field to True when absent), and explain the motivation (reduce resource usage) and the WebUI change (advanced: False) so users understand the behavior change and where to look.
🧹 Nitpick comments (2)
dashboard/src/lib/plugin-stats.ts (1)
174-178: 💤 Low value缓存更新时间戳设置可能导致缓存过早失效或过度延长。
updateCachedPluginStats在每次局部更新时都将timestamp设为Date.now(),这会重置缓存 TTL。如果用户频繁点赞/评分,缓存可能永远不会过期;反之如果原始数据已经很旧,局部更新会让过时数据"看起来"很新。建议保留原始
timestamp或单独追踪局部更新时间。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@dashboard/src/lib/plugin-stats.ts` around lines 174 - 178, 当前实现每次调用 updateCachedPluginStats 时把 pluginStatsSummaryCache.timestamp 设为 Date.now(),导致局部点赞/评分操作会重置缓存 TTL;请在更新逻辑中保留已有的原始 timestamp(pluginStatsSummaryCache.timestamp)而不是覆盖,或者在插件统计结构中新增一个单独字段(例如 lastPartialUpdateAt 或 partialUpdateTimestamp)来记录局部更新时间;在调用 writePluginStatsSummaryStorageCache(nextData) 前确保 nextData.timestamp 保持原始缓存时间并且把局部更新时间写入新字段或单独存储,以避免频繁局部操作永久延长或错误刷新缓存。dashboard/src/routes/plugins/index.tsx (1)
295-303: ⚖️ Poor tradeoff异步统计刷新使用的
mergedData可能在组件卸载或重新渲染后变为过时闭包。
getPluginStatsSummary().then()中捕获了mergedData,但如果组件在请求返回前重新渲染或卸载,setPluginStats会使用过时的插件列表构建映射。虽然已有isUnmounted检查,但mergedData本身可能已经不是最新的。考虑使用
setPluginStats的函数式更新或引用最新的 plugins 状态。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@dashboard/src/routes/plugins/index.tsx` around lines 295 - 303, The async callback for getPluginStatsSummary captures a stale mergedData closure; instead ensure setPluginStats uses the latest plugins when applying buildPluginStatsMap by either (a) switching to a functional state update (setPluginStats(prev => buildPluginStatsMap(currentMergedDataFromPrevOrRef, statsSummary))) or (b) keep a ref to the latest mergedData and read ref.current inside the then() before calling setPluginStats; update the code paths that call getPluginStatsSummary, setPluginStats, buildPluginStatsMap and respect isUnmounted as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@dashboard/src/components/layout/Header.tsx`:
- Line 204: In Header.tsx update the onClick handler that calls
window.open('https://docs.mai-mai.org', '_blank') to prevent window.opener
attacks by passing the security features (e.g. 'noopener,noreferrer') as the
third argument or replace the clickable element with an <a> element using
target="_blank" and rel="noopener noreferrer"; locate the onClick in the Header
component (the handler attached to the docs button/element) and apply one of
these fixes so external links are opened safely.
- Line 215: The Button in Header.tsx currently hardcodes title and aria-label to
Chinese ("切换语言"), causing mixed-language accessibility text; update the Button
component (the language toggle Button in the Header component) to use i18n keys
instead of literal strings—replace the hardcoded title and aria-label with
translated values fetched via the project's i18n utility (e.g.,
t('header.toggleLanguage') or equivalent) so both attributes use the localized
string from the translation function.
In `@dashboard/src/components/plugin-stats.tsx`:
- Around line 42-59: The loadStats function currently uses Promise.all which
will reject if either getPluginStats or getPluginUserState fails, leaving
loading stuck true; change the implementation to use Promise.allSettled (or wrap
each call in its own try/catch) so you handle success and failure independently
for getPluginStats and getPluginUserState, only call
setStats/setLiked/setDisliked/setUserRating/setUserComment when the
corresponding promise fulfilled, and ensure setLoading(false) runs in a finally
block so loading is always cleared even if one request fails.
In `@dashboard/src/lib/api-error.ts`:
- Around line 42-50: The hasUsableMessage function currently treats any object
as usable which causes empty objects like { detail: {} } to overshadow a valid
message; update hasUsableMessage to explicitly handle plain objects by returning
false for objects with no own enumerable keys (e.g., typeof value === 'object'
&& !Array.isArray(value) => Object.keys(value).length > 0), keep the existing
array and empty-string checks, and ensure other primitive types (number,
boolean) remain true; modify the function implementation (hasUsableMessage)
accordingly so empty objects no longer count as usable messages.
In `@dashboard/src/lib/plugin-stats.ts`:
- Around line 293-302: When forceRefresh creates a new Promise it can be
overwritten by a previous in-flight request's .finally(), so change the flow in
the pluginStatsSummaryRequest block: create a localRequest =
fetchPluginStatsSummaryUncached(), assign pluginStatsSummaryRequest =
localRequest, and in the Promise handlers only update shared state
(pluginStatsSummaryCache, writePluginStatsSummaryStorageCache) and clear
pluginStatsSummaryRequest if pluginStatsSummaryRequest === localRequest; this
ensures only the most recent request updates or clears the shared variables
(references: pluginStatsSummaryRequest, fetchPluginStatsSummaryUncached,
pluginStatsSummaryCache, writePluginStatsSummaryStorageCache).
In `@dashboard/src/lib/theme/palette.ts`:
- Around line 64-67: The isLegacyYellowAccent function currently uses overly
broad H/S/L ranges and incorrectly classifies many user-selected yellows/oranges
as legacy, causing normalizeAccentColor to coerce them to the default green;
update isLegacyYellowAccent (which uses parseHSL) to perform a tight tolerance
check centered on the legacy H/S/L default (use the legacy h0, s0, l0 values
from the theme) — e.g. ±2–5° for h and ±3–5% for s and l — so only colors very
close to the legacy default are matched, leaving other user accents untouched.
In `@dashboard/src/lib/theme/pipeline.ts`:
- Around line 45-56: The default-accent branch currently writes
config.accentColor directly, which can persist legacy values; first compute a
normalized value (e.g., normalizedAccent =
normalizeAccentColor(config.accentColor)) and use that normalizedAccent both for
the is-default check and when merging tokens (replace uses of config.accentColor
in the isDefaultAccentColor() call, the mergeTokens block that sets accent and
'accent-foreground' and in the else branch where generatePalette is used) so the
code always reads and writes the normalized color; update references around
isDefaultAccentColor, mergeTokens, getReadableForeground, and generatePalette to
use the normalized variable.
In `@dashboard/src/routes/config/prompts.tsx`:
- Around line 272-273: The JSX contains hard-coded Chinese strings (e.g.,
title="刷新" and aria-label="刷新" and similar strings at the other noted spots)
that must be replaced with i18n lookups; import/use the translation helper
(e.g., const { t } = useTranslation() or the existing t) in this file and
replace these literal attributes with t('key') calls (create sensible keys like
'refresh', 'addPlaceholder', etc.), ensuring both title and aria-label use the
same t(...) values so UI text and accessibility labels are localized uniformly
(apply this change for the occurrences around the lines referenced: the
title/aria-label pair and the strings at the other three locations).
In `@src/webui/routers/plugin/stats_proxy.py`:
- Around line 56-59: The GET endpoint get_plugin_user_state currently accepts
raw plugin_id and user_id strings; add FastAPI query parameter validation using
Query with the same min_length/max_length (and any format constraints) used by
POST models (e.g., VoteRequest) to prevent oversized values, e.g., change the
signature to accept plugin_id: str = Query(..., min_length=..., max_length=...)
and user_id: str = Query(..., min_length=..., max_length=...), then build the
quoted query and call _request_stats_service("GET",
f"/stats/user-state?{query}") as before; ensure you import Query from fastapi
and match the exact length limits used elsewhere.
In `@src/webui/routers/reasoning_process.py`:
- Around line 480-484: The code builds sessions_to_resolve for multi-session
searches but still calls _collect_prompt_files with selected_session only, so
cross-session collection fails; change the call to collect from all
sessions_to_resolve (either by passing sessions_to_resolve into
_collect_prompt_files or by iterating sessions_to_resolve and merging results)
and ensure session_info_map (from _list_session_infos) is used to resolve
names/IDs for each session; update any call sites or signature of
_collect_prompt_files (if needed) to accept and process multiple session
identifiers rather than a single selected_session.
---
Outside diff comments:
In `@src/config/official_configs.py`:
- Around line 2338-2351: The default for enable_precise_expression_selection was
changed from True to False but an upgrade hook in config_upgrade_hooks.py
(around the upgrade hook handling at/near the logic that sets this field when
missing) preserves existing deployments; update the release notes and deployment
documentation to explicitly state that enable_precise_expression_selection now
defaults to False for fresh installs, that existing installs are preserved by
the upgrade hook in config_upgrade_hooks.py (refer to the hook that sets the
field to True when absent), and explain the motivation (reduce resource usage)
and the WebUI change (advanced: False) so users understand the behavior change
and where to look.
---
Nitpick comments:
In `@dashboard/src/lib/plugin-stats.ts`:
- Around line 174-178: 当前实现每次调用 updateCachedPluginStats 时把
pluginStatsSummaryCache.timestamp 设为 Date.now(),导致局部点赞/评分操作会重置缓存
TTL;请在更新逻辑中保留已有的原始
timestamp(pluginStatsSummaryCache.timestamp)而不是覆盖,或者在插件统计结构中新增一个单独字段(例如
lastPartialUpdateAt 或 partialUpdateTimestamp)来记录局部更新时间;在调用
writePluginStatsSummaryStorageCache(nextData) 前确保 nextData.timestamp
保持原始缓存时间并且把局部更新时间写入新字段或单独存储,以避免频繁局部操作永久延长或错误刷新缓存。
In `@dashboard/src/routes/plugins/index.tsx`:
- Around line 295-303: The async callback for getPluginStatsSummary captures a
stale mergedData closure; instead ensure setPluginStats uses the latest plugins
when applying buildPluginStatsMap by either (a) switching to a functional state
update (setPluginStats(prev =>
buildPluginStatsMap(currentMergedDataFromPrevOrRef, statsSummary))) or (b) keep
a ref to the latest mergedData and read ref.current inside the then() before
calling setPluginStats; update the code paths that call getPluginStatsSummary,
setPluginStats, buildPluginStatsMap and respect isUnmounted as before.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8d6715e5-e0ca-4d5f-a819-064ade6c03ef
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (27)
dashboard/src/components/layout/Header.tsxdashboard/src/components/layout/NavItem.tsxdashboard/src/components/plugin-stats.tsxdashboard/src/components/ui/nested-key-value-editor.tsxdashboard/src/index.cssdashboard/src/lib/__tests__/api-error.test.tsdashboard/src/lib/api-error.tsdashboard/src/lib/asset-store.tsdashboard/src/lib/expression-api.tsdashboard/src/lib/id.tsdashboard/src/lib/plugin-stats.tsdashboard/src/lib/reasoning-process-api.tsdashboard/src/lib/theme/palette.tsdashboard/src/lib/theme/pipeline.tsdashboard/src/lib/theme/storage.tsdashboard/src/lib/theme/tokens.tsdashboard/src/routes/config/prompts.tsxdashboard/src/routes/mcp-settings.tsxdashboard/src/routes/plugins/MarketplaceTab.tsxdashboard/src/routes/plugins/index.tsxdashboard/src/routes/plugins/types.tsdashboard/src/routes/reasoning-process.tsxsrc/cli/maisaka_cli.pysrc/config/official_configs.pysrc/maisaka/runtime.pysrc/webui/routers/plugin/stats_proxy.pysrc/webui/routers/reasoning_process.py
💤 Files with no reviewable changes (1)
- dashboard/src/components/layout/NavItem.tsx
| variant="ghost" | ||
| size="sm" | ||
| size="icon" | ||
| onClick={() => window.open('https://docs.mai-mai.org', '_blank')} |
There was a problem hiding this comment.
window.open 缺少 noopener/noreferrer 安全属性。
使用 _blank 打开外链时未隔离 window.opener,存在反向标签劫持风险。建议补上第三个参数(或改为 <a rel="noopener noreferrer">)。
建议修复
- onClick={() => window.open('https://docs.mai-mai.org', '_blank')}
+ onClick={() => window.open('https://docs.mai-mai.org', '_blank', 'noopener,noreferrer')}📝 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.
| onClick={() => window.open('https://docs.mai-mai.org', '_blank')} | |
| onClick={() => window.open('https://docs.mai-mai.org', '_blank', 'noopener,noreferrer')} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@dashboard/src/components/layout/Header.tsx` at line 204, In Header.tsx update
the onClick handler that calls window.open('https://docs.mai-mai.org', '_blank')
to prevent window.opener attacks by passing the security features (e.g.
'noopener,noreferrer') as the third argument or replace the clickable element
with an <a> element using target="_blank" and rel="noopener noreferrer"; locate
the onClick in the Header component (the handler attached to the docs
button/element) and apply one of these fixes so external links are opened
safely.
| <DropdownMenu> | ||
| <DropdownMenuTrigger asChild> | ||
| <Button variant="ghost" size="sm" className="gap-2 px-2 sm:px-3"> | ||
| <Button variant="ghost" size="icon" title="切换语言" aria-label="切换语言"> |
There was a problem hiding this comment.
语言切换按钮可访问性文案被硬编码为中文。
title 和 aria-label 写死为“切换语言”,会在非中文界面下出现混杂语言,建议改为 i18n 文案键。
建议修复
- <Button variant="ghost" size="icon" title="切换语言" aria-label="切换语言">
+ <Button
+ variant="ghost"
+ size="icon"
+ title={t('header.switchLanguage')}
+ aria-label={t('header.switchLanguage')}
+ >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@dashboard/src/components/layout/Header.tsx` at line 215, The Button in
Header.tsx currently hardcodes title and aria-label to Chinese ("切换语言"), causing
mixed-language accessibility text; update the Button component (the language
toggle Button in the Header component) to use i18n keys instead of literal
strings—replace the hardcoded title and aria-label with translated values
fetched via the project's i18n utility (e.g., t('header.toggleLanguage') or
equivalent) so both attributes use the localized string from the translation
function.
| const loadStats = async () => { | ||
| setLoading(true) | ||
| const data = await getPluginStats(pluginId) | ||
| if (data) { | ||
| setStats(data) | ||
| const [statsData, userState] = await Promise.all([ | ||
| getPluginStats(pluginId), | ||
| getPluginUserState(pluginId), | ||
| ]) | ||
|
|
||
| if (statsData) { | ||
| setStats(statsData) | ||
| } | ||
| if (userState) { | ||
| setLiked(userState.liked) | ||
| setDisliked(userState.disliked) | ||
| setUserRating(userState.rating) | ||
| setUserComment(userState.comment) | ||
| } | ||
| setLoading(false) | ||
| } |
There was a problem hiding this comment.
loadStats 中 Promise.all 若其中一个失败会导致整体失败。
当 getPluginStats 或 getPluginUserState 中任一请求失败时,Promise.all 会拒绝,loading 状态将保持为 true(setLoading(false) 不会执行)。建议使用 Promise.allSettled 或分别处理错误。
🛡️ 建议使用 Promise.allSettled 或独立 try-catch
const loadStats = async () => {
setLoading(true)
- const [statsData, userState] = await Promise.all([
- getPluginStats(pluginId),
- getPluginUserState(pluginId),
- ])
+ const [statsResult, userStateResult] = await Promise.allSettled([
+ getPluginStats(pluginId),
+ getPluginUserState(pluginId),
+ ])
- if (statsData) {
+ const statsData = statsResult.status === 'fulfilled' ? statsResult.value : null
+ const userState = userStateResult.status === 'fulfilled' ? userStateResult.value : null
+
+ if (statsData) {
setStats(statsData)
}
if (userState) {
setLiked(userState.liked)
setDisliked(userState.disliked)
setUserRating(userState.rating)
setUserComment(userState.comment)
}
setLoading(false)
}📝 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.
| const loadStats = async () => { | |
| setLoading(true) | |
| const data = await getPluginStats(pluginId) | |
| if (data) { | |
| setStats(data) | |
| const [statsData, userState] = await Promise.all([ | |
| getPluginStats(pluginId), | |
| getPluginUserState(pluginId), | |
| ]) | |
| if (statsData) { | |
| setStats(statsData) | |
| } | |
| if (userState) { | |
| setLiked(userState.liked) | |
| setDisliked(userState.disliked) | |
| setUserRating(userState.rating) | |
| setUserComment(userState.comment) | |
| } | |
| setLoading(false) | |
| } | |
| const loadStats = async () => { | |
| setLoading(true) | |
| const [statsResult, userStateResult] = await Promise.allSettled([ | |
| getPluginStats(pluginId), | |
| getPluginUserState(pluginId), | |
| ]) | |
| const statsData = statsResult.status === 'fulfilled' ? statsResult.value : null | |
| const userState = userStateResult.status === 'fulfilled' ? userStateResult.value : null | |
| if (statsData) { | |
| setStats(statsData) | |
| } | |
| if (userState) { | |
| setLiked(userState.liked) | |
| setDisliked(userState.disliked) | |
| setUserRating(userState.rating) | |
| setUserComment(userState.comment) | |
| } | |
| setLoading(false) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@dashboard/src/components/plugin-stats.tsx` around lines 42 - 59, The
loadStats function currently uses Promise.all which will reject if either
getPluginStats or getPluginUserState fails, leaving loading stuck true; change
the implementation to use Promise.allSettled (or wrap each call in its own
try/catch) so you handle success and failure independently for getPluginStats
and getPluginUserState, only call
setStats/setLiked/setDisliked/setUserRating/setUserComment when the
corresponding promise fulfilled, and ensure setLoading(false) runs in a finally
block so loading is always cleared even if one request fails.
| function hasUsableMessage(value: unknown): boolean { | ||
| if (value === null || value === undefined || value === '') { | ||
| return false | ||
| } | ||
| if (Array.isArray(value)) { | ||
| return value.length > 0 | ||
| } | ||
| return true | ||
| } |
There was a problem hiding this comment.
避免空对象 detail 抢占有效错误信息。
当前对象类型会被直接判定为“可用消息”,导致 { detail: {}, message: 'xxx' } 命中 detail 并可能显示 "{}",而不是更有意义的 message。
建议修改
function hasUsableMessage(value: unknown): boolean {
if (value === null || value === undefined || value === '') {
return false
}
if (Array.isArray(value)) {
return value.length > 0
}
+ if (typeof value === 'object') {
+ const detail = value as ApiErrorDetail
+ const msg = detail.msg ?? detail.message
+ if (msg !== null && msg !== undefined && msg !== '') {
+ return true
+ }
+ return Object.keys(value as Record<string, unknown>).length > 0
+ }
return true
}Also applies to: 69-69
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@dashboard/src/lib/api-error.ts` around lines 42 - 50, The hasUsableMessage
function currently treats any object as usable which causes empty objects like {
detail: {} } to overshadow a valid message; update hasUsableMessage to
explicitly handle plain objects by returning false for objects with no own
enumerable keys (e.g., typeof value === 'object' && !Array.isArray(value) =>
Object.keys(value).length > 0), keep the existing array and empty-string checks,
and ensure other primitive types (number, boolean) remain true; modify the
function implementation (hasUsableMessage) accordingly so empty objects no
longer count as usable messages.
| if (!pluginStatsSummaryRequest || options.forceRefresh) { | ||
| pluginStatsSummaryRequest = fetchPluginStatsSummaryUncached() | ||
| .then((data) => { | ||
| pluginStatsSummaryCache = { timestamp: Date.now(), data } | ||
| writePluginStatsSummaryStorageCache(data) | ||
| return data | ||
| }) | ||
| .finally(() => { | ||
| pluginStatsSummaryRequest = null | ||
| }) |
There was a problem hiding this comment.
forceRefresh 时存在竞态条件,可能导致旧请求覆盖新请求结果。
当 forceRefresh=true 且上一次请求仍在 flight 时,会创建新的 Promise 并覆盖 pluginStatsSummaryRequest。但旧请求的 .finally() 仍会执行,将 pluginStatsSummaryRequest 置为 null,导致后续调用无法正确合并到新请求。
🐛 建议使用请求计数或取消机制
+let pluginStatsSummaryRequestId = 0
+
export async function getPluginStatsSummary(
options: { forceRefresh?: boolean } = {}
): Promise<Record<string, PluginStatsData>> {
// ... cache checks ...
if (!pluginStatsSummaryRequest || options.forceRefresh) {
+ const currentRequestId = ++pluginStatsSummaryRequestId
pluginStatsSummaryRequest = fetchPluginStatsSummaryUncached()
.then((data) => {
- pluginStatsSummaryCache = { timestamp: Date.now(), data }
- writePluginStatsSummaryStorageCache(data)
+ if (currentRequestId === pluginStatsSummaryRequestId) {
+ pluginStatsSummaryCache = { timestamp: Date.now(), data }
+ writePluginStatsSummaryStorageCache(data)
+ }
return data
})
.finally(() => {
- pluginStatsSummaryRequest = null
+ if (currentRequestId === pluginStatsSummaryRequestId) {
+ pluginStatsSummaryRequest = null
+ }
})
}
return pluginStatsSummaryRequest
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@dashboard/src/lib/plugin-stats.ts` around lines 293 - 302, When forceRefresh
creates a new Promise it can be overwritten by a previous in-flight request's
.finally(), so change the flow in the pluginStatsSummaryRequest block: create a
localRequest = fetchPluginStatsSummaryUncached(), assign
pluginStatsSummaryRequest = localRequest, and in the Promise handlers only
update shared state (pluginStatsSummaryCache,
writePluginStatsSummaryStorageCache) and clear pluginStatsSummaryRequest if
pluginStatsSummaryRequest === localRequest; this ensures only the most recent
request updates or clears the shared variables (references:
pluginStatsSummaryRequest, fetchPluginStatsSummaryUncached,
pluginStatsSummaryCache, writePluginStatsSummaryStorageCache).
| const isLegacyYellowAccent = (hsl: string): boolean => { | ||
| const { h, s, l } = parseHSL(hsl) | ||
| return h >= 30 && h <= 40 && s >= 70 && l >= 40 && l <= 55 | ||
| } |
There was a problem hiding this comment.
isLegacyYellowAccent 判定范围过宽,会误归一用户自定义黄/橙色。
当前区间匹配会覆盖一大片正常可选颜色,最终在 normalizeAccentColor 中被强制改成默认绿色,影响自定义主题可用性。建议改为围绕 legacy 默认值做小容差匹配。
建议修改
const isLegacyYellowAccent = (hsl: string): boolean => {
const { h, s, l } = parseHSL(hsl)
- return h >= 30 && h <= 40 && s >= 70 && l >= 40 && l <= 55
+ const legacy = parseHSL(LEGACY_DEFAULT_ACCENT_HSL)
+ return Math.abs(h - legacy.h) <= 1
+ && Math.abs(s - legacy.s) <= 2
+ && Math.abs(l - legacy.l) <= 2
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@dashboard/src/lib/theme/palette.ts` around lines 64 - 67, The
isLegacyYellowAccent function currently uses overly broad H/S/L ranges and
incorrectly classifies many user-selected yellows/oranges as legacy, causing
normalizeAccentColor to coerce them to the default green; update
isLegacyYellowAccent (which uses parseHSL) to perform a tight tolerance check
centered on the legacy H/S/L default (use the legacy h0, s0, l0 values from the
theme) — e.g. ±2–5° for h and ±3–5% for s and l — so only colors very close to
the legacy default are matched, leaving other user accents untouched.
| if (isDefaultAccentColor(config.accentColor)) { | ||
| mergedTokens = mergeTokens(mergedTokens, { | ||
| color: { | ||
| ...mergedTokens.color, | ||
| accent: config.accentColor, | ||
| 'accent-foreground': getReadableForeground(config.accentColor), | ||
| }, | ||
| }) | ||
| } else { | ||
| mergedTokens = mergeTokens(mergedTokens, { | ||
| color: generatePalette(config.accentColor, isDark), | ||
| }) |
There was a problem hiding this comment.
默认强调色分支应使用归一化后的颜色值。
命中默认色分支后直接使用 config.accentColor 会把 legacy 值原样写回 accent,与“默认色归一化”策略不一致。建议先归一化,再参与判断和写入。
建议修改
-import { generatePalette, getReadableForeground, isDefaultAccentColor } from './palette'
+import { generatePalette, getReadableForeground, isDefaultAccentColor, normalizeAccentColor } from './palette'
@@
if (config.accentColor) {
- if (isDefaultAccentColor(config.accentColor)) {
+ const normalizedAccent = normalizeAccentColor(config.accentColor)
+ if (isDefaultAccentColor(normalizedAccent)) {
mergedTokens = mergeTokens(mergedTokens, {
color: {
...mergedTokens.color,
- accent: config.accentColor,
- 'accent-foreground': getReadableForeground(config.accentColor),
+ accent: normalizedAccent,
+ 'accent-foreground': getReadableForeground(normalizedAccent),
},
})
} else {
mergedTokens = mergeTokens(mergedTokens, {
- color: generatePalette(config.accentColor, isDark),
+ color: generatePalette(normalizedAccent, isDark),
})
}
}📝 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.
| if (isDefaultAccentColor(config.accentColor)) { | |
| mergedTokens = mergeTokens(mergedTokens, { | |
| color: { | |
| ...mergedTokens.color, | |
| accent: config.accentColor, | |
| 'accent-foreground': getReadableForeground(config.accentColor), | |
| }, | |
| }) | |
| } else { | |
| mergedTokens = mergeTokens(mergedTokens, { | |
| color: generatePalette(config.accentColor, isDark), | |
| }) | |
| if (config.accentColor) { | |
| const normalizedAccent = normalizeAccentColor(config.accentColor) | |
| if (isDefaultAccentColor(normalizedAccent)) { | |
| mergedTokens = mergeTokens(mergedTokens, { | |
| color: { | |
| ...mergedTokens.color, | |
| accent: normalizedAccent, | |
| 'accent-foreground': getReadableForeground(normalizedAccent), | |
| }, | |
| }) | |
| } else { | |
| mergedTokens = mergeTokens(mergedTokens, { | |
| color: generatePalette(normalizedAccent, isDark), | |
| }) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@dashboard/src/lib/theme/pipeline.ts` around lines 45 - 56, The default-accent
branch currently writes config.accentColor directly, which can persist legacy
values; first compute a normalized value (e.g., normalizedAccent =
normalizeAccentColor(config.accentColor)) and use that normalizedAccent both for
the is-default check and when merging tokens (replace uses of config.accentColor
in the isDefaultAccentColor() call, the mergeTokens block that sets accent and
'accent-foreground' and in the else branch where generatePalette is used) so the
code always reads and writes the normalized color; update references around
isDefaultAccentColor, mergeTokens, getReadableForeground, and generatePalette to
use the normalized variable.
| title="刷新" | ||
| aria-label="刷新" |
There was a problem hiding this comment.
新增按钮与输入框文案未走 i18n。
这几处新增文案是硬编码中文,切换语言后会出现混合语言。建议统一改为 t(...)。
Also applies to: 302-302, 373-373, 382-382
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@dashboard/src/routes/config/prompts.tsx` around lines 272 - 273, The JSX
contains hard-coded Chinese strings (e.g., title="刷新" and aria-label="刷新" and
similar strings at the other noted spots) that must be replaced with i18n
lookups; import/use the translation helper (e.g., const { t } = useTranslation()
or the existing t) in this file and replace these literal attributes with
t('key') calls (create sensible keys like 'refresh', 'addPlaceholder', etc.),
ensuring both title and aria-label use the same t(...) values so UI text and
accessibility labels are localized uniformly (apply this change for the
occurrences around the lines referenced: the title/aria-label pair and the
strings at the other three locations).
| @router.get("/stats-proxy/stats/user-state") | ||
| async def get_plugin_user_state(plugin_id: str, user_id: str) -> JSONResponse: | ||
| query = f"plugin_id={quote(plugin_id, safe='')}&user_id={quote(user_id, safe='')}" | ||
| return await _request_stats_service("GET", f"/stats/user-state?{query}") |
There was a problem hiding this comment.
GET 端点缺少 plugin_id 和 user_id 的长度/格式校验。
其他 POST 端点(如 VoteRequest)对 plugin_id 和 user_id 有 min_length/max_length 限制,但此 GET 端点直接接受裸字符串参数。恶意用户可以传入超长字符串,可能导致上游服务问题或日志膨胀。
🛡️ 建议添加 Query 参数校验
+from fastapi import Query
+
`@router.get`("/stats-proxy/stats/user-state")
-async def get_plugin_user_state(plugin_id: str, user_id: str) -> JSONResponse:
+async def get_plugin_user_state(
+ plugin_id: str = Query(..., min_length=1, max_length=200),
+ user_id: str = Query(..., min_length=1, max_length=300),
+) -> JSONResponse:
query = f"plugin_id={quote(plugin_id, safe='')}&user_id={quote(user_id, safe='')}"
return await _request_stats_service("GET", f"/stats/user-state?{query}")📝 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.
| @router.get("/stats-proxy/stats/user-state") | |
| async def get_plugin_user_state(plugin_id: str, user_id: str) -> JSONResponse: | |
| query = f"plugin_id={quote(plugin_id, safe='')}&user_id={quote(user_id, safe='')}" | |
| return await _request_stats_service("GET", f"/stats/user-state?{query}") | |
| from fastapi import Query | |
| `@router.get`("/stats-proxy/stats/user-state") | |
| async def get_plugin_user_state( | |
| plugin_id: str = Query(..., min_length=1, max_length=200), | |
| user_id: str = Query(..., min_length=1, max_length=300), | |
| ) -> JSONResponse: | |
| query = f"plugin_id={quote(plugin_id, safe='')}&user_id={quote(user_id, safe='')}" | |
| return await _request_stats_service("GET", f"/stats/user-state?{query}") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/webui/routers/plugin/stats_proxy.py` around lines 56 - 59, The GET
endpoint get_plugin_user_state currently accepts raw plugin_id and user_id
strings; add FastAPI query parameter validation using Query with the same
min_length/max_length (and any format constraints) used by POST models (e.g.,
VoteRequest) to prevent oversized values, e.g., change the signature to accept
plugin_id: str = Query(..., min_length=..., max_length=...) and user_id: str =
Query(..., min_length=..., max_length=...), then build the quoted query and call
_request_stats_service("GET", f"/stats/user-state?{query}") as before; ensure
you import Query from fastapi and match the exact length limits used elsewhere.
| sessions_to_resolve = sessions if normalized_search else ([selected_session] if selected_session else []) | ||
| session_infos = _list_session_infos(selected_stage, sessions_to_resolve) | ||
| session_info_map = {item.name: item for item in session_infos} | ||
| items = _collect_prompt_files(selected_stage, selected_session, session_info_map) | ||
|
|
There was a problem hiding this comment.
搜索模式下仍只收集单会话文件,跨会话搜索会失效。
当 normalized_search 非空时你已把 sessions_to_resolve 扩展为多会话,但 items 仍只来自 selected_session,导致会话名/真实会话 ID 搜索无法覆盖其它会话。
💡 建议修复
- sessions_to_resolve = sessions if normalized_search else ([selected_session] if selected_session else [])
+ sessions_to_resolve = sessions if normalized_search else ([selected_session] if selected_session else [])
session_infos = _list_session_infos(selected_stage, sessions_to_resolve)
session_info_map = {item.name: item for item in session_infos}
- items = _collect_prompt_files(selected_stage, selected_session, session_info_map)
+ sessions_to_collect = sessions if normalized_search else ([selected_session] if selected_session else [])
+ items = [
+ item
+ for session_name in sessions_to_collect
+ for item in _collect_prompt_files(selected_stage, session_name, session_info_map)
+ ]
+ items.sort(key=lambda item: (item.modified_at, item.timestamp or 0), reverse=True)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/webui/routers/reasoning_process.py` around lines 480 - 484, The code
builds sessions_to_resolve for multi-session searches but still calls
_collect_prompt_files with selected_session only, so cross-session collection
fails; change the call to collect from all sessions_to_resolve (either by
passing sessions_to_resolve into _collect_prompt_files or by iterating
sessions_to_resolve and merging results) and ensure session_info_map (from
_list_session_infos) is used to resolve names/IDs for each session; update any
call sites or signature of _collect_prompt_files (if needed) to accept and
process multiple session identifiers rather than a single selected_session.
zh-CN目标翻译作为常规 GitHub 编辑面;常规翻译以 Crowdin ->l10n_*PR 回流为准,详见docs/i18n.md请填写以下内容
(删除掉中括号内的空格,并替换为小写的x)
main分支 禁止修改,请确认本次提交的分支 不是main分支src/A_memorix,我确认已阅读src/A_memorix/MODIFICATION_POLICY.md,不涉及则无需勾选其他信息
Summary by CodeRabbit
新特性
界面调整
样式更新
其他调整