重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。

同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-26 08:34:33 +08:00
parent 4a23b715a2
commit dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions

View file

@ -0,0 +1,21 @@
# App Store Connect API key (pick one approach)
#
# Recommended (use the downloaded .p8 directly):
# ASC_KEY_ID=XXXXXXXXXX
# ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# ASC_KEY_PATH=/absolute/path/to/AuthKey_XXXXXXXXXX.p8
#
# Or (JSON key file):
# APP_STORE_CONNECT_API_KEY_PATH=/absolute/path/to/AuthKey_XXXXXX.json
#
# Or:
# ASC_KEY_ID=XXXXXXXXXX
# ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# ASC_KEY_CONTENT=BASE64_P8_CONTENT
# Code signing
# IOS_DEVELOPMENT_TEAM=XXXXXXXXXX
# Deliver toggles (off by default)
# DELIVER_METADATA=1
# DELIVER_SCREENSHOTS=1

View file

@ -0,0 +1,15 @@
app_identifier("ai.openclaw.client")
# Auth is expected via App Store Connect API key.
# Provide either:
# - APP_STORE_CONNECT_API_KEY_PATH=/path/to/AuthKey_XXXXXX.p8.json (recommended)
# or:
# - ASC_KEY_PATH=/path/to/AuthKey_XXXXXX.p8 with ASC_KEY_ID and ASC_ISSUER_ID
# - ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_CONTENT (base64 or raw p8 content)
# - ASC_KEY_ID and ASC_ISSUER_ID plus Keychain fallback:
# ASC_KEYCHAIN_SERVICE (default: openclaw-asc-key)
# ASC_KEYCHAIN_ACCOUNT (default: USER/LOGNAME)
#
# Optional deliver app lookup overrides:
# - ASC_APP_IDENTIFIER (bundle ID)
# - ASC_APP_ID (numeric App Store Connect app ID)

View file

@ -0,0 +1,346 @@
require "shellwords"
require "open3"
require "json"
default_platform(:ios)
BETA_APP_IDENTIFIER = "ai.openclaw.client"
def load_env_file(path)
return unless File.exist?(path)
File.foreach(path) do |line|
stripped = line.strip
next if stripped.empty? || stripped.start_with?("#")
key, value = stripped.split("=", 2)
next if key.nil? || key.empty? || value.nil?
ENV[key] = value if ENV[key].nil? || ENV[key].strip.empty?
end
end
def env_present?(value)
!value.nil? && !value.strip.empty?
end
def clear_empty_env_var(key)
return unless ENV.key?(key)
ENV.delete(key) unless env_present?(ENV[key])
end
def maybe_decode_hex_keychain_secret(value)
return value unless env_present?(value)
candidate = value.strip
return candidate unless candidate.match?(/\A[0-9a-fA-F]+\z/) && candidate.length.even?
begin
decoded = [candidate].pack("H*")
return candidate unless decoded.valid_encoding?
# `security find-generic-password -w` can return hex when the stored secret
# includes newlines/non-printable bytes (like PEM files).
beginPemMarker = %w[BEGIN PRIVATE KEY].join(" ") # pragma: allowlist secret
endPemMarker = %w[END PRIVATE KEY].join(" ")
if decoded.include?(beginPemMarker) || decoded.include?(endPemMarker)
UI.message("Decoded hex-encoded ASC key content from Keychain.")
return decoded
end
rescue StandardError
return candidate
end
candidate
end
def read_asc_key_content_from_keychain
service = ENV["ASC_KEYCHAIN_SERVICE"]
service = "openclaw-asc-key" unless env_present?(service)
account = ENV["ASC_KEYCHAIN_ACCOUNT"]
account = ENV["USER"] unless env_present?(account)
account = ENV["LOGNAME"] unless env_present?(account)
return nil unless env_present?(account)
begin
stdout, _stderr, status = Open3.capture3(
"security",
"find-generic-password",
"-s",
service,
"-a",
account,
"-w"
)
return nil unless status.success?
key_content = stdout.to_s.strip
key_content = maybe_decode_hex_keychain_secret(key_content)
return nil unless env_present?(key_content)
UI.message("Loaded ASC key content from Keychain service '#{service}' (account '#{account}').")
key_content
rescue Errno::ENOENT
nil
end
end
def repo_root
File.expand_path("../../..", __dir__)
end
def ios_root
File.expand_path("..", __dir__)
end
def read_ios_version_metadata
script_path = File.join(repo_root, "scripts", "ios-version.ts")
stdout, stderr, status = Open3.capture3(
"node",
"--import",
"tsx",
script_path,
"--json",
chdir: repo_root
)
unless status.success?
detail = stderr.to_s.strip
detail = stdout.to_s.strip if detail.empty?
UI.user_error!("Failed to read iOS version metadata: #{detail}")
end
parsed = JSON.parse(stdout)
version = parsed["canonicalVersion"].to_s.strip
short_version = parsed["marketingVersion"].to_s.strip
if !env_present?(version) || !env_present?(short_version)
UI.user_error!("iOS version helper returned incomplete metadata.")
end
{
short_version: short_version,
version: version
}
rescue JSON::ParserError => e
UI.user_error!("Invalid JSON from iOS version helper: #{e.message}")
end
def sync_ios_versioning!
script_path = File.join(repo_root, "scripts", "ios-sync-versioning.ts")
stdout, stderr, status = Open3.capture3(
"node",
"--import",
"tsx",
script_path,
"--check",
chdir: repo_root
)
return if status.success?
detail = stderr.to_s.strip
detail = stdout.to_s.strip if detail.empty?
UI.user_error!("iOS versioning artifacts are stale. Run `pnpm ios:version:sync`.\n#{detail}")
end
def shell_join(parts)
Shellwords.join(parts.compact)
end
def resolve_beta_build_number(api_key:, short_version:)
explicit = ENV["IOS_BETA_BUILD_NUMBER"]
if env_present?(explicit)
UI.user_error!("Invalid IOS_BETA_BUILD_NUMBER '#{explicit}'. Expected digits only.") unless explicit.match?(/\A\d+\z/)
UI.message("Using explicit iOS beta build number #{explicit}.")
return explicit
end
latest_build = latest_testflight_build_number(
api_key: api_key,
app_identifier: BETA_APP_IDENTIFIER,
version: short_version,
initial_build_number: 0
)
next_build = latest_build.to_i + 1
UI.message("Resolved iOS beta build number #{next_build} for #{short_version} (latest TestFlight build: #{latest_build}).")
next_build.to_s
end
def beta_build_number_needs_asc_auth?
explicit = ENV["IOS_BETA_BUILD_NUMBER"]
!env_present?(explicit)
end
def prepare_beta_release!(version:, build_number:)
script_path = File.join(repo_root, "scripts", "ios-beta-prepare.sh")
UI.message("Preparing iOS beta release #{version} (build #{build_number}).")
sh(shell_join(["bash", script_path, "--build-number", build_number]))
beta_xcconfig = File.join(ios_root, "build", "BetaRelease.xcconfig")
UI.user_error!("Missing beta xcconfig at #{beta_xcconfig}.") unless File.exist?(beta_xcconfig)
ENV["XCODE_XCCONFIG_FILE"] = beta_xcconfig
beta_xcconfig
end
def build_beta_release(context)
version = context[:version]
output_directory = File.join("build", "beta")
archive_path = File.join(output_directory, "OpenClaw-#{version}.xcarchive")
build_app(
project: "OpenClaw.xcodeproj",
scheme: "OpenClaw",
configuration: "Release",
export_method: "app-store",
clean: true,
skip_profile_detection: true,
build_path: "build",
archive_path: archive_path,
output_directory: output_directory,
output_name: "OpenClaw-#{version}.ipa",
xcargs: "-allowProvisioningUpdates",
export_xcargs: "-allowProvisioningUpdates",
export_options: {
signingStyle: "automatic"
}
)
{
archive_path: archive_path,
build_number: context[:build_number],
ipa_path: lane_context[SharedValues::IPA_OUTPUT_PATH],
short_version: context[:short_version],
version: version
}
end
platform :ios do
private_lane :asc_api_key do
load_env_file(File.join(__dir__, ".env"))
clear_empty_env_var("APP_STORE_CONNECT_API_KEY_PATH")
clear_empty_env_var("ASC_KEY_PATH")
clear_empty_env_var("ASC_KEY_CONTENT")
api_key = nil
key_path = ENV["APP_STORE_CONNECT_API_KEY_PATH"]
if env_present?(key_path)
api_key = app_store_connect_api_key(path: key_path)
else
p8_path = ENV["ASC_KEY_PATH"]
if env_present?(p8_path)
key_id = ENV["ASC_KEY_ID"]
issuer_id = ENV["ASC_ISSUER_ID"]
UI.user_error!("Missing ASC_KEY_ID or ASC_ISSUER_ID for ASC_KEY_PATH auth.") if [key_id, issuer_id].any? { |v| !env_present?(v) }
api_key = app_store_connect_api_key(
key_id: key_id,
issuer_id: issuer_id,
key_filepath: p8_path
)
else
key_id = ENV["ASC_KEY_ID"]
issuer_id = ENV["ASC_ISSUER_ID"]
key_content = ENV["ASC_KEY_CONTENT"]
key_content = read_asc_key_content_from_keychain unless env_present?(key_content)
UI.user_error!(
"Missing App Store Connect API key. Set APP_STORE_CONNECT_API_KEY_PATH (json), ASC_KEY_PATH (p8), or ASC_KEY_ID/ASC_ISSUER_ID with ASC_KEY_CONTENT (or Keychain via ASC_KEYCHAIN_SERVICE/ASC_KEYCHAIN_ACCOUNT)."
) if [key_id, issuer_id, key_content].any? { |v| !env_present?(v) }
is_base64 = key_content.include?("BEGIN PRIVATE KEY") ? false : true
api_key = app_store_connect_api_key(
key_id: key_id,
issuer_id: issuer_id,
key_content: key_content,
is_key_content_base64: is_base64
)
end
end
api_key
end
private_lane :prepare_beta_context do |options|
require_api_key = options[:require_api_key] == true
needs_api_key = require_api_key || beta_build_number_needs_asc_auth?
api_key = needs_api_key ? asc_api_key : nil
sync_ios_versioning!
version_metadata = read_ios_version_metadata
version = version_metadata[:version]
short_version = version_metadata[:short_version]
build_number = resolve_beta_build_number(api_key: api_key, short_version: short_version)
beta_xcconfig = prepare_beta_release!(version: version, build_number: build_number)
{
api_key: api_key,
beta_xcconfig: beta_xcconfig,
build_number: build_number,
short_version: short_version,
version: version
}
end
desc "Build a beta archive locally without uploading"
lane :beta_archive do
context = prepare_beta_context(require_api_key: false)
build = build_beta_release(context)
UI.success("Built iOS beta archive: version=#{build[:version]} short=#{build[:short_version]} build=#{build[:build_number]}")
build
ensure
ENV.delete("XCODE_XCCONFIG_FILE")
end
desc "Build + upload a beta to TestFlight"
lane :beta do
context = prepare_beta_context(require_api_key: true)
build = build_beta_release(context)
upload_to_testflight(
api_key: context[:api_key],
ipa: build[:ipa_path],
skip_waiting_for_build_processing: true,
uses_non_exempt_encryption: false
)
UI.success("Uploaded iOS beta: version=#{build[:version]} short=#{build[:short_version]} build=#{build[:build_number]}")
ensure
ENV.delete("XCODE_XCCONFIG_FILE")
end
desc "Upload App Store metadata (and optionally screenshots)"
lane :metadata do
sync_ios_versioning!
api_key = asc_api_key
clear_empty_env_var("APP_STORE_CONNECT_API_KEY_PATH")
app_identifier = ENV["ASC_APP_IDENTIFIER"]
app_id = ENV["ASC_APP_ID"]
app_identifier = nil unless env_present?(app_identifier)
app_id = nil unless env_present?(app_id)
deliver_options = {
api_key: api_key,
force: true,
skip_screenshots: ENV["DELIVER_SCREENSHOTS"] != "1",
skip_metadata: ENV["DELIVER_METADATA"] != "1",
run_precheck_before_submit: false
}
deliver_options[:app_identifier] = app_identifier if app_identifier
if app_id && app_identifier.nil?
# `deliver` prefers app_identifier from Appfile unless explicitly blanked.
deliver_options[:app_identifier] = ""
deliver_options[:app] = app_id
end
deliver(**deliver_options)
end
desc "Validate App Store Connect API auth"
lane :auth_check do
asc_api_key
UI.success("App Store Connect API auth loaded successfully.")
end
end

View file

@ -0,0 +1,149 @@
# fastlane setup (OpenClaw iOS)
Install:
```bash
brew install fastlane
```
Create an App Store Connect API key:
- App Store Connect → Users and Access → Keys → App Store Connect API → Generate API Key
- Download the `.p8`, note the **Issuer ID** and **Key ID**
Recommended (macOS): store the private key in Keychain and write non-secret vars:
```bash
scripts/ios-asc-keychain-setup.sh \
--key-path /absolute/path/to/AuthKey_XXXXXXXXXX.p8 \
--issuer-id YOUR_ISSUER_ID \
--write-env
```
This writes these auth variables in `apps/ios/fastlane/.env`:
```bash
ASC_KEY_ID=YOUR_KEY_ID
ASC_ISSUER_ID=YOUR_ISSUER_ID
ASC_KEYCHAIN_SERVICE=openclaw-asc-key
ASC_KEYCHAIN_ACCOUNT=YOUR_MAC_USERNAME
```
Important: `apps/ios/fastlane/.env` is only for Fastlane/App Store Connect auth and optional beta-archive settings. It does **not** configure gateway-side direct APNs push delivery for local iOS builds.
Optional app targeting variables (helpful if Fastlane cannot auto-resolve app by bundle):
```bash
ASC_APP_IDENTIFIER=ai.openclaw.client
# or
ASC_APP_ID=YOUR_APP_STORE_CONNECT_APP_ID
```
File-based fallback (CI/non-macOS):
```bash
ASC_KEY_ID=YOUR_KEY_ID
ASC_ISSUER_ID=YOUR_ISSUER_ID
ASC_KEY_PATH=/absolute/path/to/AuthKey_XXXXXXXXXX.p8
```
Code signing variable (optional in `.env`):
```bash
IOS_DEVELOPMENT_TEAM=YOUR_TEAM_ID
```
Tip: run `scripts/ios-team-id.sh` from repo root to print a Team ID for `.env`. The helper prefers the canonical OpenClaw team (`Y5PE65HELJ`) when present locally; otherwise it prefers the first non-personal team from your Xcode account (then personal team if needed). Fastlane uses this helper automatically if `IOS_DEVELOPMENT_TEAM` is missing.
For local/manual iOS builds that stay on direct APNs, configure the gateway host separately with `OPENCLAW_APNS_TEAM_ID`, `OPENCLAW_APNS_KEY_ID`, and either `OPENCLAW_APNS_PRIVATE_KEY_P8` or `OPENCLAW_APNS_PRIVATE_KEY_PATH`. Those gateway runtime env vars are separate from Fastlane's `.env`.
Validate auth:
```bash
cd apps/ios
fastlane ios auth_check
```
ASC auth is only required when:
- uploading to TestFlight
- auto-resolving the next build number from App Store Connect
If you pass `--build-number` to `pnpm ios:beta:archive`, the local archive path does not need ASC auth.
Archive locally without upload:
```bash
pnpm ios:beta:archive
```
Upload to TestFlight:
```bash
pnpm ios:beta
```
Direct Fastlane entry point:
```bash
cd apps/ios
fastlane ios beta
```
Maintainer recovery path for a fresh clone on the same Mac:
1. Reuse the existing Keychain-backed ASC key on that machine.
2. Restore or recreate `apps/ios/fastlane/.env` so it contains the non-secret variables:
```bash
ASC_KEY_ID=YOUR_KEY_ID
ASC_ISSUER_ID=YOUR_ISSUER_ID
ASC_KEYCHAIN_SERVICE=openclaw-asc-key
ASC_KEYCHAIN_ACCOUNT=YOUR_MAC_USERNAME
```
3. Re-run auth validation:
```bash
cd apps/ios
fastlane ios auth_check
```
4. If you are starting a brand-new production release train, pin iOS to the current gateway version:
```bash
pnpm ios:version:pin -- --from-gateway
```
5. Set the official/TestFlight relay URL before release:
```bash
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
```
6. Upload:
```bash
pnpm ios:beta
```
Quick verification after upload:
- confirm `apps/ios/build/beta/OpenClaw-<version>.ipa` exists
- confirm Fastlane prints `Uploaded iOS beta: version=<version> short=<short> build=<build>`
- remember that TestFlight processing can take a few minutes after the upload succeeds
Versioning rules:
- `apps/ios/version.json` is the pinned iOS release version source
- `apps/ios/CHANGELOG.md` is the iOS-only changelog and release-note source
- Supported pinned iOS versions use CalVer: `YYYY.M.D`
- `pnpm ios:version:pin -- --from-gateway` promotes the current root gateway version into the pinned iOS release version
- Fastlane uses the pinned iOS version only; changing `package.json.version` alone does not change the iOS app version
- Fastlane sets `CFBundleShortVersionString` to the pinned iOS version, for example `2026.4.10`
- Fastlane resolves `CFBundleVersion` as the next integer TestFlight build number for that short version
- Run `pnpm ios:version:sync` after changing `apps/ios/version.json` or `apps/ios/CHANGELOG.md`
- `pnpm ios:version:check` validates that checked-in iOS version artifacts are in sync
- The beta flow regenerates `apps/ios/OpenClaw.xcodeproj` from `apps/ios/project.yml` before archiving
- Local beta signing uses a temporary generated xcconfig and leaves local development signing overrides untouched
- See `apps/ios/VERSIONING.md` for the detailed workflow

View file

@ -0,0 +1,50 @@
# App Store metadata (Fastlane deliver)
This directory is used by `fastlane deliver` for App Store Connect text metadata.
## Upload metadata only
```bash
cd apps/ios
ASC_APP_ID=YOUR_APP_STORE_CONNECT_APP_ID \
DELIVER_METADATA=1 fastlane ios metadata
```
## Optional: include screenshots
```bash
cd apps/ios
DELIVER_METADATA=1 DELIVER_SCREENSHOTS=1 fastlane ios metadata
```
## Auth
The `ios metadata` lane uses App Store Connect API key auth from `apps/ios/fastlane/.env`:
- Keychain-backed (recommended on macOS):
- `ASC_KEY_ID`
- `ASC_ISSUER_ID`
- `ASC_KEYCHAIN_SERVICE` (default: `openclaw-asc-key`)
- `ASC_KEYCHAIN_ACCOUNT` (default: current user)
- File/path fallback:
- `ASC_KEY_ID`
- `ASC_ISSUER_ID`
- `ASC_KEY_PATH`
Or set `APP_STORE_CONNECT_API_KEY_PATH`.
## Notes
- Locale files live under `metadata/en-US/`.
- `release_notes.txt` is generated from `apps/ios/CHANGELOG.md`; after changelog updates, run `pnpm ios:version:sync`.
- Release notes resolve from `## <pinned iOS version>` first, then fall back to `## Unreleased` while a TestFlight train is still in progress.
- When starting a new production release train, pin the iOS version first with `pnpm ios:version:pin -- --from-gateway`.
- `privacy_url.txt` is set to `https://openclaw.ai/privacy`.
- If app lookup fails in `deliver`, set one of:
- `ASC_APP_IDENTIFIER` (bundle ID)
- `ASC_APP_ID` (numeric App Store Connect app ID, e.g. from `/apps/<id>/...` URL)
- For first app versions, include review contact files under `metadata/review_information/`:
- `first_name.txt`
- `last_name.txt`
- `email_address.txt`
- `phone_number.txt` (E.164-ish, e.g. `+1 415 555 0100`)

View file

@ -0,0 +1,18 @@
OpenClaw is a personal AI assistant you run on your own devices.
Pair this iPhone app with your OpenClaw Gateway to connect your phone as a secure node for voice, camera, and device automation.
What you can do:
- Chat with your assistant from iPhone
- Use voice wake and push-to-talk
- Capture photos and short clips on request
- Record screen snippets for troubleshooting and workflows
- Share text, links, and media directly from iOS into OpenClaw
- Run location-aware and device-aware automations
OpenClaw is local-first: you control your gateway, keys, and configuration.
Getting started:
1) Set up your OpenClaw Gateway
2) Open the iOS app and pair with your gateway
3) Start using commands and automations from your phone

View file

@ -0,0 +1 @@
openclaw,ai assistant,local ai,voice assistant,automation,gateway,chat,agent,node

View file

@ -0,0 +1 @@
https://openclaw.ai

View file

@ -0,0 +1 @@
OpenClaw - iOS Client

View file

@ -0,0 +1 @@
https://openclaw.ai/privacy

View file

@ -0,0 +1 @@
Run OpenClaw from your iPhone: pair with your own gateway, trigger automations, and use voice, camera, and share actions.

View file

@ -0,0 +1 @@
Maintenance update for the current OpenClaw release.

View file

@ -0,0 +1 @@
Personal AI on your devices

View file

@ -0,0 +1 @@
https://docs.openclaw.ai/platforms/ios

View file

@ -0,0 +1 @@
support@openclaw.ai

View file

@ -0,0 +1 @@
OpenClaw

View file

@ -0,0 +1 @@
OpenClaw iOS client for gateway-connected workflows. Reviewers can follow the standard onboarding and pairing flow in-app.

View file

@ -0,0 +1 @@
+1 415 555 0100