Conversation
类图:更新日志功能的核心组件和实用程序classDiagram
direction LR
class Status {
<<Component (DashboardPage)>>
#isChangelogOpen: boolean
#npmLatest: string | false
#proxyFn: (url: string) => string
+handleTooltipClick(): void
}
Status "1" --o "1" UpdateLogModal : 显示
class UpdateLogModal {
<<Component>>
+currentVersion: string
+isOpen: boolean
+onOpenChange(isOpen: boolean): void
+npmLatest: string | false
+proxyFn(url: string): string
#isLoadingRelease: boolean
#releaseData: GithubRelease[]
#updateLogs: GithubRelease[]
+useEffect(): void
+handleCloseModal(): void
}
UpdateLogModal "1" --o "1" Markdown : 用于渲染
UpdateLogModal "1" --o "1" UpdateButtons : 嵌入
UpdateLogModal ..> version_ts : 使用 extractUpdateLogs
UpdateLogModal ..> GithubRelease : 显示来自的数据
class UpdateButtons {
<<Component>>
+handleCloseModal(): void
#running: boolean
+onUpdate(): void
}
class Markdown {
<<Component>>
+content: string
+className?: string
}
class version_ts {
<<Utility Module>>
+getPackageType(tagName: string): string | null
+extractUpdateLogs(releases: GithubRelease[], currentCoreVersion: string): GithubRelease[]
}
version_ts ..> GithubRelease : 处理
class GithubRelease {
<<Data Type>>
+tag_name: string
+body: string
+created_at: string
+published_at: string
+prerelease: boolean
+name: string
+html_url: string
+author: object
+assets: object[]
}
文件级别更改
提示和命令与 Sourcery 互动
自定义您的体验访问您的 仪表板 以:
获取帮助Original review guide in EnglishReviewer's GuideThis PR refactors the update log display into a standalone UpdateLogModal component (with its own fetching and UI), extracts the update button logic into an UpdateButtons component, enhances the Markdown renderer for better styling and extensibility, improves the version library’s log extraction algorithm, and cleans up the dashboard page by removing legacy inline logic and unused imports. Sequence Diagram: Displaying Update Logs in ModalsequenceDiagram
actor User
participant DashboardPage as "DashboardPage (Status Comp.)"
participant UpdateLogModal as "UpdateLogModal"
participant version_ts as "version.ts"
participant GitHubAPI as "GitHub API (via proxyFn)"
User->>DashboardPage: Clicks 'View Changelog'
DashboardPage->>DashboardPage: Sets isChangelogOpen = true
DashboardPage->>UpdateLogModal: Renders (isOpen=true, currentVersion, proxyFn)
activate UpdateLogModal
alt Data not yet loaded OR isOpen becomes true
UpdateLogModal->>UpdateLogModal: setIsLoadingRelease = true
UpdateLogModal->>GitHubAPI: GET releases.json (via proxyFn)
activate GitHubAPI
GitHubAPI-->>UpdateLogModal: Returns GithubRelease[] data
deactivate GitHubAPI
UpdateLogModal->>version_ts: extractUpdateLogs(data, currentVersion)
activate version_ts
version_ts-->>UpdateLogModal: Returns filtered/sorted updateLogs
deactivate version_ts
UpdateLogModal->>UpdateLogModal: setIsLoadingRelease = false, sets updateLogs
end
UpdateLogModal->>User: Displays update logs
deactivate UpdateLogModal
Class Diagram: Core Components and Utilities for Update Log FeatureclassDiagram
direction LR
class Status {
<<Component (DashboardPage)>>
#isChangelogOpen: boolean
#npmLatest: string | false
#proxyFn: (url: string) => string
+handleTooltipClick(): void
}
Status "1" --o "1" UpdateLogModal : displays
class UpdateLogModal {
<<Component>>
+currentVersion: string
+isOpen: boolean
+onOpenChange(isOpen: boolean): void
+npmLatest: string | false
+proxyFn(url: string): string
#isLoadingRelease: boolean
#releaseData: GithubRelease[]
#updateLogs: GithubRelease[]
+useEffect(): void
+handleCloseModal(): void
}
UpdateLogModal "1" --o "1" Markdown : uses for rendering
UpdateLogModal "1" --o "1" UpdateButtons : embeds
UpdateLogModal ..> version_ts : uses extractUpdateLogs
UpdateLogModal ..> GithubRelease : displays data from
class UpdateButtons {
<<Component>>
+handleCloseModal(): void
#running: boolean
+onUpdate(): void
}
class Markdown {
<<Component>>
+content: string
+className?: string
}
class version_ts {
<<Utility Module>>
+getPackageType(tagName: string): string | null
+extractUpdateLogs(releases: GithubRelease[], currentCoreVersion: string): GithubRelease[]
}
version_ts ..> GithubRelease : processes
class GithubRelease {
<<Data Type>>
+tag_name: string
+body: string
+created_at: string
+published_at: string
+prerelease: boolean
+name: string
+html_url: string
+author: object
+assets: object[]
}
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
你可以通过以下命令安装该版本: |
There was a problem hiding this comment.
嘿 @ikenxuan - 我已经查看了你的更改,它们看起来很棒!
AI代理的提示
请解决此代码审查中的评论:
## 单独评论
### 评论 1
<location> `packages/web/src/pages/dashboard/index.tsx:178` </location>
<code_context>
+ // pollingInterval: 5000,
})
const handleTooltipClick = () => {
</code_context>
<issue_to_address>
Modal可能在代理函数准备好之前获取数据
由于fetch现在立即运行,请确保它仅在`proxyFn`初始化之后执行——可以通过检查`proxyFnInitialized`或将此标志传递给`UpdateLogModal`。否则,可能会意外使用默认URL。
</issue_to_address>
### 评论 2
<location> `packages/web/src/components/UpdateLogModal.tsx:31` </location>
<code_context>
+}: UpdateLogModalProps) {
+ const [isLoadingRelease, setIsLoadingRelease] = useState(false)
+
+ const { data: releaseData, error: releaseError, run: fetchRelease } = useRequest(
+ async () => {
+ setIsLoadingRelease(true)
</code_context>
<issue_to_address>
fetchRelease回调关闭初始proxyFn
由于`proxyFn`未包含在依赖项中,因此对其的更新不会反映在`fetchRelease`中。将`proxyFn`添加到`refreshDeps`或在其更改时重新创建请求。
</issue_to_address>
### 评论 3
<location> `packages/web/src/components/UpdateLogModal.tsx:50` </location>
<code_context>
+ )
+
+ // 当模态框打开时自动获取数据
+ useEffect(() => {
+ if (isOpen && !releaseData && !isLoadingRelease) {
+ fetchRelease()
+ }
+ }, [isOpen, releaseData, isLoadingRelease, fetchRelease])
+
+ const updateLogs = releaseData ? extractUpdateLogs(releaseData, currentVersion) : []
</code_context>
<issue_to_address>
获取错误时可能出现无限重试循环
考虑通过检查错误状态或使用标志来避免无限重试,从而防止在失败后重复尝试获取。
</issue_to_address>
### 评论 4
<location> `packages/web/src/components/UpdateLogModal.tsx:58` </location>
<code_context>
+
+ const updateLogs = releaseData ? extractUpdateLogs(releaseData, currentVersion) : []
+
+ const middleVersions = useMemo(() => {
+ return updateLogs || []
+ }, [updateLogs])
+
+ const handleCloseModal = useCallback(() => {
</code_context>
<issue_to_address>
`middleVersions`的冗余useMemo
您可以直接使用`updateLogs`并删除不必要的`useMemo`用于`middleVersions`。
</issue_to_address>
### 评论 5
<location> `packages/web/src/components/UpdateButtons.tsx:72` </location>
<code_context>
+ return (
+ <div className='flex gap-2 ml-auto'>
+ {running && <FullScreenLoader />}
+ <Button
+ ref={window.innerWidth <= 768 ? liquidGlassButtonRef1 : undefined}
+ color='danger'
+ variant='flat'
+ isDisabled={running}
+ onPress={handleCloseModal}
+ className='glass-effect'
+ >
+ 关闭
+ </Button>
+ <Button
+ ref={window.innerWidth <= 768 ? liquidGlassButtonRef2 : undefined}
+ color='primary'
+ variant='flat'
+ isDisabled={running}
+ onPress={onUpdate}
+ className='glass-effect'
+ >
</code_context>
<issue_to_address>
直接使用`window.innerWidth`可能会破坏SSR并且不具有反应性
这种方法会导致服务器端渲染期间出现错误,并且不会响应视口更改。在使用之前,使用媒体查询钩子或检查`window`。
</issue_to_address>
### 评论 6
<location> `packages/web/src/lib/version.ts:67` </location>
<code_context>
+ * @param currentCoreVersion 当前 core 版本(纯版本号,如 '1.10.3')
* @returns 更新日志
*/
export const extractUpdateLogs = (releases: GithubRelease[], currentCoreVersion: string): GithubRelease[] => {
- // 找到最新 core 版本
- releases
</code_context>
<issue_to_address>
考虑重构该函数以使用单次映射进行注释、整合排序和更清晰的链式操作。
```suggestion
// Extract common utilities
const TARGET_PACKAGES = ['core', 'web', 'cli', 'create-karin'] as const;
type PackageType = typeof TARGET_PACKAGES[number];
const getPackageType = (tag: string): PackageType | null => {
if (tag.startsWith('core-v')) return 'core';
if (tag.startsWith('web-v')) return 'web';
if (tag.startsWith('cli-v')) return 'cli';
if (tag.startsWith('create-karin-v') || tag.startsWith('create-v')) return 'create-karin';
return null;
};
const parseTs = (r: GithubRelease) => Date.parse(r.created_at);
const sortByDateDesc = (a: GithubRelease, b: GithubRelease) =>
parseTs(b) - parseTs(a);
export const extractUpdateLogs = (
releases: GithubRelease[],
currentCoreVersion: string
): GithubRelease[] => {
// 1) filter & annotate
const enriched = releases
.map(r => ({ r, type: getPackageType(r.tag_name), ts: parseTs(r) }))
.filter(({ type }) => type && TARGET_PACKAGES.includes(type));
const currentTag = `core-v${currentCoreVersion}`;
const current = enriched.find(({ r }) => r.tag_name === currentTag);
// 2) not found → return all sorted
if (!current) {
return enriched.map(({ r }) => r).sort(sortByDateDesc);
}
// 3) find latest core
const latestCore = enriched
.filter(({ type }) => type === 'core')
.map(({ r }) => r)
.reduce((best, cur) =>
compareVersion(cur.tag_name, best.tag_name) > 0 ? cur : best,
current.r
);
// 4) already up-to-date
if (compareVersion(latestCore.tag_name, currentTag) <= 0) {
return [];
}
// 5) filter by time‐range and sort once
const fromTs = current.ts;
const toTs = Date.parse(latestCore.created_at);
return enriched
.map(({ r, ts }) => ({ r, ts }))
.filter(({ r, ts }) =>
r.tag_name !== currentTag &&
ts > fromTs &&
ts <= toTs
)
.map(({ r }) => r)
.sort(sortByDateDesc);
};
```
- 单次传递给`map`→附加`type`和`ts`,然后对包类型进行一次`filter`。
- 每次发布调用一次`parseTs`。
- 将排序合并到`sortByDateDesc`中。
- 链式操作(没有嵌套的`if`/`filter`块)。
- 保留所有原始行为。
</issue_to_address>
### 评论 7
<location> `packages/web/src/components/UpdateLogModal.tsx:120` </location>
<code_context>
+ )
+ : (
+ <>
+ {middleVersions.map((versionInfo) => (
+ <div
+ key={versionInfo.tag_name}
</code_context>
<issue_to_address>
考虑提取内联标签和日期逻辑,以及发布项目渲染,到单独的辅助函数和一个子组件中,以简化主组件的JSX。
```suggestion
内联IIFE/switch和日期格式化为您的JSX添加了很多噪音。将它们提取到小的纯辅助函数和/或一个`<ReleaseItem />`子组件中。例如:
1) helpers/tag.ts
```ts
export type TagInfo = { label: string; color: 'primary'|'warning'|'secondary'|'success' };
export function getTagInfo(tag: string): TagInfo {
if (tag.includes('web')) return { label: 'WEB 界面', color: 'warning' };
if (tag.includes('cli')) return { label: '命令行工具', color: 'secondary' };
if (tag.includes('create')) return { label: '脚手架', color: 'success' };
// default to core
return { label: '本体', color: 'primary' };
}
```
2) helpers/date.ts
```ts
export function formatDateZh(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('zh-CN', {
year: 'numeric', month: 'short', day: 'numeric',
});
}
```
3) components/ReleaseItem.tsx
```tsx
import { Chip } from '@heroui/chip';
import Markdown from '@/components/Markdown';
import { getTagInfo } from '@/helpers/tag';
import { formatDateZh } from '@/helpers/date';
export function ReleaseItem({ info }) {
const { label, color } = getTagInfo(info.tag_name);
return (
<div className="p-5 …">
<div className="flex justify-between mb-3">
<div className="flex gap-2">
<Chip color={color} …>{label}</Chip>
<span className="font-mono"> {info.tag_name}</span>
</div>
<div className="text-right">
<div className="text-xs">{formatDateZh(info.published_at)}</div>
{info.prerelease && <Chip color="warning">预发布</Chip>}
</div>
</div>
{info.name && <h4>…{info.name}</h4>}
<Markdown content={info.body} className="prose …" />
{/* …assets + footer… */}
</div>
);
}
```
4) 在您的模态JSX中,将内联IIFE和日期逻辑替换为:
```tsx
{middleVersions.map(v => (
<ReleaseItem key={v.tag_name} info={v} />
))}
```
这保留了所有功能,但将复杂逻辑移出了渲染,使组件更易于阅读。
</issue_to_address>
### 评论 8
<location> `packages/web/src/components/UpdateButtons.tsx:30` </location>
<code_context>
+ if (status === 'ok') {
+ toast.success('更新成功,正在重启......')
+ await restartRequest({ isPm2: true })
+ await new Promise(resolve => {
+ const interval = setInterval(async () => {
+ try {
</code_context>
<issue_to_address>
考虑将轮询逻辑提取到辅助函数中,并整合重复的钩子以简化组件。
```suggestion
考虑将轮询逻辑提取到可重用的辅助函数中,并将重复的钩子折叠为一个。例如:
// utils/server.ts
export async function waitForServerUp(
timeout = 2000
): Promise<void> {
while (true) {
try {
await request.serverGet('/api/v1/ping')
return
} catch {
await new Promise((r) => setTimeout(r, timeout))
}
}
}
// UpdateButtons.tsx
import { waitForServerUp } from '@/utils/server'
export default function UpdateButtons({ handleCloseModal }: UpdateButtonsProps) {
const [running, setRunning] = useState(false)
const dialog = useDialog()
// single ref usage
const glassRef = useLiquidGlassButton({
gaussianBlur: 0,
scale: 20,
transparency: 0,
})
const onUpdate = () => {
dialog.confirm({
title: '更新',
content: '确认更新吗?',
onConfirm: async () => {
setRunning(true)
try {
const { status } = await request.serverGet<{ status: 'ok' | 'failed' }>(
'/api/v1/system/update',
{ timeout: 30000 }
)
if (status !== 'ok') throw new Error('更新失败')
toast.success('更新成功,正在重启…')
await restartRequest({ isPm2: true })
await waitForServerUp()
toast.success('重启成功')
window.location.reload()
} catch (err: any) {
toast.error(err.message || '操作失败')
} finally {
setRunning(false)
}
},
})
}
const applyRef = window.innerWidth <= 768 ? glassRef : undefined
return (
<div className="flex gap-2 ml-auto">
{running && <FullScreenLoader />}
<Button
ref={applyRef}
color="danger"
variant="flat"
isDisabled={running}
onPress={handleCloseModal}
className="glass-effect"
>
关闭
</Button>
<Button
ref={applyRef}
color="primary"
variant="flat"
isDisabled={running}
onPress={onUpdate}
className="glass-effect"
>
更新
</Button>
</div>
)
}
```
减少复杂性的步骤:
1. 将轮询拉入`waitForServerUp`以消除内联循环。
2. 将嵌套的`try/catch`合并为一个具有清晰流程的块。
3. 使用单个`useLiquidGlassButton`钩子并有条件地应用其ref。
</issue_to_address>帮助我更有用!请点击每个评论上的👍或👎,我将使用反馈来改进您的评论。
Original comment in English
Hey @ikenxuan - I've reviewed your changes and they look great!
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `packages/web/src/pages/dashboard/index.tsx:178` </location>
<code_context>
+ // pollingInterval: 5000,
})
const handleTooltipClick = () => {
</code_context>
<issue_to_address>
Modal may fetch before proxy function is ready
Since fetch now runs immediately, ensure it only executes after `proxyFn` is initialized—either by checking `proxyFnInitialized` or passing this flag to `UpdateLogModal`. Otherwise, the default URL may be used unintentionally.
</issue_to_address>
### Comment 2
<location> `packages/web/src/components/UpdateLogModal.tsx:31` </location>
<code_context>
+}: UpdateLogModalProps) {
+ const [isLoadingRelease, setIsLoadingRelease] = useState(false)
+
+ const { data: releaseData, error: releaseError, run: fetchRelease } = useRequest(
+ async () => {
+ setIsLoadingRelease(true)
</code_context>
<issue_to_address>
fetchRelease callback closes over initial proxyFn
Since `proxyFn` is not included in the dependencies, updates to it won't be reflected in `fetchRelease`. Add `proxyFn` to `refreshDeps` or recreate the request when it changes.
</issue_to_address>
### Comment 3
<location> `packages/web/src/components/UpdateLogModal.tsx:50` </location>
<code_context>
+ )
+
+ // 当模态框打开时自动获取数据
+ useEffect(() => {
+ if (isOpen && !releaseData && !isLoadingRelease) {
+ fetchRelease()
+ }
+ }, [isOpen, releaseData, isLoadingRelease, fetchRelease])
+
+ const updateLogs = releaseData ? extractUpdateLogs(releaseData, currentVersion) : []
</code_context>
<issue_to_address>
Potential infinite retry loop on fetch errors
Consider preventing repeated fetch attempts after a failure by checking for an error state or using a flag to avoid infinite retries.
</issue_to_address>
### Comment 4
<location> `packages/web/src/components/UpdateLogModal.tsx:58` </location>
<code_context>
+
+ const updateLogs = releaseData ? extractUpdateLogs(releaseData, currentVersion) : []
+
+ const middleVersions = useMemo(() => {
+ return updateLogs || []
+ }, [updateLogs])
+
+ const handleCloseModal = useCallback(() => {
</code_context>
<issue_to_address>
Redundant useMemo for `middleVersions`
You can use `updateLogs` directly and remove the unnecessary `useMemo` for `middleVersions`.
</issue_to_address>
### Comment 5
<location> `packages/web/src/components/UpdateButtons.tsx:72` </location>
<code_context>
+ return (
+ <div className='flex gap-2 ml-auto'>
+ {running && <FullScreenLoader />}
+ <Button
+ ref={window.innerWidth <= 768 ? liquidGlassButtonRef1 : undefined}
+ color='danger'
+ variant='flat'
+ isDisabled={running}
+ onPress={handleCloseModal}
+ className='glass-effect'
+ >
+ 关闭
+ </Button>
+ <Button
+ ref={window.innerWidth <= 768 ? liquidGlassButtonRef2 : undefined}
+ color='primary'
+ variant='flat'
+ isDisabled={running}
+ onPress={onUpdate}
+ className='glass-effect'
+ >
</code_context>
<issue_to_address>
Direct `window.innerWidth` usage may break SSR & is not reactive
This approach will cause errors during server-side rendering and won't respond to viewport changes. Use a media-query hook or check for `window` before accessing it.
</issue_to_address>
### Comment 6
<location> `packages/web/src/lib/version.ts:67` </location>
<code_context>
+ * @param currentCoreVersion 当前 core 版本(纯版本号,如 '1.10.3')
* @returns 更新日志
*/
export const extractUpdateLogs = (releases: GithubRelease[], currentCoreVersion: string): GithubRelease[] => {
- // 找到最新 core 版本
- releases
</code_context>
<issue_to_address>
Consider refactoring the function to use a single-pass mapping for annotation, consolidated sorting, and clearer chained operations.
```suggestion
// Extract common utilities
const TARGET_PACKAGES = ['core', 'web', 'cli', 'create-karin'] as const;
type PackageType = typeof TARGET_PACKAGES[number];
const getPackageType = (tag: string): PackageType | null => {
if (tag.startsWith('core-v')) return 'core';
if (tag.startsWith('web-v')) return 'web';
if (tag.startsWith('cli-v')) return 'cli';
if (tag.startsWith('create-karin-v') || tag.startsWith('create-v')) return 'create-karin';
return null;
};
const parseTs = (r: GithubRelease) => Date.parse(r.created_at);
const sortByDateDesc = (a: GithubRelease, b: GithubRelease) =>
parseTs(b) - parseTs(a);
export const extractUpdateLogs = (
releases: GithubRelease[],
currentCoreVersion: string
): GithubRelease[] => {
// 1) filter & annotate
const enriched = releases
.map(r => ({ r, type: getPackageType(r.tag_name), ts: parseTs(r) }))
.filter(({ type }) => type && TARGET_PACKAGES.includes(type));
const currentTag = `core-v${currentCoreVersion}`;
const current = enriched.find(({ r }) => r.tag_name === currentTag);
// 2) not found → return all sorted
if (!current) {
return enriched.map(({ r }) => r).sort(sortByDateDesc);
}
// 3) find latest core
const latestCore = enriched
.filter(({ type }) => type === 'core')
.map(({ r }) => r)
.reduce((best, cur) =>
compareVersion(cur.tag_name, best.tag_name) > 0 ? cur : best,
current.r
);
// 4) already up-to-date
if (compareVersion(latestCore.tag_name, currentTag) <= 0) {
return [];
}
// 5) filter by time‐range and sort once
const fromTs = current.ts;
const toTs = Date.parse(latestCore.created_at);
return enriched
.map(({ r, ts }) => ({ r, ts }))
.filter(({ r, ts }) =>
r.tag_name !== currentTag &&
ts > fromTs &&
ts <= toTs
)
.map(({ r }) => r)
.sort(sortByDateDesc);
};
```
- Single pass to `map` → attach `type` and `ts`, then one `filter` on package type.
- One `parseTs` call per release.
- Consolidated sort into `sortByDateDesc`.
- Chained operations (no nested `if`/`filter` blocks).
- Preserves all original behavior.
</issue_to_address>
### Comment 7
<location> `packages/web/src/components/UpdateLogModal.tsx:120` </location>
<code_context>
+ )
+ : (
+ <>
+ {middleVersions.map((versionInfo) => (
+ <div
+ key={versionInfo.tag_name}
</code_context>
<issue_to_address>
Consider extracting the inline tag and date logic, as well as the release item rendering, into separate helper functions and a sub-component to simplify the main component's JSX.
```suggestion
The inline IIFE/switch and date formatting are adding a lot of noise to your JSX. Extract those into small pure helpers and/or a `<ReleaseItem />` sub-component. For example:
1) helpers/tag.ts
```ts
export type TagInfo = { label: string; color: 'primary'|'warning'|'secondary'|'success' };
export function getTagInfo(tag: string): TagInfo {
if (tag.includes('web')) return { label: 'WEB 界面', color: 'warning' };
if (tag.includes('cli')) return { label: '命令行工具', color: 'secondary' };
if (tag.includes('create')) return { label: '脚手架', color: 'success' };
// default to core
return { label: '本体', color: 'primary' };
}
```
2) helpers/date.ts
```ts
export function formatDateZh(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('zh-CN', {
year: 'numeric', month: 'short', day: 'numeric',
});
}
```
3) components/ReleaseItem.tsx
```tsx
import { Chip } from '@heroui/chip';
import Markdown from '@/components/Markdown';
import { getTagInfo } from '@/helpers/tag';
import { formatDateZh } from '@/helpers/date';
export function ReleaseItem({ info }) {
const { label, color } = getTagInfo(info.tag_name);
return (
<div className="p-5 …">
<div className="flex justify-between mb-3">
<div className="flex gap-2">
<Chip color={color} …>{label}</Chip>
<span className="font-mono"> {info.tag_name}</span>
</div>
<div className="text-right">
<div className="text-xs">{formatDateZh(info.published_at)}</div>
{info.prerelease && <Chip color="warning">预发布</Chip>}
</div>
</div>
{info.name && <h4>…{info.name}</h4>}
<Markdown content={info.body} className="prose …" />
{/* …assets + footer… */}
</div>
);
}
```
4) In your modal JSX, replace the inline IIFE and date logic with:
```tsx
{middleVersions.map(v => (
<ReleaseItem key={v.tag_name} info={v} />
))}
```
This keeps all functionality but moves the complex logic out of the render, making the component far easier to read.
</issue_to_address>
### Comment 8
<location> `packages/web/src/components/UpdateButtons.tsx:30` </location>
<code_context>
+ if (status === 'ok') {
+ toast.success('更新成功,正在重启......')
+ await restartRequest({ isPm2: true })
+ await new Promise(resolve => {
+ const interval = setInterval(async () => {
+ try {
</code_context>
<issue_to_address>
Consider extracting the polling logic into a helper function and consolidating duplicate hooks to simplify the component.
```suggestion
Consider extracting the polling logic into a reusable helper and collapsing duplicate hooks into one. For example:
// utils/server.ts
export async function waitForServerUp(
timeout = 2000
): Promise<void> {
while (true) {
try {
await request.serverGet('/api/v1/ping')
return
} catch {
await new Promise((r) => setTimeout(r, timeout))
}
}
}
// UpdateButtons.tsx
import { waitForServerUp } from '@/utils/server'
export default function UpdateButtons({ handleCloseModal }: UpdateButtonsProps) {
const [running, setRunning] = useState(false)
const dialog = useDialog()
// single ref usage
const glassRef = useLiquidGlassButton({
gaussianBlur: 0,
scale: 20,
transparency: 0,
})
const onUpdate = () => {
dialog.confirm({
title: '更新',
content: '确认更新吗?',
onConfirm: async () => {
setRunning(true)
try {
const { status } = await request.serverGet<{ status: 'ok' | 'failed' }>(
'/api/v1/system/update',
{ timeout: 30000 }
)
if (status !== 'ok') throw new Error('更新失败')
toast.success('更新成功,正在重启…')
await restartRequest({ isPm2: true })
await waitForServerUp()
toast.success('重启成功')
window.location.reload()
} catch (err: any) {
toast.error(err.message || '操作失败')
} finally {
setRunning(false)
}
},
})
}
const applyRef = window.innerWidth <= 768 ? glassRef : undefined
return (
<div className="flex gap-2 ml-auto">
{running && <FullScreenLoader />}
<Button
ref={applyRef}
color="danger"
variant="flat"
isDisabled={running}
onPress={handleCloseModal}
className="glass-effect"
>
关闭
</Button>
<Button
ref={applyRef}
color="primary"
variant="flat"
isDisabled={running}
onPress={onUpdate}
className="glass-effect"
>
更新
</Button>
</div>
)
}
```
Steps to reduce complexity:
1. Pull polling into `waitForServerUp` to eliminate inline loops.
2. Merge nested `try/catch` into one block with clear flow.
3. Use a single `useLiquidGlassButton` hook and conditionally apply its ref.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if (typeof fn === 'function') { | ||
| setProxyFn(fn) | ||
| } else { | ||
| // 如果不是函数,保持默认的 url => url 函数 | ||
| console.warn('testGithub 返回的不是函数,使用默认代理函数') | ||
| } | ||
| setProxyFnInitialized(true) |
There was a problem hiding this comment.
issue (bug_risk): Modal可能在代理函数准备好之前获取数据
由于fetch现在立即运行,请确保它仅在proxyFn初始化之后执行——可以通过检查proxyFnInitialized或将此标志传递给UpdateLogModal。否则,可能会意外使用默认URL。
Original comment in English
issue (bug_risk): Modal may fetch before proxy function is ready
Since fetch now runs immediately, ensure it only executes after proxyFn is initialized—either by checking proxyFnInitialized or passing this flag to UpdateLogModal. Otherwise, the default URL may be used unintentionally.
| }: UpdateLogModalProps) { | ||
| const [isLoadingRelease, setIsLoadingRelease] = useState(false) | ||
|
|
||
| const { data: releaseData, error: releaseError, run: fetchRelease } = useRequest( |
There was a problem hiding this comment.
issue (bug_risk): fetchRelease回调关闭初始proxyFn
由于proxyFn未包含在依赖项中,因此对其的更新不会反映在fetchRelease中。将proxyFn添加到refreshDeps或在其更改时重新创建请求。
Original comment in English
issue (bug_risk): fetchRelease callback closes over initial proxyFn
Since proxyFn is not included in the dependencies, updates to it won't be reflected in fetchRelease. Add proxyFn to refreshDeps or recreate the request when it changes.
| useEffect(() => { | ||
| if (isOpen && !releaseData && !isLoadingRelease) { | ||
| fetchRelease() | ||
| } | ||
| }, [isOpen, releaseData, isLoadingRelease, fetchRelease]) |
There was a problem hiding this comment.
issue (bug_risk): 获取错误时可能出现无限重试循环
考虑通过检查错误状态或使用标志来避免无限重试,从而防止在失败后重复尝试获取。
Original comment in English
issue (bug_risk): Potential infinite retry loop on fetch errors
Consider preventing repeated fetch attempts after a failure by checking for an error state or using a flag to avoid infinite retries.
| const middleVersions = useMemo(() => { | ||
| return updateLogs || [] | ||
| }, [updateLogs]) |
There was a problem hiding this comment.
nitpick: middleVersions的冗余useMemo
您可以直接使用updateLogs并删除不必要的useMemo用于middleVersions。
Original comment in English
nitpick: Redundant useMemo for middleVersions
You can use updateLogs directly and remove the unnecessary useMemo for middleVersions.
| <Button | ||
| ref={window.innerWidth <= 768 ? liquidGlassButtonRef1 : undefined} | ||
| color='danger' | ||
| variant='flat' | ||
| isDisabled={running} | ||
| onPress={handleCloseModal} | ||
| className='glass-effect' | ||
| > | ||
| 关闭 | ||
| </Button> |
There was a problem hiding this comment.
issue (bug_risk): 直接使用window.innerWidth可能会破坏SSR并且不具有反应性
这种方法会导致服务器端渲染期间出现错误,并且不会响应视口更改。在使用之前,使用媒体查询钩子或检查window。
Original comment in English
issue (bug_risk): Direct window.innerWidth usage may break SSR & is not reactive
This approach will cause errors during server-side rendering and won't respond to viewport changes. Use a media-query hook or check for window before accessing it.
| * @param currentCoreVersion 当前 core 版本(纯版本号,如 '1.10.3') | ||
| * @returns 更新日志 | ||
| */ | ||
| export const extractUpdateLogs = (releases: GithubRelease[], currentCoreVersion: string): GithubRelease[] => { |
There was a problem hiding this comment.
issue (complexity): 考虑重构该函数以使用单次映射进行注释、整合排序和更清晰的链式操作。
| export const extractUpdateLogs = (releases: GithubRelease[], currentCoreVersion: string): GithubRelease[] => { | |
| // Extract common utilities | |
| const TARGET_PACKAGES = ['core', 'web', 'cli', 'create-karin'] as const; | |
| type PackageType = typeof TARGET_PACKAGES[number]; | |
| const getPackageType = (tag: string): PackageType | null => { | |
| if (tag.startsWith('core-v')) return 'core'; | |
| if (tag.startsWith('web-v')) return 'web'; | |
| if (tag.startsWith('cli-v')) return 'cli'; | |
| if (tag.startsWith('create-karin-v') || tag.startsWith('create-v')) return 'create-karin'; | |
| return null; | |
| }; | |
| const parseTs = (r: GithubRelease) => Date.parse(r.created_at); | |
| const sortByDateDesc = (a: GithubRelease, b: GithubRelease) => | |
| parseTs(b) - parseTs(a); | |
| export const extractUpdateLogs = ( | |
| releases: GithubRelease[], | |
| currentCoreVersion: string | |
| ): GithubRelease[] => { | |
| // 1) filter & annotate | |
| const enriched = releases | |
| .map(r => ({ r, type: getPackageType(r.tag_name), ts: parseTs(r) })) | |
| .filter(({ type }) => type && TARGET_PACKAGES.includes(type)); | |
| const currentTag = `core-v${currentCoreVersion}`; | |
| const current = enriched.find(({ r }) => r.tag_name === currentTag); | |
| // 2) not found → return all sorted | |
| if (!current) { | |
| return enriched.map(({ r }) => r).sort(sortByDateDesc); | |
| } | |
| // 3) find latest core | |
| const latestCore = enriched | |
| .filter(({ type }) => type === 'core') | |
| .map(({ r }) => r) | |
| .reduce((best, cur) => | |
| compareVersion(cur.tag_name, best.tag_name) > 0 ? cur : best, | |
| current.r | |
| ); | |
| // 4) already up-to-date | |
| if (compareVersion(latestCore.tag_name, currentTag) <= 0) { | |
| return []; | |
| } | |
| // 5) filter by time‐range and sort once | |
| const fromTs = current.ts; | |
| const toTs = Date.parse(latestCore.created_at); | |
| return enriched | |
| .map(({ r, ts }) => ({ r, ts })) | |
| .filter(({ r, ts }) => | |
| r.tag_name !== currentTag && | |
| ts > fromTs && | |
| ts <= toTs | |
| ) | |
| .map(({ r }) => r) | |
| .sort(sortByDateDesc); | |
| }; |
- 单次传递给
map→附加type和ts,然后对包类型进行一次filter。 - 每次发布调用一次
parseTs。 - 将排序合并到
sortByDateDesc中。 - 链式操作(没有嵌套的
if/filter块)。 - 保留所有原始行为。
Original comment in English
issue (complexity): Consider refactoring the function to use a single-pass mapping for annotation, consolidated sorting, and clearer chained operations.
| export const extractUpdateLogs = (releases: GithubRelease[], currentCoreVersion: string): GithubRelease[] => { | |
| // Extract common utilities | |
| const TARGET_PACKAGES = ['core', 'web', 'cli', 'create-karin'] as const; | |
| type PackageType = typeof TARGET_PACKAGES[number]; | |
| const getPackageType = (tag: string): PackageType | null => { | |
| if (tag.startsWith('core-v')) return 'core'; | |
| if (tag.startsWith('web-v')) return 'web'; | |
| if (tag.startsWith('cli-v')) return 'cli'; | |
| if (tag.startsWith('create-karin-v') || tag.startsWith('create-v')) return 'create-karin'; | |
| return null; | |
| }; | |
| const parseTs = (r: GithubRelease) => Date.parse(r.created_at); | |
| const sortByDateDesc = (a: GithubRelease, b: GithubRelease) => | |
| parseTs(b) - parseTs(a); | |
| export const extractUpdateLogs = ( | |
| releases: GithubRelease[], | |
| currentCoreVersion: string | |
| ): GithubRelease[] => { | |
| // 1) filter & annotate | |
| const enriched = releases | |
| .map(r => ({ r, type: getPackageType(r.tag_name), ts: parseTs(r) })) | |
| .filter(({ type }) => type && TARGET_PACKAGES.includes(type)); | |
| const currentTag = `core-v${currentCoreVersion}`; | |
| const current = enriched.find(({ r }) => r.tag_name === currentTag); | |
| // 2) not found → return all sorted | |
| if (!current) { | |
| return enriched.map(({ r }) => r).sort(sortByDateDesc); | |
| } | |
| // 3) find latest core | |
| const latestCore = enriched | |
| .filter(({ type }) => type === 'core') | |
| .map(({ r }) => r) | |
| .reduce((best, cur) => | |
| compareVersion(cur.tag_name, best.tag_name) > 0 ? cur : best, | |
| current.r | |
| ); | |
| // 4) already up-to-date | |
| if (compareVersion(latestCore.tag_name, currentTag) <= 0) { | |
| return []; | |
| } | |
| // 5) filter by time‐range and sort once | |
| const fromTs = current.ts; | |
| const toTs = Date.parse(latestCore.created_at); | |
| return enriched | |
| .map(({ r, ts }) => ({ r, ts })) | |
| .filter(({ r, ts }) => | |
| r.tag_name !== currentTag && | |
| ts > fromTs && | |
| ts <= toTs | |
| ) | |
| .map(({ r }) => r) | |
| .sort(sortByDateDesc); | |
| }; |
- Single pass to
map→ attachtypeandts, then onefilteron package type. - One
parseTscall per release. - Consolidated sort into
sortByDateDesc. - Chained operations (no nested
if/filterblocks). - Preserves all original behavior.
| ) | ||
| : ( | ||
| <> | ||
| {middleVersions.map((versionInfo) => ( |
There was a problem hiding this comment.
issue (complexity): 考虑提取内联标签和日期逻辑,以及发布项目渲染,到单独的辅助函数和一个子组件中,以简化主组件的JSX。
| {middleVersions.map((versionInfo) => ( | |
| 内联IIFE/switch和日期格式化为您的JSX添加了很多噪音。将它们提取到小的纯辅助函数和/或一个`<ReleaseItem />`子组件中。例如: | |
| 1) helpers/tag.ts | |
| ```ts | |
| export type TagInfo = { label: string; color: 'primary'|'warning'|'secondary'|'success' }; | |
| export function getTagInfo(tag: string): TagInfo { | |
| if (tag.includes('web')) return { label: 'WEB 界面', color: 'warning' }; | |
| if (tag.includes('cli')) return { label: '命令行工具', color: 'secondary' }; | |
| if (tag.includes('create')) return { label: '脚手架', color: 'success' }; | |
| // default to core | |
| return { label: '本体', color: 'primary' }; | |
| } |
- helpers/date.ts
export function formatDateZh(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('zh-CN', {
year: 'numeric', month: 'short', day: 'numeric',
});
}- components/ReleaseItem.tsx
import { Chip } from '@heroui/chip';
import Markdown from '@/components/Markdown';
import { getTagInfo } from '@/helpers/tag';
import { formatDateZh } from '@/helpers/date';
export function ReleaseItem({ info }) {
const { label, color } = getTagInfo(info.tag_name);
return (
<div className="p-5 …">
<div className="flex justify-between mb-3">
<div className="flex gap-2">
<Chip color={color} …>{label}</Chip>
<span className="font-mono"> {info.tag_name}</span>
</div>
<div className="text-right">
<div className="text-xs">{formatDateZh(info.published_at)}</div>
{info.prerelease && <Chip color="warning">预发布</Chip>}
</div>
</div>
{info.name && <h4>…{info.name}</h4>}
<Markdown content={info.body} className="prose …" />
{/* …assets + footer… */}
</div>
);
}- 在您的模态JSX中,将内联IIFE和日期逻辑替换为:
{middleVersions.map(v => (
<ReleaseItem key={v.tag_name} info={v} />
))}这保留了所有功能,但将复杂逻辑移出了渲染,使组件更易于阅读。
Original comment in English
issue (complexity): Consider extracting the inline tag and date logic, as well as the release item rendering, into separate helper functions and a sub-component to simplify the main component's JSX.
| {middleVersions.map((versionInfo) => ( | |
| The inline IIFE/switch and date formatting are adding a lot of noise to your JSX. Extract those into small pure helpers and/or a `<ReleaseItem />` sub-component. For example: | |
| 1) helpers/tag.ts | |
| ```ts | |
| export type TagInfo = { label: string; color: 'primary'|'warning'|'secondary'|'success' }; | |
| export function getTagInfo(tag: string): TagInfo { | |
| if (tag.includes('web')) return { label: 'WEB 界面', color: 'warning' }; | |
| if (tag.includes('cli')) return { label: '命令行工具', color: 'secondary' }; | |
| if (tag.includes('create')) return { label: '脚手架', color: 'success' }; | |
| // default to core | |
| return { label: '本体', color: 'primary' }; | |
| } |
- helpers/date.ts
export function formatDateZh(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('zh-CN', {
year: 'numeric', month: 'short', day: 'numeric',
});
}- components/ReleaseItem.tsx
import { Chip } from '@heroui/chip';
import Markdown from '@/components/Markdown';
import { getTagInfo } from '@/helpers/tag';
import { formatDateZh } from '@/helpers/date';
export function ReleaseItem({ info }) {
const { label, color } = getTagInfo(info.tag_name);
return (
<div className="p-5 …">
<div className="flex justify-between mb-3">
<div className="flex gap-2">
<Chip color={color} …>{label}</Chip>
<span className="font-mono"> {info.tag_name}</span>
</div>
<div className="text-right">
<div className="text-xs">{formatDateZh(info.published_at)}</div>
{info.prerelease && <Chip color="warning">预发布</Chip>}
</div>
</div>
{info.name && <h4>…{info.name}</h4>}
<Markdown content={info.body} className="prose …" />
{/* …assets + footer… */}
</div>
);
}- In your modal JSX, replace the inline IIFE and date logic with:
{middleVersions.map(v => (
<ReleaseItem key={v.tag_name} info={v} />
))}This keeps all functionality but moves the complex logic out of the render, making the component far easier to read.
| if (status === 'ok') { | ||
| toast.success('更新成功,正在重启......') | ||
| await restartRequest({ isPm2: true }) | ||
| await new Promise(resolve => { |
There was a problem hiding this comment.
issue (complexity): 考虑将轮询逻辑提取到辅助函数中,并整合重复的钩子以简化组件。
| await new Promise(resolve => { | |
| 考虑将轮询逻辑提取到可重用的辅助函数中,并将重复的钩子折叠为一个。例如: | |
| // utils/server.ts | |
| export async function waitForServerUp( | |
| timeout = 2000 | |
| ): Promise<void> { | |
| while (true) { | |
| try { | |
| await request.serverGet('/api/v1/ping') | |
| return | |
| } catch { | |
| await new Promise((r) => setTimeout(r, timeout)) | |
| } | |
| } | |
| } | |
| // UpdateButtons.tsx | |
| import { waitForServerUp } from '@/utils/server' | |
| export default function UpdateButtons({ handleCloseModal }: UpdateButtonsProps) { | |
| const [running, setRunning] = useState(false) | |
| const dialog = useDialog() | |
| // single ref usage | |
| const glassRef = useLiquidGlassButton({ | |
| gaussianBlur: 0, | |
| scale: 20, | |
| transparency: 0, | |
| }) | |
| const onUpdate = () => { | |
| dialog.confirm({ | |
| title: '更新', | |
| content: '确认更新吗?', | |
| onConfirm: async () => { | |
| setRunning(true) | |
| try { | |
| const { status } = await request.serverGet<{ status: 'ok' | 'failed' }>( | |
| '/api/v1/system/update', | |
| { timeout: 30000 } | |
| ) | |
| if (status !== 'ok') throw new Error('更新失败') | |
| toast.success('更新成功,正在重启…') | |
| await restartRequest({ isPm2: true }) | |
| await waitForServerUp() | |
| toast.success('重启成功') | |
| window.location.reload() | |
| } catch (err: any) { | |
| toast.error(err.message || '操作失败') | |
| } finally { | |
| setRunning(false) | |
| } | |
| }, | |
| }) | |
| } | |
| const applyRef = window.innerWidth <= 768 ? glassRef : undefined | |
| return ( | |
| <div className="flex gap-2 ml-auto"> | |
| {running && <FullScreenLoader />} | |
| <Button | |
| ref={applyRef} | |
| color="danger" | |
| variant="flat" | |
| isDisabled={running} | |
| onPress={handleCloseModal} | |
| className="glass-effect" | |
| > | |
| 关闭 | |
| </Button> | |
| <Button | |
| ref={applyRef} | |
| color="primary" | |
| variant="flat" | |
| isDisabled={running} | |
| onPress={onUpdate} | |
| className="glass-effect" | |
| > | |
| 更新 | |
| </Button> | |
| </div> | |
| ) | |
| } |
减少复杂性的步骤:
- 将轮询拉入
waitForServerUp以消除内联循环。 - 将嵌套的
try/catch合并为一个具有清晰流程的块。 - 使用单个
useLiquidGlassButton钩子并有条件地应用其ref。
Original comment in English
issue (complexity): Consider extracting the polling logic into a helper function and consolidating duplicate hooks to simplify the component.
| await new Promise(resolve => { | |
| Consider extracting the polling logic into a reusable helper and collapsing duplicate hooks into one. For example: | |
| // utils/server.ts | |
| export async function waitForServerUp( | |
| timeout = 2000 | |
| ): Promise<void> { | |
| while (true) { | |
| try { | |
| await request.serverGet('/api/v1/ping') | |
| return | |
| } catch { | |
| await new Promise((r) => setTimeout(r, timeout)) | |
| } | |
| } | |
| } | |
| // UpdateButtons.tsx | |
| import { waitForServerUp } from '@/utils/server' | |
| export default function UpdateButtons({ handleCloseModal }: UpdateButtonsProps) { | |
| const [running, setRunning] = useState(false) | |
| const dialog = useDialog() | |
| // single ref usage | |
| const glassRef = useLiquidGlassButton({ | |
| gaussianBlur: 0, | |
| scale: 20, | |
| transparency: 0, | |
| }) | |
| const onUpdate = () => { | |
| dialog.confirm({ | |
| title: '更新', | |
| content: '确认更新吗?', | |
| onConfirm: async () => { | |
| setRunning(true) | |
| try { | |
| const { status } = await request.serverGet<{ status: 'ok' | 'failed' }>( | |
| '/api/v1/system/update', | |
| { timeout: 30000 } | |
| ) | |
| if (status !== 'ok') throw new Error('更新失败') | |
| toast.success('更新成功,正在重启…') | |
| await restartRequest({ isPm2: true }) | |
| await waitForServerUp() | |
| toast.success('重启成功') | |
| window.location.reload() | |
| } catch (err: any) { | |
| toast.error(err.message || '操作失败') | |
| } finally { | |
| setRunning(false) | |
| } | |
| }, | |
| }) | |
| } | |
| const applyRef = window.innerWidth <= 768 ? glassRef : undefined | |
| return ( | |
| <div className="flex gap-2 ml-auto"> | |
| {running && <FullScreenLoader />} | |
| <Button | |
| ref={applyRef} | |
| color="danger" | |
| variant="flat" | |
| isDisabled={running} | |
| onPress={handleCloseModal} | |
| className="glass-effect" | |
| > | |
| 关闭 | |
| </Button> | |
| <Button | |
| ref={applyRef} | |
| color="primary" | |
| variant="flat" | |
| isDisabled={running} | |
| onPress={onUpdate} | |
| className="glass-effect" | |
| > | |
| 更新 | |
| </Button> | |
| </div> | |
| ) | |
| } |
Steps to reduce complexity:
- Pull polling into
waitForServerUpto eliminate inline loops. - Merge nested
try/catchinto one block with clear flow. - Use a single
useLiquidGlassButtonhook and conditionally apply its ref.
好的,这是翻译成简体中文的 pull request 总结:
Sourcery 总结
提取并重构更新日志显示和更新控制到专用组件中,简化仪表板中的版本处理,并通过一致的样式增强 Markdown 渲染。
增强功能:
UpdateLogModal组件中。UpdateButtons组件中。getPackageType并改进extractUpdateLogs以过滤和排序跨多个包类型的发布日志。Markdown组件以接受className并支持其他元素(pre、li、strong、em),并统一样式。npmLatest的状态类型,并删除状态检查中不必要的轮询间隔。Original summary in English
Summary by Sourcery
Extract and refactor update log display and update controls into dedicated components, simplify version handling in the dashboard, and enhance markdown rendering with consistent styling.
Enhancements: