feat(changelog): bundle changelogs locally

This commit is contained in:
digua authored and digua committed 2026-05-26 23:19:47 +08:00
1 parent ab90e06089
commit 9e88dcbd09
6 files changed
+78 -110

No files matched your search

-49
View File
@@ -1,49 +0,0 @@
name: Sync changelogs to chatlab.fun
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'changelogs/**'
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout ChatLab
uses: actions/checkout@v4
with:
sparse-checkout: changelogs
- name: Checkout chatlab.fun
uses: actions/checkout@v4
with:
repository: hellodigua/chatlab.fun
token: ${{ secrets.CHATLAB_FUN_TOKEN }}
path: chatlab-fun
- name: Sync changelogs
run: |
rsync -av --delete \
changelogs/ chatlab-fun/public/changelogs/
- name: Commit and push
id: push
working-directory: chatlab-fun
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add public/changelogs/
if git diff --cached --quiet; then
echo "No changes to commit"
echo "pushed=false" >> $GITHUB_OUTPUT
else
git commit -m "sync: update changelogs from ChatLab@${GITHUB_SHA::7}"
git push
echo "pushed=true" >> $GITHUB_OUTPUT
fi
- name: Trigger EdgeOne deploy
if: steps.push.outputs.pushed == 'true'
run: curl -s -X POST "${{ secrets.WEBSITE_DEPLOY_HOOK_URL }}"
+1
View File
@@ -3,6 +3,7 @@
- 边界:只解决当前需求,不做与需求无关的重构和"顺手优化"
- 每次完成任务后,对产生修改的文件进行类型检查、lint检查和format格式化,指定修改的文件路径去执行,确保代码质量
- 在执行检查时,如果有其他与本次修改无关的报错,也需要一并修复
- 测试文件位置:与单个业务模块强相关的单元测试应就近放在被测文件同目录,命名为 `*.test.ts` / `*.test.js`;跨模块、集成、E2E、测试工具或不明显归属某个业务文件的测试放在根目录 `tests/`;某个 app/package 专属测试放在对应的 `apps/*` 或 `packages/*` 内,遵循该子项目现有约定
- 多语言:代码中的日志、注释、AI 工具描述、错误消息等非 UI 文本默认使用英文。当有运行时 locale 可用时(如工具返回结果、AI 看到的文本),应通过 `isChineseLocale(locale)` 等机制支持中英双语。数据清洗中与聊天平台格式匹配的标签(如 `[分享]`、`[图片]`)保持原始语言不变。UI 文案的国际化遵循 `.docs/rules/i18n.md`
- 多端复用:维护 Electron 和 CLI Web 的共享业务逻辑时,优先在 `packages/node-runtime/src/services/` 下实现,禁止在路由/IPC handler 中绕过 core 直接写 SQL。详见 `.docs/README.md` 的"多端逻辑复用"章节。
- Commit 规范:使用 Conventional Commits。scope 规则——通用改动 scope 随意(如 `ai`、`import`、`sidebar` 等模块名);仅当改动是**平台特有**时才使用平台 scope(`electron`、`cli`、`web`)。
+6
View File
@@ -77,6 +77,12 @@ interface ChatApi {
error?: string
diagnostics?: ImportDiagnostics
}>
importDirectory: (dirPath: string) => Promise<{
success: boolean
sessionId?: string
error?: string
diagnostics?: ImportDiagnostics
}>
importWithOptions: (
filePath: string,
formatOptions: Record<string, unknown>
-2
View File
@@ -128,7 +128,6 @@ async function handleDirectoryDrop(dirEntry: FileSystemDirectoryEntry, fileList:
// Electron: derive directory path from the native file path
if (fileList.length > 0) {
try {
// @ts-expect-error Electron webUtils
const filePath: string | undefined = window.electron?.webUtils?.getPathForFile?.(fileList[0])
if (filePath) {
// fileList[0] may be the directory itself or a file inside it;
@@ -192,7 +191,6 @@ function processFiles(files: File[]) {
// 尝试获取文件路径(Electron 环境)
for (const file of files) {
try {
// @ts-ignore - Electron webUtils
const path = window.electron?.webUtils?.getPathForFile?.(file)
if (path) {
paths.push(path)
+24 -59
View File
@@ -1,14 +1,20 @@
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { CHATLAB_SITE_BASE, getChatlabSiteLocalePath } from '@/utils/chatlabSiteLocale'
import { storeToRefs } from 'pinia'
import { useSettingsStore } from '@/stores/settings'
import { sanitizeSummary } from '@/utils/sanitizeSummary'
import { getChangeTypeConfig } from './changelogTypeConfig'
import { IS_ELECTRON } from '@/utils/platform'
import { usePlatformService } from '@/services'
import CaptureButton from '@/components/common/CaptureButton.vue'
import {
getBundledChangelogs,
getBundledLatestVersion,
hasBundledChangelogVersion,
isBundledLatestVersion,
normalizeChangelogVersion,
type ChangelogItem,
} from './changelogData'
const { t } = useI18n()
const settingsStore = useSettingsStore()
@@ -47,11 +53,6 @@ function toggleVersion(version: string, index: number) {
expandedState.value.set(version, !currentState)
}
// 版本号统一格式,避免 v 前缀造成匹配失败
function normalizeVersion(version?: string | null) {
return version ? version.trim().replace(/^v/i, '') : null
}
// 判断版本是否展开
function isExpanded(version: string, index: number) {
// 如果有明确设置的状态,使用该状态
@@ -68,52 +69,22 @@ function isExpanded(version: string, index: number) {
// 判断是否是当前软件版本
function isCurrentVersion(version: string) {
const current = normalizeVersion(currentAppVersion.value)
return current ? normalizeVersion(version) === current : false
}
// Changelog 数据结构
interface ChangelogItem {
version: string
date: string
summary: string
changes: {
type: 'feat' | 'fix' | 'refactor' | 'docs' | 'chore' | 'style'
items: string[]
}[]
const current = normalizeChangelogVersion(currentAppVersion.value)
return current ? normalizeChangelogVersion(version) === current : false
}
// Changelog 数据
const changelogs = ref<ChangelogItem[]>([])
function getChangelogUrl(lang: string) {
const localePath = getChatlabSiteLocalePath(lang)
const langPath = localePath || 'en'
return `${CHATLAB_SITE_BASE}/changelogs/${langPath}.json`
}
// 从服务端获取 changelog 数据
// 从打包产物中读取 changelog 数据
async function fetchChangelogs() {
isLoading.value = true
loadError.value = null
try {
const url = getChangelogUrl(locale.value)
let data: unknown
if (IS_ELECTRON) {
const result = await usePlatformService().fetchRemoteConfig(url)
if (!result.success || !result.data) {
throw new Error(result.error || 'Failed to fetch')
}
data = result.data
} else {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data = await res.json()
}
changelogs.value = data as ChangelogItem[]
changelogs.value = getBundledChangelogs(locale.value)
} catch (error) {
console.error('Failed to fetch changelogs:', error)
console.error('Failed to load bundled changelogs:', error)
loadError.value = t('home.changelog.loadError')
} finally {
isLoading.value = false
@@ -161,7 +132,6 @@ function markVersionAsRead(version: string) {
// 检查是否需要显示新版本日志(冷启动时自动检查)
async function checkNewVersion() {
if (!IS_ELECTRON) return
try {
if (!localStorage.getItem(AGREEMENT_KEY)) {
return
@@ -169,12 +139,12 @@ async function checkNewVersion() {
// 1. 获取当前软件版本号
const rawVersion = await usePlatformService().getVersion()
const currentVersion = normalizeVersion(rawVersion)
const currentVersion = normalizeChangelogVersion(rawVersion)
if (!currentVersion) return
// 2. 获取 localStorage 中存储的已读版本号
const rawReadVersion = localStorage.getItem(CHANGELOG_READ_KEY)
const readVersion = normalizeVersion(rawReadVersion)
const readVersion = normalizeChangelogVersion(rawReadVersion)
// 2.1 如果是全新用户(从未设置过该 key),静默标记当前版本为已读,不弹窗
if (rawReadVersion === null) {
@@ -187,22 +157,19 @@ async function checkNewVersion() {
return
}
// 4. readVersion 为空或不等于 currentVersion,需要请求远程 changelog 数据
const result = await usePlatformService().fetchRemoteConfig(getChangelogUrl(locale.value))
if (!result.success || !result.data) return
const data = result.data as ChangelogItem[]
const latestChangelogVersion = normalizeVersion(data[0]?.version)
// 4. readVersion 为空或不等于 currentVersion,需要读取本地打包的 changelog 数据
const data = getBundledChangelogs(locale.value)
const latestChangelogVersion = normalizeChangelogVersion(getBundledLatestVersion(locale.value))
if (!latestChangelogVersion) return
// 仅当“当前版本就是最新版本”时才弹窗
// 避免当前版本较旧时(存在更高版本日志)也弹出阅读
if (currentVersion !== latestChangelogVersion) {
if (!isBundledLatestVersion(locale.value, currentVersion)) {
return
}
// 5. 在 changelog 中查找当前软件版本
const currentVersionExists = data.some((log) => normalizeVersion(log.version) === currentVersion)
const currentVersionExists = hasBundledChangelogVersion(locale.value, currentVersion)
// 如果在 changelog 中找不到当前版本,说明日志还没更新到当前版本,不显示弹窗
if (!currentVersionExists) {
@@ -227,12 +194,10 @@ async function checkNewVersion() {
// 手动打开弹窗(用户点击时调用),会自动获取数据
async function open() {
// 手动打开也标记当前版本,避免标签缺失
if (IS_ELECTRON) {
try {
currentAppVersion.value = normalizeVersion(await usePlatformService().getVersion())
} catch {
currentAppVersion.value = null
}
try {
currentAppVersion.value = normalizeChangelogVersion(await usePlatformService().getVersion())
} catch {
currentAppVersion.value = null
}
expandedState.value.clear()
showModal.value = true
@@ -0,0 +1,47 @@
import cnChangelogs from '../../../../changelogs/cn.json'
import enChangelogs from '../../../../changelogs/en.json'
import jaChangelogs from '../../../../changelogs/ja.json'
import twChangelogs from '../../../../changelogs/tw.json'
export type ChangelogChangeType = 'feat' | 'fix' | 'refactor' | 'docs' | 'chore' | 'style' | 'ci'
export interface ChangelogItem {
version: string
date: string
summary: string
changes: {
type: ChangelogChangeType
items: string[]
}[]
}
const BUNDLED_CHANGELOGS: Record<string, ChangelogItem[]> = {
'en-US': enChangelogs as ChangelogItem[],
'zh-CN': cnChangelogs as ChangelogItem[],
'zh-TW': twChangelogs as ChangelogItem[],
'ja-JP': jaChangelogs as ChangelogItem[],
}
export function normalizeChangelogVersion(version?: string | null) {
return version ? version.trim().replace(/^v/i, '') : null
}
export function getBundledChangelogs(locale: string): ChangelogItem[] {
return BUNDLED_CHANGELOGS[locale] ?? BUNDLED_CHANGELOGS['en-US']
}
export function getBundledLatestVersion(locale: string) {
return getBundledChangelogs(locale)[0]?.version || null
}
export function hasBundledChangelogVersion(locale: string, version?: string | null) {
const normalizedVersion = normalizeChangelogVersion(version)
if (!normalizedVersion) return false
return getBundledChangelogs(locale).some((log) => normalizeChangelogVersion(log.version) === normalizedVersion)
}
export function isBundledLatestVersion(locale: string, version?: string | null) {
const normalizedVersion = normalizeChangelogVersion(version)
const latestVersion = normalizeChangelogVersion(getBundledLatestVersion(locale))
return Boolean(normalizedVersion && latestVersion && normalizedVersion === latestVersion)
}