diff --git a/Cargo.lock b/Cargo.lock index b0d5bd1a81..56a756128b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6804,48 +6804,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "openbitfun-skin-market-server" -version = "1.0.10" -dependencies = [ - "anyhow", - "axum", - "openbitfun-skin-market-service", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "openbitfun-skin-market-service" -version = "1.0.10" -dependencies = [ - "anyhow", - "axum", - "base64 0.22.1", - "chrono", - "hex", - "hmac", - "image 0.25.10", - "openbitfun-product-domains", - "openbitfun-services-core", - "reqwest", - "semver", - "serde", - "serde_json", - "sha2", - "sqlx", - "tempfile", - "tokio", - "tokio-util", - "tower", - "tower-http", - "tracing", - "url", - "uuid", - "zip 4.6.1", -] - [[package]] name = "openbitfun-static-hook-support" version = "1.0.10" diff --git a/Cargo.toml b/Cargo.toml index 7dfd5302ce..6973f98ec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ members = [ "src/apps/server", "src/apps/relay-server", "src/apps/miniapp-market-server", - "src/apps/skin-market-server", "src/crates/interfaces/acp", "src/crates/interfaces/app-server", "src/crates/interfaces/app-server-client", @@ -37,7 +36,6 @@ members = [ "src/crates/services/legacy-migration", "src/crates/services/legacy-migration-adapters", "src/crates/services/miniapp-market-service", - "src/crates/services/skin-market-service", "src/crates/services/relay-service", "src/crates/services/page-function-runtime", "src/crates/services/terminal", diff --git a/README.md b/README.md index f544e9a3e4..f40687f2b8 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Install and reuse the apps you build, or explore the [Mini App Marketplace](http - **Build a specialist agent.** Choose a model, define its role, and equip it with the tools and workflows it needs for motion comic production, visual design, code review, or your own field. - **Connect tools and workflows.** Connect tools through MCP, turn repeatable processes into Skills, and use Hooks to customize how tasks run. -- **Change the interface and source.** Install [skins](https://market.openbitfun.com/skin/) for a new look, or modify the UI, tools, and runtime source to build your own version of OpenBitFun. +- **Change the interface and source.** Choose a built-in theme, or modify the UI, tools, and runtime source to build your own version of OpenBitFun. **You can ask OpenBitFun's Code Agent to modify and extend OpenBitFun itself.** @@ -133,7 +133,7 @@ Getting there means making long-running tasks more reliable, the runtime more ef **Share what you build, offer feedback, or contribute code to help shape OpenBitFun.** -Share your use cases and experience, contribute agents, Skills, Mini Apps, or skins, or help improve the docs, interface, and runtime. Ideas and questions are welcome in this repository's Issues and Discussions. See the [contribution guide](./CONTRIBUTING.md) for the development workflow. +Share your use cases and experience, contribute agents, Skills, or Mini Apps, or help improve the docs, interface, and runtime. Ideas and questions are welcome in this repository's Issues and Discussions. See the [contribution guide](./CONTRIBUTING.md) for the development workflow.
Download, setup, and running from source diff --git a/README.zh-CN.md b/README.zh-CN.md index 07c10e4a5b..933e34e25a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -96,7 +96,7 @@ OpenBitFun 是一个高度可定制的开源 Agent 工作台,让任务按你 - **定制专属 Agent。** 围绕漫剧创作、视觉设计、代码评审等领域,选择模型、设定角色,组合需要的工具与工作方法。 - **接入工具与流程。** 通过 MCP 连接专业工具,把常用工作方法保存为 Skills,用 Hooks 扩展任务执行流程。 -- **改变界面与源码。** 安装[皮肤](https://market.openbitfun.com/skin/)调整外观,也可以修改 UI、工具与 Runtime 源码,构建自己的 OpenBitFun。 +- **改变界面与源码。** 选择内置主题调整外观,也可以修改 UI、工具与 Runtime 源码,构建自己的 OpenBitFun。 **你可以直接让 OpenBitFun 的 Code Agent 修改和扩展 OpenBitFun 本身。** diff --git a/deploy/README.md b/deploy/README.md index e2ac0f69a5..54b7bfc555 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -11,7 +11,6 @@ own environment before running commands. | --- | --- | | [market-data-migration/](market-data-migration/README.md) | Legacy market data migration tooling | | [miniapp-market/](miniapp-market/README.md) | MiniApp market | -| [skin-market/](skin-market/README.md) | Skin market | | [relay-v1/](relay-v1/README.md) | Versioned Relay deployment | | [outbound-mail/](outbound-mail/README.md) | Send-only Postfix/OpenDKIM: auth integration, DNS, maintenance and rollback | | [model-router/](model-router/README.md) | Client-side router launcher and system prompt | @@ -20,7 +19,6 @@ Component source guides: - Relay: [`src/apps/relay-server/README.md`](../src/apps/relay-server/README.md) - MiniApp market server: [`src/apps/miniapp-market-server/README.md`](../src/apps/miniapp-market-server/README.md) -- Skin market service: [`src/crates/services/skin-market-service/README.md`](../src/crates/services/skin-market-service/README.md) For deployment changes, read the relevant component guide and keep every example hostname, path, and credential as a placeholder. diff --git a/deploy/market-data-migration/README.md b/deploy/market-data-migration/README.md index eef7cdbf43..8ff92d7aea 100644 --- a/deploy/market-data-migration/README.md +++ b/deploy/market-data-migration/README.md @@ -25,7 +25,6 @@ python3 -m unittest test_migrate_market_data_v1.py ## Related guides - MiniApp market: [`../miniapp-market/README.md`](../miniapp-market/README.md) -- Skin market: [`../skin-market/README.md`](../skin-market/README.md) - Relay: [`../relay-v1/README.md`](../relay-v1/README.md) and [`../../src/apps/relay-server/README.md`](../../src/apps/relay-server/README.md) - Outbound mail: [`../outbound-mail/README.md`](../outbound-mail/README.md) diff --git a/deploy/miniapp-market/README.md b/deploy/miniapp-market/README.md index d6bbbb513b..1df799d948 100644 --- a/deploy/miniapp-market/README.md +++ b/deploy/miniapp-market/README.md @@ -38,11 +38,6 @@ MiniApp 市场网页源码在 `src/miniapp-market-web/`,后端入口在 不要单独上传 `dist/`,也不要在服务器上直接改源码。 -Skin 市场复用本服务的 GitHub OAuth、桌面 token 和 Web 会话别名。修改 -`miniapp-market-service/src/auth.rs`、`routes.rs` 或 OAuth 回跳参数时,必须同时 -验证 Skin 的“我的投稿”和“审核”接口;Skin 容器只能通过它自己的部署手册和 -专用 checkout 更新,不能从 MiniApp Compose 项目重建。 - ## 部署边界 本目录只能部署 MiniApp 市场。如果同一主机上还运行 Relay、官网或其他服务,不要 @@ -497,8 +492,8 @@ backup root;不要放宽该保护。 GitHub OAuth App 的注册 callback 为 `https://auth.example.com/api/v1/auth/github/callback`。统一入口将 code/state 原样转交市场的兼容完成端点;只有市场 -host 写入两组 host-only Cookie(`/miniapp` 与 `/skin`),然后桌面授权跳转到 -独立完成页;不要通过 `Domain=.example.com` 扩大 Cookie 信任范围。桌面 +host 写入 host-only Cookie(`/miniapp`),然后桌面授权跳转到独立完成页;不要通过 +`Domain=.example.com` 扩大 Cookie 信任范围。桌面 poll token 仍受一次性 transaction secret 约束。旧市场 API 与旧完成页路径保留, 已有安装无需手动迁移。 @@ -515,7 +510,7 @@ API 的 Relay/客户端。两个市场各自的网页仍在所属 Compose 项目 ## 邮箱验证码发布补充 邮箱登录与 GitHub 登录是独立账号,不自动绑定或合并。部署前需同时验证身份服务、 -当前版本 Relay 和 Skin 身份消费者;保留旧 GitHub 数字账号及旧授权启动行为。 +当前版本 Relay 身份消费者;保留旧 GitHub 数字账号及旧授权启动行为。 客户端使用 `methods=all` 主动选择统一登录页,旧客户端继续走原 GitHub URL。 SMTP 配置单独存放在 `/etc/openbitfun-miniapp-market/smtp.env`,root:root 0600, diff --git a/deploy/miniapp-market/nginx-market.conf b/deploy/miniapp-market/nginx-market.conf index eaa13dddfc..0f865b8d38 100644 --- a/deploy/miniapp-market/nginx-market.conf +++ b/deploy/miniapp-market/nginx-market.conf @@ -37,58 +37,12 @@ server { proxy_read_timeout 60s; } - location = /skin { - return 308 https://market.example.com/skin/; - } - - location ^~ /skin/ { - client_max_body_size 100m; - error_page 413 = @skin_payload_too_large; - error_page 429 = @skin_rate_limited; - limit_req zone=skin_market_api burst=30 nodelay; - limit_req zone=skin_market_upload burst=2 nodelay; - limit_req_status 429; - limit_conn skin_market_connections 8; - limit_conn_status 429; - - proxy_pass http://127.0.0.1:9720; - proxy_http_version 1.1; - proxy_request_buffering off; - proxy_buffering on; - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Proto https; - # The http-level Skin config accepts XFF only from the explicit WAF - # peer set. Forward one normalized address instead of the caller chain. - proxy_set_header X-Real-IP $skin_market_client_address; - proxy_set_header X-Forwarded-For $skin_market_client_address; - proxy_set_header X-Request-ID $request_id; - - proxy_connect_timeout 5s; - proxy_send_timeout 300s; - proxy_read_timeout 300s; - } - location @market_payload_too_large { default_type application/json; add_header X-Request-ID $request_id always; return 413 '{"error":{"code":"payload_too_large","message":"Uploads must not exceed 25 MiB.","requestId":"$request_id"}}'; } - location @skin_payload_too_large { - default_type application/json; - add_header X-Request-ID $request_id always; - return 413 '{"error":{"code":"payload_too_large","message":"Appearance uploads must not exceed 96 MiB.","requestId":"$request_id"}}'; - } - - location @skin_rate_limited { - internal; - default_type application/json; - add_header X-Request-ID $request_id always; - return 429 '{"error":{"code":"rate_limited","message":"Too many Skin market requests. Retry shortly.","requestId":"$request_id"}}'; - } - location / { return 404; } diff --git a/deploy/skin-market/README.md b/deploy/skin-market/README.md deleted file mode 100644 index 30a66e3f4f..0000000000 --- a/deploy/skin-market/README.md +++ /dev/null @@ -1,272 +0,0 @@ -# OpenBitFun Skin market self-hosted deployment - -This directory deploys the Appearance package catalog exposed to users as the -**Skin market** at `https://market.example.com/skin/`. - -This is a community self-deployment template. `$DEPLOY_HOST`, -`market.example.com`, and the `/srv/openbitfun-*` and `/etc/openbitfun-*` paths -are examples; replace them with your own SSH host, domains, and directories. -Set `DEPLOY_HOST` in the current shell before running the commands: - -```bash -DEPLOY_HOST=root@your-server -``` - -Official hosted-service configuration is not published in this repository. - -It is intentionally isolated from the MiniApp market deployment: - -| Resource | Skin market | -| --- | --- | -| Container | `openbitfun-skin-market` | -| Loopback origin | `127.0.0.1:9720` | -| Checkout | `/srv/openbitfun-skin-market/app` | -| SQLite | `/srv/openbitfun-skin-market/data/market.sqlite` | -| Artifacts | `/srv/openbitfun-skin-market/artifacts` | -| Backups | `/srv/openbitfun-skin-market/backups` | -| Root-only environment | `/etc/openbitfun-skin-market/market.env` | -| Deploy ref | `refs/heads/skin-market-deploy` | - -The Skin server has no OAuth client or credential database. Desktop clients -reuse the MiniApp market credential vault and Skin forwards each Bearer token -to the configured MiniApp `/me` endpoint. Web contribution and review routes -use the MiniApp auth broker and a `/skin`-scoped alias of that broker's Web -session. Unsafe browser requests require the matching CSRF alias. Never copy -the MiniApp OAuth secret into the Skin environment. - -The MiniApp identity service must run a commit that implements the same shared -account contract as Skin. Changes under `miniapp-market-service/src/auth.rs` or -its OAuth routes must be deployed with the MiniApp runbook before Skin is -considered healthy. Deploy the two containers separately from their dedicated -checkouts; do not recreate MiniApp from the Skin Compose project. - -## Agent safety contract - -1. Deploy only a committed, explicit commit. Never build a dirty checkout or a - floating branch name. -2. Do not read, copy or print `/etc/openbitfun-skin-market/market.env`. -3. Record the previous commit and verify a current SQLite backup before a - backend or schema deployment. -4. Do not recreate or modify other services running on the host. -5. Use `--force-with-lease` for the dedicated deploy ref; never unconditional - force. -6. Run `nginx -t` before reload. Install only the market vhost changed by this - deployment and the namespaced Skin rate-limit zones. -7. Report success only after container health, image revision, origin and public - health all match the target commit. -8. Do not automatically roll an older binary over an incompatible migration. - -## Local release checks - -Choose the smallest relevant checks, then create one explicit commit: - -```bash -pnpm run fmt:rs -cargo test -p openbitfun-product-domains --no-default-features --features appearance-market -cargo test -p openbitfun-skin-market-service -cargo check -p openbitfun-skin-market-server -pnpm run type-check:skin-market -pnpm run test:skin-market -pnpm run build:skin-market -git diff --check -``` - -Record the immutable target: - -```bash -DEPLOY_COMMIT="$(git rev-parse HEAD)" -git show --stat --oneline "$DEPLOY_COMMIT" -``` - -## First installation - -Create only the dedicated roots; UID/GID `10002` owns live data, never secrets: - -```bash -ssh "$DEPLOY_HOST" 'set -eu -install -d -m 0755 /srv/openbitfun-skin-market -install -d -o 10002 -g 10002 -m 0750 \ - /srv/openbitfun-skin-market/data /srv/openbitfun-skin-market/artifacts -install -d -m 0750 /srv/openbitfun-skin-market/backups -install -d -m 0700 /etc/openbitfun-skin-market -test ! -e /srv/openbitfun-skin-market/app -git clone --no-hardlinks /srv/openbitfun-miniapp-market/app /srv/openbitfun-skin-market/app -git -C /srv/openbitfun-skin-market/app checkout --detach -git -C /srv/openbitfun-skin-market/app status --short' -``` - -Create the environment with a controlled editor, using -`market.env.example` as a field list. Generate the download hash secret on the -server; do not pass it as a command argument or paste it into logs: - -```bash -ssh -t "$DEPLOY_HOST" 'umask 077; vi /etc/openbitfun-skin-market/market.env' -ssh "$DEPLOY_HOST" 'chmod 600 /etc/openbitfun-skin-market/market.env; \ - stat -c "%U:%G %a %n" /etc/openbitfun-skin-market/market.env' -``` - -Production identity verification should use -`https://market.example.com/miniapp/api/v1/me`. Public HTTPS avoids coupling -the two independent Compose networks. Skin forwards either the Bearer -Authorization header or the exact Skin session and CSRF aliases required for -the request; it never proxies the browser's full Cookie header. - -Install backup jobs after the checkout contains this deployment, but do not -start the timer until the Skin container has created a healthy UID-10002-owned -database: - -```bash -ssh "$DEPLOY_HOST" 'set -eu -install -m 0750 /srv/openbitfun-skin-market/app/deploy/skin-market/backup.sh \ - /usr/local/sbin/openbitfun-skin-market-backup -install -m 0750 /srv/openbitfun-skin-market/app/deploy/skin-market/restore-drill.sh \ - /usr/local/sbin/openbitfun-skin-market-restore-drill -install -m 0644 /srv/openbitfun-skin-market/app/deploy/skin-market/openbitfun-skin-market-backup.service \ - /etc/systemd/system/openbitfun-skin-market-backup.service -install -m 0644 /srv/openbitfun-skin-market/app/deploy/skin-market/openbitfun-skin-market-backup.timer \ - /etc/systemd/system/openbitfun-skin-market-backup.timer -systemctl daemon-reload' -``` - -## Deploy an exact commit - -Inspect the target and existing deployment first. On an initial install, the -container inspect/health commands are expected not to find a container: - -```bash -ssh "$DEPLOY_HOST" 'set -eu -git -C /srv/openbitfun-skin-market/app status --short -git -C /srv/openbitfun-skin-market/app rev-parse HEAD -docker inspect --format "{{.Config.Image}} {{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}" openbitfun-skin-market 2>/dev/null || true -curl -fsS http://127.0.0.1:9720/skin/api/v1/health 2>/dev/null || true -stat -c "%U:%G %a %n" /etc/openbitfun-skin-market/market.env' -``` - -For an existing deployment, save the rollback target and confirm or create -today's backup before changing the checkout: - -```bash -PREVIOUS_COMMIT="$(ssh "$DEPLOY_HOST" 'git -C /srv/openbitfun-skin-market/app rev-parse HEAD')" -ssh "$DEPLOY_HOST" 'set -eu -if test -f /srv/openbitfun-skin-market/data/market.sqlite; then - today="$(date -u +%F)" - target="/srv/openbitfun-skin-market/backups/daily/${today}" - if test -d "$target"; then - (cd "$target" && sha256sum -c SHA256SUMS) - test "$(sqlite3 "$target/market.sqlite" "PRAGMA integrity_check;")" = ok - else - /usr/local/sbin/openbitfun-skin-market-backup - fi -fi' -``` - -Send only the explicit commit to the dedicated ref: - -```bash -if REMOTE_REF_COMMIT="$(ssh "$DEPLOY_HOST" 'git -C /srv/openbitfun-skin-market/app \ - rev-parse refs/heads/skin-market-deploy 2>/dev/null')"; then - DEPLOY_LEASE="refs/heads/skin-market-deploy:${REMOTE_REF_COMMIT}" -else - # An empty expected value means the initial push succeeds only while the - # deploy ref is still absent; it remains a compare-and-swap operation. - DEPLOY_LEASE="refs/heads/skin-market-deploy:" -fi -git push \ - --force-with-lease="${DEPLOY_LEASE}" \ - ssh://$DEPLOY_HOST/srv/openbitfun-skin-market/app \ - "${DEPLOY_COMMIT}:refs/heads/skin-market-deploy" -ssh "$DEPLOY_HOST" "set -eu -test -z \"\$(git -C /srv/openbitfun-skin-market/app status --porcelain)\" -git -C /srv/openbitfun-skin-market/app checkout --detach '$DEPLOY_COMMIT' -test \"\$(git -C /srv/openbitfun-skin-market/app rev-parse HEAD)\" = '$DEPLOY_COMMIT'" -``` - -Build while any existing container continues serving, then recreate only Skin: - -```bash -ssh "$DEPLOY_HOST" "set -eu -cd /srv/openbitfun-skin-market/app -test \"\$(git rev-parse HEAD)\" = '$DEPLOY_COMMIT' -export MARKET_GIT_COMMIT='$DEPLOY_COMMIT' -docker compose -f deploy/skin-market/docker-compose.yml build skin-market -docker compose -f deploy/skin-market/docker-compose.yml \ - up -d --no-build --force-recreate skin-market" -``` - -Wait for health and verify revision plus both network paths: - -```bash -ssh "$DEPLOY_HOST" 'set -eu -for attempt in $(seq 1 45); do - status="$(docker inspect --format \ - "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}" \ - openbitfun-skin-market)" - test "$status" = healthy && exit 0 - sleep 2 -done -docker inspect --format "{{json .State}}" openbitfun-skin-market -exit 1' - -DEPLOYED_COMMIT="$(ssh "$DEPLOY_HOST" 'docker inspect --format \ - "{{index .Config.Labels \"org.opencontainers.image.revision\"}}" \ - openbitfun-skin-market')" -test "$DEPLOYED_COMMIT" = "$DEPLOY_COMMIT" -ssh "$DEPLOY_HOST" 'curl -fsS http://127.0.0.1:9720/skin/api/v1/health' -``` - -Create and drill the first backup only after the container is healthy, then -enable the timer: - -```bash -ssh "$DEPLOY_HOST" 'set -eu -/usr/local/sbin/openbitfun-skin-market-backup -today="$(date -u +%F)" -/usr/local/sbin/openbitfun-skin-market-restore-drill \ - "/srv/openbitfun-skin-market/backups/daily/${today}" -systemctl enable --now openbitfun-skin-market-backup.timer' -``` - -## Nginx route - -The shared market vhost is owned in -`deploy/miniapp-market/nginx-market.conf` (template file name). -It keeps MiniApp at 25 MiB and grants only `/skin/` the 100 MiB transport -ceiling. Skin application validation remains 96 MiB. Install this file only -when its diff is part of the target commit: - -The trusted proxy ranges in `nginx-skin-market-http.conf` are an explicit trust -boundary, not a generic proxy list. Before each deployment, compare them with -your reverse proxy or WAF provider's current egress source-IP list and the -origin firewall allowlist. Stop if any of the sets differ. - -```bash -ssh "$DEPLOY_HOST" 'set -eu -install -m 0644 \ - /srv/openbitfun-skin-market/app/deploy/skin-market/nginx-skin-market-http.conf \ - /etc/nginx/conf.d/skin-market.conf -install -m 0644 \ - /srv/openbitfun-skin-market/app/deploy/miniapp-market/nginx-market.conf \ - /etc/nginx/sites-available/market.example.com.conf -nginx -t -systemctl reload nginx -curl -fsS http://127.0.0.1:9720/skin/api/v1/health' -curl -fsS https://market.example.com/skin/api/v1/health -curl -fsS https://market.example.com/skin/ >/dev/null -``` - -Point your domain's DNS and TLS certificate at this host. If a WAF or load -balancer terminates TLS, verify its body-size policy before publishing a -package near the 96 MiB application limit. - -## Rollback - -For a backward-compatible release, detach the dedicated checkout at -`$PREVIOUS_COMMIT`, build if its image is absent, and recreate only -`skin-market`. Repeat all health and revision checks. Use a leased push to move -`skin-market-deploy` back afterwards. If the new binary applied a migration an -older binary cannot read, stop and design an explicit data rollback; restoring -a backup can discard submissions created after that snapshot. - -Daily backups retain 14 snapshots and Sunday snapshots retain 8 weeks. The -restore drill validates same-host recovery only; it is not off-site disaster -recovery. diff --git a/deploy/skin-market/backup.sh b/deploy/skin-market/backup.sh deleted file mode 100755 index 457a606b05..0000000000 --- a/deploy/skin-market/backup.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -readonly MARKET_ROOT="${MARKET_ROOT:-/srv/openbitfun-skin-market}" -readonly DATA_ROOT="${MARKET_ROOT}/data" -readonly ARTIFACT_ROOT="${MARKET_ROOT}/artifacts" -readonly BACKUP_ROOT="${MARKET_ROOT}/backups" -readonly DAILY_ROOT="${BACKUP_ROOT}/daily" -readonly WEEKLY_ROOT="${BACKUP_ROOT}/weekly" -readonly TODAY="$(date -u +%F)" -readonly WEEK="$(date -u +%G-W%V)" -readonly DAILY_TARGET="${DAILY_ROOT}/${TODAY}" -readonly DATABASE="${DATA_ROOT}/market.sqlite" -readonly SKIN_MARKET_UID="${SKIN_MARKET_UID:-10002}" - -if [[ "${MARKET_ROOT}" != /* || "${MARKET_ROOT}" == "/" ]]; then - echo "MARKET_ROOT must be an absolute path below /" >&2 - exit 1 -fi - -install -d -m 0750 "${DAILY_ROOT}" "${WEEKLY_ROOT}" -if [[ ! -f "${DATABASE}" ]]; then - echo "Skin market database does not exist; refusing to create an empty backup source" >&2 - exit 1 -fi -if [[ "$(stat -c %u "${DATABASE}")" != "${SKIN_MARKET_UID}" ]]; then - echo "Skin market database must be owned by runtime UID ${SKIN_MARKET_UID}" >&2 - exit 1 -fi -if [[ "$(sqlite3 "${DATABASE}" "PRAGMA quick_check;")" != "ok" ]]; then - echo "Skin market source database failed quick_check" >&2 - exit 1 -fi -if [[ -e "${DAILY_TARGET}" ]]; then - if [[ ! -d "${DAILY_TARGET}" ]]; then - echo "Daily backup target exists but is not a directory" >&2 - exit 1 - fi - ( - cd "${DAILY_TARGET}" - sha256sum -c SHA256SUMS - ) - if [[ "$(sqlite3 "${DAILY_TARGET}/market.sqlite" "PRAGMA integrity_check;")" != "ok" ]]; then - echo "Existing daily backup failed integrity_check" >&2 - exit 1 - fi - echo "Verified existing Skin market backup: ${DAILY_TARGET}" - exit 0 -fi - -readonly STAGING_TARGET="$(mktemp -d "${DAILY_ROOT}/.${TODAY}.staging.XXXXXX")" -cleanup_staging() { - if [[ -d "${STAGING_TARGET}" && "${STAGING_TARGET}" == "${DAILY_ROOT}/.${TODAY}.staging."* ]]; then - rm -rf -- "${STAGING_TARGET}" - fi -} -trap cleanup_staging EXIT - -sqlite3 "${DATABASE}" ".timeout 10000" ".backup '${STAGING_TARGET}/market.sqlite'" - -readonly INTEGRITY="$(sqlite3 "${STAGING_TARGET}/market.sqlite" "PRAGMA integrity_check;")" -if [[ "${INTEGRITY}" != "ok" ]]; then - echo "SQLite integrity check failed: ${INTEGRITY}" >&2 - exit 1 -fi - -tar -C "${ARTIFACT_ROOT}" -czf "${STAGING_TARGET}/artifacts.tar.gz" . -( - cd "${STAGING_TARGET}" - sha256sum market.sqlite artifacts.tar.gz > SHA256SUMS -) -mv "${STAGING_TARGET}" "${DAILY_TARGET}" -trap - EXIT - -if [[ "$(date -u +%u)" == "7" ]]; then - readonly WEEKLY_TARGET="${WEEKLY_ROOT}/${WEEK}" - if [[ ! -e "${WEEKLY_TARGET}" ]]; then - install -d -m 0750 "${WEEKLY_TARGET}" - cp --reflink=auto "${DAILY_TARGET}/market.sqlite" "${WEEKLY_TARGET}/market.sqlite" - cp --reflink=auto "${DAILY_TARGET}/artifacts.tar.gz" "${WEEKLY_TARGET}/artifacts.tar.gz" - cp "${DAILY_TARGET}/SHA256SUMS" "${WEEKLY_TARGET}/SHA256SUMS" - fi -fi - -prune_backups() { - local root="$1" - local keep="$2" - local candidate - - while IFS= read -r candidate; do - [[ "${candidate}" == "${root}/"* ]] || { - echo "Refusing to prune unexpected path: ${candidate}" >&2 - exit 1 - } - rm -rf -- "${candidate}" - done < <(find "${root}" -mindepth 1 -maxdepth 1 -type d -print | sort -r | tail -n "+$((keep + 1))") -} - -prune_backups "${DAILY_ROOT}" 14 -prune_backups "${WEEKLY_ROOT}" 8 - -echo "Skin market backup completed: ${DAILY_TARGET}" diff --git a/deploy/skin-market/docker-compose.yml b/deploy/skin-market/docker-compose.yml deleted file mode 100644 index 8672584048..0000000000 --- a/deploy/skin-market/docker-compose.yml +++ /dev/null @@ -1,41 +0,0 @@ -services: - skin-market: - build: - context: ../.. - dockerfile: src/apps/skin-market-server/Dockerfile - args: - CARGO_BUILD_JOBS: ${MARKET_CARGO_BUILD_JOBS:-} - MARKET_GIT_COMMIT: ${MARKET_GIT_COMMIT:?Set MARKET_GIT_COMMIT to the deployed commit} - image: openbitfun-skin-market:${MARKET_GIT_COMMIT} - container_name: openbitfun-skin-market - restart: unless-stopped - user: "10002:10002" - read_only: true - security_opt: - - no-new-privileges:true - cap_drop: - - ALL - ports: - - "127.0.0.1:9720:9720" - env_file: - - ${SKIN_MARKET_ENV_FILE:-/etc/openbitfun-skin-market/market.env} - environment: - SKIN_MARKET_BIND: 0.0.0.0:9720 - SKIN_MARKET_DATABASE_PATH: /app/data/market.sqlite - SKIN_MARKET_ARTIFACT_DIR: /app/artifacts - SKIN_MARKET_WEB_DIR: /app/web - volumes: - - ${SKIN_MARKET_ROOT:-/srv/openbitfun-skin-market}/data:/app/data - - ${SKIN_MARKET_ROOT:-/srv/openbitfun-skin-market}/artifacts:/app/artifacts - tmpfs: - - /tmp:rw,noexec,nosuid,nodev,size=64m - mem_limit: 1g - cpus: 1.5 - healthcheck: - test: - - CMD-SHELL - - curl -fsS http://127.0.0.1:9720/skin/api/v1/health || exit 1 - interval: 20s - timeout: 5s - retries: 5 - start_period: 15s diff --git a/deploy/skin-market/market.env.example b/deploy/skin-market/market.env.example deleted file mode 100644 index 44bcd0573c..0000000000 --- a/deploy/skin-market/market.env.example +++ /dev/null @@ -1,5 +0,0 @@ -SKIN_MARKET_PUBLIC_BASE_URL=https://market.example.com/skin -SKIN_MARKET_IDENTITY_ME_URL=https://market.example.com/miniapp/api/v1/me -SKIN_MARKET_DOWNLOAD_HASH_SECRET=replace-with-at-least-32-random-characters -SKIN_MARKET_PUBLIC_BROWSE=true -RUST_LOG=openbitfun_skin_market=info,tower_http=info diff --git a/deploy/skin-market/nginx-skin-market-http.conf b/deploy/skin-market/nginx-skin-market-http.conf deleted file mode 100644 index 18d9f1424d..0000000000 --- a/deploy/skin-market/nginx-skin-market-http.conf +++ /dev/null @@ -1,30 +0,0 @@ -# Loaded in Nginx's http context. Replace the placeholder ranges with the -# egress ranges of your own reverse proxy or WAF, and keep them synchronized -# with the origin firewall. XFF is considered only when the direct peer belongs -# to this explicit set. -geo $skin_market_trusted_waf_peer { - default 0; - 192.0.2.0/24 1; - 2001:db8::/32 1; -} - -map "$skin_market_trusted_waf_peer:$http_x_forwarded_for" $skin_market_client_address { - "~^1:\s*(?[0-9]{1,3}(?:\.[0-9]{1,3}){3})(?:\s*,|\s*$)" $skin_market_client_ipv4; - "~^1:\s*(?[0-9A-Fa-f:]{2,45})(?:\s*,|\s*$)" $skin_market_client_ipv6; - default $remote_addr; -} - -map $uri $skin_market_api_limit_key { - default ""; - ~^/skin/api/v1/submissions/[^/]+/package$ ""; - ~^/skin/api/v1/ $skin_market_client_address; -} - -map $uri $skin_market_upload_limit_key { - default ""; - ~^/skin/api/v1/submissions/[^/]+/package$ $skin_market_client_address; -} - -limit_req_zone $skin_market_api_limit_key zone=skin_market_api:10m rate=10r/s; -limit_req_zone $skin_market_upload_limit_key zone=skin_market_upload:10m rate=6r/m; -limit_conn_zone $skin_market_client_address zone=skin_market_connections:10m; diff --git a/deploy/skin-market/openbitfun-skin-market-backup.service b/deploy/skin-market/openbitfun-skin-market-backup.service deleted file mode 100644 index 2d7a08a842..0000000000 --- a/deploy/skin-market/openbitfun-skin-market-backup.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Back up OpenBitFun Skin market data -After=docker.service - -[Service] -Type=oneshot -ExecStart=/usr/local/sbin/openbitfun-skin-market-backup -User=root -Group=root -Nice=10 -IOSchedulingClass=best-effort -IOSchedulingPriority=7 diff --git a/deploy/skin-market/openbitfun-skin-market-backup.timer b/deploy/skin-market/openbitfun-skin-market-backup.timer deleted file mode 100644 index 12f8c00a1e..0000000000 --- a/deploy/skin-market/openbitfun-skin-market-backup.timer +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Daily OpenBitFun Skin market backup - -[Timer] -OnCalendar=*-*-* 03:37:00 UTC -Persistent=true -RandomizedDelaySec=900 -Unit=openbitfun-skin-market-backup.service - -[Install] -WantedBy=timers.target diff --git a/deploy/skin-market/restore-drill.sh b/deploy/skin-market/restore-drill.sh deleted file mode 100755 index bf569ba407..0000000000 --- a/deploy/skin-market/restore-drill.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -readonly MARKET_ROOT="${MARKET_ROOT:-/srv/openbitfun-skin-market}" -readonly BACKUP_ROOT="${MARKET_ROOT}/backups" -readonly SOURCE="${1:-}" - -if [[ "${MARKET_ROOT}" != /* || "${MARKET_ROOT}" == "/" ]]; then - echo "MARKET_ROOT must be an absolute path below /" >&2 - exit 2 -fi - -if [[ -z "${SOURCE}" || "${SOURCE}" != "${BACKUP_ROOT}/"* || ! -d "${SOURCE}" ]]; then - echo "Usage: $0 ${BACKUP_ROOT}/{daily|weekly}/" >&2 - exit 2 -fi - -readonly DRILL_DIR="$(mktemp -d "${BACKUP_ROOT}/restore-drill.XXXXXX")" -cleanup() { - [[ "${DRILL_DIR}" == "${BACKUP_ROOT}/restore-drill."* ]] && rm -rf -- "${DRILL_DIR}" -} -trap cleanup EXIT - -( - cd "${SOURCE}" - sha256sum -c SHA256SUMS -) -cp "${SOURCE}/market.sqlite" "${DRILL_DIR}/market.sqlite" -mkdir "${DRILL_DIR}/artifacts" -tar -C "${DRILL_DIR}/artifacts" -xzf "${SOURCE}/artifacts.tar.gz" - -readonly INTEGRITY="$(sqlite3 "${DRILL_DIR}/market.sqlite" "PRAGMA integrity_check;")" -readonly TABLE_COUNT="$(sqlite3 "${DRILL_DIR}/market.sqlite" \ - "SELECT count(*) FROM sqlite_master WHERE type IN ('table','view');")" -readonly ARTIFACT_COUNT="$(find "${DRILL_DIR}/artifacts" -type f | wc -l | tr -d ' ')" - -if [[ "${INTEGRITY}" != "ok" || "${TABLE_COUNT}" -lt 1 ]]; then - echo "Restore drill failed" >&2 - exit 1 -fi - -sqlite3 -separator '|' "${DRILL_DIR}/market.sqlite" \ - "SELECT package_sha256, package_size FROM releases - UNION - SELECT package_sha256, package_size FROM submissions - WHERE package_sha256 IS NOT NULL;" > "${DRILL_DIR}/package-references" -sqlite3 "${DRILL_DIR}/market.sqlite" \ - "SELECT preview_sha256 FROM releases - UNION - SELECT preview_sha256 FROM submissions - WHERE preview_sha256 IS NOT NULL;" > "${DRILL_DIR}/preview-references" - -REFERENCE_COUNT=0 -while IFS='|' read -r sha256 expected_size; do - [[ "${sha256}" =~ ^[0-9a-f]{64}$ && "${expected_size}" =~ ^[0-9]+$ ]] || { - echo "Restore drill found an invalid package reference" >&2 - exit 1 - } - package="${DRILL_DIR}/artifacts/packages/${sha256:0:2}/${sha256}.openbitfun-appearance" - [[ -f "${package}" && "$(stat -c %s "${package}")" == "${expected_size}" ]] || { - echo "Restore drill found a missing or incorrectly sized package artifact" >&2 - exit 1 - } - [[ "$(sha256sum "${package}" | awk '{print $1}')" == "${sha256}" ]] || { - echo "Restore drill found a package artifact hash mismatch" >&2 - exit 1 - } - REFERENCE_COUNT=$((REFERENCE_COUNT + 1)) -done < "${DRILL_DIR}/package-references" - -while IFS= read -r sha256; do - [[ "${sha256}" =~ ^[0-9a-f]{64}$ ]] || { - echo "Restore drill found an invalid preview reference" >&2 - exit 1 - } - preview="${DRILL_DIR}/artifacts/previews/${sha256:0:2}/${sha256}.webp" - [[ -f "${preview}" ]] || { - echo "Restore drill found a missing preview artifact" >&2 - exit 1 - } - [[ "$(sha256sum "${preview}" | awk '{print $1}')" == "${sha256}" ]] || { - echo "Restore drill found a preview artifact hash mismatch" >&2 - exit 1 - } - REFERENCE_COUNT=$((REFERENCE_COUNT + 1)) -done < "${DRILL_DIR}/preview-references" - -echo "Restore drill passed: tables=${TABLE_COUNT}, artifacts=${ARTIFACT_COUNT}" -echo "Verified database artifact references: ${REFERENCE_COUNT}" -echo "This drill validates same-host backups only; it is not off-site disaster recovery." diff --git a/design-system/apps/design-lab/src/i18n/semanticToolMessages.ts b/design-system/apps/design-lab/src/i18n/semanticToolMessages.ts index 385a9c81d0..b26ab1a96f 100644 --- a/design-system/apps/design-lab/src/i18n/semanticToolMessages.ts +++ b/design-system/apps/design-lab/src/i18n/semanticToolMessages.ts @@ -18,7 +18,6 @@ export const semanticToolEn = { "flowChat.builtin.tools.FrontendWorkbench": "Frontend customization", "flowChat.builtin.tools.FinalizeMiniApp": "Update Mini App", "flowChat.builtin.tools.PublishMiniApp": "Submit Mini App", - "flowChat.builtin.tools.PublishAppearance": "Submit Skin", "flowChat.builtin.tools.Playbook": "Read operation guide", "flowChat.builtin.tools.DesktopCard": "Desktop card", "flowChat.builtin.fields.results": "Results", @@ -212,7 +211,6 @@ export const semanticToolZhCN = { "flowChat.builtin.tools.FrontendWorkbench": "界面定制", "flowChat.builtin.tools.FinalizeMiniApp": "更新小应用", "flowChat.builtin.tools.PublishMiniApp": "提交小应用审核", - "flowChat.builtin.tools.PublishAppearance": "提交皮肤审核", "flowChat.builtin.tools.Playbook": "读取操作流程", "flowChat.builtin.tools.DesktopCard": "小卡片", "flowChat.builtin.fields.results": "结果", @@ -406,7 +404,6 @@ export const semanticToolZhTW = { "flowChat.builtin.tools.FrontendWorkbench": "介面自訂", "flowChat.builtin.tools.FinalizeMiniApp": "更新小應用程式", "flowChat.builtin.tools.PublishMiniApp": "提交小應用程式審核", - "flowChat.builtin.tools.PublishAppearance": "提交外觀審核", "flowChat.builtin.tools.Playbook": "讀取操作流程", "flowChat.builtin.tools.DesktopCard": "小卡片", "flowChat.builtin.fields.results": "結果", diff --git a/design-system/apps/design-lab/src/preview/FlowChatMockTools.tsx b/design-system/apps/design-lab/src/preview/FlowChatMockTools.tsx index 5c3dfa768f..cd03307637 100644 --- a/design-system/apps/design-lab/src/preview/FlowChatMockTools.tsx +++ b/design-system/apps/design-lab/src/preview/FlowChatMockTools.tsx @@ -13,7 +13,7 @@ const groupForTool = { SessionHistory: 'collaboration', analyze_image: 'files', generate_image: 'files', GetTime: 'execution', ListMCPResources: 'execution', ReadMCPResource: 'execution', ListMCPPrompts: 'execution', GetMCPPrompt: 'execution', Worktree: 'files', PortForward: 'execution', ReviewPlatform: 'collaboration', FrontendWorkbench: 'web', - FinalizeMiniApp: 'web', PublishMiniApp: 'web', PublishAppearance: 'web', Playbook: 'execution', + FinalizeMiniApp: 'web', PublishMiniApp: 'web', Playbook: 'execution', DesktopCard: 'collaboration', Read: 'files', Write: 'files', Edit: 'files', Delete: 'files', Grep: 'files', Glob: 'files', LS: 'files', GetFileDiff: 'files', view_image: 'files', diff --git a/design-system/apps/design-lab/src/preview/SemanticToolCardPreview.tsx b/design-system/apps/design-lab/src/preview/SemanticToolCardPreview.tsx index 8e045c8dc2..ebb5299911 100644 --- a/design-system/apps/design-lab/src/preview/SemanticToolCardPreview.tsx +++ b/design-system/apps/design-lab/src/preview/SemanticToolCardPreview.tsx @@ -27,7 +27,7 @@ function SemanticPreview({ component, interactive, specimen, state }: Parameters const text = (key: string) => t(`flowChat.builtin.${key}` as MessageKey); const tool = specimen?.tool ?? toolsForComponent(component)[0] ?? component; const prominent = ['create_goal', 'update_goal', 'AgentDelete', 'Worktree', 'PortForward', 'ReviewPlatform', - 'FrontendWorkbench', 'FinalizeMiniApp', 'PublishMiniApp', 'PublishAppearance', 'DesktopCard'].includes(tool); + 'FrontendWorkbench', 'FinalizeMiniApp', 'PublishMiniApp', 'DesktopCard'].includes(tool); const props: SemanticToolCardProps = { action: text(`tools.${tool}`), status: state === 'loading' ? 'running' : state === 'error' ? 'error' : 'completed', attention: prominent ? 'prominent' : 'ambient', isExpanded: expanded, @@ -85,7 +85,7 @@ function SemanticPreview({ component, interactive, specimen, state }: Parameters } if (props.status !== 'completed') props.outcome = undefined; if (component === 'AgentRosterToolCard') return ; - if (component === 'MarketplacePublishToolCard') return ; + if (component === 'MarketplacePublishToolCard') return ; if (component === 'McpResourceToolCard') return ; const View = views[component]; diff --git a/design-system/packages/ui/README.md b/design-system/packages/ui/README.md index ef14ce0b88..e863bf2937 100644 --- a/design-system/packages/ui/README.md +++ b/design-system/packages/ui/README.md @@ -87,6 +87,21 @@ gamut. Palette colors, the current-color ring and the lightness ramp represent editable color content; they deliberately do not follow theme or status colors. Chrome, borders, focus and shadows use existing semantic/system tokens. No theme color literal or new token is needed. The component has no storage or host API. +## Decorative paint ownership + +Components that paint their background in an existing `::before` layer advertise +`data-openbitfun-paint-layer="before"` on that part (Button, IconButton and +TabGroup tabs). Glass variants use `self`. Product paint adapters can target the +existing background without inserting children, replacing pseudo-elements or +changing geometry. TabGroup also advertises `data-openbitfun-edge-layer="before"`: +its border and shadow follow the expanding surface, including the close action. +Button and IconButton borders remain on their native element. Unmarked parts paint their own box. Switch exposes `input`, +`track` and `thumb` parts; its native input still owns checked/disabled behavior. + +`data-openbitfun-paint-layer="none"` delegates a part's decorations to its +container. SearchField uses it for embedded/panel inner Inputs and for its +unpainted default wrapper. Skin adapters must omit those part decorations, +including focus shadows, while retaining native input behavior and token colors. ## Pane layouts diff --git a/design-system/packages/ui/src/components/Button/Button.tsx b/design-system/packages/ui/src/components/Button/Button.tsx index 1d843a1eed..2c165d437c 100644 --- a/design-system/packages/ui/src/components/Button/Button.tsx +++ b/design-system/packages/ui/src/components/Button/Button.tsx @@ -41,6 +41,7 @@ export const Button = forwardRef(function Button className={classNames(styles.button, variant === "glass" && glassStyles.surface, className)} data-openbitfun-component="button" data-openbitfun-part="root" + data-openbitfun-paint-layer={variant === 'glass' ? "self" : "before"} data-openbitfun-tone={tone} data-openbitfun-variant={variant} data-loading={loading ? "true" : "false"} diff --git a/design-system/packages/ui/src/components/Combobox/Combobox.module.css b/design-system/packages/ui/src/components/Combobox/Combobox.module.css index cabb3eb8a2..92a348bbae 100644 --- a/design-system/packages/ui/src/components/Combobox/Combobox.module.css +++ b/design-system/packages/ui/src/components/Combobox/Combobox.module.css @@ -83,6 +83,12 @@ inline-size: 100%; min-inline-size: 0; block-size: var(--_combobox-height); + /* The control is a public paint target behind the visible trigger. */ + border-radius: var(--openbitfun-control-select-radius); + } + + .root[data-variant="pill"] .control { + border-radius: var(--openbitfun-control-status-pill-radius); } .trigger { diff --git a/design-system/packages/ui/src/components/IconButton/IconButton.tsx b/design-system/packages/ui/src/components/IconButton/IconButton.tsx index 450eeaf081..1f672f769a 100644 --- a/design-system/packages/ui/src/components/IconButton/IconButton.tsx +++ b/design-system/packages/ui/src/components/IconButton/IconButton.tsx @@ -43,6 +43,7 @@ export const IconButton = forwardRef(functio aria-label={ariaLabel} className={classNames(styles.button, resolvedVariant === "glass" && glassStyles.surface, className)} data-openbitfun-component="icon-button" + data-openbitfun-paint-layer={resolvedVariant === 'glass' ? "self" : "before"} data-openbitfun-shape={floating ? "circle" : shape} data-openbitfun-tone={tone} data-openbitfun-variant={resolvedVariant} diff --git a/design-system/packages/ui/src/components/Input/Input.tsx b/design-system/packages/ui/src/components/Input/Input.tsx index 2602a686ca..4f8da374ef 100644 --- a/design-system/packages/ui/src/components/Input/Input.tsx +++ b/design-system/packages/ui/src/components/Input/Input.tsx @@ -14,6 +14,8 @@ import styles from "./Input.module.css"; export interface InputProps extends Omit, "className" | "size"> { className?: string; + /** Product anatomy: an embedded field delegates decorative paint to its container. */ + "data-openbitfun-paint-layer"?: "self" | "none"; /** Hide the browser reveal button when the caller provides a visibility control. */ hideNativePasswordReveal?: boolean; invalid?: boolean; @@ -26,6 +28,7 @@ export interface InputProps export const Input = forwardRef(function Input({ "aria-invalid": ariaInvalid, + "data-openbitfun-paint-layer": paintLayer, className, disabled, hideNativePasswordReveal = false, @@ -58,6 +61,7 @@ export const Input = forwardRef(function Input({ ( data-active={active ? "true" : "false"} data-overflow-active={active ? "true" : undefined} data-openbitfun-listbox-option="" + data-openbitfun-component="listbox-option" data-openbitfun-part="option" data-selected={selected ? "true" : "false"} data-value={value} diff --git a/design-system/packages/ui/src/components/SearchField/SearchField.tsx b/design-system/packages/ui/src/components/SearchField/SearchField.tsx index c8d43ccfe5..dd903974d9 100644 --- a/design-system/packages/ui/src/components/SearchField/SearchField.tsx +++ b/design-system/packages/ui/src/components/SearchField/SearchField.tsx @@ -82,9 +82,10 @@ export const SearchField = forwardRef(functi ); return ( - + (function Switch( onChange={handleChange} ref={ref} role="switch" + data-openbitfun-part="input" type="checkbox" /> -