Skip to content

Dev#1699

Merged
SengokuCola merged 12 commits into
mainfrom
dev
May 16, 2026
Merged

Dev#1699
SengokuCola merged 12 commits into
mainfrom
dev

Conversation

@SengokuCola
Copy link
Copy Markdown
Collaborator

@SengokuCola SengokuCola commented May 16, 2026

  • ✅ 接受:与main直接相关的Bug修复:提交到dev分支
  • 新增功能类pr需要经过issue提前讨论,否则不会被合并
  • 🌐 i18n 提醒:除 bootstrap 或紧急修复外,请不要把非 zh-CN 目标翻译作为常规 GitHub 编辑面;常规翻译以 Crowdin -> l10n_* PR 回流为准,详见 docs/i18n.md

请填写以下内容

(删除掉中括号内的空格,并替换为小写的x

    • main 分支 禁止修改,请确认本次提交的分支 不是 main 分支
    • 我确认我阅读了贡献指南
    • 本次更新类型为:BUG修复
    • 本次更新类型为:功能新增
    • 本次更新是否经过测试
    • 如果本次修改涉及 src/A_memorix,我确认已阅读 src/A_memorix/MODIFICATION_POLICY.md,不涉及则无需勾选
  1. 请填写破坏性更新的具体内容(如有):
  2. 请简要说明本次更新的内容和目的:

其他信息

  • 关联 Issue:Close #
  • 截图/GIF
  • 附加信息:

Summary by CodeRabbit

  • 新特性

    • 插件市场新增排序(下载/点赞/评分)
    • 插件点赞/点踩/评分支持即时反馈与本地缓存
    • 新增推理过程阶段选择视图
  • 界面调整

    • 顶部操作栏更紧凑、图标化按钮
    • Prompt 管理页与插件列表布局与按钮优化
    • 移除导航项左侧高亮条
  • 样式更新

    • 主题调色板与浅/深色变量同步更新(强调色切换为绿色)
  • 其他调整

    • 改进后端错误消息格式化与相关测试
    • 精细表达选择默认关闭

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 948ce6bf-74f8-4de1-8489-41238eec6b42

📥 Commits

Reviewing files that changed from the base of the PR and between 5dc7f3e and 1698ac7.

📒 Files selected for processing (5)
  • AGENTS.md
  • changelogs/changelog.md
  • dashboard/package.json
  • dashboard/src/lib/version.ts
  • pyproject.toml

Walkthrough

本PR新增通用ID工具与错误格式化、为插件统计添加本地缓存与用户状态接口、引入插件市场排序、更新主题色与CSS变量,并在若干前端页面(Header、Prompts、Plugins、ReasoningProcess)做布局与交互调整;同时同步后端路由与配置默认值更新。

Changes

跨层级功能增强与优化

Layer / File(s) Summary
ID生成工具统一与错误处理
dashboard/src/lib/id.ts, dashboard/src/lib/api-error.ts, dashboard/src/lib/__tests__/api-error.test.ts, dashboard/src/lib/asset-store.ts, dashboard/src/components/ui/nested-key-value-editor.tsx
新增 generateId() 并替换项目中直接使用 crypto.randomUUID() 的位置;新增 formatApiError() 统一后端错误消息格式化并补充测试。
主题调色板与令牌更新
dashboard/src/lib/theme/palette.ts, dashboard/src/lib/theme/pipeline.ts, dashboard/src/lib/theme/tokens.ts, dashboard/src/lib/theme/storage.ts, dashboard/src/index.css
默认强调色由橙色调整为绿色,新增归一化与 legacy 兼容判断,更新 pipeline 的合并策略,并同步替换 CSS/Token HSL 值。
插件统计缓存与用户状态
dashboard/src/lib/plugin-stats.ts, dashboard/src/lib/expression-api.ts, src/webui/routers/plugin/stats_proxy.py
为插件统计实现本地缓存(TTL、localStorage、单例 in-flight 合并)、新增 getPluginUserState()、细化vote/rating/download响应类型,并在操作成功时更新缓存;后端代理新增 user-state 与 download 字段扩展。
插件市场排序与加载优化
dashboard/src/routes/plugins/index.tsx, dashboard/src/routes/plugins/MarketplaceTab.tsx, dashboard/src/routes/plugins/types.ts
新增 marketplace 排序键与 UI;MarketplaceTab 引入排序计算函数;页面在加载时优先使用缓存统计并异步刷新。
PluginStats 交互重构
dashboard/src/components/plugin-stats.tsx
组件并行加载统计与用户状态,增加 actionLoading,实现点赞/点踩/评分的即时缓存更新、禁用态与 toast 交互,非紧凑模式改为卡片网格展示。
Header 与 NavItem 紧凑化
dashboard/src/components/layout/Header.tsx, dashboard/src/components/layout/NavItem.tsx
搜索/文档/语言/登出触发器改为仅图标样式(保留 title/aria-label),移除 ShortcutKbd;NavItem 移除左侧激活高亮条。
Prompts 配置页布局调整
dashboard/src/routes/config/prompts.tsx
移除说明段落、刷新按钮改为图标、左侧文件列表改为 Badge+搜索、列表项移除大小信息,编辑器头部新增恢复/查看默认按钮。
推理过程阶段 API 与页面集成
dashboard/src/lib/reasoning-process-api.ts, dashboard/src/routes/reasoning-process.tsx, src/webui/routers/reasoning_process.py
新增 stages 类型与 listReasoningPromptStages(),后端新增 /reasoning-process/stages,前端页面新增阶段加载 effect 并调整选中回退与文件加载流程。
配置与用户信息来源调整
src/cli/maisaka_cli.py, src/maisaka/runtime.py, src/config/official_configs.py
CLI 与 runtime 的默认用户昵称逻辑改为固定“用户”优先;ExpressionConfig.enable_precise_expression_selection 默认值由 true 改为 false(并移除 advanced 标记)。
版本与文档
dashboard/package.json, dashboard/src/lib/version.ts, pyproject.toml, changelogs/changelog.md, AGENTS.md
更新 dashboard 版本至 1.1.4,pyproject 版本及依赖约束至 1.0.0-pre.21 / >=1.1.4;补充变更日志与 changelog 撰写建议。

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs:

总体概览

本PR涵盖前后端多个层面的协调更新:工具函数统一(ID生成)、错误处理标准化、插件统计缓存机制与排序功能、主题色系更新、推理过程API扩展,以及UI组件与配置的微调。共影响30+个文件,核心是围绕插件统计体验优化与界面紧凑化。

变更详情

插件统计与缓存

层级 / 文件 摘要
插件统计缓存机制与用户状态查询
dashboard/src/lib/plugin-stats.ts, dashboard/src/lib/__tests__/*, src/webui/routers/plugin/stats_proxy.py
新增localStorage缓存TTL与单例Promise合并、getPluginUserState() 用户状态查询、细化vote/rating/download响应类型、自动缓存更新。后端新增user-state代理路由与download请求字段。
插件市场排序与加载优化
dashboard/src/routes/plugins/index.tsx, dashboard/src/routes/plugins/MarketplaceTab.tsx, dashboard/src/routes/plugins/types.ts
新增MarketplaceSortKey排序类型、排序UI与逻辑(默认/下载/点赞/评分四维度)、缓存优先加载、异步刷新机制、兼容性筛选加载浮层。
PluginStats组件交互重构
dashboard/src/components/plugin-stats.tsx
引入actionLoading状态、点赞/点踩/评分即时缓存更新、非紧凑模式网格卡片布局、toast反馈与弹窗交互调整。

基础设施与主题

层级 / 文件 摘要
ID生成工具统一
dashboard/src/lib/id.ts, dashboard/src/lib/asset-store.ts, dashboard/src/components/ui/nested-key-value-editor.tsx, dashboard/src/routes/mcp-settings.tsx
新增 generateId() 工具函数(crypto.randomUUID fallback),替换分散在各模块的直接调用。
错误处理标准化
dashboard/src/lib/api-error.ts, dashboard/src/lib/api-error.test.ts, dashboard/src/lib/expression-api.ts
新增 formatApiError() 统一后端错误格式化(FastAPI验证、对象详情、fallback处理),覆盖expression-api的多个API方法。
主题调色板系统更新
dashboard/src/index.css, dashboard/src/lib/theme/palette.ts, dashboard/src/lib/theme/pipeline.ts, dashboard/src/lib/theme/storage.ts, dashboard/src/lib/theme/tokens.ts
默认强调色由橙色(#e68600)改为绿色(#55AB49),新增归一化与legacy兼容逻辑,同步CSS变量与TypeScript tokens。

UI与页面调整

层级 / 文件 摘要
Header与导航紧凑化
dashboard/src/components/layout/Header.tsx, dashboard/src/components/layout/NavItem.tsx
搜索/文档/语言/登出按钮改为仅图标、移除ShortcutKbd;NavItem移除激活状态左侧条。
Prompts配置页重构
dashboard/src/routes/config/prompts.tsx
移除说明段落、左侧改为Badge+搜索、按钮布局调整、恢复/查看默认按钮迁移到编辑器卡片头。

推理过程与配置

层级 / 文件 摘要
推理过程阶段API与集成
dashboard/src/lib/reasoning-process-api.ts, dashboard/src/routes/reasoning-process.tsx, src/webui/routers/reasoning_process.py
前端新增 listReasoningPromptStages() 查询;页面新增阶段加载effect、选中回退策略调整;后端扩展stages接口与session解析。
配置与用户信息调整
src/cli/maisaka_cli.py, src/maisaka/runtime.py, src/config/official_configs.py
CLI用户昵称改为固定"用户"、runtime优先级调整、ExpressionConfig默认值改为False。

相关PR

  • Mai-with-u/MaiBot#1692: 同样修改mcp-settings中的_uuid生成逻辑,PR#1692复用_uuid作为React key防止焦点丢失。
  • Mai-with-u/MaiBot#1693: 本PR的formatApiError与api-error.test.ts变更与PR#1693一致。

🎯 4 (Complex) | ⏱️ ~50 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between d1e9a59 and 5dc7f3e.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • dashboard/src/components/layout/Header.tsx
  • dashboard/src/components/layout/NavItem.tsx
  • dashboard/src/components/plugin-stats.tsx
  • dashboard/src/components/ui/nested-key-value-editor.tsx
  • dashboard/src/index.css
  • dashboard/src/lib/__tests__/api-error.test.ts
  • dashboard/src/lib/api-error.ts
  • dashboard/src/lib/asset-store.ts
  • dashboard/src/lib/expression-api.ts
  • dashboard/src/lib/id.ts
  • dashboard/src/lib/plugin-stats.ts
  • dashboard/src/lib/reasoning-process-api.ts
  • dashboard/src/lib/theme/palette.ts
  • dashboard/src/lib/theme/pipeline.ts
  • dashboard/src/lib/theme/storage.ts
  • dashboard/src/lib/theme/tokens.ts
  • dashboard/src/routes/config/prompts.tsx
  • dashboard/src/routes/mcp-settings.tsx
  • dashboard/src/routes/plugins/MarketplaceTab.tsx
  • dashboard/src/routes/plugins/index.tsx
  • dashboard/src/routes/plugins/types.ts
  • dashboard/src/routes/reasoning-process.tsx
  • src/cli/maisaka_cli.py
  • src/config/official_configs.py
  • src/maisaka/runtime.py
  • src/webui/routers/plugin/stats_proxy.py
  • src/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')}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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="切换语言">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

语言切换按钮可访问性文案被硬编码为中文。

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

Comment on lines 42 to 59
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)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

loadStats 中 Promise.all 若其中一个失败会导致整体失败。

getPluginStatsgetPluginUserState 中任一请求失败时,Promise.all 会拒绝,loading 状态将保持为 truesetLoading(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.

Suggested change
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.

Comment on lines +42 to +50
function hasUsableMessage(value: unknown): boolean {
if (value === null || value === undefined || value === '') {
return false
}
if (Array.isArray(value)) {
return value.length > 0
}
return true
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

避免空对象 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.

Comment on lines +293 to +302
if (!pluginStatsSummaryRequest || options.forceRefresh) {
pluginStatsSummaryRequest = fetchPluginStatsSummaryUncached()
.then((data) => {
pluginStatsSummaryCache = { timestamp: Date.now(), data }
writePluginStatsSummaryStorageCache(data)
return data
})
.finally(() => {
pluginStatsSummaryRequest = null
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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

Comment on lines +64 to +67
const isLegacyYellowAccent = (hsl: string): boolean => {
const { h, s, l } = parseHSL(hsl)
return h >= 30 && h <= 40 && s >= 70 && l >= 40 && l <= 55
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +45 to +56
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),
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

默认强调色分支应使用归一化后的颜色值。

命中默认色分支后直接使用 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.

Suggested change
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.

Comment on lines +272 to +273
title="刷新"
aria-label="刷新"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

新增按钮与输入框文案未走 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).

Comment on lines +56 to +59
@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}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

GET 端点缺少 plugin_iduser_id 的长度/格式校验。

其他 POST 端点(如 VoteRequest)对 plugin_iduser_idmin_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.

Suggested change
@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.

Comment on lines +480 to 484
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

搜索模式下仍只收集单会话文件,跨会话搜索会失效。

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.

@SengokuCola SengokuCola merged commit 5f530fd into main May 16, 2026
3 of 5 checks passed
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.

2 participants