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
2 changes: 1 addition & 1 deletion .github/agents/spx-upgrader.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ You are a release specialist dedicated to upgrading spx across goplus/builder sa
- Require the requester to specify the target spx version or pseudo-version, and stop immediately with an error message if it is missing
- For released versions, verify they exist by running `gh release view --repo goplus/spx v<version>`
- For pseudo-versioned dev commits, verify the matching `ghcr.io/goplus/spx:web-zip-<version>` package exists
- Update `spx-gui/.env` so `VITE_SPX_VERSION` matches the target
- Update `spx-gui/src/apps/xbuilder/.env` so `VITE_SPX_VERSION` matches the target
- Refresh Go modules in `tools/ai/`, `tools/spxls/`, and `tools/ispx/` via `go get github.com/goplus/spx/v2@v<version>` followed by `go mod tidy` in each directory
- Execute `bash spx-gui/install-spx.sh` to download the matching runtime assets and remove any temporary archives
- Execute `bash build-wasm.sh` in `spx-gui/` to build Wasm components
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
context: .
file: ${{ matrix.dockerfile }}
build-args: |
NODE_ENV=staging
VITE_MODE=staging

@nighca nighca Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

对于 staging/production/production-cn,构建时实际的 NODE_ENV 值都是 production(通过 npm script build 控制),之前外部指定 NODE_ENVstaging/production/production-cn 的做法是容易造成误解的

这里换成 VITE_MODE,明确是用于控制 vite mode,对应于最终配置文件的选用

provenance: false
sbom: false
push: ${{ github.event_name == 'push' && github.ref_name == 'dev' }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:

- name: Build account frontend for mainland China deployment
working-directory: spx-gui
run: NODE_ENV=production.cn npm run build:account
run: VITE_MODE=production-cn npm run build:account

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

.env 的加载本身有一定策略,比如当指定 mode: abc 的时候,.env.abc.local 会优先级高于 .env.abc(详见 .env Files);考虑到这里 . 用于分隔 mode name 和 local 这样的特殊标识,因此在 mode name 中把原来的 . 换成 -,减少把 .cn 误解为像 .local 这样的特殊标记的可能性


- name: Deploy builder_account_cn
uses: ./.github/actions/deploy
Expand Down
16 changes: 16 additions & 0 deletions spx-gui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ Keep import statements in order:
* Use the remainder of the key to identify the owning feature and stored state, for example `builder-user` or `builder-account-pending-authorization`.
* Migrate existing keys gradually when changing their owning storage logic. Do not rename unrelated existing keys solely for consistency.

### App Env and Configuration

* App env belongs to the app that owns it. Define app-specific env values under `src/apps/<app>/env.ts` and app-specific `.env*` files under `src/apps/<app>/`.

* Code that does not clearly belong to a single app must not import a concrete app env module such as `@/apps/xbuilder/env` or `@/apps/account/env`.
Instead, the shared module should define the smallest configuration interface it needs, then expose an explicit configuration surface such as:
- a setter on an exported singleton, for example an API client `setBaseUrl(...)`;
- a `provide` / `inject` pair for Vue component trees;
- a function parameter when the dependency is local to one operation.

* App entry and setup code under `src/apps/<app>/` is responsible for reading that app's env and passing only the required values into shared modules.

* Code that is clearly app-owned, especially files under `src/apps/<app>/`, may import and consume that app's env directly when that is simpler.

* Keep shared module configuration narrow. Avoid passing a whole app env object into shared business logic when the module only needs one or two values.

### Identifier Resolution

When working with backend unique string identifiers such as `username`, project owner, and project name, distinguish unresolved identifiers from canonical identifiers.
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ COPY tutorial ../tutorial
# Copy assets (with wasm)
COPY --from=go-builder /app/spx-gui/src/assets /app/spx-gui/src/assets

ARG NODE_ENV
ARG VITE_MODE

ENV NODE_OPTIONS=--max-old-space-size=4096
RUN npm run build
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/build/Dockerfile.account
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ RUN npm install --ignore-scripts

COPY spx-gui .

ARG NODE_ENV
ARG VITE_MODE

ENV NODE_OPTIONS=--max-old-space-size=4096
RUN npm run build:account
Expand Down
3 changes: 2 additions & 1 deletion spx-gui/build/vite-plugins/app-html-entry-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export function createAppHtmlEntryPlugin(path: string): Plugin {
name: 'app-html-entry',
transformIndexHtml: {
order: 'pre',
async handler() {
async handler(html, ctx) {
if (ctx.path !== '/' && ctx.path !== '/index.html') return html
return await fs.promises.readFile(path, 'utf8')
}
}
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/install-spx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -e

cd "$(dirname "$0")"

# Keep this version in sync with `VITE_SPX_VERSION` in `.env`.
# Keep this version in sync with `VITE_SPX_VERSION` in `src/apps/xbuilder/.env`.
SPX_VERSION="2.0.4"

SPX_NAME="spx_${SPX_VERSION}"
Expand Down
4 changes: 2 additions & 2 deletions spx-gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
"postinstall": "./install-spx.sh",
"predev": "./install-spx.sh && ./build-wasm.sh && ./build-tutorial-books.sh",
"dev": "vite",
"build": "./build-tutorial-books.sh && vue-tsc --build --force && NODE_ENV=production vite build --mode ${NODE_ENV:-production}",
"build": "./build-tutorial-books.sh && vue-tsc --build --force && NODE_ENV=production vite build --mode ${VITE_MODE:-production}",
"preview": "vite preview",
"dev:account": "vite --config vite.config.account.ts --port 5174",
"build:account": "NODE_ENV=production vite build --config vite.config.account.ts --mode ${NODE_ENV:-production}",
"build:account": "NODE_ENV=production vite build --config vite.config.account.ts --mode ${VITE_MODE:-production}",
"type-check": "vue-tsc --build --force",
"format-check": "prettier --check ./src",
"format": "prettier --write ./src",
Expand Down
9 changes: 3 additions & 6 deletions spx-gui/src/apis/account/oauth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Client } from '@/apis/common/client'
import { apiBaseUrl } from '@/utils/env'
import type { OAuthAPIs } from '@/utils/oauth'
import { accountClient } from './common'

Expand Down Expand Up @@ -85,11 +84,9 @@ class AccountOAuthApis implements OAuthAPIs {
}

/** Account OAuth APIs for use by app xbuilder. */
export const accountOAuthApisForXBuilder = new AccountOAuthApis(
new Client({
baseUrl: apiBaseUrl + '/account'
})
)
export const accountOAuthClientForXBuilder = new Client()

export const accountOAuthApisForXBuilder = new AccountOAuthApis(accountOAuthClientForXBuilder)

/** Account OAuth APIs for use by app account. */
export const accountOAuthApis = new AccountOAuthApis(accountClient)
27 changes: 18 additions & 9 deletions spx-gui/src/apis/common/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,37 @@ export type JSONSSEEvent = {
}

export type ClientOptions = {
baseUrl: string
baseUrl?: string
fetchFn?: typeof fetch
}

export class Client {
constructor(options: ClientOptions) {
this.baseUrl = options.baseUrl
this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis)
constructor(options?: ClientOptions) {
this.baseUrl = options?.baseUrl ?? null
this.fetchFn = options?.fetchFn ?? globalThis.fetch.bind(globalThis)
}

private tokenProvider: TokenProvider = async () => null
setTokenProvider(provider: TokenProvider) {
this.tokenProvider = provider
}

private baseUrl: string
setBaseUrl(baseUrl: string) {
this.baseUrl = baseUrl
}

private baseUrl: string | null
private fetchFn: typeof fetch
private defaultTimeout = 10 * 1000 // 10 seconds

private ensureBaseUrl() {
if (this.baseUrl == null) throw new Error('API client base URL is not set')
return this.baseUrl
}

/** Get full URL for a given API path */
urlFor(path: string) {
const concated = this.baseUrl + path
const concated = this.ensureBaseUrl() + path
return new URL(concated, window.location.origin)
}

Expand All @@ -103,7 +112,7 @@ export class Client {
const traceData = Sentry.getTraceData()
const sentryTraceHeader = traceData['sentry-trace']
const sentryBaggageHeader = traceData['baggage']
const url = this.baseUrl + path
const url = this.ensureBaseUrl() + path
const method = options?.method ?? 'GET'
const body = payload != null ? JSON.stringify(payload) : null
const headers = options?.headers ?? new Headers()
Expand All @@ -119,7 +128,7 @@ export class Client {
const traceData = Sentry.getTraceData()
const sentryTraceHeader = traceData['sentry-trace']
const sentryBaggageHeader = traceData['baggage']
const url = this.baseUrl + path
const url = this.ensureBaseUrl() + path
const method = options?.method ?? 'POST'
const body = new URLSearchParams()
Object.entries(payload).forEach(([key, value]) => {
Expand Down Expand Up @@ -178,7 +187,7 @@ export class Client {
const traceData = Sentry.getTraceData()
const sentryTraceHeader = traceData['sentry-trace']
const sentryBaggageHeader = traceData['baggage']
const url = this.baseUrl + path
const url = this.ensureBaseUrl() + path
const method = options?.method ?? 'GET'
const headers = options?.headers ?? new Headers()
await this.injectAuthorization(headers, options?.signal)
Expand Down
9 changes: 2 additions & 7 deletions spx-gui/src/apis/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { apiBaseUrl } from '@/utils/env'
import { Client } from './client'

export type PaginationParams = {
Expand Down Expand Up @@ -44,13 +43,9 @@ export function timeStringify(time: number) {

/**
* The default client instance for app XBuilder to make requests to spx-backend APIs.
* Requests made through this client will have the base URL set to `apiBaseUrl` from environment variables.
* The token provider is expected to be set separately on app initialization (see details in setup.ts)
* so credentials will be included in requests.
* The base URL and token provider are expected to be configured on app initialization.
*/
export const client = new Client({
baseUrl: apiBaseUrl
})
export const client = new Client()

/** Art style indicates the visual style or aesthetic approach used in the creation of graphics */
export const enum ArtStyle {
Expand Down
21 changes: 14 additions & 7 deletions spx-gui/src/apis/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* @desc File-related APIs of spx-backend
*/

import { usercontentBaseUrl, usercontentBucket } from '@/utils/env'
import { client, type UniversalUrl, type UniversalToWebUrlMap } from './common'
import { UniversalUrlScheme, parseUniversalUrl } from '@/utils/universal-url'

Expand All @@ -19,26 +18,34 @@ export type UploadSession = {
region: string
}

export type FileURLSignatureConfig = {
baseUrl: string
bucket: string
}

export function createUploadSession() {
return client.post('/upload-sessions') as Promise<UploadSession>
}

export async function createFileURLSignatures(objects: UniversalUrl[]): Promise<UniversalToWebUrlMap> {
export async function createFileURLSignatures(
objects: UniversalUrl[],
config: FileURLSignatureConfig
): Promise<UniversalToWebUrlMap> {
// TODO(#1598): Restore the `/file-url-signatures` call after signed file URLs are fixed.
return workAroundIssue1598(objects)
return workAroundIssue1598(objects, config)

// const result = (await client.post('/file-url-signatures', { objects })) as { objectUrls: UniversalToWebUrlMap }
// return result.objectUrls
}

/** Workaround for https://github.com/goplus/builder/issues/1598 */
function workAroundIssue1598(objects: UniversalUrl[]): UniversalToWebUrlMap {
function workAroundIssue1598(objects: UniversalUrl[], config: FileURLSignatureConfig): UniversalToWebUrlMap {
const { baseUrl, bucket } = config
return objects.reduce((map, universalUrl) => {
const parsed = parseUniversalUrl(universalUrl)
if (parsed.scheme === UniversalUrlScheme.Kodo) {
if (parsed.bucket !== usercontentBucket)
console.warn(`unexpected bucket ${parsed.bucket}, expected ${usercontentBucket}`)
map[universalUrl] = `${usercontentBaseUrl}/${parsed.key}`
if (parsed.bucket !== bucket) console.warn(`unexpected bucket ${parsed.bucket}, expected ${bucket}`)
map[universalUrl] = `${baseUrl}/${parsed.key}`
} else {
map[universalUrl] = universalUrl
}
Expand Down
32 changes: 32 additions & 0 deletions spx-gui/src/apps/account/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# This file documents app account configuration. Most values are left empty,
# as actual values should be set in the corresponding `.env.[mode]` files. For
# options that require defaults, values are set here.

# Target URL that the Account Vite dev server proxies `/api/*` requests to
# during local development.
#
# This controls where local `/api` requests are sent. It may point to the
# Account Web origin below, or to a local Account Backend. The prefix `/api`
# is stripped from the request path before forwarding.
VITE_API_PROXY_TARGET=""

# Public Account Web origin used during local Account development.
#
# This should match the corresponding Account Backend's `ACCOUNT_WEB_BASE_URL`
# origin.
#
# This origin serves two related roles:
# - Browser Hijack Plugin redirects matching browser requests from this origin
# back to the local Account Vite dev server.
# - The local `/api` proxy presents requests as coming through this Account Web
# origin by setting `Origin`, `Host`, and forwarded host/proto headers
# accordingly.
VITE_WEB_ORIGIN=""

# Sentry configuration for app account.
VITE_SENTRY_DSN=""
VITE_SENTRY_TRACES_SAMPLE_RATE="0.8"
VITE_SENTRY_LSP_SAMPLE_RATE="0.1"

# Default language for app account, e.g. `en`, `zh`.
VITE_DEFAULT_LANG="en"
File renamed without changes.
3 changes: 3 additions & 0 deletions spx-gui/src/apps/account/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Config for env production

VITE_SENTRY_DSN="https://0d463740215eb87f7e06f6572d64c93e@o4509472134987776.ingest.us.sentry.io/4509472136167424"
4 changes: 4 additions & 0 deletions spx-gui/src/apps/account/.env.production-cn
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Config for env production-cn

VITE_SENTRY_DSN="https://3b532e8d49bece95a4ad2d14c73c2e0b@o4509472134987776.ingest.us.sentry.io/4509868940263424"
VITE_DEFAULT_LANG="zh"
6 changes: 6 additions & 0 deletions spx-gui/src/apps/account/.env.staging
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Config for env staging

VITE_API_PROXY_TARGET="https://goplus-builder-account.qiniu.io/api"
VITE_WEB_ORIGIN="https://goplus-builder-account.qiniu.io"

VITE_SENTRY_DSN="https://0d463740215eb87f7e06f6572d64c93e@o4509472134987776.ingest.us.sentry.io/4509472136167424"
8 changes: 8 additions & 0 deletions spx-gui/src/apps/account/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const defaultLang = (import.meta.env.VITE_DEFAULT_LANG as string) || 'en'
const sentryTracesSampleRate = parseFloat(import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE as string)
const sentryLSPSampleRate = parseFloat(import.meta.env.VITE_SENTRY_LSP_SAMPLE_RATE as string)
export const sentry = {
dsn: (import.meta.env.VITE_SENTRY_DSN as string) || '',
tracesSampleRate: Number.isNaN(sentryTracesSampleRate) ? 0.1 : sentryTracesSampleRate,
lspSampleRate: Number.isNaN(sentryLSPSampleRate) ? 0.1 : sentryLSPSampleRate
}
5 changes: 3 additions & 2 deletions spx-gui/src/apps/account/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { initDayjs } from '@/setup/dayjs'
import { initI18n } from '@/setup/i18n'
import { initSentry } from '@/setup/sentry'

import * as env from './env'
import App from './App.vue'
import router from './router'

Expand All @@ -15,7 +16,7 @@ import router from './router'
initDayjs()

const app = createApp(App)
initSentry(app, router)
initI18n(app)
initSentry(app, router, env.sentry)
initI18n(app, env.defaultLang)
app.use(router)
app.mount('#app')
5 changes: 2 additions & 3 deletions spx-gui/src/apps/account/pages/home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ import { computed } from 'vue'

import { getSession } from '@/apis/account'
import { UIError, UILoading } from '@/components/ui'
import { useAvatarUrl } from '@/stores/user/avatar'
import { useQuery } from '@/utils/query'
import { usePageTitle } from '@/utils/utils'
import { useExternalUrl, usePageTitle } from '@/utils/utils'

const {
isLoading,
Expand All @@ -65,7 +64,7 @@ const {
zh: '无法加载登录状态'
})

const avatarUrl = useAvatarUrl(() => session.value?.user.avatar ?? null)
const avatarUrl = useExternalUrl(() => session.value?.user.avatar ?? null)
const initial = computed(() =>
session.value == null ? '?' : session.value.user.displayName.trim().charAt(0).toUpperCase() || '?'
)
Expand Down
Loading