mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-19 21:07:28 +08:00
fix: windows desktop workflow
This commit is contained in:
45
.github/workflows/release-win7.yml
vendored
45
.github/workflows/release-win7.yml
vendored
@@ -118,26 +118,47 @@ jobs:
|
|||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
# Mirror the installer to R2 so it downloads fast (GitHub artifacts are
|
# Mirror the installer to R2 so it downloads fast (GitHub artifacts are
|
||||||
# painfully slow from China). Reuses the same cow-skills bucket +
|
# painfully slow from China). Mirrors the MAIN pipeline's publish-r2 job:
|
||||||
# cdn.cowagent.ai domain as the main pipeline, but under a dedicated
|
# a SEPARATE ubuntu-latest job with NO setup-node, so it uses the runner's
|
||||||
# desktop/win7/<version>/ prefix. NO D1 write: this is an unpublished test
|
# built-in Node 22+ and wrangler@latest works (the build job runs Node 20 for
|
||||||
# build. Skips cleanly when Cloudflare secrets aren't configured.
|
# Electron, where wrangler@latest — which needs Node >=22 — would abort).
|
||||||
|
# Reuses the same cow-skills bucket + cdn.cowagent.ai domain, under a
|
||||||
|
# dedicated desktop/win7/<version>/ prefix. NO D1 write: this is an
|
||||||
|
# unpublished test build. Skips cleanly when Cloudflare secrets are absent.
|
||||||
|
publish-r2:
|
||||||
|
name: Publish to R2
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Guard on Cloudflare secrets
|
||||||
|
id: guard
|
||||||
|
env:
|
||||||
|
CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
if [ -n "$CF_TOKEN" ]; then
|
||||||
|
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::notice::CLOUDFLARE_API_TOKEN not set — skipping R2 upload (use the artifact instead)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Download build artifact
|
||||||
|
if: steps.guard.outputs.enabled == 'true'
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cowagent-win7-x64
|
||||||
|
path: dist
|
||||||
|
|
||||||
- name: Upload installer to R2
|
- name: Upload installer to R2
|
||||||
shell: bash
|
if: steps.guard.outputs.enabled == 'true'
|
||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
R2_BUCKET: ${{ vars.R2_BUCKET != '' && vars.R2_BUCKET || 'cow-skills' }}
|
R2_BUCKET: ${{ vars.R2_BUCKET != '' && vars.R2_BUCKET || 'cow-skills' }}
|
||||||
VER: ${{ github.event.inputs.version }}
|
VER: ${{ github.event.inputs.version }}
|
||||||
run: |
|
run: |
|
||||||
# Skip cleanly when Cloudflare isn't configured (secrets are only
|
|
||||||
# readable inside run:, so we gate here rather than in the step `if`).
|
|
||||||
if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
|
|
||||||
echo "::notice::CLOUDFLARE_API_TOKEN not set — skipping R2 upload (use the artifact instead)."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
for f in desktop/release/*.exe; do
|
for f in dist/*.exe; do
|
||||||
base="$(basename "$f")"
|
base="$(basename "$f")"
|
||||||
key="desktop/win7/v${VER}/${base}"
|
key="desktop/win7/v${VER}/${base}"
|
||||||
echo "==> Uploading $base -> r2://${R2_BUCKET}/${key}"
|
echo "==> Uploading $base -> r2://${R2_BUCKET}/${key}"
|
||||||
|
|||||||
@@ -27,21 +27,21 @@
|
|||||||
--danger-border: rgba(239, 68, 68, 0.3);
|
--danger-border: rgba(239, 68, 68, 0.3);
|
||||||
--info: #3b82f6;
|
--info: #3b82f6;
|
||||||
|
|
||||||
/* Light theme — layered neutral surfaces */
|
/* Light theme — aligned with the web console (Tailwind gray/slate palette) */
|
||||||
--bg-base: #fafafa; /* app background */
|
--bg-base: #f9fafb; /* app background (gray-50) */
|
||||||
--bg-surface: #ffffff; /* panels, cards */
|
--bg-surface: #ffffff; /* panels, cards */
|
||||||
--bg-surface-2: #f4f4f5; /* nested surfaces, hover fills */
|
--bg-surface-2: #f1f5f9; /* nested surfaces, hover fills (slate-100) */
|
||||||
--bg-elevated: #ffffff; /* popovers, menus, modals */
|
--bg-elevated: #ffffff; /* popovers, menus, modals */
|
||||||
--bg-inset: #f4f4f5; /* inputs, code blocks */
|
--bg-inset: #f1f5f9; /* inputs, code blocks (slate-100) */
|
||||||
|
|
||||||
--text-primary: #18181b; /* headings, primary text (contrast > 4.5:1) */
|
--text-primary: #1e293b; /* headings, primary text (slate-800) */
|
||||||
--text-secondary: #52525b; /* body, labels */
|
--text-secondary: #475569; /* body, labels (slate-600) */
|
||||||
--text-tertiary: #71717a; /* hints, captions */
|
--text-tertiary: #64748b; /* hints, captions (slate-500) */
|
||||||
--text-disabled: #a1a1aa;
|
--text-disabled: #94a3b8; /* slate-400 */
|
||||||
|
|
||||||
--border-default: #e4e4e7;
|
--border-default: #e2e8f0; /* slate-200 */
|
||||||
--border-strong: #d4d4d8;
|
--border-strong: #cbd5e1; /* slate-300 */
|
||||||
--border-subtle: #f0f0f1;
|
--border-subtle: #f1f5f9; /* slate-100 */
|
||||||
|
|
||||||
--overlay: rgba(0, 0, 0, 0.4);
|
--overlay: rgba(0, 0, 0, 0.4);
|
||||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.06);
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import apiClient from '../api/client'
|
|||||||
import type { Attachment, ChatMessage } from '../types'
|
import type { Attachment, ChatMessage } from '../types'
|
||||||
import { useChatStore } from '../store/chatStore'
|
import { useChatStore } from '../store/chatStore'
|
||||||
import { useSessionStore } from '../store/sessionStore'
|
import { useSessionStore } from '../store/sessionStore'
|
||||||
|
import { useUIStore } from '../store/uiStore'
|
||||||
|
|
||||||
interface ChatPageProps {
|
interface ChatPageProps {
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
@@ -54,6 +55,8 @@ const ChatPage: React.FC<ChatPageProps> = ({ baseUrl }) => {
|
|||||||
const deleteMessage = useChatStore((s) => s.deleteMessage)
|
const deleteMessage = useChatStore((s) => s.deleteMessage)
|
||||||
const loadHistory = useChatStore((s) => s.loadHistory)
|
const loadHistory = useChatStore((s) => s.loadHistory)
|
||||||
const ensureSession = useChatStore((s) => s.ensureSession)
|
const ensureSession = useChatStore((s) => s.ensureSession)
|
||||||
|
const clearContext = useChatStore((s) => s.clearContext)
|
||||||
|
const setSessionsCollapsed = useUIStore((s) => s.setSessionsCollapsed)
|
||||||
|
|
||||||
const messages = session?.messages ?? []
|
const messages = session?.messages ?? []
|
||||||
const isStreaming = session?.isStreaming ?? false
|
const isStreaming = session?.isStreaming ?? false
|
||||||
@@ -170,15 +173,14 @@ const ChatPage: React.FC<ChatPageProps> = ({ baseUrl }) => {
|
|||||||
const id = newSession()
|
const id = newSession()
|
||||||
ensureSession(id)
|
ensureSession(id)
|
||||||
loadHistory(id, 1)
|
loadHistory(id, 1)
|
||||||
}, [newSession, ensureSession, loadHistory])
|
// Auto-expand the session list so the user sees the new/switched session.
|
||||||
|
setSessionsCollapsed(false)
|
||||||
|
}, [newSession, ensureSession, loadHistory, setSessionsCollapsed])
|
||||||
|
|
||||||
const handleClearContext = useCallback(async () => {
|
const handleClearContext = useCallback(async () => {
|
||||||
try {
|
await clearContext(activeId)
|
||||||
await apiClient.clearContext(activeId)
|
scrollToBottom(true)
|
||||||
} catch {
|
}, [clearContext, activeId, scrollToBottom])
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}, [activeId])
|
|
||||||
|
|
||||||
const handleStop = useCallback(() => cancel(activeId), [cancel, activeId])
|
const handleStop = useCallback(() => cancel(activeId), [cancel, activeId])
|
||||||
|
|
||||||
@@ -242,7 +244,9 @@ const ChatPage: React.FC<ChatPageProps> = ({ baseUrl }) => {
|
|||||||
<div className="flex flex-col items-center justify-center h-full px-6 py-12">
|
<div className="flex flex-col items-center justify-center h-full px-6 py-12">
|
||||||
<img src="./logo.jpg" alt="CowAgent" className="w-16 h-16 rounded-2xl mb-5 shadow-md" />
|
<img src="./logo.jpg" alt="CowAgent" className="w-16 h-16 rounded-2xl mb-5 shadow-md" />
|
||||||
<h1 className="text-xl font-semibold text-content mb-2">{t('chat_welcome')}</h1>
|
<h1 className="text-xl font-semibold text-content mb-2">{t('chat_welcome')}</h1>
|
||||||
<p className="text-content-tertiary text-sm text-center max-w-md mb-8">{t('chat_empty_hint')}</p>
|
<p className="text-content-tertiary text-sm text-center max-w-md mb-8 leading-relaxed whitespace-pre-line">
|
||||||
|
{t('welcome_subtitle')}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 w-full max-w-2xl">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 w-full max-w-2xl">
|
||||||
{SUGGESTIONS.map(({ key, send, icon: Icon, iconClass, bgClass }) => (
|
{SUGGESTIONS.map(({ key, send, icon: Icon, iconClass, bgClass }) => (
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ interface ChatState {
|
|||||||
deleteMessage: (sid: string, userSeq: number, cascade: boolean) => Promise<void>
|
deleteMessage: (sid: string, userSeq: number, cascade: boolean) => Promise<void>
|
||||||
|
|
||||||
loadHistory: (sid: string, page?: number) => Promise<void>
|
loadHistory: (sid: string, page?: number) => Promise<void>
|
||||||
|
clearContext: (sid: string) => Promise<boolean>
|
||||||
clearLocal: (sid: string) => void
|
clearLocal: (sid: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +398,25 @@ export const useChatStore = create<ChatState>((set, get) => {
|
|||||||
cancel: async (sid) => {
|
cancel: async (sid) => {
|
||||||
const s = get().sessions[sid]
|
const s = get().sessions[sid]
|
||||||
if (!s?.requestId) return
|
if (!s?.requestId) return
|
||||||
|
// Optimistically stop the UI right away: mark the last assistant bubble
|
||||||
|
// cancelled, free the input, and tear down the local SSE stream so no
|
||||||
|
// further deltas render after the user hit stop. The backend still gets
|
||||||
|
// the cancel request to abort the running agent task.
|
||||||
|
patchMessages(sid, (msgs) => {
|
||||||
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||||
|
if (msgs[i].role === 'assistant') {
|
||||||
|
msgs[i] = { ...msgs[i], isCancelled: true, isStreaming: false }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...msgs]
|
||||||
|
})
|
||||||
|
patchSession(sid, { isStreaming: false, requestId: null })
|
||||||
|
const es = streams[sid]
|
||||||
|
if (es) {
|
||||||
|
es.close()
|
||||||
|
delete streams[sid]
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await apiClient.cancel({ requestId: s.requestId, sessionId: sid })
|
await apiClient.cancel({ requestId: s.requestId, sessionId: sid })
|
||||||
} catch {
|
} catch {
|
||||||
@@ -476,6 +496,28 @@ export const useChatStore = create<ChatState>((set, get) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearContext: async (sid) => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.clearContext(sid)
|
||||||
|
if (res.status !== 'success') return false
|
||||||
|
// Append a visual divider so the user sees the context was cleared
|
||||||
|
// (mirrors the web console's context-divider).
|
||||||
|
patchMessages(sid, (msgs) => [
|
||||||
|
...msgs,
|
||||||
|
{
|
||||||
|
id: uid('divider'),
|
||||||
|
role: 'system',
|
||||||
|
kind: 'divider',
|
||||||
|
content: '',
|
||||||
|
timestamp: Date.now() / 1000,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
clearLocal: (sid) => {
|
clearLocal: (sid) => {
|
||||||
const es = streams[sid]
|
const es = streams[sid]
|
||||||
if (es) {
|
if (es) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface UIState {
|
|||||||
/** Session list panel collapsed (hidden) vs expanded. */
|
/** Session list panel collapsed (hidden) vs expanded. */
|
||||||
sessionsCollapsed: boolean
|
sessionsCollapsed: boolean
|
||||||
toggleSessions: () => void
|
toggleSessions: () => void
|
||||||
|
setSessionsCollapsed: (v: boolean) => void
|
||||||
|
|
||||||
/** Currently active session id (Chat page). */
|
/** Currently active session id (Chat page). */
|
||||||
activeSessionId: string | null
|
activeSessionId: string | null
|
||||||
@@ -42,6 +43,10 @@ export const useUIStore = create<UIState>((set) => ({
|
|||||||
localStorage.setItem(SESSIONS_KEY, next ? '1' : '0')
|
localStorage.setItem(SESSIONS_KEY, next ? '1' : '0')
|
||||||
return { sessionsCollapsed: next }
|
return { sessionsCollapsed: next }
|
||||||
}),
|
}),
|
||||||
|
setSessionsCollapsed: (v) => {
|
||||||
|
localStorage.setItem(SESSIONS_KEY, v ? '1' : '0')
|
||||||
|
set({ sessionsCollapsed: v })
|
||||||
|
},
|
||||||
|
|
||||||
activeSessionId: null,
|
activeSessionId: null,
|
||||||
setActiveSessionId: (id) => set({ activeSessionId: id }),
|
setActiveSessionId: (id) => set({ activeSessionId: id }),
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export interface BackendStatusEvent {
|
|||||||
// Chat / messages / streaming
|
// Chat / messages / streaming
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
export type Role = 'user' | 'assistant'
|
export type Role = 'user' | 'assistant' | 'system'
|
||||||
|
|
||||||
/** A single ordered step inside an assistant turn (matches backend history). */
|
/** A single ordered step inside an assistant turn (matches backend history). */
|
||||||
export interface MessageStep {
|
export interface MessageStep {
|
||||||
@@ -83,8 +83,8 @@ export interface ChatMessage {
|
|||||||
/** Sequence numbers from backend (for delete/regenerate). */
|
/** Sequence numbers from backend (for delete/regenerate). */
|
||||||
userSeq?: number
|
userSeq?: number
|
||||||
botSeq?: number
|
botSeq?: number
|
||||||
/** Self-evolution bubble flag. */
|
/** Self-evolution bubble flag; 'divider' renders a context-cleared separator. */
|
||||||
kind?: 'evolution'
|
kind?: 'evolution' | 'divider'
|
||||||
extras?: Record<string, unknown>
|
extras?: Record<string, unknown>
|
||||||
isStreaming?: boolean
|
isStreaming?: boolean
|
||||||
isCancelled?: boolean
|
isCancelled?: boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user