Compare commits

..
3 Commits
7 changed files with 76 additions and 10 deletions

No files matched your search

+7 -1
View File
@@ -25,12 +25,18 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
1. `package.json` 版本号 +1,写 `RELEASE_NOTES.md`(仓库根,已 gitignore) 1. `package.json` 版本号 +1,写 `RELEASE_NOTES.md`(仓库根,已 gitignore)
2. `npm run dist` 构建 2. `npm run dist` 构建
3. `node scripts/release.cjs <版本号>`,例如 `node scripts/release.cjs 1.0.2` 3. `node scripts/release.cjs <版本号>`,例如 `node scripts/release.cjs 1.0.2`
- 依次完成:Gitea release(含 msi/exe 资产)→ Gitea 更新通道(`api/packages/admin/generic/openterminal-update/stable`,更新 latest.yml/blockmap/exe)→ GitHub release - 依次完成:Gitea release(含 msi/exe 资产)→ Gitea 更新通道(`api/packages/admin/generic/openterminal-update/stable`,更新 latest.yml/blockmap/exe/release-notes.md)→ GitHub release
- 可用 `--skip-github` / `--skip-gitea` 跳过某步 - 可用 `--skip-github` / `--skip-gitea` 跳过某步
- 访问 GitHub API 不通时,设 `HTTPS_PROXY=http://127.0.0.1:7897` 再走代理 - 访问 GitHub API 不通时,设 `HTTPS_PROXY=http://127.0.0.1:7897` 再走代理
4. 验证更新通道:`curl https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/latest.yml` 应返回新版本号 4. 验证更新通道:`curl https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/latest.yml` 应返回新版本号
5. `git tag v<版本号>` 并推送两个远程 5. `git tag v<版本号>` 并推送两个远程
## 更新机制
- 检查更新:先 Gitea 更新通道(强制直连,不走系统代理),失败回退 GitHub(走系统代理)。 electron-updater 用独立 session(partition `electron-updater`),代理模式在 `useFeed` 里按源切换
- 更新日志:Gitea 仓库是私有的(匿名 API 404),改为从更新通道的 `release-notes.md` 读取(直连 session `openterminal-update-direct`),再回退 Gitea/GitHub releases API
- electron-updater 不支持 MSI 自动更新,自动更新只走 NSIS exe
## 主题机制 ## 主题机制
- 终端主题由 xterm 主题派生 UI 配色:`src/renderer/src/theme/chrome.ts` 的 `applyChromeTheme` 写入 `--chrome-bg/-bg-deep/-border/-hover` CSS 变量,antd token 在 `main.tsx` ThemedConfigProvider 派生 - 终端主题由 xterm 主题派生 UI 配色:`src/renderer/src/theme/chrome.ts` 的 `applyChromeTheme` 写入 `--chrome-bg/-bg-deep/-border/-hover` CSS 变量,antd token 在 `main.tsx` ThemedConfigProvider 派生
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "open-terminal", "name": "open-terminal",
"productName": "OpenTerminal", "productName": "OpenTerminal",
"version": "1.0.2", "version": "1.0.3",
"description": "Open-source terminal with SSH, split panes, themes and fonts", "description": "Open-source terminal with SSH, split panes, themes and fonts",
"main": "out/main/index.js", "main": "out/main/index.js",
"author": "Bill", "author": "Bill",
+9 -2
View File
@@ -75,8 +75,15 @@ async function giteaChannel() {
const base = 'https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable' const base = 'https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable'
const del = await fetch(base, { method: 'DELETE', headers: { Authorization: `token ${env.GIT_TOKEN}` } }) const del = await fetch(base, { method: 'DELETE', headers: { Authorization: `token ${env.GIT_TOKEN}` } })
console.log(' delete old stable:', del.status) console.log(' delete old stable:', del.status)
for (const f of [path.join(R, 'latest.yml'), path.join(R, `OpenTerminal-${V}-setup.exe.blockmap`), path.join(R, `OpenTerminal-${V}-setup.exe`)]) { const channelFiles: Array<[string, string]> = [
const name = path.basename(f) [path.join(R, 'latest.yml'), 'latest.yml'],
[path.join(R, `OpenTerminal-${V}-setup.exe.blockmap`), `OpenTerminal-${V}-setup.exe.blockmap`],
[path.join(R, `OpenTerminal-${V}-setup.exe`), `OpenTerminal-${V}-setup.exe`],
// Changelog served from the channel: the Gitea repo is private (anonymous
// releases API 404s), but the generic package is publicly readable.
[path.join(ROOT, 'RELEASE_NOTES.md'), 'release-notes.md']
]
for (const [f, name] of channelFiles) {
const stat = fs.statSync(f) const stat = fs.statSync(f)
const resp = await fetch(`${base}/${encodeURIComponent(name)}`, { const resp = await fetch(`${base}/${encodeURIComponent(name)}`, {
method: 'PUT', method: 'PUT',
+2 -2
View File
@@ -34,7 +34,7 @@ function showMainWindow(showOrCreate: () => void): void {
/** Rebuild the context menu so the close-action radio items reflect settings. */ /** Rebuild the context menu so the close-action radio items reflect settings. */
function refreshContextMenu(showOrCreate: () => void): void { function refreshContextMenu(showOrCreate: () => void): void {
if (!tray) return if (!tray) return
const action = loadSettings().system.closeAction ?? 'ask' const action = loadSettings().system.closeAction ?? 'tray'
const setAction = (value: CloseAction) => (): void => { const setAction = (value: CloseAction) => (): void => {
persistCloseAction(value) persistCloseAction(value)
refreshContextMenu(showOrCreate) refreshContextMenu(showOrCreate)
@@ -86,7 +86,7 @@ function hideToTray(win: BrowserWindow): void {
*/ */
export async function onMainWindowClose(win: BrowserWindow, e: Electron.Event, showOrCreate: () => void): Promise<void> { export async function onMainWindowClose(win: BrowserWindow, e: Electron.Event, showOrCreate: () => void): Promise<void> {
if (quitting) return if (quitting) return
const action = loadSettings().system.closeAction ?? 'ask' const action = loadSettings().system.closeAction ?? 'tray'
if (action === 'tray') { if (action === 'tray') {
e.preventDefault() e.preventDefault()
hideToTray(win) hideToTray(win)
+40 -3
View File
@@ -1,4 +1,4 @@
import { app, ipcMain, net } from 'electron' import { app, ipcMain, net, session } from 'electron'
import { autoUpdater } from 'electron-updater' import { autoUpdater } from 'electron-updater'
import { Ipc, type ReleaseNote, type UpdateState } from '../shared/ipc' import { Ipc, type ReleaseNote, type UpdateState } from '../shared/ipc'
import { broadcast } from './broadcast' import { broadcast } from './broadcast'
@@ -30,6 +30,8 @@ function setState(patch: Partial<UpdateState>): void {
function useFeed(feed: 'gitea' | 'github'): void { function useFeed(feed: 'gitea' | 'github'): void {
activeFeed = feed activeFeed = feed
if (feed === 'gitea') { if (feed === 'gitea') {
// Domestic feed: always direct — a system proxy only breaks it.
void autoUpdater.netSession.setProxy({ mode: 'direct' })
const token = process.env.OT_UPDATE_TOKEN const token = process.env.OT_UPDATE_TOKEN
autoUpdater.setFeedURL({ autoUpdater.setFeedURL({
provider: 'generic', provider: 'generic',
@@ -37,6 +39,8 @@ function useFeed(feed: 'gitea' | 'github'): void {
...(token ? { requestHeaders: { Authorization: `token ${token}` } } : {}) ...(token ? { requestHeaders: { Authorization: `token ${token}` } } : {})
}) })
} else { } else {
// GitHub usually needs the system proxy (when one is enabled).
void autoUpdater.netSession.setProxy({ mode: 'system' })
autoUpdater.setFeedURL({ provider: 'github', ...GITHUB_REPO }) autoUpdater.setFeedURL({ provider: 'github', ...GITHUB_REPO })
} }
} }
@@ -67,8 +71,14 @@ async function checkWithFallback(): Promise<void> {
} catch (err) { } catch (err) {
if (activeFeed !== 'gitea') throw err if (activeFeed !== 'gitea') throw err
console.warn('[updater] gitea feed failed, falling back to github:', err) console.warn('[updater] gitea feed failed, falling back to github:', err)
const giteaErr = err instanceof Error ? err.message : String(err)
useFeed('github') useFeed('github')
await autoUpdater.checkForUpdates() try {
await autoUpdater.checkForUpdates()
} catch (err2) {
const ghErr = err2 instanceof Error ? err2.message : String(err2)
throw new Error(`国内源: ${giteaErr};GitHub: ${ghErr}`)
}
} }
} }
@@ -96,8 +106,35 @@ async function handleDownload(): Promise<void> {
} }
} }
/** Gitea releases first, GitHub releases as fallback; both are public reads. */ /** Direct-connection fetch for the domestic update channel (ignores system proxy). */
async function directFetch(url: string): Promise<Response> {
const s = session.fromPartition('openterminal-update-direct', { cache: false })
await s.setProxy({ mode: 'direct' })
return s.fetch(url, { headers: { 'User-Agent': 'OpenTerminal' } })
}
/**
* Changelog sources, in order:
* 1. update channel's release-notes.md — the Gitea repo itself is private
* (anonymous API reads 404), but the generic package is publicly readable.
* 2. Gitea / GitHub releases APIs (full history when reachable).
*/
async function fetchChangelog(): Promise<ReleaseNote[]> { async function fetchChangelog(): Promise<ReleaseNote[]> {
try {
const [notesResp, ymlResp] = await Promise.all([
directFetch(`${GITEA_FEED}release-notes.md`),
directFetch(`${GITEA_FEED}latest.yml`)
])
if (notesResp.ok) {
const body = await notesResp.text()
const yml = ymlResp.ok ? await ymlResp.text() : ''
const version = yml.match(/^version:\s*(\S+)/m)?.[1] ?? ''
const date = yml.match(/^releaseDate:\s*'?(\S+?)'?$/m)?.[1]?.slice(0, 10) ?? ''
if (body.trim()) return [{ version: version ? `v${version}` : 'latest', date, body }]
}
} catch {
// fall through to releases APIs
}
for (const url of [GITEA_RELEASES_API, GITHUB_RELEASES_API]) { for (const url of [GITEA_RELEASES_API, GITHUB_RELEASES_API]) {
try { try {
const resp = await net.fetch(url, { headers: { 'User-Agent': 'OpenTerminal' } }) const resp = await net.fetch(url, { headers: { 'User-Agent': 'OpenTerminal' } })
@@ -317,6 +317,22 @@ export function SystemSettingsTab(): React.JSX.Element {
/> />
} }
/> />
<SettingRow
label="关闭按钮行为"
desc="点击窗口关闭按钮时的动作;最小化到托盘后可在托盘图标右键菜单中退出"
control={
<Select
className="settings-select"
value={settings.system.closeAction ?? 'tray'}
onChange={(value) => void updateSystem({ closeAction: value })}
options={[
{ value: 'tray', label: '最小化到托盘' },
{ value: 'exit', label: '直接退出' },
{ value: 'ask', label: '每次询问' }
]}
/>
}
/>
<SettingRow <SettingRow
label="全局唤起快捷键" label="全局唤起快捷键"
desc="Control+Shift+Alt+T 等,留空禁用" desc="Control+Shift+Alt+T 等,留空禁用"
+1 -1
View File
@@ -107,5 +107,5 @@ export const DEFAULT_SETTINGS: AppSettings = {
}, },
customThemes: [], customThemes: [],
highlightRules: DEFAULT_HIGHLIGHT_RULES, highlightRules: DEFAULT_HIGHLIGHT_RULES,
system: { launchAtLogin: false, preventSleep: false, globalShowHide: '', closeAction: 'ask', autoCheckUpdate: true } system: { launchAtLogin: false, preventSleep: false, globalShowHide: '', closeAction: 'tray', autoCheckUpdate: true }
} }