Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 52 additions & 17 deletions .github/workflows/sync-release-to-gitcode.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
name: Sync Release to GitCode

on:
release:
types: [created, published, prereleased]
workflow_run:
workflows: ['Release']
types:
- completed
branches: [main, alpha, beta]
workflow_dispatch:
inputs:
release_name:
Expand Down Expand Up @@ -118,8 +121,36 @@ jobs:
echo "prerelease=${{ github.event.inputs.prerelease }}" >> $GITHUB_OUTPUT
echo "draft=${{ github.event.inputs.draft }}" >> $GITHUB_OUTPUT
echo "test_mode=${{ github.event.inputs.test_mode }}" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
# Automatic trigger from workflow_run - get release info from API
echo "🚀 Workflow run trigger detected, fetching latest release from GitHub API"

# Get the latest release from GitHub API
latest_release=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/latest")

if [ "$(echo "$latest_release" | jq -r '.message // empty')" = "Not Found" ]; then
echo "❌ No releases found in repository"
exit 1
fi

TAG_NAME=$(echo "$latest_release" | jq -r '.tag_name')
RELEASE_NAME=$(echo "$latest_release" | jq -r '.name')
RELEASE_BODY=$(echo "$latest_release" | jq -r '.body // ""')
PRERELEASE=$(echo "$latest_release" | jq -r '.prerelease')
DRAFT=$(echo "$latest_release" | jq -r '.draft')

echo "Found latest release: $TAG_NAME"
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
echo "release_name=$RELEASE_NAME" >> $GITHUB_OUTPUT
echo "release_body<<EOF" >> $GITHUB_OUTPUT
echo "$RELEASE_BODY" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "prerelease=$PRERELEASE" >> $GITHUB_OUTPUT
echo "draft=$DRAFT" >> $GITHUB_OUTPUT
echo "test_mode=false" >> $GITHUB_OUTPUT
else
Comment on lines +124 to 152
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

workflow_run 分支:使用“最新发布”可能与触发的运行不匹配,存在同步错误风险。

  • 目前总是读取 /releases/latest,若并发多个发布或回滚时,可能取错 tag。
  • 建议基于 workflow_run 上下文定位“本次运行对应的 tag”,优先用 head_sha 反查 tag(git tag --points-at),否则再回退到 API。

示例最小修改(仅示意关键片段):

- elif [ "${{ github.event_name }}" = "workflow_run" ]; then
-   latest_release=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-     "https://api.github.com/repos/${{ github.repository }}/releases/latest")
-   TAG_NAME=$(echo "$latest_release" | jq -r '.tag_name')
+ elif [ "${{ github.event_name }}" = "workflow_run" ]; then
+   echo "🔎 Deriving tag from workflow_run head_sha..."
+   HEAD_SHA="${{ github.event.workflow_run.head_sha }}"
+   # 尝试用 head_sha 反查 tag(需已 checkout)
+   TAG_NAME=$(git tag --points-at "$HEAD_SHA" | head -n1)
+   if [ -z "$TAG_NAME" ]; then
+     echo "⚠️ No tag points at $HEAD_SHA, falling back to latest release"
+     latest_release=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
+       "https://api.github.com/repos/${{ github.repository }}/releases/latest")
+     TAG_NAME=$(echo "$latest_release" | jq -r '.tag_name')
+   fi
+   # 其余字段通过 /releases/tags/{tag} 获取,确保与 TAG_NAME 一致
+   tag_release=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
+     "https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG_NAME")
+   RELEASE_NAME=$(echo "$tag_release" | jq -r '.name')
+   RELEASE_BODY=$(echo "$tag_release" | jq -r '.body // ""')
+   PRERELEASE=$(echo "$tag_release" | jq -r '.prerelease')
+   DRAFT=$(echo "$tag_release" | jq -r '.draft')

Committable suggestion skipped: line range outside the PR's diff.

# Automatic trigger - use release event data
# Fallback - use release event data (for backward compatibility)
echo "🚀 Release event detected, using release data"
echo "tag_name=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
echo "release_name=${{ github.event.release.name }}" >> $GITHUB_OUTPUT
Expand Down Expand Up @@ -205,22 +236,17 @@ jobs:
run: |
mkdir -p ./release-assets

if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# Manual trigger - fetch release data from GitHub API
echo "📦 Fetching release assets for tag: ${{ steps.release.outputs.tag_name }}"
# Always fetch release data from GitHub API using the tag name
echo "📦 Fetching release assets for tag: ${{ steps.release.outputs.tag_name }}"

release_response=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ steps.release.outputs.tag_name }}")
release_response=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ steps.release.outputs.tag_name }}")

if [ "$(echo "$release_response" | jq -r '.message // empty')" = "Not Found" ]; then
echo "⚠️ Release not found for tag: ${{ steps.release.outputs.tag_name }}"
assets_json='[]'
else
assets_json=$(echo "$release_response" | jq '.assets')
fi
if [ "$(echo "$release_response" | jq -r '.message // empty')" = "Not Found" ]; then
echo "⚠️ Release not found for tag: ${{ steps.release.outputs.tag_name }}"
assets_json='[]'
else
# Automatic trigger - use event data
assets_json='${{ toJson(github.event.release.assets) }}'
assets_json=$(echo "$release_response" | jq '.assets')
fi

echo "Assets to download:"
Expand Down Expand Up @@ -470,7 +496,16 @@ jobs:
fi
echo "" >> $GITHUB_STEP_SUMMARY

echo "**Trigger:** ${{ github.event_name == 'workflow_dispatch' && '🔧 Manual' || '🚀 Automatic' }}" >> $GITHUB_STEP_SUMMARY
# Determine trigger type
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TRIGGER_TYPE="🔧 Manual"
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
TRIGGER_TYPE="🔄 Automatic (Workflow Run)"
else
TRIGGER_TYPE="🚀 Automatic"
fi

echo "**Trigger:** $TRIGGER_TYPE" >> $GITHUB_STEP_SUMMARY
echo "**Release:** ${{ steps.release.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Name:** ${{ steps.release.outputs.release_name }}" >> $GITHUB_STEP_SUMMARY
echo "**GitCode Repository:** $GITCODE_OWNER/$GITCODE_REPO" >> $GITHUB_STEP_SUMMARY
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ resources/media-server
.ffmpeg-cache
.ffprobe-cache
.cursor
openspec/changes
96 changes: 96 additions & 0 deletions openspec/specs/config-management/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
## Purpose

配置管理服务 SHALL 为应用程序提供统一的配置项管理功能,包括 API Key、用户偏好设置等的存储、读取和更新。系统 SHALL 支持多种配置类型的安全存储和实时通知机制。
Comment on lines +1 to +3
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

补充一级标题以通过 MD041。

建议将首行改为顶级标题,示例:

-## Purpose
+# 配置管理(Config Management)
+
+## Purpose
📝 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
## Purpose
配置管理服务 SHALL 为应用程序提供统一的配置项管理功能,包括 API Key、用户偏好设置等的存储、读取和更新。系统 SHALL 支持多种配置类型的安全存储和实时通知机制。
# 配置管理(Config Management)
## Purpose
配置管理服务 SHALL 为应用程序提供统一的配置项管理功能,包括 API Key、用户偏好设置等的存储、读取和更新。系统 SHALL 支持多种配置类型的安全存储和实时通知机制。
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 Prompt for AI Agents
openspec/specs/config-management/spec.md lines 1-3: the file starts with a
level-2 header ("## Purpose"), which triggers MD041 requiring a top-level
heading as the first line; update the first line to be a level-1 header (change
"## Purpose" to "# Purpose") while leaving the following content unchanged so
the document begins with a proper H1.


## Requirements

### Requirement: Zhipu API Key 配置管理

ConfigManager SHALL 支持 Zhipu API Key 的配置管理功能,包括配置枚举扩展、默认值设置、getter/setter 方法实现,以及配置变更通知机制。

#### Scenario: 配置枚举扩展

- **WHEN** 系统初始化 ConfigManager 时
- **THEN** ConfigKeys 枚举 SHALL 包含 `ZhipuApiKey = 'zhipuApiKey'` 配置项
- **AND** 类型系统 SHALL 正确识别新的配置项

#### Scenario: 配置方法实现

- **WHEN** 调用 `configManager.setZhipuApiKey('zhipu-api-key')` 时
- **THEN** 系统 SHALL 正确保存 API Key
- **AND** 调用 `configManager.getZhipuApiKey()` SHALL 返回设置的值

#### Scenario: 默认值配置

- **WHEN** 新安装应用程序或配置未设置时
- **THEN** `configManager.getZhipuApiKey()` SHALL 返回空字符串作为默认值

### Requirement: 配置持久化存储

Zhipu API Key SHALL 使用 electron-conf 进行安全存储,支持配置变更时的自动通知,并提供配置验证和错误处理功能。

Comment on lines +30 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

“使用 electron-conf 进行安全存储”表述不准确,存在安全姿态风险。

  • electron-conf 为本地明文配置,不等同安全存储;API Key 应写入系统凭据管理(如 keytar/Keychain/Credential Manager),conf 仅保存引用标识或必要元数据,并确保日志全程脱敏。
  • 建议在规范中明确:
    • 必须使用 OS 密钥环保存密钥;失败时回退方案需高亮风险与用户提示。
    • 禁止在日志/崩溃上报中记录明文或可逆加密的 Key。
    • 定义轮换与删除流程(包括迁移与回滚)。
🤖 Prompt for AI Agents
In openspec/specs/config-management/spec.md around lines 30–31, the current text
that mandates "使用 electron-conf 进行安全存储" is inaccurate and creates a security
risk; update the spec to require storing API keys in the OS credential store
(e.g., Keychain/Windows Credential Manager via keytar or equivalent), persist
only a reference or metadata in electron-conf (never the plaintext or reversible
encryption), and explicitly forbid logging or crash-reporting of plaintext/
reversible-key material (ensure all logs are masked). Also add language that: 1)
mandates a clear fallback behavior when OS keyring is unavailable (highlighting
risk and requiring explicit user consent and warning), 2) requires documented
key rotation and deletion procedures including migration and rollback steps, and
3) requires validation and error handling for key store operations with
user-facing error messages.

#### Scenario: 配置持久化

- **WHEN** 应用程序重启后
- **THEN** 之前设置的 Zhipu API Key SHALL 能够正确恢复
- **AND** 配置值 SHALL 保持不变

#### Scenario: 配置变更通知

- **WHEN** 调用 `configManager.setZhipuApiKey('new-key')` 时
- **THEN** 系统 SHALL 触发订阅者回调通知配置变更
- **AND** 回调函数 SHALL 接收到新的配置值

### Requirement: 默认值管理

ConfigManager SHALL 在 defaultValues 对象中包含所有配置项的默认值,新添加的 Zhipu API Key 配置项 SHALL 有相应的默认值设置。

#### Scenario: 默认值配置更新

- **WHEN** 初始化 defaultValues 对象时
- **THEN** 对象 SHALL 包含 `[ConfigKeys.ZhipuApiKey]: ''` 配置项
- **AND** 默认值 SHALL 为空字符串

### Requirement: 配置类型定义

配置管理 SHALL 确保所有配置项都有明确的 TypeScript 类型定义,保证编译时类型安全。

#### Scenario: 类型定义完整性

- **WHEN** 使用 `ConfigKeys.ZhipuApiKey` 时
- **THEN** TypeScript 编译器 SHALL 正确识别类型
- **AND** 不应产生类型错误

## 现有配置项

### Deepgram 配置

- **DeepgramApiKey**: Deepgram 语音识别 API Key
- **DefaultLanguage**: 默认转写语言
- **Model**: 转写模型选择

### 通用配置

- 各种用户偏好设置
- 应用程序行为配置
- 界面显示选项

## 技术要求

### 存储机制

- 使用 electron-conf 进行配置持久化
- 支持配置的原子性更新
- 提供配置变更的事件通知

### 类型安全

- 所有配置项都有明确的 TypeScript 类型定义
- 枚举值确保配置键的一致性
- 编译时类型检查防止配置错误

### 错误处理

- 配置读取失败时提供合理的默认值
- 配置写入失败时记录错误日志
- 支持配置验证和格式检查
168 changes: 168 additions & 0 deletions openspec/specs/translation-service/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
## Purpose
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

修复 markdownlint MD041:首行应为一级标题

将首行 “## Purpose” 提升为 H1。

-## Purpose
+# Purpose
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 Prompt for AI Agents
In openspec/specs/translation-service/spec.md around line 1, the file starts
with a level-2 heading "## Purpose" which violates markdownlint MD041 requiring
the first line be a top-level heading; change that first line to a level-1
heading by replacing "## Purpose" with "# Purpose" so the document begins with
an H1.


字幕翻译服务 SHALL 使用 Zhipu AI 的 glm-4.5-flash 模型将生成的字幕自动翻译成中文,为语言学习者提供更好的学习体验。系统 SHALL 支持批量处理、智能翻译、错误重试和数据库集成。

## Requirements

### Requirement: 翻译服务核心功能

SubtitleTranslationService SHALL 提供字幕翻译的核心功能,包括服务初始化、批量翻译处理和翻译结果管理。

#### Scenario: 服务初始化

- **WHEN** 创建新的 SubtitleTranslationService 实例时
- **THEN** 服务 SHALL 能够正确初始化而不抛出异常
- **AND** SHALL 准备好接收翻译请求

#### Scenario: 批量翻译调用

- **WHEN** 调用 `translationService.translateSubtitles()` 时
- **THEN** 系统 SHALL 返回包含成功状态和翻译映射的结果
- **AND** SHALL 支持指定目标语言、API Key 和视频文件名

### Requirement: 智能翻译处理

翻译服务 SHALL 构建包含上下文信息的智能翻译提示词,提供高质量的本土化翻译。

#### Scenario: 智能提示词构建

- **WHEN** 构建翻译提示词时
- **THEN** 系统 SHALL 包含视频文件名作为上下文
- **AND** SHALL 为每条字幕提供前后字幕作为翻译参考
- **AND** SHALL 明确要求本土化翻译,避免直译

#### Scenario: 批量翻译处理

- **WHEN** 处理大量字幕时
- **THEN** 系统 SHALL 将字幕分成 10-20 条的小批次
- **AND** SHALL 为每批字幕提供前后上下文
- **AND** SHALL 最多并发处理 2 个批次

### Requirement: 错误处理和重试机制

翻译服务 SHALL 实现完善的错误处理机制,确保系统稳定性。

#### Scenario: 网络错误重试

- **WHEN** 遇到网络错误时
- **THEN** 系统 SHALL 自动重试最多 3 次
- **AND** SHALL 使用指数退避策略
- **AND** 最终失败时 SHALL 返回错误信息

#### Scenario: API Key 错误处理

- **WHEN** API Key 无效时
- **THEN** 系统 SHALL 立即失败并返回适当的错误信息
- **AND** SHALL 不进行重试操作

### Requirement: 数据库集成

翻译服务 SHALL 支持将翻译结果批量更新到数据库中。

#### Scenario: 批量更新翻译

- **WHEN** 翻译完成时
- **THEN** 系统 SHALL 使用字幕 ID 映射翻译结果
- **AND** SHALL 批量更新数据库中的 translatedText 字段
- **AND** 更新失败时 SHALL 记录详细错误信息

### Requirement: ASR 服务集成

翻译服务 SHALL 与 ASR 字幕生成流程无缝集成。

#### Scenario: 自动翻译触发

- **WHEN** ASR 字幕保存到数据库后且配置了 Zhipu API Key 时
- **THEN** 系统 SHALL 自动启动后台翻译任务
- **AND** SHALL 不阻塞字幕生成的主流程

#### Scenario: 翻译失败隔离

- **WHEN** 翻译任务失败时
- **THEN** 系统 SHALL 记录错误日志
- **AND** SHALL 不影响字幕生成的成功状态
- **AND** 原始字幕 SHALL 正常显示给用户

### Requirement: 数据库层扩展

数据访问层 SHALL 支持字幕翻译的批量更新操作。

#### Scenario: 批量更新方法

- **WHEN** 调用 `updateSubtitleTranslations()` 方法时
- **THEN** 系统 SHALL 接收字幕 ID 和翻译文本的映射
- **AND** SHALL 支持事务处理确保数据一致性
- **AND** SHALL 返回详细的更新统计信息

## 与其他服务的集成

### ASR 服务集成

#### Requirement: ASR-015: 在 ASR 流程中集成翻译功能

**Description**: 在字幕生成完成后自动启动后台翻译任务

**Acceptance Criteria**:

- 在字幕保存到数据库后检查 Zhipu API Key 配置
- 使用 Promise.resolve().then() 启动后台任务,不阻塞主流程
- 翻译过程完全静默进行,不影响用户操作
- 翻译完成后更新数据库中的 translatedText 字段

**File**: `src/main/services/ASRSubtitleService.ts`

#### Scenario: ASR-015-01: 自动翻译触发

```typescript
// 在 generateSubtitle 方法的字幕保存后
if (subtitleLibraryId && configManager.getZhipuApiKey()) {
// 应该启动后台翻译任务
// 不应该阻塞字幕生成流程
}
```

#### Scenario: ASR-015-02: 翻译失败处理

```typescript
// 翻译任务失败时
// 应该记录错误日志
// 不应该影响字幕生成的成功状态
// 原始字幕应该正常显示给用户
```

### 数据库层集成

#### Requirement: DB-010: 添加批量更新翻译方法

**Description**: 在数据访问层添加批量更新字幕翻译的方法

**Acceptance Criteria**:

- 添加 `updateSubtitleTranslations(subtitleLibraryId, translations)` 方法
- 接收字幕 ID 和翻译文本的映射
- 支持事务处理,确保数据一致性
- 提供详细的错误信息和统计结果

**File**: `src/main/db/dao.ts`

#### Scenario: DB-010-01: 批量更新实现

```typescript
const result = await db.subtitleLibrary.updateSubtitleTranslations(subtitleLibraryId, translations)
// 应该返回更新统计信息
Comment on lines +136 to +152
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

规范与实现存在“批量更新”方法命名偏差

规范使用 updateSubtitleTranslations(...)(复数),实现中(SubtitleTranslationService.processBatch)调用的是逐条 updateSubtitleTranslation(...)。请二选一对齐:要么实现批量 DAO 方法,要么调整规范描述。

// 应该处理部分更新的情况
```

## 技术约束

- **当前版本仅支持翻译为中文**(代码中需要 TODO 标记未来扩展)
- **使用 zhipu-ai-provider npm 包**(已安装)
- **遵循项目的异步文件操作规范**
- **使用 loggerService 记录日志**

## 性能要求

- 批量处理大小:10-20 条字幕/批次
- 并发批次数量:最多 2 个
- 重试策略:指数退避,最多 3 次重试
- 翻译过程必须在后台进行,不阻塞用户操作
Loading