mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +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
|
||||
|
||||
# Mirror the installer to R2 so it downloads fast (GitHub artifacts are
|
||||
# painfully slow from China). Reuses the same cow-skills bucket +
|
||||
# cdn.cowagent.ai domain as the main pipeline, but under a dedicated
|
||||
# desktop/win7/<version>/ prefix. NO D1 write: this is an unpublished test
|
||||
# build. Skips cleanly when Cloudflare secrets aren't configured.
|
||||
# painfully slow from China). Mirrors the MAIN pipeline's publish-r2 job:
|
||||
# a SEPARATE ubuntu-latest job with NO setup-node, so it uses the runner's
|
||||
# built-in Node 22+ and wrangler@latest works (the build job runs Node 20 for
|
||||
# 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
|
||||
shell: bash
|
||||
if: steps.guard.outputs.enabled == 'true'
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
R2_BUCKET: ${{ vars.R2_BUCKET != '' && vars.R2_BUCKET || 'cow-skills' }}
|
||||
VER: ${{ github.event.inputs.version }}
|
||||
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
|
||||
for f in desktop/release/*.exe; do
|
||||
for f in dist/*.exe; do
|
||||
base="$(basename "$f")"
|
||||
key="desktop/win7/v${VER}/${base}"
|
||||
echo "==> Uploading $base -> r2://${R2_BUCKET}/${key}"
|
||||
|
||||
@@ -27,21 +27,21 @@
|
||||
--danger-border: rgba(239, 68, 68, 0.3);
|
||||
--info: #3b82f6;
|
||||
|
||||
/* Light theme — layered neutral surfaces */
|
||||
--bg-base: #fafafa; /* app background */
|
||||
/* Light theme — aligned with the web console (Tailwind gray/slate palette) */
|
||||
--bg-base: #f9fafb; /* app background (gray-50) */
|
||||
--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-inset: #f4f4f5; /* inputs, code blocks */
|
||||
--bg-inset: #f1f5f9; /* inputs, code blocks (slate-100) */
|
||||
|
||||
--text-primary: #18181b; /* headings, primary text (contrast > 4.5:1) */
|
||||
--text-secondary: #52525b; /* body, labels */
|
||||
--text-tertiary: #71717a; /* hints, captions */
|
||||
--text-disabled: #a1a1aa;
|
||||
--text-primary: #1e293b; /* headings, primary text (slate-800) */
|
||||
--text-secondary: #475569; /* body, labels (slate-600) */
|
||||
--text-tertiary: #64748b; /* hints, captions (slate-500) */
|
||||
--text-disabled: #94a3b8; /* slate-400 */
|
||||
|
||||
--border-default: #e4e4e7;
|
||||
--border-strong: #d4d4d8;
|
||||
--border-subtle: #f0f0f1;
|
||||
--border-default: #e2e8f0; /* slate-200 */
|
||||
--border-strong: #cbd5e1; /* slate-300 */
|
||||
--border-subtle: #f1f5f9; /* slate-100 */
|
||||
|
||||
--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);
|
||||
|
||||
@@ -17,6 +17,7 @@ import apiClient from '../api/client'
|
||||
import type { Attachment, ChatMessage } from '../types'
|
||||
import { useChatStore } from '../store/chatStore'
|
||||
import { useSessionStore } from '../store/sessionStore'
|
||||
import { useUIStore } from '../store/uiStore'
|
||||
|
||||
interface ChatPageProps {
|
||||
baseUrl: string
|
||||
@@ -54,6 +55,8 @@ const ChatPage: React.FC<ChatPageProps> = ({ baseUrl }) => {
|
||||
const deleteMessage = useChatStore((s) => s.deleteMessage)
|
||||
const loadHistory = useChatStore((s) => s.loadHistory)
|
||||
const ensureSession = useChatStore((s) => s.ensureSession)
|
||||
const clearContext = useChatStore((s) => s.clearContext)
|
||||
const setSessionsCollapsed = useUIStore((s) => s.setSessionsCollapsed)
|
||||
|
||||
const messages = session?.messages ?? []
|
||||
const isStreaming = session?.isStreaming ?? false
|
||||
@@ -170,15 +173,14 @@ const ChatPage: React.FC<ChatPageProps> = ({ baseUrl }) => {
|
||||
const id = newSession()
|
||||
ensureSession(id)
|
||||
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 () => {
|
||||
try {
|
||||
await apiClient.clearContext(activeId)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, [activeId])
|
||||
await clearContext(activeId)
|
||||
scrollToBottom(true)
|
||||
}, [clearContext, activeId, scrollToBottom])
|
||||
|
||||
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">
|
||||
<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>
|
||||
<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">
|
||||
{SUGGESTIONS.map(({ key, send, icon: Icon, iconClass, bgClass }) => (
|
||||
|
||||
@@ -31,6 +31,7 @@ interface ChatState {
|
||||
deleteMessage: (sid: string, userSeq: number, cascade: boolean) => Promise<void>
|
||||
|
||||
loadHistory: (sid: string, page?: number) => Promise<void>
|
||||
clearContext: (sid: string) => Promise<boolean>
|
||||
clearLocal: (sid: string) => void
|
||||
}
|
||||
|
||||
@@ -397,6 +398,25 @@ export const useChatStore = create<ChatState>((set, get) => {
|
||||
cancel: async (sid) => {
|
||||
const s = get().sessions[sid]
|
||||
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 {
|
||||
await apiClient.cancel({ requestId: s.requestId, sessionId: sid })
|
||||
} 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) => {
|
||||
const es = streams[sid]
|
||||
if (es) {
|
||||
|
||||
@@ -12,6 +12,7 @@ interface UIState {
|
||||
/** Session list panel collapsed (hidden) vs expanded. */
|
||||
sessionsCollapsed: boolean
|
||||
toggleSessions: () => void
|
||||
setSessionsCollapsed: (v: boolean) => void
|
||||
|
||||
/** Currently active session id (Chat page). */
|
||||
activeSessionId: string | null
|
||||
@@ -42,6 +43,10 @@ export const useUIStore = create<UIState>((set) => ({
|
||||
localStorage.setItem(SESSIONS_KEY, next ? '1' : '0')
|
||||
return { sessionsCollapsed: next }
|
||||
}),
|
||||
setSessionsCollapsed: (v) => {
|
||||
localStorage.setItem(SESSIONS_KEY, v ? '1' : '0')
|
||||
set({ sessionsCollapsed: v })
|
||||
},
|
||||
|
||||
activeSessionId: null,
|
||||
setActiveSessionId: (id) => set({ activeSessionId: id }),
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface BackendStatusEvent {
|
||||
// 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). */
|
||||
export interface MessageStep {
|
||||
@@ -83,8 +83,8 @@ export interface ChatMessage {
|
||||
/** Sequence numbers from backend (for delete/regenerate). */
|
||||
userSeq?: number
|
||||
botSeq?: number
|
||||
/** Self-evolution bubble flag. */
|
||||
kind?: 'evolution'
|
||||
/** Self-evolution bubble flag; 'divider' renders a context-cleared separator. */
|
||||
kind?: 'evolution' | 'divider'
|
||||
extras?: Record<string, unknown>
|
||||
isStreaming?: boolean
|
||||
isCancelled?: boolean
|
||||
|
||||
Reference in New Issue
Block a user