diff --git a/.agents/skills/PR_WORKFLOW.md b/.agents/skills/PR_WORKFLOW.md index 4030650735594..8696ba1b69e9d 100644 --- a/.agents/skills/PR_WORKFLOW.md +++ b/.agents/skills/PR_WORKFLOW.md @@ -3,10 +3,6 @@ Please read this in full and do not skip sections. This is the single source of truth for the maintainer PR workflow. -## Triage order - -Process PRs **oldest to newest**. Older PRs are more likely to have merge conflicts and stale dependencies; resolving them first keeps the queue healthy and avoids snowballing rebase pain. - ## Working rule Skills execute workflow. Maintainers provide judgment. @@ -110,7 +106,7 @@ Before any substantive review or prep work, **always rebase the PR branch onto c - During `prepare-pr`, use concise, action-oriented subjects **without** PR numbers or thanks; reserve `(#) thanks @` for the final merge/squash commit. - Group related changes; avoid bundling unrelated refactors. - Changelog workflow: keep the latest released version at the top (no `Unreleased`); after publishing, bump the version and start a new top section. -- When working on a PR: add a changelog entry with the PR number and thank the contributor (mandatory in this workflow). +- When working on a PR: add a changelog entry line with the PR number `(#)` and `thanks @` when author metadata is available (mandatory in this workflow). - When working on an issue: reference the issue in the changelog entry. - In this workflow, changelog is always required even for internal/test-only changes. diff --git a/.agents/skills/merge-pr/SKILL.md b/.agents/skills/merge-pr/SKILL.md index 041e79a676836..91b7c4aa4e2e5 100644 --- a/.agents/skills/merge-pr/SKILL.md +++ b/.agents/skills/merge-pr/SKILL.md @@ -41,11 +41,12 @@ scripts/pr-merge scripts/pr-merge run ``` -3. Ensure output reports: +3. Capture and report these values in a human-readable summary (not raw `key=value` lines): -- `merge_sha=` -- `merge_author_email=` -- `comment_url=` +- Merge commit SHA +- Merge author email +- Merge completion comment URL +- PR URL ## Steps @@ -97,3 +98,4 @@ Cleanup is handled by `run` after merge success. - End in `MERGED`, never `CLOSED`. - Cleanup only after confirmed merge. +- In final chat output, use labeled lines or bullets; do not paste raw wrapper diagnostics unless debugging. diff --git a/.agents/skills/prepare-pr/SKILL.md b/.agents/skills/prepare-pr/SKILL.md index 462e5bc2bd480..d2834a84b4a15 100644 --- a/.agents/skills/prepare-pr/SKILL.md +++ b/.agents/skills/prepare-pr/SKILL.md @@ -74,6 +74,11 @@ jq -r '.changelog' .local/review.json jq -r '.docs' .local/review.json ``` +Changelog gate requirement: + +- `CHANGELOG.md` must include a newly added changelog entry line. +- When PR author metadata is available, that same changelog entry line must include `(#) thanks @`. + 4. Commit scoped changes Use concise, action-oriented subject lines without PR numbers/thanks. The final merge/squash commit is the only place we include PR numbers and contributor thanks. diff --git a/.env.example b/.env.example index 8bc4defd4290d..41df435b8f9af 100644 --- a/.env.example +++ b/.env.example @@ -37,6 +37,16 @@ OPENCLAW_GATEWAY_TOKEN=change-me-to-a-long-random-token # ANTHROPIC_API_KEY=sk-ant-... # GEMINI_API_KEY=... # OPENROUTER_API_KEY=sk-or-... +# OPENCLAW_LIVE_OPENAI_KEY=sk-... +# OPENCLAW_LIVE_ANTHROPIC_KEY=sk-ant-... +# OPENCLAW_LIVE_GEMINI_KEY=... +# OPENAI_API_KEY_1=... +# ANTHROPIC_API_KEY_1=... +# GEMINI_API_KEY_1=... +# GOOGLE_API_KEY=... +# OPENAI_API_KEYS=sk-1,sk-2 +# ANTHROPIC_API_KEYS=sk-ant-1,sk-ant-2 +# GEMINI_API_KEYS=key-1,key-2 # Optional additional providers # ZAI_API_KEY=... diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index e660d2a97612a..f02fbddb3e85f 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -4,8 +4,11 @@ self-hosted-runner: labels: # Blacksmith CI runners - - blacksmith-4vcpu-ubuntu-2404 - - blacksmith-4vcpu-windows-2025 + - blacksmith-8vcpu-ubuntu-2404 + - blacksmith-8vcpu-windows-2025 + - blacksmith-16vcpu-ubuntu-2404 + - blacksmith-16vcpu-windows-2025 + - blacksmith-16vcpu-ubuntu-2404-arm # Ignore patterns for known issues paths: @@ -15,3 +18,5 @@ paths: - "shellcheck reported issue.+" # Ignore intentional if: false for disabled jobs - 'constant expression "false" in condition' + # actionlint's built-in runner label allowlist lags Blacksmith additions. + - 'label "blacksmith-16vcpu-[^"]+" is unknown\.' diff --git a/.github/actions/setup-node-env/action.yml b/.github/actions/setup-node-env/action.yml index 5fa4f6728bc1c..334cd3c24fb94 100644 --- a/.github/actions/setup-node-env/action.yml +++ b/.github/actions/setup-node-env/action.yml @@ -37,7 +37,7 @@ runs: exit 1 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ inputs.node-version }} check-latest: true @@ -52,7 +52,7 @@ runs: if: inputs.install-bun == 'true' uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: "1.3.9+cf6cdbbba" - name: Runtime versions shell: bash @@ -70,14 +70,29 @@ runs: shell: bash env: CI: "true" + FROZEN_LOCKFILE: ${{ inputs.frozen-lockfile }} run: | + set -euo pipefail export PATH="$NODE_BIN:$PATH" which node node -v pnpm -v - LOCKFILE_FLAG="" - if [ "${{ inputs.frozen-lockfile }}" = "true" ]; then - LOCKFILE_FLAG="--frozen-lockfile" + case "$FROZEN_LOCKFILE" in + true) LOCKFILE_FLAG="--frozen-lockfile" ;; + false) LOCKFILE_FLAG="" ;; + *) + echo "::error::Invalid frozen-lockfile input: '$FROZEN_LOCKFILE' (expected true or false)" + exit 2 + ;; + esac + + install_args=( + install + --ignore-scripts=false + --config.engine-strict=false + --config.enable-pre-post-scripts=true + ) + if [ -n "$LOCKFILE_FLAG" ]; then + install_args+=("$LOCKFILE_FLAG") fi - pnpm install $LOCKFILE_FLAG --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || \ - pnpm install $LOCKFILE_FLAG --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true + pnpm "${install_args[@]}" || pnpm "${install_args[@]}" diff --git a/.github/actions/setup-pnpm-store-cache/action.yml b/.github/actions/setup-pnpm-store-cache/action.yml index c866393ee430b..8e25492ac9229 100644 --- a/.github/actions/setup-pnpm-store-cache/action.yml +++ b/.github/actions/setup-pnpm-store-cache/action.yml @@ -14,11 +14,17 @@ runs: steps: - name: Setup pnpm (corepack retry) shell: bash + env: + PNPM_VERSION: ${{ inputs.pnpm-version }} run: | set -euo pipefail + if [[ ! "$PNPM_VERSION" =~ ^[0-9]+(\.[0-9]+){1,2}([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "::error::Invalid pnpm-version input: '$PNPM_VERSION'" + exit 2 + fi corepack enable for attempt in 1 2 3; do - if corepack prepare "pnpm@${{ inputs.pnpm-version }}" --activate; then + if corepack prepare "pnpm@$PNPM_VERSION" --activate; then pnpm -v exit 0 fi diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml index e3987c500c3e7..aae5788325035 100644 --- a/.github/workflows/auto-response.yml +++ b/.github/workflows/auto-response.yml @@ -13,7 +13,7 @@ jobs: permissions: issues: write pull-requests: write - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token @@ -48,7 +48,7 @@ jobs: label: "r: third-party-extension", close: true, message: - "This would be better made as a third-party extension with our SDK that you maintain yourself. Docs: https://docs.openclaw.ai/plugin.", + "Please make this as a third-party plugin that you maintain yourself in your own repo. Docs: https://docs.openclaw.ai/plugin. Feel free to open a PR after to add it to our community plugins page: https://docs.openclaw.ai/plugins/community", }, { label: "r: moltbook", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f69c7ae269814..60e42dd572d1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,14 +6,14 @@ on: pull_request: concurrency: - group: ci-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: # Detect docs-only changes to skip heavy jobs (test, build, Windows, macOS, Android). # Lint and format always run. Fail-safe: if detection fails, run everything. docs-scope: - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 outputs: docs_only: ${{ steps.check.outputs.docs_only }} docs_changed: ${{ steps.check.outputs.docs_changed }} @@ -33,7 +33,7 @@ jobs: changed-scope: needs: [docs-scope] if: needs.docs-scope.outputs.docs_only != 'true' - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 outputs: run_node: ${{ steps.scope.outputs.run_node }} run_macos: ${{ steps.scope.outputs.run_macos }} @@ -127,7 +127,7 @@ jobs: build-artifacts: needs: [docs-scope, changed-scope, check] if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout uses: actions/checkout@v4 @@ -153,7 +153,7 @@ jobs: release-check: needs: [docs-scope, build-artifacts] if: github.event_name == 'push' && needs.docs-scope.outputs.docs_only != 'true' - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout uses: actions/checkout@v4 @@ -177,7 +177,7 @@ jobs: checks: needs: [docs-scope, changed-scope, check] if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: blacksmith-16vcpu-ubuntu-2404 strategy: fail-fast: false matrix: @@ -192,29 +192,46 @@ jobs: task: test command: pnpm canvas:a2ui:bundle && bunx vitest run --config vitest.unit.config.ts steps: + - name: Skip bun lane on push + if: github.event_name == 'push' && matrix.runtime == 'bun' + run: echo "Skipping bun test lane on push events." + - name: Checkout + if: github.event_name != 'push' || matrix.runtime != 'bun' uses: actions/checkout@v4 with: submodules: false - name: Setup Node environment + if: matrix.runtime != 'bun' || github.event_name != 'push' uses: ./.github/actions/setup-node-env + with: + install-bun: "${{ matrix.runtime == 'bun' }}" - name: Configure vitest JSON reports - if: matrix.task == 'test' && matrix.runtime == 'node' + if: (github.event_name != 'push' || matrix.runtime != 'bun') && matrix.task == 'test' && matrix.runtime == 'node' run: echo "OPENCLAW_VITEST_REPORT_DIR=$RUNNER_TEMP/vitest-reports" >> "$GITHUB_ENV" + - name: Configure Node test resources + if: (github.event_name != 'push' || matrix.runtime != 'bun') && matrix.task == 'test' && matrix.runtime == 'node' + run: | + # `pnpm test` runs `scripts/test-parallel.mjs`, which spawns multiple Node processes. + # Default heap limits have been too low on Linux CI (V8 OOM near 4GB). + echo "OPENCLAW_TEST_WORKERS=2" >> "$GITHUB_ENV" + echo "OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144" >> "$GITHUB_ENV" + - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) + if: matrix.runtime != 'bun' || github.event_name != 'push' run: ${{ matrix.command }} - name: Summarize slowest tests - if: matrix.task == 'test' && matrix.runtime == 'node' + if: (github.event_name != 'push' || matrix.runtime != 'bun') && matrix.task == 'test' && matrix.runtime == 'node' run: | node scripts/vitest-slowest.mjs --dir "$OPENCLAW_VITEST_REPORT_DIR" --top 50 --out "$RUNNER_TEMP/vitest-slowest.md" > /dev/null echo "Slowest test summary written to $RUNNER_TEMP/vitest-slowest.md" - name: Upload vitest reports - if: matrix.task == 'test' && matrix.runtime == 'node' + if: (github.event_name != 'push' || matrix.runtime != 'bun') && matrix.task == 'test' && matrix.runtime == 'node' uses: actions/upload-artifact@v4 with: name: vitest-reports-${{ runner.os }}-${{ matrix.runtime }} @@ -227,7 +244,7 @@ jobs: name: "check" needs: [docs-scope] if: needs.docs-scope.outputs.docs_only != 'true' - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout uses: actions/checkout@v4 @@ -236,6 +253,8 @@ jobs: - name: Setup Node environment uses: ./.github/actions/setup-node-env + with: + install-bun: "false" - name: Check types and lint and oxfmt run: pnpm check @@ -244,7 +263,7 @@ jobs: check-docs: needs: [docs-scope] if: needs.docs-scope.outputs.docs_changed == 'true' - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout uses: actions/checkout@v4 @@ -253,12 +272,14 @@ jobs: - name: Setup Node environment uses: ./.github/actions/setup-node-env + with: + install-bun: "false" - name: Check docs run: pnpm check:docs secrets: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout uses: actions/checkout@v4 @@ -285,10 +306,10 @@ jobs: checks-windows: needs: [docs-scope, changed-scope, build-artifacts, check] if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') - runs-on: blacksmith-4vcpu-windows-2025 + runs-on: blacksmith-16vcpu-windows-2025 env: NODE_OPTIONS: --max-old-space-size=4096 - # Keep total concurrency predictable on the 4 vCPU runner: + # Keep total concurrency predictable on the 16 vCPU runner: # `scripts/test-parallel.mjs` runs some vitest suites in parallel processes. OPENCLAW_TEST_WORKERS: 2 defaults: @@ -347,7 +368,7 @@ jobs: test -s dist/plugin-sdk/index.js - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22.x check-latest: true @@ -358,16 +379,10 @@ jobs: pnpm-version: "10.23.0" cache-key-suffix: "node22" - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Runtime versions run: | node -v npm -v - bun -v pnpm -v - name: Capture node path @@ -645,7 +660,7 @@ jobs: android: needs: [docs-scope, changed-scope, check] if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_android == 'true') - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: blacksmith-16vcpu-ubuntu-2404 strategy: fail-fast: false matrix: @@ -664,7 +679,8 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: 21 + # setup-android's sdkmanager currently crashes on JDK 21 in CI. + java-version: 17 - name: Setup Android SDK uses: android-actions/setup-android@v3 diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index a286026ae3268..fc0d97d409139 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -13,6 +13,10 @@ on: - ".agents/**" - "skills/**" +concurrency: + group: docker-release-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} @@ -20,13 +24,12 @@ env: jobs: # Build amd64 image build-amd64: - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 permissions: packages: write contents: read outputs: image-digest: ${{ steps.build.outputs.digest }} - image-metadata: ${{ steps.meta.outputs.json }} steps: - name: Checkout uses: actions/checkout@v4 @@ -41,18 +44,30 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{version}},suffix=-amd64 - type=semver,pattern={{version}},suffix=-arm64 - type=ref,event=branch,suffix=-amd64 - type=ref,event=branch,suffix=-arm64 + - name: Resolve image tags (amd64) + id: tags + shell: bash + env: + IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + run: | + set -euo pipefail + tags=() + if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then + tags+=("${IMAGE}:main-amd64") + fi + if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then + version="${GITHUB_REF#refs/tags/v}" + tags+=("${IMAGE}:${version}-amd64") + fi + if [[ ${#tags[@]} -eq 0 ]]; then + echo "::error::No amd64 tags resolved for ref ${GITHUB_REF}" + exit 1 + fi + { + echo "value<> "$GITHUB_OUTPUT" - name: Build and push amd64 image id: build @@ -60,8 +75,7 @@ jobs: with: context: . platforms: linux/amd64 - labels: ${{ steps.meta.outputs.labels }} - tags: ${{ steps.meta.outputs.tags }} + tags: ${{ steps.tags.outputs.value }} cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:amd64 cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:amd64,mode=max provenance: false @@ -69,13 +83,12 @@ jobs: # Build arm64 image build-arm64: - runs-on: ubuntu-24.04-arm + runs-on: blacksmith-16vcpu-ubuntu-2404-arm permissions: packages: write contents: read outputs: image-digest: ${{ steps.build.outputs.digest }} - image-metadata: ${{ steps.meta.outputs.json }} steps: - name: Checkout uses: actions/checkout@v4 @@ -90,18 +103,30 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{version}},suffix=-amd64 - type=semver,pattern={{version}},suffix=-arm64 - type=ref,event=branch,suffix=-amd64 - type=ref,event=branch,suffix=-arm64 + - name: Resolve image tags (arm64) + id: tags + shell: bash + env: + IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + run: | + set -euo pipefail + tags=() + if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then + tags+=("${IMAGE}:main-arm64") + fi + if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then + version="${GITHUB_REF#refs/tags/v}" + tags+=("${IMAGE}:${version}-arm64") + fi + if [[ ${#tags[@]} -eq 0 ]]; then + echo "::error::No arm64 tags resolved for ref ${GITHUB_REF}" + exit 1 + fi + { + echo "value<> "$GITHUB_OUTPUT" - name: Build and push arm64 image id: build @@ -109,8 +134,7 @@ jobs: with: context: . platforms: linux/arm64 - labels: ${{ steps.meta.outputs.labels }} - tags: ${{ steps.meta.outputs.tags }} + tags: ${{ steps.tags.outputs.value }} cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:arm64 cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:arm64,mode=max provenance: false @@ -118,12 +142,15 @@ jobs: # Create multi-platform manifest create-manifest: - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 permissions: packages: write contents: read needs: [build-amd64, build-arm64] steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: @@ -131,19 +158,41 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata for manifest - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=semver,pattern={{version}} + - name: Resolve manifest tags + id: tags + shell: bash + env: + IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + run: | + set -euo pipefail + tags=() + if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then + tags+=("${IMAGE}:main") + fi + if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then + version="${GITHUB_REF#refs/tags/v}" + tags+=("${IMAGE}:${version}") + fi + if [[ ${#tags[@]} -eq 0 ]]; then + echo "::error::No manifest tags resolved for ref ${GITHUB_REF}" + exit 1 + fi + { + echo "value<> "$GITHUB_OUTPUT" - name: Create and push manifest + shell: bash run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + set -euo pipefail + mapfile -t tags <<< "${{ steps.tags.outputs.value }}" + args=() + for tag in "${tags[@]}"; do + [ -z "$tag" ] && continue + args+=("-t" "$tag") + done + docker buildx imagetools create "${args[@]}" \ ${{ needs.build-amd64.outputs.image-digest }} \ ${{ needs.build-arm64.outputs.image-digest }} - env: - DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} diff --git a/.github/workflows/formal-conformance.yml b/.github/workflows/formal-conformance.yml deleted file mode 100644 index a8ec86bfce7d1..0000000000000 --- a/.github/workflows/formal-conformance.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: Formal models (informational conformance) - -on: - pull_request: - -concurrency: - group: formal-conformance-${{ github.event.pull_request.number || github.ref_name }} - cancel-in-progress: true - -jobs: - formal_conformance: - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - pull-requests: write - - steps: - - name: Checkout openclaw (PR) - uses: actions/checkout@v4 - with: - path: openclaw - - - name: Checkout formal models - uses: actions/checkout@v4 - with: - repository: vignesh07/clawdbot-formal-models - ref: main - path: clawdbot-formal-models - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "22" - - - name: Regenerate extracted constants from openclaw - run: | - set -euo pipefail - cd clawdbot-formal-models - export OPENCLAW_REPO_DIR="${GITHUB_WORKSPACE}/openclaw" - node scripts/extract-tool-groups.mjs - node scripts/check-tool-group-alias.mjs - - # Drift is about extracted artifacts only; compute it before model checking - # to avoid any incidental file touches affecting the result. - - name: Compute drift (generated/*) - id: drift - run: | - set -euo pipefail - cd clawdbot-formal-models - - if git diff --quiet -- generated; then - echo "drift=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - echo "drift=true" >> "$GITHUB_OUTPUT" - git diff -- generated > "${GITHUB_WORKSPACE}/formal-models-drift.diff" - - - name: Model check (green suite) - run: | - set -euo pipefail - cd clawdbot-formal-models - make \ - precedence groups elevated nodes-policy \ - attacker approvals approvals-token nodes-pipeline \ - gateway-exposure gateway-exposure-v2 gateway-exposure-v2-protected \ - gateway-auth-conformance gateway-auth-tailscale gateway-auth-proxy \ - pairing pairing-cap pairing-idempotency pairing-refresh pairing-refresh-race \ - ingress-gating ingress-idempotency ingress-dedupe-fallback ingress-trace ingress-trace2 \ - routing-isolation routing-precedence routing-identitylinks routing-identity-transitive routing-identity-symmetry routing-identity-channel-override \ - routing-thread-parent discord-pluralkit \ - ingress-retry session-key-stability session-explosion-bound config-normalization \ - queue-drain delivery-route-stability delivery-pipeline retry-termination retry-eventual-success \ - no-cross-stream multi-event-eventual-emission \ - dedupe-collision-fallback crash-restart-dedupe two-worker-dedupe openclaw-session-key-conformance \ - routing-thread-parent-channel-override routing-trirule gateway-auth-proxy-header-spoof \ - group-alias-check - - - name: Model check (negative suite, expected violations) - continue-on-error: true - run: | - set -euo pipefail - cd clawdbot-formal-models - make -k \ - precedence-negative groups-negative elevated-negative nodes-policy-negative \ - attacker-negative attacker-nodes-negative attacker-nodes-allowlist-negative attacker-nodes-allowlist-negative \ - approvals-negative approvals-token-negative nodes-pipeline-negative \ - gateway-exposure-negative gateway-exposure-v2-negative gateway-exposure-v2-protected-negative \ - gateway-exposure-v2-unsafe-custom gateway-exposure-v2-unsafe-tailnet gateway-exposure-v2-unsafe-auto \ - gateway-auth-conformance-negative gateway-auth-tailscale-negative gateway-auth-proxy-negative \ - pairing-negative pairing-cap-negative pairing-idempotency-negative pairing-refresh-negative pairing-refresh-race-negative \ - ingress-gating-negative ingress-idempotency-negative ingress-dedupe-fallback-negative ingress-trace-negative ingress-trace2-negative \ - routing-isolation-negative routing-precedence-negative routing-identitylinks-negative routing-identity-transitive-negative routing-identity-symmetry-negative routing-identity-channel-override-negative \ - routing-thread-parent-negative discord-pluralkit-negative \ - ingress-retry-negative session-key-stability-negative config-normalization-negative \ - queue-drain delivery-route-stability-negative delivery-pipeline-negative retry-termination-negative retry-eventual-success-negative \ - no-cross-stream-negative multi-event-eventual-emission-negative \ - dedupe-collision-fallback-negative crash-restart-dedupe-negative two-worker-dedupe-negative openclaw-session-key-conformance-negative \ - routing-thread-parent-channel-override-negative routing-trirule-negative gateway-auth-proxy-header-spoof-negative - - - name: Upload drift diff artifact - if: steps.drift.outputs.drift == 'true' - uses: actions/upload-artifact@v4 - with: - name: formal-models-conformance-drift - path: formal-models-drift.diff - - - name: Comment on PR (informational) - if: steps.drift.outputs.drift == 'true' - uses: actions/github-script@v7 - with: - script: | - const body = [ - '⚠️ **Formal models conformance drift detected**', - '', - 'The formal models extracted constants (`generated/*`) do not match this openclaw PR.', - '', - 'This check is **informational** (not blocking merges yet).', - 'See the `formal-models-conformance-drift` artifact for the diff.', - '', - 'If this change is intentional, follow up by updating the formal models repo or regenerating the extracted artifacts there.', - ].join('\n'); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - body, - }); - - - name: Summary - run: | - if [ "${{ steps.drift.outputs.drift }}" = "true" ]; then - echo "Formal conformance drift detected (informational)." - else - echo "Formal conformance: no drift." - fi diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index e6c0914f0181c..03e87db82b9e5 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -7,12 +7,12 @@ on: workflow_dispatch: concurrency: - group: install-smoke-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true + group: install-smoke-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: docs-scope: - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 outputs: docs_only: ${{ steps.check.outputs.docs_only }} steps: @@ -28,24 +28,22 @@ jobs: install-smoke: needs: [docs-scope] if: needs.docs-scope.outputs.docs_only != 'true' - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout CLI uses: actions/checkout@v4 - - name: Setup pnpm (corepack retry) - run: | - set -euo pipefail - corepack enable - for attempt in 1 2 3; do - if corepack prepare pnpm@10.23.0 --activate; then - pnpm -v - exit 0 - fi - echo "corepack prepare failed (attempt $attempt/3). Retrying..." - sleep $((attempt * 10)) - done - exit 1 + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22.x + check-latest: true + + - name: Setup pnpm + cache store + uses: ./.github/actions/setup-pnpm-store-cache + with: + pnpm-version: "10.23.0" + cache-key-suffix: "node22" - name: Install pnpm deps (minimal) run: pnpm install --ignore-scripts --frozen-lockfile diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 2bae5a6116023..9ac44dfa6b671 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -23,7 +23,7 @@ jobs: permissions: contents: read pull-requests: write - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token @@ -200,7 +200,7 @@ jobs: permissions: contents: read pull-requests: write - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token @@ -440,7 +440,7 @@ jobs: label-issues: permissions: issues: write - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token diff --git a/.github/workflows/sandbox-common-smoke.yml b/.github/workflows/sandbox-common-smoke.yml new file mode 100644 index 0000000000000..26c0dcc106f8e --- /dev/null +++ b/.github/workflows/sandbox-common-smoke.yml @@ -0,0 +1,56 @@ +name: Sandbox Common Smoke + +on: + push: + branches: [main] + paths: + - Dockerfile.sandbox + - Dockerfile.sandbox-common + - scripts/sandbox-common-setup.sh + pull_request: + paths: + - Dockerfile.sandbox + - Dockerfile.sandbox-common + - scripts/sandbox-common-setup.sh + +concurrency: + group: sandbox-common-smoke-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + sandbox-common-smoke: + runs-on: blacksmith-16vcpu-ubuntu-2404 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: false + + - name: Build minimal sandbox base (USER sandbox) + shell: bash + run: | + set -euo pipefail + + docker build -t openclaw-sandbox-smoke-base:bookworm-slim - <<'EOF' + FROM debian:bookworm-slim + RUN useradd --create-home --shell /bin/bash sandbox + USER sandbox + WORKDIR /home/sandbox + EOF + + - name: Build sandbox-common image (root for installs, sandbox at runtime) + shell: bash + run: | + set -euo pipefail + + BASE_IMAGE="openclaw-sandbox-smoke-base:bookworm-slim" \ + TARGET_IMAGE="openclaw-sandbox-common-smoke:bookworm-slim" \ + PACKAGES="ca-certificates" \ + INSTALL_PNPM=0 \ + INSTALL_BUN=0 \ + INSTALL_BREW=0 \ + FINAL_USER=sandbox \ + scripts/sandbox-common-setup.sh + + u="$(docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc 'id -un')" + test "$u" = "sandbox" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ccafcf01a184c..6248a93dce774 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: permissions: issues: write pull-requests: write - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token @@ -31,7 +31,7 @@ jobs: stale-pr-label: stale exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale exempt-pr-labels: maintainer,no-stale - operations-per-run: 500 + operations-per-run: 10000 exempt-all-assignees: true remove-stale-when-updated: true stale-issue-message: | diff --git a/.github/workflows/workflow-sanity.yml b/.github/workflows/workflow-sanity.yml index 14fe6ae429f21..19668e697ad28 100644 --- a/.github/workflows/workflow-sanity.yml +++ b/.github/workflows/workflow-sanity.yml @@ -6,12 +6,12 @@ on: branches: [main] concurrency: - group: workflow-sanity-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true + group: workflow-sanity-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: no-tabs: - runs-on: ubuntu-latest + runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout uses: actions/checkout@v4 @@ -40,3 +40,28 @@ jobs: print(f"- {path}") sys.exit(1) PY + + actionlint: + runs-on: blacksmith-16vcpu-ubuntu-2404 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install actionlint + shell: bash + run: | + set -euo pipefail + ACTIONLINT_VERSION="1.7.11" + archive="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" + base_url="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}" + curl -sSfL -o "${archive}" "${base_url}/${archive}" + curl -sSfL -o checksums.txt "${base_url}/actionlint_${ACTIONLINT_VERSION}_checksums.txt" + grep " ${archive}\$" checksums.txt | sha256sum -c - + tar -xzf "${archive}" actionlint + sudo install -m 0755 actionlint /usr/local/bin/actionlint + + - name: Lint workflows + run: actionlint + + - name: Disallow direct inputs interpolation in composite run blocks + run: python3 scripts/check-composite-action-input-interpolation.py diff --git a/.gitignore b/.gitignore index e8c8baf330ec1..4278a24b0f6f5 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,8 @@ apps/android/.cxx/ *.bun-build apps/macos/.build/ apps/shared/MoltbotKit/.build/ +apps/shared/OpenClawKit/.build/ +apps/shared/OpenClawKit/Package.resolved **/ModuleCache/ bin/ bin/clawdbot-mac @@ -34,10 +36,13 @@ bin/docs-list apps/macos/.build-local/ apps/macos/.swiftpm/ apps/shared/MoltbotKit/.swiftpm/ +apps/shared/OpenClawKit/.swiftpm/ Core/ apps/ios/*.xcodeproj/ apps/ios/*.xcworkspace/ apps/ios/.swiftpm/ +apps/ios/.derivedData/ +apps/ios/.local-signing.xcconfig vendor/ apps/ios/Clawdbot.xcodeproj/ apps/ios/Clawdbot.xcodeproj/** @@ -84,3 +89,7 @@ USER.md !.agent/workflows/ /local/ package-lock.json +.claude/settings.local.json + +# Local iOS signing overrides +apps/ios/LocalSigning.xcconfig diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc index f7208b4da3d6c..445d62b7efb5e 100644 --- a/.oxfmtrc.jsonc +++ b/.oxfmtrc.jsonc @@ -6,14 +6,18 @@ "experimentalSortPackageJson": { "sortScripts": true, }, + "tabWidth": 2, + "useTabs": false, "ignorePatterns": [ "apps/", "assets/", + "docker-compose.yml", "dist/", "docs/_layouts/", "node_modules/", "patches/", "pnpm-lock.yaml/", + "src/auto-reply/reply/export-html/", "Swabble/", "vendor/", ], diff --git a/.oxlintrc.json b/.oxlintrc.json index 4097a58f2d56f..687b5bb5eb53e 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -11,6 +11,8 @@ "eslint-plugin-unicorn/prefer-array-find": "off", "eslint/no-await-in-loop": "off", "eslint/no-new": "off", + "eslint/no-shadow": "off", + "eslint/no-unmodified-loop-condition": "off", "oxc/no-accumulating-spread": "off", "oxc/no-async-endpoint-handlers": "off", "oxc/no-map-spread": "off", @@ -27,8 +29,9 @@ "extensions/", "node_modules/", "patches/", - "pnpm-lock.yaml/", + "pnpm-lock.yaml", "skills/", + "src/auto-reply/reply/export-html/template.js", "src/canvas-host/a2ui/a2ui.bundle.js", "Swabble/", "vendor/" diff --git a/.secrets.baseline b/.secrets.baseline index 826d5b4def1f1..089515fe250c1 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -146,1730 +146,12434 @@ } ], "results": { - ".env.example": [ + ".detect-secrets.cfg": [ { - "type": "Twilio API Key", - "filename": ".env.example", - "hashed_secret": "3c7206eff845bc69cf12d904d0f95f9aec15535e", + "type": "Private Key", + "filename": ".detect-secrets.cfg", + "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 2 + "line_number": 17 + }, + { + "type": "Secret Keyword", + "filename": ".detect-secrets.cfg", + "hashed_secret": "fe88fceb47e040ba1bfafa4ac639366188df2f6d", + "is_verified": false, + "line_number": 19 } ], "appcast.xml": [ { "type": "Base64 High Entropy String", "filename": "appcast.xml", - "hashed_secret": "4e5f0a148d9ef42afeb73b1c77643e2ef2dee0b9", + "hashed_secret": "2bc43713edb8f775582c6314953b7c020d691aba", "is_verified": false, - "line_number": 90 + "line_number": 141 }, { "type": "Base64 High Entropy String", "filename": "appcast.xml", - "hashed_secret": "f1ccdaf78c308ec2cf608818da13f5f1e4809ed1", + "hashed_secret": "2fcd83b35235522978c19dbbab2884a09aa64f35", "is_verified": false, - "line_number": 138 + "line_number": 209 }, { "type": "Base64 High Entropy String", "filename": "appcast.xml", - "hashed_secret": "2691dc9c9ded92ba62a2d8ee589e2d78e2aa0479", + "hashed_secret": "78b65f0952ed8a557e0f67b2364ff67cb6863bc8", "is_verified": false, - "line_number": 212 + "line_number": 310 } ], - "apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift": [ - { - "type": "Secret Keyword", - "filename": "apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift", - "hashed_secret": "e761624445731fcb8b15da94343c6b92e507d190", - "is_verified": false, - "line_number": 26 - }, + "apps/android/app/src/test/java/ai/openclaw/android/node/AppUpdateHandlerTest.kt": [ { - "type": "Secret Keyword", - "filename": "apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift", - "hashed_secret": "a23c8630c8a5fbaa21f095e0269c135c20d21689", + "type": "Hex High Entropy String", + "filename": "apps/android/app/src/test/java/ai/openclaw/android/node/AppUpdateHandlerTest.kt", + "hashed_secret": "ee662f2bc691daa48d074542722d8e1b0587673c", "is_verified": false, - "line_number": 42 + "line_number": 58 } ], - "apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift": [ + "apps/ios/Sources/Gateway/GatewaySettingsStore.swift": [ { "type": "Secret Keyword", - "filename": "apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift", - "hashed_secret": "19dad5cecb110281417d1db56b60e1b006d55bb4", + "filename": "apps/ios/Sources/Gateway/GatewaySettingsStore.swift", + "hashed_secret": "5f7c0c35e552780b67fe1c0ee186764354793be3", "is_verified": false, - "line_number": 61 + "line_number": 28 } ], - "apps/macos/Tests/ClawdbotIPCTests/GatewayLaunchAgentManagerTests.swift": [ + "apps/ios/Tests/DeepLinkParserTests.swift": [ { "type": "Secret Keyword", - "filename": "apps/macos/Tests/ClawdbotIPCTests/GatewayLaunchAgentManagerTests.swift", + "filename": "apps/ios/Tests/DeepLinkParserTests.swift", "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", "is_verified": false, - "line_number": 13 + "line_number": 89 } ], - "apps/macos/Tests/ClawdbotIPCTests/TailscaleIntegrationSectionTests.swift": [ + "apps/macos/Sources/OpenClawProtocol/GatewayModels.swift": [ { "type": "Secret Keyword", - "filename": "apps/macos/Tests/ClawdbotIPCTests/TailscaleIntegrationSectionTests.swift", - "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "filename": "apps/macos/Sources/OpenClawProtocol/GatewayModels.swift", + "hashed_secret": "7990585255d25249fb1e6eac3d2bd6c37429b2cd", "is_verified": false, - "line_number": 27 + "line_number": 1492 } ], - "apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift": [ + "apps/macos/Tests/OpenClawIPCTests/AnthropicAuthResolverTests.swift": [ { "type": "Secret Keyword", - "filename": "apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift", - "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", + "filename": "apps/macos/Tests/OpenClawIPCTests/AnthropicAuthResolverTests.swift", + "hashed_secret": "e761624445731fcb8b15da94343c6b92e507d190", "is_verified": false, - "line_number": 100 - } - ], - "docs/brave-search.md": [ + "line_number": 26 + }, { "type": "Secret Keyword", - "filename": "docs/brave-search.md", - "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", + "filename": "apps/macos/Tests/OpenClawIPCTests/AnthropicAuthResolverTests.swift", + "hashed_secret": "a23c8630c8a5fbaa21f095e0269c135c20d21689", "is_verified": false, - "line_number": 26 + "line_number": 42 } ], - "docs/channels/bluebubbles.md": [ + "apps/macos/Tests/OpenClawIPCTests/GatewayEndpointStoreTests.swift": [ { "type": "Secret Keyword", - "filename": "docs/channels/bluebubbles.md", - "hashed_secret": "555da20df20d4172e00f1b73d7c3943802055270", + "filename": "apps/macos/Tests/OpenClawIPCTests/GatewayEndpointStoreTests.swift", + "hashed_secret": "19dad5cecb110281417d1db56b60e1b006d55bb4", "is_verified": false, - "line_number": 32 + "line_number": 61 } ], - "docs/channels/matrix.md": [ + "apps/macos/Tests/OpenClawIPCTests/GatewayLaunchAgentManagerTests.swift": [ { "type": "Secret Keyword", - "filename": "docs/channels/matrix.md", - "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", + "filename": "apps/macos/Tests/OpenClawIPCTests/GatewayLaunchAgentManagerTests.swift", + "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", "is_verified": false, - "line_number": 58 + "line_number": 13 } ], - "docs/channels/nextcloud-talk.md": [ + "apps/macos/Tests/OpenClawIPCTests/TailscaleIntegrationSectionTests.swift": [ { "type": "Secret Keyword", - "filename": "docs/channels/nextcloud-talk.md", - "hashed_secret": "76ed0a056aa77060de25754586440cff390791d0", + "filename": "apps/macos/Tests/OpenClawIPCTests/TailscaleIntegrationSectionTests.swift", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 47 + "line_number": 27 } ], - "docs/channels/nostr.md": [ + "apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift": [ { "type": "Secret Keyword", - "filename": "docs/channels/nostr.md", - "hashed_secret": "edeb23e25a619c434d22bb7f1c3ca4841166b4e8", + "filename": "apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift", + "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", "is_verified": false, - "line_number": 65 + "line_number": 106 } ], - "docs/channels/slack.md": [ + "apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift": [ { "type": "Secret Keyword", - "filename": "docs/channels/slack.md", - "hashed_secret": "3f4800fb7c1fb79a9a48bfd562d90bc6b2e2b718", + "filename": "apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift", + "hashed_secret": "7990585255d25249fb1e6eac3d2bd6c37429b2cd", "is_verified": false, - "line_number": 141 + "line_number": 1492 } ], - "docs/concepts/memory.md": [ + "docs/.i18n/zh-CN.tm.jsonl": [ { - "type": "Secret Keyword", - "filename": "docs/concepts/memory.md", - "hashed_secret": "39d711243bfcee9fec8299b204e1aa9c3430fa12", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6ba7bb7047f44b28279fbb11350e1a7bf4e7de59", "is_verified": false, - "line_number": 108 + "line_number": 1 }, { - "type": "Secret Keyword", - "filename": "docs/concepts/memory.md", - "hashed_secret": "1a8abbf465c52363ab4c9c6ad945b8e857cbea55", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e83ec66165edcee8f2b408b5e6bafe4844071f8f", "is_verified": false, - "line_number": 131 + "line_number": 2 }, { - "type": "Secret Keyword", - "filename": "docs/concepts/memory.md", - "hashed_secret": "b9f640d6095b9f6b5a65983f7b76dbbb254e0044", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8793597fb80169cbcefe08a1b0151138b7ab78bd", "is_verified": false, - "line_number": 373 - } - ], - "docs/concepts/model-providers.md": [ + "line_number": 3 + }, { - "type": "Secret Keyword", - "filename": "docs/concepts/model-providers.md", - "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "af6b2a2ef841b637288e2eb2726e20ed9c3974c0", "is_verified": false, - "line_number": 168 + "line_number": 4 }, { - "type": "Secret Keyword", - "filename": "docs/concepts/model-providers.md", - "hashed_secret": "ef83ad68b9b66e008727b7c417c6a8f618b5177e", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "db1f9e54942e872f3a7b29aa174c70a3167d76f2", "is_verified": false, - "line_number": 255 - } - ], - "docs/environment.md": [ + "line_number": 5 + }, { - "type": "Secret Keyword", - "filename": "docs/environment.md", - "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f66de1a7ae418bd55115d4fac319824deb0d88cb", "is_verified": false, - "line_number": 29 + "line_number": 6 }, { - "type": "Secret Keyword", - "filename": "docs/environment.md", - "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "98510d5b8050a30514bc7fa147af6f66e5e34804", "is_verified": false, - "line_number": 31 - } - ], - "docs/gateway/configuration-examples.md": [ + "line_number": 7 + }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration-examples.md", - "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b03e1a8bbe1b422cb64d7aea071d94088b6c1768", "is_verified": false, - "line_number": 53 + "line_number": 8 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration-examples.md", - "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6f72b03efde2d701a7e882dcaed1e935484a8e67", "is_verified": false, - "line_number": 55 + "line_number": 9 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration-examples.md", - "hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "57d35c7411cff6f679c4a437d3251c0532fbe3cb", "is_verified": false, - "line_number": 319 + "line_number": 10 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration-examples.md", - "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fbffe72a354d73fad191eec6605543d3e8e5f549", "is_verified": false, - "line_number": 414 + "line_number": 11 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration-examples.md", - "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ceb3b4e53c22f7e28ab7006c9e1931bd31d534e1", "is_verified": false, - "line_number": 548 - } - ], - "docs/gateway/configuration.md": [ + "line_number": 12 + }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration.md", - "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3eb65eb5d24ab5bd58a57bcd1a1894c1d05ad7f6", "is_verified": false, - "line_number": 272 + "line_number": 13 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration.md", - "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "88e065467489c885d4d80d8f582707f3ca6284e6", "is_verified": false, - "line_number": 274 + "line_number": 14 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration.md", - "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fd9e2dd936c475429f6d461056c5d97d1635de2e", "is_verified": false, - "line_number": 1029 + "line_number": 15 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration.md", - "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b7a629ae866eda49b01fe2eccbf842b52594442a", "is_verified": false, - "line_number": 1470 + "line_number": 16 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration.md", - "hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "67c615ed823ff022c807fcb65d52bd454a52bc1f", "is_verified": false, - "line_number": 1486 + "line_number": 17 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration.md", - "hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "121e6974c091fafcc6e493892b7e7ffe3c81e7eb", "is_verified": false, - "line_number": 2268 + "line_number": 18 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration.md", - "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2be720cb8d166c422e71de2c43dbb5832c952df5", "is_verified": false, - "line_number": 2344 + "line_number": 19 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration.md", - "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e44ba9d2b09e8923191b76eb9f58127ad9980cae", "is_verified": false, - "line_number": 2658 + "line_number": 20 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/configuration.md", - "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ff53d507245282f09d082321e8ef511a3e2af5ff", "is_verified": false, - "line_number": 2844 - } - ], - "docs/gateway/local-models.md": [ + "line_number": 21 + }, { - "type": "Secret Keyword", - "filename": "docs/gateway/local-models.md", - "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7ecbf8a10b1e8bc096b49c27d3b70812778205eb", "is_verified": false, - "line_number": 32 + "line_number": 22 }, { - "type": "Secret Keyword", - "filename": "docs/gateway/local-models.md", - "hashed_secret": "49fd535e63175a827aab3eff9ac58a9e82460ac9", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5628e70d1f7717c328418619beb0ae164fb5075c", "is_verified": false, - "line_number": 121 - } - ], - "docs/gateway/tailscale.md": [ + "line_number": 23 + }, { - "type": "Secret Keyword", - "filename": "docs/gateway/tailscale.md", - "hashed_secret": "9cb0dc5383312aa15b9dc6745645bde18ff5ade9", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b0b8efbb45c2854a57241d51c2b556838eaebc00", "is_verified": false, - "line_number": 75 - } - ], - "docs/help/faq.md": [ + "line_number": 24 + }, { - "type": "Secret Keyword", - "filename": "docs/help/faq.md", - "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "686c14971a01fa1737cc2c00790933213b688e52", "is_verified": false, - "line_number": 925 + "line_number": 25 }, { - "type": "Secret Keyword", - "filename": "docs/help/faq.md", - "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6311a112d1ef120acc3247c79a07721b9dc52f5b", "is_verified": false, - "line_number": 1113 + "line_number": 26 }, { - "type": "Secret Keyword", - "filename": "docs/help/faq.md", - "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0765cbc88514c95526bffd2e5b5144e050969aae", "is_verified": false, - "line_number": 1114 + "line_number": 27 }, { - "type": "Secret Keyword", - "filename": "docs/help/faq.md", - "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8d4d995d95dae479362773b1fe5ff943f735dd97", "is_verified": false, - "line_number": 1439 + "line_number": 28 }, { - "type": "Secret Keyword", - "filename": "docs/help/faq.md", - "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6da60e76ffee6f074c22f89fbfe1969b9b5bbbe2", "is_verified": false, - "line_number": 1715 - } - ], + "line_number": 29 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "40efc129489cfc37e7f114be79db3843adfd6549", + "is_verified": false, + "line_number": 30 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "976e548e417838885ab177817cf2b04f9c390571", + "is_verified": false, + "line_number": 31 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "26ad87428b833b4d5d569c10ec5bd7cc32019a0a", + "is_verified": false, + "line_number": 32 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "45f8de688074faa92a647dcf9f67b670de68a2b0", + "is_verified": false, + "line_number": 33 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "24d6fb4ef117d39c5f9c45a205faf1c85f356fa0", + "is_verified": false, + "line_number": 34 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "172a6875ed57d321409cb9c27d425b0b41eacb29", + "is_verified": false, + "line_number": 35 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bf13e4219d558c0deff114eb6b6098dd12d30e90", + "is_verified": false, + "line_number": 36 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1c91d3756008237ba0540b5831e88763e45a4fa9", + "is_verified": false, + "line_number": 37 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "63f55dcafa051c764eebfc72939788ec777fa3b5", + "is_verified": false, + "line_number": 38 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2fec58745fb43cefe32e523ca60285baa33825c3", + "is_verified": false, + "line_number": 39 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7dc4fc41a5c1ba307be067570a0e458f3b139696", + "is_verified": false, + "line_number": 40 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "26e2d413623e29e208ee2e71dd8aa02db3f0daa5", + "is_verified": false, + "line_number": 41 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "816184e85b856e06b4d70967ce713e72b22292e5", + "is_verified": false, + "line_number": 42 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "874b4362c636af8f5b4aebe013ae321ab0b83fd9", + "is_verified": false, + "line_number": 43 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8e89a4e4945335d905762eb2dc5e8510abc9716d", + "is_verified": false, + "line_number": 44 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7d4eb519b7fa3bce189b20609de596db82b56fae", + "is_verified": false, + "line_number": 45 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "22f878f965c38ebecdfd6ba0229e118cbfc80b00", + "is_verified": false, + "line_number": 46 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2b2b5ced0fb09d74ab6fba9f058139ef47ad6bda", + "is_verified": false, + "line_number": 47 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ff5c4ac7b55661c8bb699005b3ba9e0299b66ec9", + "is_verified": false, + "line_number": 48 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "541344e343f0f02cb1548729b073161d0b44c373", + "is_verified": false, + "line_number": 49 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "886979ee264082f1daebc1a2c95e9376281869fa", + "is_verified": false, + "line_number": 50 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d1c7b012097938e3b75365359d49aa134768f64f", + "is_verified": false, + "line_number": 51 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9c6a58787264a4fb0a823f9e20fd2c9abf82b96d", + "is_verified": false, + "line_number": 52 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "79e2c2821ed6a8b47486b4ddea90be8c7d4ad5b8", + "is_verified": false, + "line_number": 53 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ae8e49c80ed43d16eef9f633c28879b3166318ab", + "is_verified": false, + "line_number": 54 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f96db0197e1d67eab1197a03c107b07a71cd0ce7", + "is_verified": false, + "line_number": 55 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cf799fdab5d19a32f25735f5b6a1265b6e30c33d", + "is_verified": false, + "line_number": 56 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9d2165cc2b208ca555fb00ddaa1768455c89c4d0", + "is_verified": false, + "line_number": 57 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9139a8402a3454c747b23df0d7c8e957312dd6d2", + "is_verified": false, + "line_number": 58 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "00bb66a6c79ba6cfebbf1018a83af7129a29a479", + "is_verified": false, + "line_number": 59 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5b43b45627cffb5959d10386ec63025d28dbeec4", + "is_verified": false, + "line_number": 60 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c99e2f9d7726da2ea48cb07e71a33a757cb12118", + "is_verified": false, + "line_number": 61 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1880416d744d0693237d330f6ca744b59e7e12b4", + "is_verified": false, + "line_number": 62 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2ed0dc836758d77d6a96c6b96d054697a59d64f0", + "is_verified": false, + "line_number": 63 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8f34c522fe85146a367d92efe27488718791707e", + "is_verified": false, + "line_number": 64 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5bc1ce83e698af25ed3427553c8a3fcf8aaefdc9", + "is_verified": false, + "line_number": 65 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "05e16bf4e66e22a4a83defe89f6e746becf049b8", + "is_verified": false, + "line_number": 66 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "97b2b3d469cde6e5e88ac0089433c772d2d86b0d", + "is_verified": false, + "line_number": 67 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "149e7eb26c3598e6fa620c61de9e7562d7995e01", + "is_verified": false, + "line_number": 68 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5ec42634100091a94f71a2fd14820cb535df481e", + "is_verified": false, + "line_number": 69 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8d6ef196daa5e81bda9ac982bcb40a6f07d4f50c", + "is_verified": false, + "line_number": 70 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2d5c79b7d58642498f734dbe2c1245159a277a1e", + "is_verified": false, + "line_number": 71 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7efd41240b058195c11e1ea621060bc8c82df8fc", + "is_verified": false, + "line_number": 72 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "47f6371bd5fe1746bcade2fea59cb8d93ff5c4e0", + "is_verified": false, + "line_number": 73 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c67ce872a65c537d8748b302f45479714a04c420", + "is_verified": false, + "line_number": 74 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fc32724374d238112dd530743e85af73f1c8eb8e", + "is_verified": false, + "line_number": 75 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a01d187f1b0f38159c62f32405796de21548be31", + "is_verified": false, + "line_number": 76 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a39ae2ab785dc2d4aab7856b0a7c6e4e5875b215", + "is_verified": false, + "line_number": 77 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4ad4b170f1617e562f07cba453b69c8bc53cb5cd", + "is_verified": false, + "line_number": 78 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b0e551f8b6fbe0147169202fbc141c1a0478dfb2", + "is_verified": false, + "line_number": 79 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "02593ce120c7398316c65894a5fa4be694ea3cee", + "is_verified": false, + "line_number": 80 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "789bc546ba1936b86999373fca6d6a6a4899a787", + "is_verified": false, + "line_number": 81 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ee29461a81f3e898f4376d270ac84b8567f9b68c", + "is_verified": false, + "line_number": 82 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "235f549d4c65ec31307e0887204c428441d6229f", + "is_verified": false, + "line_number": 83 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "87b2376e9f5457bad56b7fb363c6a5f86d8f119a", + "is_verified": false, + "line_number": 84 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c3b3424f5845769977ccb309a3c2b70117989e3c", + "is_verified": false, + "line_number": 85 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "88ddc980ca5f609c2806df08e2e1b9b206153817", + "is_verified": false, + "line_number": 86 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "af48a18326858bfcef8e5f3a850fba0f9d462549", + "is_verified": false, + "line_number": 87 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c22217254346f8d551183caac2f73ec8284953b3", + "is_verified": false, + "line_number": 88 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2de7388be37ebdde032f5e169940da7c9d38ac8b", + "is_verified": false, + "line_number": 89 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "98facee0b1bf74672bacb855a27972851929dd78", + "is_verified": false, + "line_number": 90 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0a5cae7f96ade77892c5caa993b6d19cd41232fb", + "is_verified": false, + "line_number": 91 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fe0da76f124e112f6702f2e9c62514238398ba8d", + "is_verified": false, + "line_number": 92 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d5ce761d7b87445aa65b1734ad36c5d3d1d71c2a", + "is_verified": false, + "line_number": 93 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f5b70c708f3034bd837835329603a499207c4fb5", + "is_verified": false, + "line_number": 94 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "50d6381367811dd8a0ad61bf1dd2c3619ece8a44", + "is_verified": false, + "line_number": 95 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fe061e35aafc5841544633d917f55357813c0906", + "is_verified": false, + "line_number": 96 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "dc8722d30a33248ccc5dd9012fba71eefd3a44ac", + "is_verified": false, + "line_number": 97 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2fb43da561bbb79d7cf89e5d6c5102c1436f6f49", + "is_verified": false, + "line_number": 98 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cf61d12e9d98f6ba507bf40285d05f37fe158a01", + "is_verified": false, + "line_number": 99 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "dfeb7563bafd2d89888b8b440dee49d089daeb78", + "is_verified": false, + "line_number": 100 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fea45d453b5b8650cda0b2b9db6b85b60c503d6c", + "is_verified": false, + "line_number": 101 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bb7538d46b4fde60dc88be303de19d35fe89019d", + "is_verified": false, + "line_number": 102 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "08e0674faf444c6dc671036d900e3decce98d1eb", + "is_verified": false, + "line_number": 103 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e261897f1d1a99aafec462606b65228331e30583", + "is_verified": false, + "line_number": 104 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ffe19721c941dfb929b30707c8513e2f0c8c4dc7", + "is_verified": false, + "line_number": 105 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fe1fc5b0e4ca6aa0189f77a9d78b852201366b81", + "is_verified": false, + "line_number": 106 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "590787fa67e0d75346ed1a3850f98741b6a49506", + "is_verified": false, + "line_number": 107 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "eccb56a947e4d36b8e9d51d0e071caf1a978c6f2", + "is_verified": false, + "line_number": 108 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c301ee23c9e41d15d5c58c7cd5939e41e7d1eb99", + "is_verified": false, + "line_number": 109 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9f8607273e42be64e9779e59455706923081cd80", + "is_verified": false, + "line_number": 110 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "72d31fe5a3e5b6e818f5fd3ec97a9ac0042acec7", + "is_verified": false, + "line_number": 111 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bb9158c9b6e8a0a1007b93b92ec531bdd9ffd32e", + "is_verified": false, + "line_number": 112 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c2ca44d18bd79c0f1b663d8bc3dfcfb02a7e02df", + "is_verified": false, + "line_number": 113 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "eac2c4cc6263495036a0ef8d8aaf2d8075167249", + "is_verified": false, + "line_number": 114 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f55341301796552621f367fff6ea9a2bd076df29", + "is_verified": false, + "line_number": 115 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "21967ac89d793aa883840d7a71308514e9e1dc4e", + "is_verified": false, + "line_number": 116 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "679dc9deb86fd7375692381ae784de604a552ae3", + "is_verified": false, + "line_number": 117 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "dd90f8337c050490f6e9b191fb603c9ad402d8c0", + "is_verified": false, + "line_number": 118 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3c8bfe5a9f458f3884e67768465ac1c17ff80e0f", + "is_verified": false, + "line_number": 119 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3f01eb8d14a37b6e087592d109baf01e603417eb", + "is_verified": false, + "line_number": 120 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "021709695261ffbc463f12b726d9dd6c27abb6f0", + "is_verified": false, + "line_number": 121 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a09a21e3684c15de00769686d906f72dd664f663", + "is_verified": false, + "line_number": 122 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "15a62195ff8e8694bfd7045af4391df383b990ed", + "is_verified": false, + "line_number": 123 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "010fa027e45282a3941133bf3403ab98cacc9edd", + "is_verified": false, + "line_number": 124 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e19fd3f99a05ccf60d1083f5601dea6817b1ac03", + "is_verified": false, + "line_number": 125 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d17a8e92d9f18e17c7477d375dcac30af8c34ff5", + "is_verified": false, + "line_number": 126 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c33ae1092a63f763487a4e0d84720b06a2523880", + "is_verified": false, + "line_number": 127 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9486a607ef0dcb94ce9ac75a85f0a76230defd1d", + "is_verified": false, + "line_number": 128 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1d850e2d57c74a691b52e3e2526c2767865fb798", + "is_verified": false, + "line_number": 129 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "60a0c030c7e8a5beddd199d1061825b5684ab4ae", + "is_verified": false, + "line_number": 130 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2986a818d44589ee322b0d05a751b9184b74ebac", + "is_verified": false, + "line_number": 131 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "440aad6aaad76b0dab4c53eb8a9c511d38f5ee1c", + "is_verified": false, + "line_number": 132 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "372c99f2afefff2b07dd4611b07c6830ec1014f3", + "is_verified": false, + "line_number": 133 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "99678a4cbb8d20741f35f04235ee808686a5ee52", + "is_verified": false, + "line_number": 134 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3486b5c6f177ac543d846a9195d3291a0d3bd724", + "is_verified": false, + "line_number": 135 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2902179aba6cb39f2c7b774649301a368a39b969", + "is_verified": false, + "line_number": 136 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4108ee51d5c321b98393b68a262b74d6377cec76", + "is_verified": false, + "line_number": 137 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8abe8434123396924dc964759bc7823d59b31283", + "is_verified": false, + "line_number": 138 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a2a8363585b5988aeff2a2c8c878c15445322a52", + "is_verified": false, + "line_number": 139 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bbbcc1630c23a709000e6da74ca22fe18b78b919", + "is_verified": false, + "line_number": 140 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "be582fadd937879b93b46e404049076080faed08", + "is_verified": false, + "line_number": 141 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "15320eb2e8d97720f682f8dc5105cb86a539a452", + "is_verified": false, + "line_number": 142 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "611278690506b584ecc5d4c88b334dbe7e9b8c54", + "is_verified": false, + "line_number": 143 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8a08069ce7a3702f245f8c50ac49a529092384be", + "is_verified": false, + "line_number": 144 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8cf1444399ca01a1bf569233106065b30c103cd2", + "is_verified": false, + "line_number": 145 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4a5a11832d16a4c2c6914d05397ce3e6f457572f", + "is_verified": false, + "line_number": 146 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "80490973b1980ad3740d42426c7c0f2986cbe462", + "is_verified": false, + "line_number": 147 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "495d2b2d95ba56eded4e4d738b229dd5caaeea67", + "is_verified": false, + "line_number": 148 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2264d1d1a69546223eb2754465a1b40ce20ab936", + "is_verified": false, + "line_number": 149 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6e9e9f0b269aacbf7358498c088c226a9296de14", + "is_verified": false, + "line_number": 150 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1cb9e17cefe3759cb8fd0de893e8a12531c4375b", + "is_verified": false, + "line_number": 151 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ddc15a0e8c7caca06cf93d15768533595b8ba232", + "is_verified": false, + "line_number": 152 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7dbafb9953c44da0cc46c003d3dacd14a32a4438", + "is_verified": false, + "line_number": 153 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "be61d29ac11ba55400fcaf405a1b404e269e528e", + "is_verified": false, + "line_number": 154 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2e65dec5c2802e2bb8102d3cd8d0a7e031a6b130", + "is_verified": false, + "line_number": 155 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c43e69c82865cf66a55df2d00a9e842df3525669", + "is_verified": false, + "line_number": 156 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "084448bff84b39813fc1efe3ff5840807d7da8f9", + "is_verified": false, + "line_number": 157 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e175aaf2f1a6929f95138b56d92ae7b84b831ffe", + "is_verified": false, + "line_number": 158 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9d6deadf9c4eb8ea0240ecca10258afb9b39e0a2", + "is_verified": false, + "line_number": 159 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4bf318f05592507a55a872cdb1a5739ad4477293", + "is_verified": false, + "line_number": 160 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b71cc2bafb860b166886bb522c191f45d405cc76", + "is_verified": false, + "line_number": 161 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a723b7af4e7b4ede705855c03e4d3ac8b17a17a0", + "is_verified": false, + "line_number": 162 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "595c5493c18960b81043b1aaa0ada4a86a493f2b", + "is_verified": false, + "line_number": 163 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "dee9b3f8262451274b6451ead384675a75700188", + "is_verified": false, + "line_number": 164 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b300397e68cfcee9898e8e00f7395a27f8280070", + "is_verified": false, + "line_number": 165 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "44973e389b0e5b25d51439d6a9b6c9d43fdd6ee0", + "is_verified": false, + "line_number": 166 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "93ebcb14fec5ae9ae41b0bdce7d6aa2971298e47", + "is_verified": false, + "line_number": 167 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c5b1332b11dd3ba639ce2fdaaa025bad034207e9", + "is_verified": false, + "line_number": 168 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4927a4f45fa60e6d8deb3d42ca896410d791f3db", + "is_verified": false, + "line_number": 169 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "081e263d2c8f882eb19692648f71ac03a8731c09", + "is_verified": false, + "line_number": 170 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ef5eba4fd8203b259dd839628ddc0d9a3ed6f97f", + "is_verified": false, + "line_number": 171 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c90d7323630daddb2824cd0d9e637521237e2454", + "is_verified": false, + "line_number": 172 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "99e13b6a3b2c3c60603df94711c67938be98e776", + "is_verified": false, + "line_number": 173 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2c55757167c8ecf90790ad052900e790f269619e", + "is_verified": false, + "line_number": 174 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f3e5c54b01b6e69be585cd9142ed7abe5d4056e5", + "is_verified": false, + "line_number": 175 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b0dd1c28e143d597218a174dbe0274598c59b9c8", + "is_verified": false, + "line_number": 176 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9a1fe8341b21243d6116f6b3375877b7fa9b34d7", + "is_verified": false, + "line_number": 177 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e6b9bc000db030828a117a2d31a0598a84120186", + "is_verified": false, + "line_number": 178 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8e40eebcfe379882ecbfb761bb470c208826ebf8", + "is_verified": false, + "line_number": 179 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "afd7a7532b580be96e7cc3c0e368a89f31ef621c", + "is_verified": false, + "line_number": 180 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bfd20c7315b569fab2449be3018de404ed0d6fc3", + "is_verified": false, + "line_number": 181 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ccba0997cbb3cea20186ca1d3d3b170044e78f27", + "is_verified": false, + "line_number": 182 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "43cd2dcd4adf33ef138634454d93153671a58357", + "is_verified": false, + "line_number": 183 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7244b34d4c1c0014497a432c580eeea0498b7996", + "is_verified": false, + "line_number": 184 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ec96512c56ade3837920de713f54fa81e6463a5b", + "is_verified": false, + "line_number": 185 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f9ab8ac96faef103a825c131a9f6aa18aaf5c496", + "is_verified": false, + "line_number": 186 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "988b02f25fa7b8124ad9d5e3127ec7690bd7f568", + "is_verified": false, + "line_number": 187 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "71d4e0487a5ed7f3f82b2256bed1efb3797c99e2", + "is_verified": false, + "line_number": 188 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4dad8db6d2449abd1800ac11f64dd362f579a823", + "is_verified": false, + "line_number": 189 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d079b5fbe50b0b84ad69a0d061b4307a3a0a6688", + "is_verified": false, + "line_number": 190 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c2672b9214bb9991530f943c1a5a0d05977c0f0a", + "is_verified": false, + "line_number": 191 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f3a8f4566cd7f256979933da8536f6dafb05d447", + "is_verified": false, + "line_number": 192 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e3b44891d5e5ec135f1e977ec5fd79c74ca11d9c", + "is_verified": false, + "line_number": 193 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8542da23c2d0a4b0bcab3939f096b31e3131d85f", + "is_verified": false, + "line_number": 194 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fb281df2d7a6793a43236092a3fcc1b038db56c9", + "is_verified": false, + "line_number": 195 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "727686c68fa10c5edecbf37cdfec2d44f3a5f669", + "is_verified": false, + "line_number": 196 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e7957179705dafeab8797bb8f90fcaf5ad0a61ee", + "is_verified": false, + "line_number": 197 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7424aea64d7c75511030d719e479517e8bef9d25", + "is_verified": false, + "line_number": 198 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3ad22266e9a3214addc49722b44d9559eb7cbedc", + "is_verified": false, + "line_number": 199 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8b00c700bf0f6c74820e1ad93d812f961989d69e", + "is_verified": false, + "line_number": 200 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2eef664e5193da7dde51adccd6d726a988701aaf", + "is_verified": false, + "line_number": 201 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9186e0986b4b7967aa03cfe311149d508d22e6aa", + "is_verified": false, + "line_number": 202 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1a639bb9895dc305d6db698183635c1f8b173c5c", + "is_verified": false, + "line_number": 203 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b5fbec5f1451e2d940c70945a01323eda82984bd", + "is_verified": false, + "line_number": 204 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ebb046a7ba8464ce615d215edb8b1fd82a1357b6", + "is_verified": false, + "line_number": 205 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "719e3976a5a00a7473cd38f81f712ca8c6e522e1", + "is_verified": false, + "line_number": 206 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "12cde4d54e7136273e8aa76d161b6f143469ef6d", + "is_verified": false, + "line_number": 207 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e04ec69eef9a4325231986801ebd42d3159ccca7", + "is_verified": false, + "line_number": 208 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "07c8e9accb3cfcc748b91d0369629fa1ee90576f", + "is_verified": false, + "line_number": 209 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3b00038548a6119fba962ca93f6bd24035d5571e", + "is_verified": false, + "line_number": 210 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2914f579938a910fb510898044063bec779e5ad5", + "is_verified": false, + "line_number": 211 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "868cf20bb88168a03fa29c7261762c97430ea0fc", + "is_verified": false, + "line_number": 212 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0475a43ad50f08c4a7012c4a87f15eeee3762ff9", + "is_verified": false, + "line_number": 213 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5ebe715bd56f0448d0374adae8568a6d86856442", + "is_verified": false, + "line_number": 214 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9c6dff479fd398382a289dc8f60cabf06fa60a26", + "is_verified": false, + "line_number": 215 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0102959abc9fee55edba97642bb1bcc546ce07dc", + "is_verified": false, + "line_number": 216 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "45459296596dbed9d7fbf7eab7a9645eb4fa107a", + "is_verified": false, + "line_number": 217 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5a5a491d064e789e785a8b080d38d9d1cc7d207f", + "is_verified": false, + "line_number": 218 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f3005c052e76c7e804c10403bdfcd9265a9de2ea", + "is_verified": false, + "line_number": 219 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "73aaaaf5bcab49cc1b1f47b45eae9b31db783a66", + "is_verified": false, + "line_number": 220 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "13aae30474af34fdede678dc5e8c00c075612707", + "is_verified": false, + "line_number": 221 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "336edbc017f4dadc0bf047e0f6d1889679fc3b48", + "is_verified": false, + "line_number": 222 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7bff3213c39d3873551698ec233998613e6b69dc", + "is_verified": false, + "line_number": 223 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9f1a6484627a58c233e1ec3f0aeffe4ff2d8a440", + "is_verified": false, + "line_number": 224 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d7c80e31311e912fb766bb2348b02785c28d878b", + "is_verified": false, + "line_number": 225 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2c75cc7344d810bb26cb768be82e843af623001a", + "is_verified": false, + "line_number": 226 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "607df6be12ab20f70a64076c372b178d6c10bc00", + "is_verified": false, + "line_number": 227 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9b7fed64d1f0682953011eb4702467dee8cd1174", + "is_verified": false, + "line_number": 228 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e982d9359554bc4a5c58d9d8d4387843e6e5cbb4", + "is_verified": false, + "line_number": 229 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c2f3985aed2da033a083cb330fb006239b2a1c8e", + "is_verified": false, + "line_number": 230 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "23d658cf19e1e76efbfa3498d2c2ed091c60b1f4", + "is_verified": false, + "line_number": 231 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a58be87cd80825e211c567b3c5397e122f702019", + "is_verified": false, + "line_number": 232 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f96f43b99c2f249a03a2e57e097c236561a1162c", + "is_verified": false, + "line_number": 233 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2fc8f0d1c9fadfb9cc384af21c8d3716c99a40f6", + "is_verified": false, + "line_number": 234 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f229dfc403d5b25f3362e73c4a7dc05233ecd4b6", + "is_verified": false, + "line_number": 235 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cf79e1dd8ff4c91b3346f5153780ba52438830be", + "is_verified": false, + "line_number": 236 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "20a1e643e857f0f63923b810289ab4b6c848252e", + "is_verified": false, + "line_number": 237 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9754246ca2c82802cc557d5958175d94ae5c760b", + "is_verified": false, + "line_number": 238 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ca0abe4a600e610c1bbbb25de89390251811ed1c", + "is_verified": false, + "line_number": 239 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b9c7402f138d31bea12092e7243ac7050a693146", + "is_verified": false, + "line_number": 240 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "07e9e0d4ea04d51535c0ec78454f32830dcfe8da", + "is_verified": false, + "line_number": 241 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9872435a00467574f08579e551e3900c65f2b36e", + "is_verified": false, + "line_number": 242 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "eec328050797cfffad3dc2dd6dd16d8ec33675f6", + "is_verified": false, + "line_number": 243 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b3b084478fcaec50b9f7e39dfef8bda422d48d91", + "is_verified": false, + "line_number": 244 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2093470fb2ffad170981ec4b030b0292929f3022", + "is_verified": false, + "line_number": 245 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b920a9ef2ec94e4e4edac20163e006425a391da4", + "is_verified": false, + "line_number": 246 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "06455554c00ce5845d49ebef199c0021b208d5df", + "is_verified": false, + "line_number": 247 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a077b13877b651822b80de2903f4b6acdbac3433", + "is_verified": false, + "line_number": 248 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "78fd658f1b01b01b25be00348caeced0e3ad0b29", + "is_verified": false, + "line_number": 249 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "79f7d6f792cc4e4ba79e3bf7cd3538fb65e4399a", + "is_verified": false, + "line_number": 250 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8280b950e62db218766e1087ec5771ec93de3b36", + "is_verified": false, + "line_number": 251 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "11fffafcae5d1e1aacf6f3c3a0235bbed17cacb2", + "is_verified": false, + "line_number": 252 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f0aebb371b0356a2e803f625a1274299544e0472", + "is_verified": false, + "line_number": 253 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bce9139737d07f1759822ac6e458eff6c06c1dae", + "is_verified": false, + "line_number": 254 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a61bed5d464a3dd53f1814dc44da919124e2c72b", + "is_verified": false, + "line_number": 255 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9c553b7e8c46273c6e1841f82032a11f697cafe1", + "is_verified": false, + "line_number": 256 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "24535adb56bd8d682e42561ded0eaab8a1a18475", + "is_verified": false, + "line_number": 257 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7f16429d5dba0340ae2ec02921abbe054ad4d9fd", + "is_verified": false, + "line_number": 258 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "61bac3ad8d011d3db96793f70a9fdaf5def37244", + "is_verified": false, + "line_number": 259 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "413654967fff8eae5dd1fece27756c957721d131", + "is_verified": false, + "line_number": 260 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c42fd06a8e9c5ad8b9b3624c1732347dd992f665", + "is_verified": false, + "line_number": 261 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "53fbf2125f17fd346dba810d394774c191c05241", + "is_verified": false, + "line_number": 262 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "312ebc5348c48d940a08737cc70b257c7ba67358", + "is_verified": false, + "line_number": 263 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3c072673c95b839b4c75a59ffcb4e7de11df227c", + "is_verified": false, + "line_number": 264 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "67dcac03bb680bd7400daff1125821df29119a57", + "is_verified": false, + "line_number": 265 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "74ceb07916759595af8144a74de06f4622295fab", + "is_verified": false, + "line_number": 266 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "becd47f7a933263c4029eb3298bdf67e64166b72", + "is_verified": false, + "line_number": 267 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "62cbb7af58e6841cb33ae8aa20b188904e88400b", + "is_verified": false, + "line_number": 268 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1240f6fbe789e15d2488a1f63a38913ace848063", + "is_verified": false, + "line_number": 269 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b313e2c9b9b7a229486000525bd2bfd909c739c3", + "is_verified": false, + "line_number": 270 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9ccd84180f08a811fc82fc6c2baa43b92b0c6d4c", + "is_verified": false, + "line_number": 271 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fec498a62202037efd0ff28ff270b1d65600ee21", + "is_verified": false, + "line_number": 272 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5e5991defd9bf4c9cd7ad44bfc3499b021f9b306", + "is_verified": false, + "line_number": 273 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3ac80ba9980be6af93aa361f71cc0b24ebb9a80d", + "is_verified": false, + "line_number": 274 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3e58a970f8a2580b7929b87623a05bcfd18ff5d0", + "is_verified": false, + "line_number": 275 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4e95912a938c4a5d793d6147f17b1a4f4564f521", + "is_verified": false, + "line_number": 276 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b9c19621f11904336bb1c83271b6e66392139adf", + "is_verified": false, + "line_number": 277 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ea26c6b69a1fbd9d19136131f1a4904190cdc910", + "is_verified": false, + "line_number": 278 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "88806d10d6a88e386d7bffe5ed9d13a01aa30188", + "is_verified": false, + "line_number": 279 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "92c4052a065855d439918461deb8ab1d85b8dec4", + "is_verified": false, + "line_number": 280 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5a801127b30267b3143bcd1879b09ce966f4e4db", + "is_verified": false, + "line_number": 281 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "03c0a54929a02a84158ffbab6a79ba8a31bbea5e", + "is_verified": false, + "line_number": 282 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9adc71007b98c2f47eb094b8c771d0a2c81e8584", + "is_verified": false, + "line_number": 283 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "19cc3f05c05fc6ff92f9a56656d3903fb6e05af1", + "is_verified": false, + "line_number": 284 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "901c70145ec0a76f9705743bc180ac505301db81", + "is_verified": false, + "line_number": 285 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e264698710238eada7824909e03b11a1d5b94d01", + "is_verified": false, + "line_number": 286 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e74cd3a559f33f9541ef286068dee5338b7c2f5d", + "is_verified": false, + "line_number": 287 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a0b7170416566ab964d395d0cf138ecd3c65fe2c", + "is_verified": false, + "line_number": 288 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c9c183b3a85dec6b215a6a18a1f0ce82381c12a6", + "is_verified": false, + "line_number": 289 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "06b739bfeff8deb1f44a03424e08ab08f1280851", + "is_verified": false, + "line_number": 290 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "25dc7c4a6b8bfdcb8bc41e815d05dac7fa905711", + "is_verified": false, + "line_number": 291 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1b298510f55fd15ee6110b2a9250263dbc9f4fc9", + "is_verified": false, + "line_number": 292 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6403b53b45d57554b17c4388178cd5250aa7587a", + "is_verified": false, + "line_number": 293 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f944cf9178e33e14fddf0ac6149cbb69e993d05c", + "is_verified": false, + "line_number": 294 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "61b4fee247e19961be2d760ed745da4e39d8bf4e", + "is_verified": false, + "line_number": 295 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d25d1f3178dd3a9485d590ce68bd38b3029d0806", + "is_verified": false, + "line_number": 296 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9fdfeae6046b80e2ae85322799cdc6da4842f991", + "is_verified": false, + "line_number": 297 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f7143b0c85044b4b76ef20cd58177815daf7407e", + "is_verified": false, + "line_number": 298 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5e605f0950f7c24e192224fa469889b9c83c80ac", + "is_verified": false, + "line_number": 299 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "329c29edf1fb8e3427b1d79a30e77a700c01ff5c", + "is_verified": false, + "line_number": 300 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "74a03233311d2f477a3dd7ffa81c7343586b1f8e", + "is_verified": false, + "line_number": 301 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3b1df47dbd920bfaf1de8a7b957d21d552d78a76", + "is_verified": false, + "line_number": 302 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "929a23cdbe2b28de6dac28454d1e7478a4a14fea", + "is_verified": false, + "line_number": 303 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a6436a4a36cd90e5d03b33f562213dfc3d038455", + "is_verified": false, + "line_number": 304 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a010833ccd24af9e70339bac73664fb47b6ac727", + "is_verified": false, + "line_number": 305 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "53be5a9c1c894e77c4fcdfbbb3b003405252ed79", + "is_verified": false, + "line_number": 306 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "61b289fe5c2eb0d8b8bc5b1cc5e9855472daabd9", + "is_verified": false, + "line_number": 307 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "773307c58ca81fd42a4734bbc4b3c7eb8bcfd774", + "is_verified": false, + "line_number": 308 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "35f607d2769173d1672e30f60b9276d01b8250d7", + "is_verified": false, + "line_number": 309 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e602d5d9691c09f57a628600014aaae749d38489", + "is_verified": false, + "line_number": 310 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "625238f7e6c9febfca3878a385daa7b8646a2439", + "is_verified": false, + "line_number": 311 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e6ba52cd1f2f9a30963834fd94aafc869bf05b82", + "is_verified": false, + "line_number": 312 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d629b569233f71690b6e6eaed9001e44b88c50bf", + "is_verified": false, + "line_number": 313 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a001d4059055a1c86b9ec62774d044b54ddb3376", + "is_verified": false, + "line_number": 314 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bce06d4b0177a2d06399e21e0b26bc99e44d6e9b", + "is_verified": false, + "line_number": 315 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cb6af31518d65e6dcb92fb01b9f31556c3a70c5e", + "is_verified": false, + "line_number": 316 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c2a95352f382fdbe53bd8b729a718c38eacfbf73", + "is_verified": false, + "line_number": 317 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f9b16dccab1e453362789df2fc682f2ba2c9ee2a", + "is_verified": false, + "line_number": 318 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1bb4e4fd05b7c33cfab0dad062c54a16278d3423", + "is_verified": false, + "line_number": 319 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9dcc6dc6f20a71fd6880951ceb63262d34de8334", + "is_verified": false, + "line_number": 320 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "666382b579258537d6cf5e7094dbaa0684b78707", + "is_verified": false, + "line_number": 321 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "072c49f046dfdce12c1553a67756e2f5ee4d7e49", + "is_verified": false, + "line_number": 322 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "47b792bdebbbf305d87092f12c0afcd8810e054d", + "is_verified": false, + "line_number": 323 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "41d3b22a387fa43c1491d62310faf50c4ab7956a", + "is_verified": false, + "line_number": 324 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bcdc3859e08c518f75cfe65b69f3adb9f489400b", + "is_verified": false, + "line_number": 325 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fc2b22e2d43816acf209af822877aff7e82fa4d0", + "is_verified": false, + "line_number": 326 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f63542bc2eb9de2caa3bfaeafd53d7bf65485889", + "is_verified": false, + "line_number": 327 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7ab01f0f438a3d21b529df89fbde67234aa49d89", + "is_verified": false, + "line_number": 328 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fed608fe9221f0e45c84b68a80a0c065a9a2b7f1", + "is_verified": false, + "line_number": 329 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7a6394c70b925009c3e708ec195a17ee40cae8f4", + "is_verified": false, + "line_number": 330 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5d615bd2adf567fe7403c51814ff76c694b1c8d3", + "is_verified": false, + "line_number": 331 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "77f3c695d15ee63db41dabcecce126a246b266e6", + "is_verified": false, + "line_number": 332 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "78138e46003e12617c75a8011fddbe2868ff5650", + "is_verified": false, + "line_number": 333 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "89c905852505ac6168e4132b5ee29241a64b2654", + "is_verified": false, + "line_number": 334 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3d55f361c5d2bf2c1ec7d2c2551d7bec67b3cc35", + "is_verified": false, + "line_number": 335 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "89f1aec19abc18d22541dc01270e0fee325a878b", + "is_verified": false, + "line_number": 336 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "23ed3413498b5fe9fe2d6d3ae4040a0e2571c9df", + "is_verified": false, + "line_number": 337 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e7f990c94d57f6880b1e2cf856ab0646636bc46a", + "is_verified": false, + "line_number": 338 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "87dccf8b7123c723b5c35c45533d7471a19c9c22", + "is_verified": false, + "line_number": 339 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "14a222dcf6b592c1178fae0babbb73d809102462", + "is_verified": false, + "line_number": 340 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "161b87029fb1fe5f37573770659140c254b6f26d", + "is_verified": false, + "line_number": 341 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e01ccf01c8ae560637e1fba1396ec9d27a48943e", + "is_verified": false, + "line_number": 342 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0d45bd0e0858d416488ca24b5e277430fdbc29a2", + "is_verified": false, + "line_number": 343 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bd6b3d87fee3f95d7bbe77782404507c7d6d23ba", + "is_verified": false, + "line_number": 344 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "297eface47da40362e6c34af977185a96ecd4503", + "is_verified": false, + "line_number": 345 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1d908d54bd47e7b762cf149a00428daf8ab41535", + "is_verified": false, + "line_number": 346 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e0404cb2e3feaba3e7bdc52c798b9bce57f546d3", + "is_verified": false, + "line_number": 347 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8dc5b0bbc5b3c3f93405daac036e950013ae6e83", + "is_verified": false, + "line_number": 348 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c914f94ead99fe6e6b262f63f419aba9f1f65cc9", + "is_verified": false, + "line_number": 349 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5d2559e8fbde4bdf604babb1a00a92f547e9c305", + "is_verified": false, + "line_number": 350 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b28706495d2c7f4e44a064279570ec409025bce8", + "is_verified": false, + "line_number": 351 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ce77aa4f51f5ee1a1f56ba0999a3873e07bdec29", + "is_verified": false, + "line_number": 352 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c828435ec3655b9b44974c212f94811121d3183c", + "is_verified": false, + "line_number": 353 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0361b85a6a04d362a8704e834cd633a76d7c8531", + "is_verified": false, + "line_number": 354 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e8b43fe4aa4ece98317775e13e359f784187c9ea", + "is_verified": false, + "line_number": 355 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ec00a6364212bbc187bc15f3a22ec56eb7d5d201", + "is_verified": false, + "line_number": 356 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5599c260b57d92c0f8bd7613fa1233ad9f599db3", + "is_verified": false, + "line_number": 357 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d11065d4dd0b6fd8e29dd99b53bfbe17e1447ab3", + "is_verified": false, + "line_number": 358 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c8c47349a7991ac9cb1df02c20e18dde2ec48b9c", + "is_verified": false, + "line_number": 359 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e5302dc80bfbd04a37e52099a936c74b38d022ec", + "is_verified": false, + "line_number": 360 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4a4e17621d292bddf3604bcc712ed17fdd28aca2", + "is_verified": false, + "line_number": 361 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a43a1929d714363194cc42b3477dfe9b4c679036", + "is_verified": false, + "line_number": 362 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "645e56a2836118de395a78586b710ac24c6d1b9d", + "is_verified": false, + "line_number": 363 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c0f20d875c6d2d8e99539de46a245a5a30e757d0", + "is_verified": false, + "line_number": 364 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fb552bf2f6ea4da1a8d0203ac4c6b4ecb1bbea56", + "is_verified": false, + "line_number": 365 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "53c6b8e08eeb37812e6e40071ac16916c372b60f", + "is_verified": false, + "line_number": 366 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c64cf6bc4ec02fa8b2bf2f5de1c04f0a0c8ec77d", + "is_verified": false, + "line_number": 367 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e7dc30b59854ec80d81edc89378c880df83697c4", + "is_verified": false, + "line_number": 368 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e60404864ae5ddda3612f7ece72537ab2a97abf7", + "is_verified": false, + "line_number": 369 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a84bea5c674feff72b4542a20373b69d25a47b89", + "is_verified": false, + "line_number": 370 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "47cbc18c75b60b6e0ed4d8b6a56b705a918e814b", + "is_verified": false, + "line_number": 371 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cd8bc0fe19677ebb0187995618c3fa78d994bbb2", + "is_verified": false, + "line_number": 372 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "887786ac035ae25cc86bd2205542f8a1936e04d2", + "is_verified": false, + "line_number": 373 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3ef2e1c199d211d5f1805b7116cb0314d7180a5c", + "is_verified": false, + "line_number": 374 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f89746f236eab3882d16c8ff8668ed874692cde3", + "is_verified": false, + "line_number": 375 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2b3db4dc1799edfee973978b339357881c73d3ab", + "is_verified": false, + "line_number": 376 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b7254fda5baf4f83d6081229d10c2734763d58b4", + "is_verified": false, + "line_number": 377 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9af3e435c37c257b5e652e38a2dfd776ab01726e", + "is_verified": false, + "line_number": 378 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "833be77b754d40e1f889b7eda5c192ae9e3a63fe", + "is_verified": false, + "line_number": 379 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a153d9446771953d3e571c86725da1572899c284", + "is_verified": false, + "line_number": 380 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "68d2128a64a2b421d62bc4a5afeeb20649efe317", + "is_verified": false, + "line_number": 381 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "92490f06bfafdb12118f5494f08821c771abafff", + "is_verified": false, + "line_number": 382 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "84a479485dd167e8dc97cce221767e68cbe14793", + "is_verified": false, + "line_number": 383 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ca9c140d7b9b6dbf874d9124b3de861939eb834e", + "is_verified": false, + "line_number": 384 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d293b3b1e9c7e4b8adde8f2a8d68159c72582f71", + "is_verified": false, + "line_number": 385 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "120db881813bc074d8abb7a52909f1ffc4acf08b", + "is_verified": false, + "line_number": 386 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6be68465c1bce11d46731c083c86cc39b4ca4b26", + "is_verified": false, + "line_number": 387 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ec613f94f9c8e0a7c9a412e1405a0d1862888d44", + "is_verified": false, + "line_number": 388 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "53300289cf9589a5e08bfa702e1f3a09d2d088b1", + "is_verified": false, + "line_number": 389 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "aac8dac3f68993b049bcc04acbb83ee491921fa8", + "is_verified": false, + "line_number": 390 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b309b1a5cda603c764ed884401105a00c1a1b760", + "is_verified": false, + "line_number": 391 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c1d9acf0ca3757e6861a2c8eab08f6bf39f8f1a3", + "is_verified": false, + "line_number": 392 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "39860c432a27f5bcbcd30b58cdd4b2f8e6daf65f", + "is_verified": false, + "line_number": 393 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f28f8289110a85b1b99cd2089e9dfa14901a6bbe", + "is_verified": false, + "line_number": 394 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7c51dd968d2ae5ffad1bc290812c0d6d3f79b28a", + "is_verified": false, + "line_number": 395 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "19e03888ea02a1788b3e7aacdb982a5f29c67816", + "is_verified": false, + "line_number": 396 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "936e0dfc9fa79e90eabe1640e4808232112d6def", + "is_verified": false, + "line_number": 397 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "66b03fc6f79763108c0e0ebced61830ce609d769", + "is_verified": false, + "line_number": 398 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b4615dacf79e97a732e205acd45e29c655a422cb", + "is_verified": false, + "line_number": 399 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4e9cab1ac24cee599dc609b69273255207fb9703", + "is_verified": false, + "line_number": 400 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7c2d628057af1a5f9cdc10e1a94d61fa2f43671c", + "is_verified": false, + "line_number": 401 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1f76628414c76162638c6cdd002f50d35c0030df", + "is_verified": false, + "line_number": 402 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "656cd81676438907b67dc35f1dcbc7f65fb44eae", + "is_verified": false, + "line_number": 403 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2b7c94fe6035b5e6d98a65122fd66d9fbc0710f6", + "is_verified": false, + "line_number": 404 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d55f6f2d0aff7554ed2c85a4f534c421ba83601a", + "is_verified": false, + "line_number": 405 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "742a9e62c813d9b6326e2540f1f9f97dfca8542c", + "is_verified": false, + "line_number": 406 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8b446fd2f0b22dc0fdfee36b5b370643b669bd2d", + "is_verified": false, + "line_number": 407 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ce38475ba93df187a8dd9972a02437ffef9e849c", + "is_verified": false, + "line_number": 408 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e5581573b5114490af9bdc16bad95dca6177f4ba", + "is_verified": false, + "line_number": 409 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2f005879125b38683f71c8a64bd232cd11591e08", + "is_verified": false, + "line_number": 410 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7e1581a6326b6fb0d8f18d69631ee8ee2a2b3d50", + "is_verified": false, + "line_number": 411 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e5814a47cd07ed2435b048b8b97f41be6cd2c9eb", + "is_verified": false, + "line_number": 412 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "72a7b76523b4eda36ffdd63ac1bcd4f52063e387", + "is_verified": false, + "line_number": 413 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3d2aeb7f6499d336ff54871823348b2bf58e7c89", + "is_verified": false, + "line_number": 414 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ca1473b861759dfa5fb912c2a7c49316897cafa5", + "is_verified": false, + "line_number": 415 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5bc665714e4b5b73c47d7e066567db6fde6ff539", + "is_verified": false, + "line_number": 416 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8f2f91164826d44904bc522f6680822bfd758342", + "is_verified": false, + "line_number": 417 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c9c956b3f172ca5ed76808abd98502a3499268f1", + "is_verified": false, + "line_number": 418 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b0c287a3b80addbf5fe7eb56f10dd251368ba491", + "is_verified": false, + "line_number": 419 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5da8ed9d858656f49131055a4b632defccffd4dd", + "is_verified": false, + "line_number": 420 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "23dd6031c249baabd4b92e8596f896bbc407eb7e", + "is_verified": false, + "line_number": 421 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c58b01cfd3befe531fdad283418fa7ac558cea5f", + "is_verified": false, + "line_number": 422 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "32a9671da53c8e3572ffd9303171adf6ae95a919", + "is_verified": false, + "line_number": 423 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "60789728174b9ee630b33b2af057e0c6a0180947", + "is_verified": false, + "line_number": 424 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "073252599d795b92b38cbad3ed849f1c5fd5368b", + "is_verified": false, + "line_number": 425 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "761bcb628d3c585abebaa8a64b04ab193f5a559e", + "is_verified": false, + "line_number": 426 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "dd230524f2606a207b426444142d01d518781aef", + "is_verified": false, + "line_number": 427 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3b459c62a8c9fe3401808103493996348ef70870", + "is_verified": false, + "line_number": 428 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "70dbcfd2a8a038e265a0d3d6379284b679226101", + "is_verified": false, + "line_number": 429 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "29398aafd66a1c4f181e540ec90a2b76dcdfe2cc", + "is_verified": false, + "line_number": 430 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4698c1c5c6daf3f88ec2768de0693d543e81c8b5", + "is_verified": false, + "line_number": 431 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cd333285b1ef33582b502f72b4a153a16a4678a9", + "is_verified": false, + "line_number": 432 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b2c2475773928e727fd3ba3969aaae40ab2b99b2", + "is_verified": false, + "line_number": 433 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c28676c2076efac73f3d01195ed463c6d7a6f442", + "is_verified": false, + "line_number": 434 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c520370cf0e7b1bcc405af46775963a7df856b9d", + "is_verified": false, + "line_number": 435 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fcd376b4fd7ecf2299b1ad018e66732a5e74ee08", + "is_verified": false, + "line_number": 436 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f9a69a2290885d929addfd83a6c1570dc7c76646", + "is_verified": false, + "line_number": 437 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5fdb5ce747a93d7048f4fd3a428653520b3efb50", + "is_verified": false, + "line_number": 438 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4ca9129303ac0d5e4e1b810e7abf90ea11a16833", + "is_verified": false, + "line_number": 439 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f83fb00877111e23db5ceb8b74255963d17c84e9", + "is_verified": false, + "line_number": 440 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "17e35c47564c0e6fefa2946f24d71618053bcfb7", + "is_verified": false, + "line_number": 441 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fab7d05454c71ae59bade022116124571421e4c4", + "is_verified": false, + "line_number": 442 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7820b9feb8912aee44c524eedf37df78b8d90200", + "is_verified": false, + "line_number": 443 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ea2a0f7323961fd704b1bad39ae54e02c9345d2a", + "is_verified": false, + "line_number": 444 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "353fcf93df94d7081d2bd21eab903cf8e492f614", + "is_verified": false, + "line_number": 445 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7149d4db2de10af66a4390042173958d5fa1cbde", + "is_verified": false, + "line_number": 446 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "85b4428454e38494e03e227d224ae58a586ab768", + "is_verified": false, + "line_number": 447 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "df83530e6fb8ccd7f380c5dc82bc8c314b82436a", + "is_verified": false, + "line_number": 448 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "106157744da44adeb38c39220b1db267c26deb77", + "is_verified": false, + "line_number": 449 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c5e67d1eed731314ac68f5e67cb7b7dba68225f5", + "is_verified": false, + "line_number": 450 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d9737cec69cbdedea1a2d9a70d7961ff76592696", + "is_verified": false, + "line_number": 451 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7aab6c9118166720f0f0e3a9db46fd59e3ed647d", + "is_verified": false, + "line_number": 452 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "500a58b74d63b4c10c8c098743d63e51a477c9cd", + "is_verified": false, + "line_number": 453 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "69a150ffbef689cc7a14cfc019e9c808b19afd4a", + "is_verified": false, + "line_number": 454 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "49d3801a82b82e48cbcc596af60be9d4b72bbd76", + "is_verified": false, + "line_number": 455 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5f3e17df79af2812cc6b5dbc211224595f8299a8", + "is_verified": false, + "line_number": 456 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5f21f46cef784459cbac4d4dc83015d760f37bcf", + "is_verified": false, + "line_number": 457 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4a91f36506d85a30ddc1a32f9ed41545eeb1320f", + "is_verified": false, + "line_number": 458 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b99666bc5cc4bf48a44f4f7265633ebc8af6d4b7", + "is_verified": false, + "line_number": 459 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c061353e73ac0a46b366b0de2325b728e3d75c5b", + "is_verified": false, + "line_number": 460 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d17d588edde018a01f319f5f235e2d3bcbbe8879", + "is_verified": false, + "line_number": 461 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "63567656706221b839b2545375a8ba06cd8d99ae", + "is_verified": false, + "line_number": 462 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "976e5ce3af12f576a37ce83ccf034fd223616033", + "is_verified": false, + "line_number": 463 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "626b3f10041c9e9a173ca99252424b49e3377345", + "is_verified": false, + "line_number": 464 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f8ba93d3a155b11bb1f2ef51b2e3c48c2723ef8e", + "is_verified": false, + "line_number": 465 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8b4879aed0c0368438de972c19849b7835adb762", + "is_verified": false, + "line_number": 466 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d35dbaf2ea5ec4fc587bed878582bba8599f31c0", + "is_verified": false, + "line_number": 467 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c09d7037f9b01473f6d2980d71c2f9a1a666411c", + "is_verified": false, + "line_number": 468 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d53d7f86659a0602cd1eb8068a5ad80a85e16234", + "is_verified": false, + "line_number": 469 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "aa9442f71f2747b5bb2a190454e511a7c62263d8", + "is_verified": false, + "line_number": 470 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f800b1fed08ed55a8e2a9223fc3939c96f3e11e5", + "is_verified": false, + "line_number": 471 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e46a4855198ba0f803471fb44a70ae5fbd2dd58f", + "is_verified": false, + "line_number": 472 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f47b48b6b7c2847fbe206253667d1eda00880758", + "is_verified": false, + "line_number": 473 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a9d98ab785981fe0f13a721e7fe2094a6e644b5d", + "is_verified": false, + "line_number": 474 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fe151aabb001edb57e3fed654d3a96e00bc58c81", + "is_verified": false, + "line_number": 475 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "77c40b5a173e170886069d57178c0074dfe71514", + "is_verified": false, + "line_number": 476 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "04e04736dcf54eb8a8ef78638b0b0412cab69e96", + "is_verified": false, + "line_number": 477 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b13a34e3be842da54436ed8ab8f2a9758b2cc38e", + "is_verified": false, + "line_number": 478 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3971f1dcb845e4eaedcb04a6505fd69e27b60982", + "is_verified": false, + "line_number": 479 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1b8ae7b1c309866e28fe66e07927675ce0e24514", + "is_verified": false, + "line_number": 480 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4c3f6543b234d2db27b1a347b3768028dd60bc77", + "is_verified": false, + "line_number": 481 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ca4ac68931f7c54308050c1b6ac9657c4ff0d399", + "is_verified": false, + "line_number": 482 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "02cca5fc17dc903feb5088abec3d2262f604402e", + "is_verified": false, + "line_number": 483 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d864c37f23cab8cff54e9977a41676319c040928", + "is_verified": false, + "line_number": 484 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e67a5309737b99b0ac9ba746ca33d6682975cea1", + "is_verified": false, + "line_number": 485 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "aef65112b27cc0ecbcfbd3ae95847e9e0fbee0b7", + "is_verified": false, + "line_number": 486 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "40d73861d177d9e22d977dd62b8a111bbf8ee0b7", + "is_verified": false, + "line_number": 487 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "71e44d4a353467958cd9be3a7e6942385e883568", + "is_verified": false, + "line_number": 488 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e1f00f9205b689ba1d025f88e948f03a4ac77a59", + "is_verified": false, + "line_number": 489 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6a9f1470e772a7f4176e8c24b7ab0e307847b92b", + "is_verified": false, + "line_number": 490 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5959a3a8554f9ce7987b60e5e915b9e357af0d99", + "is_verified": false, + "line_number": 491 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b0a791edf8675bd6a65fc9de9ba5bcb8336d1fc0", + "is_verified": false, + "line_number": 492 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "557bcf89f60a98f72b336e21f56521a4c30a2f0c", + "is_verified": false, + "line_number": 493 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "80e8a78fd29c2ac00817f37e03d9208f8fd59441", + "is_verified": false, + "line_number": 494 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "351dded8c590b80cc8dc498021fccadc972c1d00", + "is_verified": false, + "line_number": 495 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4f55ad2c0e5a697defde047e6a388c14b3423cda", + "is_verified": false, + "line_number": 496 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "20412c530d4b4c38510d9924cbfb259126c2568c", + "is_verified": false, + "line_number": 497 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "05e66772d14918a72d1b6f45872428a35c424347", + "is_verified": false, + "line_number": 498 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c61a40f7ae13f5e26ea16a6266491d58e78f6f1f", + "is_verified": false, + "line_number": 499 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b4d93dd6c2e36056d55ce3844610991eec962277", + "is_verified": false, + "line_number": 500 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c7088e4ff6e5a3bc44ca3fdf1b06847711f3e95c", + "is_verified": false, + "line_number": 501 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5e5168774b473fb9fcc31c8f5c1518eb0f9771c1", + "is_verified": false, + "line_number": 502 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a1f86c50a6626bcab082286bec7f5474e7c8b293", + "is_verified": false, + "line_number": 503 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a9fac6e3490672c5dccd35d5e6fc1cb7b1b5931b", + "is_verified": false, + "line_number": 504 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b48c69b346d712e3df1728014956ac0397c659ea", + "is_verified": false, + "line_number": 505 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8367e351d57fa775f22fc1132dd170c458799542", + "is_verified": false, + "line_number": 506 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "972953c33baa3303c488360576bdd3bae95e79a3", + "is_verified": false, + "line_number": 507 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2ef2d21dde1d6ef435fbf1b6a049f7e94a2d5588", + "is_verified": false, + "line_number": 508 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "76bf193e8f7b54ab5f0007ee41b768ee1e3ce24d", + "is_verified": false, + "line_number": 509 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e8e93efe226e4bf62b880c14bdef1507dc67c4fe", + "is_verified": false, + "line_number": 510 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "71cd9e3eb02ec34d305a55df09540b95549f8342", + "is_verified": false, + "line_number": 511 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "34c2c4351cc369f306886089967adc3fd23202b5", + "is_verified": false, + "line_number": 512 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "95a9e6645670ef390609e97a9a94ab1af8ecb5e5", + "is_verified": false, + "line_number": 513 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7a773ead4f5cbee039dd9c90bcbd2157ff9dfe98", + "is_verified": false, + "line_number": 514 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c8974d5459c5318a865674227914120b61ee7ca8", + "is_verified": false, + "line_number": 515 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9aa53dd7b54460ca4058dc1b993c61c85016c3a5", + "is_verified": false, + "line_number": 516 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5cf42e6632ac13c10b1709348bda0d36d4cc8fe2", + "is_verified": false, + "line_number": 517 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "22368f64933f9d4b20751ed12db25bdb937f4288", + "is_verified": false, + "line_number": 518 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "558145b7f5778e24056c8de59bd9d54190950f14", + "is_verified": false, + "line_number": 519 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2068d5b68ddc59653056d96e1283951282b22267", + "is_verified": false, + "line_number": 520 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4d807498a9a96f89bb538a8308d6056a2a303a0d", + "is_verified": false, + "line_number": 521 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3457741ed34d5ad7b9d04fa9cc677a72e8c47b4d", + "is_verified": false, + "line_number": 522 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "59556e4aa33301c95feb9c58d99d10a080179646", + "is_verified": false, + "line_number": 523 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2d49954101a3bd1dd5da50b8a1847f00bf4ec16b", + "is_verified": false, + "line_number": 524 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c2f14cff186baad8445fb7997c3dc863eff10ef6", + "is_verified": false, + "line_number": 525 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "dd317a7973e49de529850041e8c1ce51b0d378df", + "is_verified": false, + "line_number": 526 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9cbaaf4ff0453e81aaac598e05d8c973991c77b3", + "is_verified": false, + "line_number": 527 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "576dd6a98701c267f16a5e568f8b6a748665713d", + "is_verified": false, + "line_number": 528 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c5ce7f45e2ddbd43d244e473e165b1400ba86dd9", + "is_verified": false, + "line_number": 529 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "04a10a70b498263467ef1968fabfb90e012fd101", + "is_verified": false, + "line_number": 530 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "482928d9b3b49339bc5f96e54f970e98f84970b7", + "is_verified": false, + "line_number": 531 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "24d25f3a906f38241bd1d3dfa750631cd4b2f91f", + "is_verified": false, + "line_number": 532 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8cc46e3c020e63d10457e32b2e5d28b5c7ce0960", + "is_verified": false, + "line_number": 533 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "da272306205373082db86bc6bc2577ab85ed9e31", + "is_verified": false, + "line_number": 534 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b03284305e4d5012e7c3cf243b2942a6dab309cc", + "is_verified": false, + "line_number": 535 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f7c91578b688a0054f2c1e18082541d6ecc6b865", + "is_verified": false, + "line_number": 536 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1f009c80b8504a856a276e8d2c66210b59e8bf2e", + "is_verified": false, + "line_number": 537 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "54490e77b2c296149b58ae26c414fea75c6b34ec", + "is_verified": false, + "line_number": 538 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d5bd68de7769dde988f99eab3781025297a7212d", + "is_verified": false, + "line_number": 539 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b6161808b7485264957a2f88c822f0929047f39a", + "is_verified": false, + "line_number": 540 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1ff88fb1bf83bca472ab129466e257c9cc412821", + "is_verified": false, + "line_number": 541 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "002e1405d3a8ea0f2241832ea5480b0bf374c4c6", + "is_verified": false, + "line_number": 542 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1058c455a959a189a2d87806d15edeff48e32077", + "is_verified": false, + "line_number": 543 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cbcf1915e42c132c29771ceea1ba465602f4907c", + "is_verified": false, + "line_number": 544 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "23738e07a26a79ab81f4d2f72dc46d89f411e234", + "is_verified": false, + "line_number": 545 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "270492f5701f4895695b3491000112ddc2c1427d", + "is_verified": false, + "line_number": 546 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "88aec41eb1eedc51148e0e36361361a6d2ecc84f", + "is_verified": false, + "line_number": 547 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7b7d73969b405098122cd3d32d75689cd37ee505", + "is_verified": false, + "line_number": 548 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "79b731de4a4426370b701ad4274d52a3dc1fc6c1", + "is_verified": false, + "line_number": 549 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5b328e2a87876ae0b6b37b90ef8637e04822a81b", + "is_verified": false, + "line_number": 550 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8638f4b78c1059177cbfccd236d764224c3cad5c", + "is_verified": false, + "line_number": 551 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ef285f61357b53010f004c1d4435b6bb9eeaff09", + "is_verified": false, + "line_number": 552 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ddd64557778a6d44ac631e92ed64691335cf80df", + "is_verified": false, + "line_number": 553 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "de486a7abd16c23dfdf2da477534329520c0c5ec", + "is_verified": false, + "line_number": 554 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0618c0886736acb309b0ad209de20783b224caa6", + "is_verified": false, + "line_number": 555 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "521ee58b56f589a8f3b116e6ef2e0d31efd4da1d", + "is_verified": false, + "line_number": 556 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5b916ff5502800f5113b33ba3a8d88671346e3b3", + "is_verified": false, + "line_number": 557 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7582e85dc9e4a416aa1e2a4ce9e38854f02e8a56", + "is_verified": false, + "line_number": 558 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b24c1e8ac697a8ff152decc54d028e08dd482e4f", + "is_verified": false, + "line_number": 559 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "923eb19912270d9a7c2614d35594711272bc33c0", + "is_verified": false, + "line_number": 560 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e0331901bcbebd698248f7ba932083b13144da42", + "is_verified": false, + "line_number": 561 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f49cc7570d7e3331425d2c1cca13e437c6eb0c86", + "is_verified": false, + "line_number": 562 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6adbf5db8ff386502f09c1dbb9fa2b37600491a6", + "is_verified": false, + "line_number": 563 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "03060c922cbe09ed17fe632cbf93ed32eb018577", + "is_verified": false, + "line_number": 564 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "71cfee01fe9f254c01da3a00f2b752cf39cbe95d", + "is_verified": false, + "line_number": 565 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "542ef00d5b90d5b9935d54e3c2ebd84c59b7e7ba", + "is_verified": false, + "line_number": 566 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4073dc551871d96e2b647f18924989272ea88177", + "is_verified": false, + "line_number": 567 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0a4afe0870fdff9777720cab41c253d7a2a1b318", + "is_verified": false, + "line_number": 568 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ef7992a75c33f682c8382997f7f93d370996ee7d", + "is_verified": false, + "line_number": 569 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a265ebf662a7b28aeacc7f61bdb9ba819782fc24", + "is_verified": false, + "line_number": 570 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2bc27f59373f1a1091eef59a7d9d23c720506614", + "is_verified": false, + "line_number": 571 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e17be476c0805f05b4445d528ae5b03fa7a13366", + "is_verified": false, + "line_number": 572 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6b8281ade6ee972b53eb2e5e173068a482250005", + "is_verified": false, + "line_number": 573 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "931c912c0da827ad7895c4e6d901dc2924ef23e4", + "is_verified": false, + "line_number": 574 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ecf0566d6b6ce6c44f7f8fb56af4a8608e72f5e4", + "is_verified": false, + "line_number": 575 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "947323679dbee5d60736f14258621626565ea1c6", + "is_verified": false, + "line_number": 576 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "05d0d9d4a4e53fa7d7f3f7f8317bec618b1bfe15", + "is_verified": false, + "line_number": 577 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6b7871d101c02971f1b9f6f95f5a969c36a8483c", + "is_verified": false, + "line_number": 578 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "05441b75c971d39d04a13b168a1b0f2c4aeb2114", + "is_verified": false, + "line_number": 579 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c9d8088c151b2a7c09676ed3fd9de0fddc490b30", + "is_verified": false, + "line_number": 580 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "07eb4a0a546de02a324550e1e1b66e306bd3f706", + "is_verified": false, + "line_number": 581 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "baa791026849604561c1dd00787a9caa598abae1", + "is_verified": false, + "line_number": 582 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8d49f6f1c3e27bdfe580816e609cab2c9ca00cc6", + "is_verified": false, + "line_number": 583 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "926d8707e359f80554585f4eca9f90b6021d3327", + "is_verified": false, + "line_number": 584 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "68982f7b9ff005fdd9d27fdf5ef5d37c9c611f58", + "is_verified": false, + "line_number": 585 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cc95ebd65aeae6dd8e774a1e90798079211554f3", + "is_verified": false, + "line_number": 586 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a76b151ddad3198ad11b962ff59170a761baf0c6", + "is_verified": false, + "line_number": 587 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8a59e160326a76b11b5fc26cfa592cfdf158fd49", + "is_verified": false, + "line_number": 588 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "784d839853e3c0966a262a542b36e259aa00e8df", + "is_verified": false, + "line_number": 589 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fbba9f2d7a916915d9535d71c785ba4491a3b733", + "is_verified": false, + "line_number": 590 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f290b3c4f8aacf898285d68358fcdffe6baf1e2e", + "is_verified": false, + "line_number": 591 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "14f10baeacada2cc41047108f58b200c6026bca3", + "is_verified": false, + "line_number": 592 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e583513a87e1f5b242e81fe86427da78faa63ede", + "is_verified": false, + "line_number": 593 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "391f7646f98c7bf123453c90b372ac45f4ea35fc", + "is_verified": false, + "line_number": 594 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "da2e4b9e552f03c36dcf672072f1d6cda917672d", + "is_verified": false, + "line_number": 595 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9c4a1dc6277cda2374666e447dceb663ac39c62a", + "is_verified": false, + "line_number": 596 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "469b9dfc4d3851edbd0c27f80b4b36c04ec52f5e", + "is_verified": false, + "line_number": 597 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c09b72b36f9e813bdfcf32f58e070a4fe98f4092", + "is_verified": false, + "line_number": 598 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6ee9dd6fd0333921cb607f274d3bfc04187bfac5", + "is_verified": false, + "line_number": 599 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9ccd2b0b5ae426a9c581621270630389e40d08e0", + "is_verified": false, + "line_number": 600 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "881f2e047f571e1ea937638ea2598581e92e4900", + "is_verified": false, + "line_number": 601 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1e5acdb5b4e970fd7be282ae31e3195d24aa98b9", + "is_verified": false, + "line_number": 602 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8b1564bd262285220c1f4cc7ba034b14836d3496", + "is_verified": false, + "line_number": 603 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2f79127d99b576c55a920ce8195d9c871296dd79", + "is_verified": false, + "line_number": 604 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0aa38b942875102db24b7ce22856fbce4dd8bca5", + "is_verified": false, + "line_number": 605 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "62f537c1449b850f2f3b66c200a85fff4e4ce6c3", + "is_verified": false, + "line_number": 606 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2f83b93fddaa24f65acbea08be3fc0b2456f3ea5", + "is_verified": false, + "line_number": 607 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0d3a416a9b47316629342cf32e4535bd5de367bd", + "is_verified": false, + "line_number": 608 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9d018c03a51c7405ca8de9dafde5fb12bf198544", + "is_verified": false, + "line_number": 609 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0e20193d744f60ef0bcd425ce45d19c73f5ff504", + "is_verified": false, + "line_number": 610 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a2ad69c925092acbbffb97ea70f2c87985fccc8e", + "is_verified": false, + "line_number": 611 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "997ad02ee3779b7ffcd11b8e19df0afe052b66f6", + "is_verified": false, + "line_number": 612 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "46bc2f629e8b64d43d23cc3429346583a7319bae", + "is_verified": false, + "line_number": 613 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "10e4c7043154dc91c0a002d88fe23f356370b80b", + "is_verified": false, + "line_number": 614 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b002194b0535528d6a24fa7502e7f76b935afc8d", + "is_verified": false, + "line_number": 615 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "43728be0f14a9413b4bebd1d22562002cbd07c2d", + "is_verified": false, + "line_number": 616 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "172cb154f89a4168cbbcc48186b6f5a2b113e893", + "is_verified": false, + "line_number": 617 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1df3a86d99563dd6124a197f28a21f1412fd438b", + "is_verified": false, + "line_number": 618 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d44276da69dfa1c411354e75dcda7d75ea6d605a", + "is_verified": false, + "line_number": 619 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "39c326b627e45a8ae4192ac750d38cda7fa55d79", + "is_verified": false, + "line_number": 620 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3c24ec7ee3be457039f1e46a4b437065ba4c4130", + "is_verified": false, + "line_number": 621 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "98b18d68b753e89b1b0c8b4ce575011326b0d2c6", + "is_verified": false, + "line_number": 622 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "95dc0c323f31332cea1b74ce77fe4af9fd0d5c5c", + "is_verified": false, + "line_number": 623 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cb0763f8b448f29101b230bf3ace6a9fc200be9b", + "is_verified": false, + "line_number": 624 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f746e396467de57bda19eb1fe555bc43b8773bf2", + "is_verified": false, + "line_number": 625 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d0878fed2da5ef58888639234936d2df27aa1380", + "is_verified": false, + "line_number": 626 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3010d3905af38cd8156a527f4d531f34c46c39a7", + "is_verified": false, + "line_number": 627 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4da40200c07f4e433a8fafc73d0567d024606752", + "is_verified": false, + "line_number": 628 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5415afc22a2c5f94eabfdadbccbe688b42341335", + "is_verified": false, + "line_number": 629 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "86f3350f28fa5af153e0021bd0f95610f50f0aa6", + "is_verified": false, + "line_number": 630 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "84541393133a5662b9b265024ec3edc3545c3802", + "is_verified": false, + "line_number": 631 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "05830a12efa0b065e55a209e1de1b7721546f2a1", + "is_verified": false, + "line_number": 632 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9e7dabf3cda36b3ab3b57fefca047d5271cb674e", + "is_verified": false, + "line_number": 633 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ef05a15dcbe9f43b719bec0f2dc74d6870cab938", + "is_verified": false, + "line_number": 634 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "35c2e8c0d488a1e0e7f4a721cb9fc5af4f91423b", + "is_verified": false, + "line_number": 635 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e4ad4eb707a0dd2b2ef876c8001f966f51f524d9", + "is_verified": false, + "line_number": 636 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f99b3161abeffa11c6be076150cccd8221fcd703", + "is_verified": false, + "line_number": 637 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4b1647cf6264941baa9ba28fb792cd82e06217cd", + "is_verified": false, + "line_number": 638 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a62b12a0505128c7094f73376a7b32b6896a8602", + "is_verified": false, + "line_number": 639 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8ac29efbb3b877bfdebdcba31d3528f2cd0809ea", + "is_verified": false, + "line_number": 640 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1aa7fb76951a195b27333fc8580b44a57e98fa9e", + "is_verified": false, + "line_number": 641 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3a29474a5fbc845f27b5bafd16ddbb4d7defa2d8", + "is_verified": false, + "line_number": 642 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b1c3e50ce69aa2cc899da1df5a55338242567ab4", + "is_verified": false, + "line_number": 643 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "841f3550b43d66f5f3138d26990ffbb161a3b827", + "is_verified": false, + "line_number": 644 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "80cfd7fb194ed700b9c0e4970bf4e47cc75257a9", + "is_verified": false, + "line_number": 645 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bc4508d089cc2186f7bc5bb14ccddeb772a04244", + "is_verified": false, + "line_number": 646 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "01b35bc3e5deb295f2dd6c43f2abae453ed7a20f", + "is_verified": false, + "line_number": 647 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fa3e9c6424f3bc18eb13d341ed64c132b4f8c929", + "is_verified": false, + "line_number": 648 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b13663ab4e5621994f9bb7909a69c769c343e542", + "is_verified": false, + "line_number": 649 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c06f704f3a0cefec9a28623bda60f64f8c038bdd", + "is_verified": false, + "line_number": 650 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a2eadafda305962f6b553a99abf919d450cc4df2", + "is_verified": false, + "line_number": 651 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "43c8cab46cbb8319ee64234130771cb99a47e034", + "is_verified": false, + "line_number": 652 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1cc137a3c9d41ba4b30464890ae6a6f08c7ba92d", + "is_verified": false, + "line_number": 653 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b43d13f2dcc835cd55d4a40733b22d07fd882167", + "is_verified": false, + "line_number": 654 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "78d7945d58ea7aaaf4861131b57b5fd4c308437f", + "is_verified": false, + "line_number": 655 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6b2f6f1c7b573efc39d8bd013cef20e89e011276", + "is_verified": false, + "line_number": 656 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d92bdf2e2be4bfe8acb991a3cf2b0f23da624825", + "is_verified": false, + "line_number": 657 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e8b7c1a13d23facf8589088b2de85f851ad53a82", + "is_verified": false, + "line_number": 658 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6d3e58158529f32b5ead6e3b94c7ca491ef27ed3", + "is_verified": false, + "line_number": 659 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "800ea2592a27f8b38f0a18253dd49f97b65a3aad", + "is_verified": false, + "line_number": 660 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0b13798c29f5879b119c807ab7490d35a0342cef", + "is_verified": false, + "line_number": 661 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0a9a21ca4e9aa08b2b5fbe769bf6afb1deb8da91", + "is_verified": false, + "line_number": 662 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "183877effc366e532c7937f2f62f7f67f299bd36", + "is_verified": false, + "line_number": 663 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e245782b2f99805ed35dab1350ac78781ae882eb", + "is_verified": false, + "line_number": 664 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9b619bf6db9561f29c4cc75e26244017cc97d305", + "is_verified": false, + "line_number": 665 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "377469b721f5e247f1ad0fee41cca960c49a1fe9", + "is_verified": false, + "line_number": 666 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f2cb896b3defe96fd6a885f608e528704b40728c", + "is_verified": false, + "line_number": 667 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7643925d0ad2652497482352b404604985b0f41e", + "is_verified": false, + "line_number": 668 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ce5594ef11357e35de0d439687defce446dd0f66", + "is_verified": false, + "line_number": 669 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "65dde318bca6689643335f831444daf0156cc4e5", + "is_verified": false, + "line_number": 670 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "143c3d69803143aa5d40372c0863df82b176b41c", + "is_verified": false, + "line_number": 671 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c32dcbc4225f3183d5f5a5df78ec5ae9afb38968", + "is_verified": false, + "line_number": 672 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cfa29e11ebef38d8e08fb599491372f6404e6b6f", + "is_verified": false, + "line_number": 673 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3d91d5f1054fc768cf87c6b19d005e6d3ccbc2f3", + "is_verified": false, + "line_number": 674 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2d6bffd0f0c9cc4790eebc50b6a56155c3789663", + "is_verified": false, + "line_number": 675 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "64110bdd2bf084ec47040ce8b25fc13add2318e7", + "is_verified": false, + "line_number": 676 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7f6bf6522a85f71bf4b93350ec369683759735f9", + "is_verified": false, + "line_number": 677 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3d53588bd3f314ef6e7bf9806e69872aa2ce1aff", + "is_verified": false, + "line_number": 678 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d5efc1772557e4bff709c55a59904928b70ffe1c", + "is_verified": false, + "line_number": 679 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b8e46dd05b23c4127cca0009514527e49b6c400f", + "is_verified": false, + "line_number": 680 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "58d30b123d121316480c37ae6222d755dc9144ca", + "is_verified": false, + "line_number": 681 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "66a2abf99d8a4a38e6d64192d347850840a580bf", + "is_verified": false, + "line_number": 682 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d434fa5b419700a92dc830da1c3d135e8ad0b3e2", + "is_verified": false, + "line_number": 683 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ee251356a77d3ec7b7134156818fac73a2972077", + "is_verified": false, + "line_number": 684 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "239cb830c56b6d22115d2905399f8518bd1a5657", + "is_verified": false, + "line_number": 685 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2e6143570c020503a4e1455ec190038b82bedc19", + "is_verified": false, + "line_number": 686 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9107d00af85969940a45efb9eccad5e87f8a87f2", + "is_verified": false, + "line_number": 687 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5a5d1ac75eb4c31c7e9650ac70bdc363a9b612c5", + "is_verified": false, + "line_number": 688 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "05a99938fdc58951b4a6a756c8317050e3f5d665", + "is_verified": false, + "line_number": 689 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "67ccbdebe626ab7af430920c1d0d6ec524bdc4f9", + "is_verified": false, + "line_number": 690 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "71fd81160a50c9d47b12b4522c5c60f2fca72b6a", + "is_verified": false, + "line_number": 691 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f150f2f043f66a564ed3b3fb2f29c0636fd2921a", + "is_verified": false, + "line_number": 692 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a1140dfe90f9a5da45451945b56877c45cb36881", + "is_verified": false, + "line_number": 693 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7533bea169a68e900d67a401cac35a7aade18d92", + "is_verified": false, + "line_number": 694 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f0dd83a2a8d653ad8b30fefcde5603b98bf1ca66", + "is_verified": false, + "line_number": 695 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "21334df57a3a5c6629c12f451eeb819a2b37b42c", + "is_verified": false, + "line_number": 696 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "99f04da5b8530b3eb79e3740fece370654d3c271", + "is_verified": false, + "line_number": 697 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c2dfd7c77cafb9193a0e77a45d14ccc1498816fb", + "is_verified": false, + "line_number": 698 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5351e6405ba12ea193b349e8b2273201bb568404", + "is_verified": false, + "line_number": 699 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cc215cb1a47a674d2b0c1fb09df87db836ce8505", + "is_verified": false, + "line_number": 700 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3078af7fa82e149420b97ff56fff9f824387b35b", + "is_verified": false, + "line_number": 701 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ac0e1537926b5bbd543ad3e731959a0bad451c73", + "is_verified": false, + "line_number": 702 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a6da4e82d314f4ca0bf7262a78875b0b6edc30aa", + "is_verified": false, + "line_number": 703 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e08c74c3fbf412c2d4f330b0414f1275679cb818", + "is_verified": false, + "line_number": 704 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7bf9ae1b766cb0b9a5aa335a0103518d7be00daf", + "is_verified": false, + "line_number": 705 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ec844560c5f208fa8723c1700f6e86b8e7ffed04", + "is_verified": false, + "line_number": 706 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6c133b025f53327eb652d2a1ca576dfe58eef1b4", + "is_verified": false, + "line_number": 707 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3dc21b9f6f63b73a241d900e379a3c7094341f8b", + "is_verified": false, + "line_number": 708 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1a012b2bf61ee9874d5af73df474051c0d235ecf", + "is_verified": false, + "line_number": 709 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b0ebf0b521ec6e6e696f9be2fe4e1845876d57ab", + "is_verified": false, + "line_number": 710 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f0a5d3ac0705186e25effb02649df87361b8c67e", + "is_verified": false, + "line_number": 711 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "385ecb845a1d5d43766d568b466d1dd237a81980", + "is_verified": false, + "line_number": 712 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "18d0416b8ea44ce305b214380de978cef27e8603", + "is_verified": false, + "line_number": 713 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "89dca45aa9146b8a31236fd77001c02769dceb60", + "is_verified": false, + "line_number": 714 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "30acd4c1f4a878883c654846b8f3c5a6ab807285", + "is_verified": false, + "line_number": 715 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7d3229ff5e754c72a8b2072d3d7a5e00749ece9b", + "is_verified": false, + "line_number": 716 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e6da9d65dc0cfb42b86ae8f9b7c1d5fe79b4a763", + "is_verified": false, + "line_number": 717 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9c85908a1bfd5f2a7337f812c68f2ce8dfbfd65e", + "is_verified": false, + "line_number": 718 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4000341e5c04854eeca9fe7537dfddfdbb7c785a", + "is_verified": false, + "line_number": 719 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ef23e2969a46edf410fab2c69d1b29b2a65f57f9", + "is_verified": false, + "line_number": 720 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4902863163e24fa9f172e61808385de2b9ee3099", + "is_verified": false, + "line_number": 721 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "31efc8d3bba9c8f66b3f54bc146443732ac15c2c", + "is_verified": false, + "line_number": 722 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "263deaf83b359554fc9dafca8e6622ece44cf75d", + "is_verified": false, + "line_number": 723 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ead7409fe5b86813e3609f7fe6e13b8fc4b0b9d6", + "is_verified": false, + "line_number": 724 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7b0d884d6cdc64a613cf3e887395d875ff738c3e", + "is_verified": false, + "line_number": 725 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fa0a0a999cb067eee81673f3d2de8bfd96a0d14c", + "is_verified": false, + "line_number": 726 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0db684d862dfc8427e8f66adb62f33fcdc9f3de8", + "is_verified": false, + "line_number": 727 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8794a8121832fd31b1871d2c5d4b00af07779b0c", + "is_verified": false, + "line_number": 728 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d6070805e7a6c25dbe13a540cbc0f16a89055e7e", + "is_verified": false, + "line_number": 729 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "56b3e8e6d14b9b459bf055900784e8aa31c306c2", + "is_verified": false, + "line_number": 730 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a4d6976637c19991da48707bf35b3cf2ded4c2fb", + "is_verified": false, + "line_number": 731 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f714e448a86a46baf2128d81014e554874f0d4f6", + "is_verified": false, + "line_number": 732 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2b03a5eb51085de41df415881ef1d425f20f9e05", + "is_verified": false, + "line_number": 733 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "99fa7285e15d91ac3047b95ddb475d339c7afc7b", + "is_verified": false, + "line_number": 734 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4a9880aa478dba526c2d311ae17578711d0f9426", + "is_verified": false, + "line_number": 735 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0cd512ccf176189c7bf36765b520d8ec2ddeade0", + "is_verified": false, + "line_number": 736 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2eb8822459b9db479752d12f62dec094ab68fc55", + "is_verified": false, + "line_number": 737 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1aab694ebb334a12ccd22baa0044a3b058db67f9", + "is_verified": false, + "line_number": 738 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ce29f8616e1c62e54a8f0b39b829d9bd7df5721c", + "is_verified": false, + "line_number": 739 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c099a1c5f639e647bda5961d9c51cc158790ff3e", + "is_verified": false, + "line_number": 740 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "78dc2b71e3614e4e802c4f578a66132ea1ae0be8", + "is_verified": false, + "line_number": 741 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0befb6d3255080ce4d051a531fc1fedb33801389", + "is_verified": false, + "line_number": 742 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "087447f269677e0947da157a5bc0bb535c6c7759", + "is_verified": false, + "line_number": 743 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8911e3aef563e1481305a379a083f7616d57cd08", + "is_verified": false, + "line_number": 744 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2846a4bb4af2826a787fb0d8a0e7342c404a1cd1", + "is_verified": false, + "line_number": 745 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3364317b783250007fcee5bcddf07b2006752ad3", + "is_verified": false, + "line_number": 746 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e1a4444540434bc0ba51a8b5e6540e82d4b17f4f", + "is_verified": false, + "line_number": 747 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f453d1221dfbe308b5c71029f5cc2fba020f2c6a", + "is_verified": false, + "line_number": 748 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3e4231678403aa61b0f4f6719081016d579fa3e4", + "is_verified": false, + "line_number": 749 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a64b90a0dd1a214d6c65a4078437eab4ada65a32", + "is_verified": false, + "line_number": 750 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0433fe0f97f7a354a3ed06d6a8a77c2f1983f947", + "is_verified": false, + "line_number": 751 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a21195a2dde808b7cff35695396ecf7699125a53", + "is_verified": false, + "line_number": 752 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6547a05519f26198981f500b703d36443958ad14", + "is_verified": false, + "line_number": 753 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fbb8441f5e8e9b911cc42a025c856470784d89d1", + "is_verified": false, + "line_number": 754 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6378293ead806f554612c82fddf04ea8fb1ab2cc", + "is_verified": false, + "line_number": 755 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3272309f5c986a45cd892d943c5bd5af5165ad70", + "is_verified": false, + "line_number": 756 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1c79d15ecac42472241726cbae8d19bb820f478b", + "is_verified": false, + "line_number": 757 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a868da324435f3b1f32bc12bbd3171e9d62fcdca", + "is_verified": false, + "line_number": 758 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c56de5d2c763355c7a508dec8c7318e0c985dfec", + "is_verified": false, + "line_number": 759 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "258e19436174463d0e1b8066eb8adfbf79f78b32", + "is_verified": false, + "line_number": 760 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "112d96e04bf661b672adc373f32126696e9c06fe", + "is_verified": false, + "line_number": 761 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bdeaea4ca3484db9e8b0769382e1ba65b62362b3", + "is_verified": false, + "line_number": 762 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fff367064d95bace4262a1b712aa5b6fb2a821d6", + "is_verified": false, + "line_number": 763 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e16dcae490d17a842f5acd262ca51eae385fb6af", + "is_verified": false, + "line_number": 764 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bad941c81722b152629cebce1794a7fd01b85ebc", + "is_verified": false, + "line_number": 765 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "65e6aaaad1727c35328c05dd79fb718d5b1f01ce", + "is_verified": false, + "line_number": 766 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b7ea9b9d7d8c84eeeb12423e69f8d4f228e37add", + "is_verified": false, + "line_number": 767 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "42bea72c021eedb1af58f249bdae3a2e948c03fa", + "is_verified": false, + "line_number": 768 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1ddcb2cad21af53ad5dd2483478f91f3c884cea0", + "is_verified": false, + "line_number": 769 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e72ad6e31d1a19d6b69a1a316486290cb2c61eab", + "is_verified": false, + "line_number": 770 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8ca884c8fb24ecd61300231b81d1d575611cda07", + "is_verified": false, + "line_number": 771 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5754688edbb69be88b9c0ea821cc97eada724c14", + "is_verified": false, + "line_number": 772 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a267e65960056589647f075496fd3a6067618928", + "is_verified": false, + "line_number": 773 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ad3424f420bf25442aa9df96533852d29eac12a9", + "is_verified": false, + "line_number": 774 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8a5a26db2b7bda6268a9250808256e08d2a62262", + "is_verified": false, + "line_number": 775 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ff90aa934268bd629b33708b7db9a10b5f0bf822", + "is_verified": false, + "line_number": 776 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9294697fb9b36decacc26c3c33c3d186fc128f82", + "is_verified": false, + "line_number": 777 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8dfc552d4f52ed53ccb13c958117ceba6c8038d8", + "is_verified": false, + "line_number": 778 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "49c6467fa09d3052faaa1a369ebd226234db892d", + "is_verified": false, + "line_number": 779 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f2a450ffba5b1fdb7f016e4add7035ef6ba2df77", + "is_verified": false, + "line_number": 780 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "79a4f5a8804b9a94b5c4801700f08a2cdef54662", + "is_verified": false, + "line_number": 781 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1baf161ffff392357bbfb8e38d95c8c2f79ef6a2", + "is_verified": false, + "line_number": 782 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "840365ccbf5f23b939e8ee15571bdb838a862cb3", + "is_verified": false, + "line_number": 783 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0e50db71a57f0d0016b2abeaf299294c3bb4fedb", + "is_verified": false, + "line_number": 784 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b108976e96b8ce856b59b4f73cc6caa2555310cf", + "is_verified": false, + "line_number": 785 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "474f1a83c946ec223093d46f5010ff081f433765", + "is_verified": false, + "line_number": 786 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3740691aa3a788e71b7b74806dbcae3009b4f7fb", + "is_verified": false, + "line_number": 787 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c11bddda98ea121b857aabafbcdf75307a18bc45", + "is_verified": false, + "line_number": 788 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3445e70b7f8f3d381c21f6ed88c28c0db545662e", + "is_verified": false, + "line_number": 789 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c368482da3144e79d4f4f8063bdcfc85b1318ca1", + "is_verified": false, + "line_number": 790 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "470e734260c3e67dd19fca5ef32dbc6ce863dcbc", + "is_verified": false, + "line_number": 791 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0dc9bbedd1b90674d2d0c81563b1b59e82f901b6", + "is_verified": false, + "line_number": 792 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "49bbe143a0a5d2d81eaa04b0ae5f02b89b2e60ce", + "is_verified": false, + "line_number": 793 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9e009fcc53e8ae16ac2cd1c31945812a8b3cb1f8", + "is_verified": false, + "line_number": 794 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fda8ab7b8d8d0e3d995648f21cb97fb6a4371008", + "is_verified": false, + "line_number": 795 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "15ca6383ad968b3f606e5600e0ee5765cc61a223", + "is_verified": false, + "line_number": 796 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c901600adaae1fae9b24fe869cc11364e07651c1", + "is_verified": false, + "line_number": 797 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2a6968448cc0520a44b0fc8eac395ef9047a0ba9", + "is_verified": false, + "line_number": 798 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e58e1397cdedc8cedfc10472af62b0e24b7d90bd", + "is_verified": false, + "line_number": 799 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3f1a00fc8f814e6e5bfbb1b38a44318af25c0149", + "is_verified": false, + "line_number": 800 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "23887318ac83e9f3953825ada42ec746364c362a", + "is_verified": false, + "line_number": 801 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c5ebf6b1cd6af76112bb20fb2ef8482bd95088fe", + "is_verified": false, + "line_number": 802 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7f2b7465a347061ef449ed6410a3fccb7805775a", + "is_verified": false, + "line_number": 803 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "35c7486eb3aab3d324e34c9f2e4149c0833e7368", + "is_verified": false, + "line_number": 804 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6bafab58fdb0248c4e31eb58b8b99d326a5fec77", + "is_verified": false, + "line_number": 805 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b5b8f84bebc143026521dd3dec400fc319c8f07f", + "is_verified": false, + "line_number": 806 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "dc663ea73f635724beef79b22fe7c40bf812907f", + "is_verified": false, + "line_number": 807 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a5f5ebcab108b702af3122c9dec85e4aed492ba1", + "is_verified": false, + "line_number": 808 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "24826ebb519bed6f61af4c6dc3008fea3ca87c62", + "is_verified": false, + "line_number": 809 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f5e2d1ee2fc9d16703269c4942a406effa9208ae", + "is_verified": false, + "line_number": 810 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f28e36af3d92643a5ca738f66b0f9b0f0906a02a", + "is_verified": false, + "line_number": 811 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "19c8b107d6fdc4b807d831334b433ba0f051ee3d", + "is_verified": false, + "line_number": 812 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fd640c778ecdae75e71f490588436bad8551dc0c", + "is_verified": false, + "line_number": 813 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b93f3e5a8f7937290e368015ec63b9faa148a091", + "is_verified": false, + "line_number": 814 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b665cd0e94b8b690e5edb8446039bc20bd4edf8f", + "is_verified": false, + "line_number": 815 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e3482306ec339930b1f4d60e13c4006b9ac9949d", + "is_verified": false, + "line_number": 816 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a2c8590320283074b40e9c0f05af26ac1671580f", + "is_verified": false, + "line_number": 817 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e30ee01ef2baf677c7592e2a339d1d4c5f3b3053", + "is_verified": false, + "line_number": 818 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b8495b9cd806dbee2e7679dc94c9ca6b675107af", + "is_verified": false, + "line_number": 819 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b175eb842c0cb4c4d2b816c80b2cfea2b81eca04", + "is_verified": false, + "line_number": 820 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7cca142d68498553dd9cd10129b64f8f6b1d130d", + "is_verified": false, + "line_number": 821 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "62709b572d8c7952674f5ca8c807aa12346d8219", + "is_verified": false, + "line_number": 822 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "260d9d5da81fc235a36890dc1df9b0b93e620051", + "is_verified": false, + "line_number": 823 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f45c83b63c8fb4ee062a5649950ed25963f72269", + "is_verified": false, + "line_number": 824 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "94ab5caccdc141879f89dff48b17d633cce7c6ae", + "is_verified": false, + "line_number": 825 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8a67f56357e2ab075ec362aa17de81e09829dd1e", + "is_verified": false, + "line_number": 826 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e47ea7fc498253e920531b2f9440df22b65b4bfb", + "is_verified": false, + "line_number": 827 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "608bda7f1c9bbb04cbcd94fbef60907b34e5107c", + "is_verified": false, + "line_number": 828 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0ef4f672781b0c8008104b4833da99758a37c2d5", + "is_verified": false, + "line_number": 829 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b84c442c7f733ee0416ab3e451b3acd4fe708d11", + "is_verified": false, + "line_number": 830 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "af40c42cfab503d271744c98fa2d912f75fe1192", + "is_verified": false, + "line_number": 831 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "088fb0ba102fd16911bc92ecad1e96d6b9d7c6e1", + "is_verified": false, + "line_number": 832 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0205ce524bdf9689abb764ade3daff0a75a9680b", + "is_verified": false, + "line_number": 833 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ffb06eac178944f7cd519dffee1bce92b7b39de0", + "is_verified": false, + "line_number": 834 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1f4fec8780ce70e3b189b9ef478d52cb508ab225", + "is_verified": false, + "line_number": 835 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2084a2c1c5c015caab2036e77747bc1bc8da1b5b", + "is_verified": false, + "line_number": 836 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6d61e0dc6e9e3786a038ce41b2645ffa55ad34dd", + "is_verified": false, + "line_number": 837 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c2eedfdfb494f1da2837db4fe02a349f6b83e34b", + "is_verified": false, + "line_number": 838 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cb90f645f60eb596ccd816c2c9cad6df1da2f7af", + "is_verified": false, + "line_number": 839 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3714fb2f7dd6cc5392456fa413a7a6ba3cceca16", + "is_verified": false, + "line_number": 840 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a2b9353093261900009e92216ad07fb712d3aeef", + "is_verified": false, + "line_number": 841 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "38abeae07fcc9d78f57c915f7ec1ef448928c8d7", + "is_verified": false, + "line_number": 842 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4aab4807666815ca001aecb2c98150fa4e998a4e", + "is_verified": false, + "line_number": 843 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a3c2b5f078ce6bd677972296a39a9b6f476ad8fb", + "is_verified": false, + "line_number": 844 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "76cb76a7b46fbebf5a3d38b4f7507f5f6f966bbb", + "is_verified": false, + "line_number": 845 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6216237ea7f4271573ad9257b04f29624b32d067", + "is_verified": false, + "line_number": 846 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c46a24ae59ed9570cd0eaaf744cbdac682131822", + "is_verified": false, + "line_number": 847 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c7f4bfd365cfeda78938b48c174e84c476e0b121", + "is_verified": false, + "line_number": 848 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "95306491cf2bf602d32f153877fa3668188e89e5", + "is_verified": false, + "line_number": 849 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0a86977039aca715fef41f075a006d08913e2f9e", + "is_verified": false, + "line_number": 850 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "98ab4de33fb607da8c4bd3e6dcde7fc48be461cb", + "is_verified": false, + "line_number": 851 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c8a681b8468ceb7be04c81c9531fc1b76a73a979", + "is_verified": false, + "line_number": 852 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c1f2b4dc85c69f47bab7f0c95934abeb21241dfe", + "is_verified": false, + "line_number": 853 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d2c65d95022c1689e545f27bdb9125abfa65014a", + "is_verified": false, + "line_number": 854 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5334888b103ace2ac1628b453dfba0374aa21563", + "is_verified": false, + "line_number": 855 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "db870d53e2dbee8610b39a18017bf2e95d9b6a1d", + "is_verified": false, + "line_number": 856 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a874dd47f5e9d721212644df27395f9d0455bc7b", + "is_verified": false, + "line_number": 857 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "24304e79b441e1689f7db990cf1380e8ea172237", + "is_verified": false, + "line_number": 858 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ed52cda8715ae3d4b24fdea5e451cf0610003eb6", + "is_verified": false, + "line_number": 859 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8b5757852d0c36e7217daf8504004e6c85212d7a", + "is_verified": false, + "line_number": 860 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "85d089a4858f5681d1828bc1d67eb3f19bbeba6f", + "is_verified": false, + "line_number": 861 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "80dbb757c0b7fb948816886168d397b09b317e0b", + "is_verified": false, + "line_number": 862 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a45b519f89630194e67ed91782425b2095083fcb", + "is_verified": false, + "line_number": 863 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "297a0f9e38f85884d7d6beb518b33f8f35349004", + "is_verified": false, + "line_number": 864 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2200c973aaaaa2f1201604176787152091904d25", + "is_verified": false, + "line_number": 865 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "07d4fef177f006578f4d37289137d90727a5fa86", + "is_verified": false, + "line_number": 866 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d68f0a891f53a354bff2a9002ce0e3c60236d0fa", + "is_verified": false, + "line_number": 867 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d101c2cdae39ce8adcf30a777effd4be14b07713", + "is_verified": false, + "line_number": 868 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7e5670956a5ca012cbfe2ec89841595ada7ffc4a", + "is_verified": false, + "line_number": 869 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d58782068176eeb0987b1850ec9b1e54764c5947", + "is_verified": false, + "line_number": 870 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d779f72f04dbb76344f4c264d19bba7242e25e90", + "is_verified": false, + "line_number": 871 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "99c57a64facfebfb9e41dfae591af95633715986", + "is_verified": false, + "line_number": 872 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a7a97bb3f0508c2ed46ad81ed8cc53ff7469edc5", + "is_verified": false, + "line_number": 873 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c8b289fb0554107bbd07c43f462a87e7b929a529", + "is_verified": false, + "line_number": 874 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3c092d1639246d4ce9167319e729dc39d1bb3793", + "is_verified": false, + "line_number": 875 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c34cc18e2fb77269d8f33529c23d4ae2a55b873e", + "is_verified": false, + "line_number": 876 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "57562f3034b2895272567bccdb4476ff4ffb387f", + "is_verified": false, + "line_number": 877 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e75aa06fcf9eb16ce4f765009f73bff5998b4d82", + "is_verified": false, + "line_number": 878 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "561dd2c1798724b1f7730df97cf07b16f27db369", + "is_verified": false, + "line_number": 879 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "548d01127e6414ebc307a1da07e1814eb28d9c43", + "is_verified": false, + "line_number": 880 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d356fdfdeab6a77435a395a60e99e988f3c7e85e", + "is_verified": false, + "line_number": 881 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7d850865aadf5851746b420805c2d1a859af11fe", + "is_verified": false, + "line_number": 882 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a2221c705b602dee5ab23632133b47700d5a1dd2", + "is_verified": false, + "line_number": 883 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0d4e54941ee10299f1064634fffb86e4b7bfd005", + "is_verified": false, + "line_number": 884 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "589f88e962e41fc2e6691090dc335a20c7520348", + "is_verified": false, + "line_number": 885 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0d9ea7340e4afb03c7564f911883428d4d0e5e01", + "is_verified": false, + "line_number": 886 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "86525dece15cc1ed811c029ebae7ce496af598aa", + "is_verified": false, + "line_number": 887 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f3add200e410ee751ec2e65f4c00d5fe546a2b46", + "is_verified": false, + "line_number": 888 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "89588ee266a0fee04980b989461d344c91f917cf", + "is_verified": false, + "line_number": 889 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c02f12006740778cceb3e14d10eef033650f0905", + "is_verified": false, + "line_number": 890 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "16d1c52b661852a0a2d801d14e5153cd2245854a", + "is_verified": false, + "line_number": 891 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bd48b759e75395bd491df6811d82ada954b1a8f8", + "is_verified": false, + "line_number": 892 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f9d8d2bcc1f978b39c12409b8bd5c35e1fd3caef", + "is_verified": false, + "line_number": 893 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bd7006183d8fc08da5a29edc7dce2833b7d67c29", + "is_verified": false, + "line_number": 894 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b4f7d597cf8d0e4a8bdd47b462ffaf7f753906f6", + "is_verified": false, + "line_number": 895 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "10d3f4cb2e16143374e3db5c6828184d97cef711", + "is_verified": false, + "line_number": 896 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6045891b6aed86c8d19a6aecd12b2df1a32e3921", + "is_verified": false, + "line_number": 897 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f09ecd7a19945614bd73b5be04331b691d2bc030", + "is_verified": false, + "line_number": 898 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f0cf1445d72e773713d17ed9ecbf6f805206cc80", + "is_verified": false, + "line_number": 899 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "34cba93b5c522de558e25672a78a5d75028a02fc", + "is_verified": false, + "line_number": 900 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b08833d65be532022a038652bffe2445f840479f", + "is_verified": false, + "line_number": 901 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ed24a43ca6ed9df8d933b25418889701bdf1492d", + "is_verified": false, + "line_number": 902 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f081d33d1093e834b3fe9e678720c07c7dfbaef7", + "is_verified": false, + "line_number": 903 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fbd0b56627efce28202a4ebc927ed09fb338cf24", + "is_verified": false, + "line_number": 904 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8f79ecdca6ff2d1240ab55db0395f3babd8e0cd7", + "is_verified": false, + "line_number": 905 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0d42925b4018649775d5543b6e5ccd1096eea954", + "is_verified": false, + "line_number": 906 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5564f26e8a7f58c2e525d04261557b54ccb3eeae", + "is_verified": false, + "line_number": 907 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7e61f7e6fbbccc54b49c5932dfee56e4d05d8bb6", + "is_verified": false, + "line_number": 908 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d28c82f5235be5773d7b556004493d197863e47e", + "is_verified": false, + "line_number": 909 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ead7a2d8ba1098da1203103338f6077d384ec789", + "is_verified": false, + "line_number": 910 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "57b73b00541a671b1c0f9b49b1a5b9b6d43e386f", + "is_verified": false, + "line_number": 911 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "00d3ba478bd4e0005ba325c0fa3bbb80969a4072", + "is_verified": false, + "line_number": 912 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "63497e9fab38614d05946c0b9dd1338983132696", + "is_verified": false, + "line_number": 913 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bf7915a186cac89cbf27b479b4318af45d334f3e", + "is_verified": false, + "line_number": 914 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9e5791210452015df2676f6a7706415ad7c8149e", + "is_verified": false, + "line_number": 915 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "149a819c93748d871763fdd157fbf2c93fcff33d", + "is_verified": false, + "line_number": 916 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5c0e33a6cdc2bcfa911e665929ae524093e8d4a8", + "is_verified": false, + "line_number": 917 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0a04734c82ec76181682c537a590934fbe46fe44", + "is_verified": false, + "line_number": 918 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fb96412139d649dc332fc596841dc2d7543a09d3", + "is_verified": false, + "line_number": 919 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c48b721469472686b78de0db8d34ccfbe5113804", + "is_verified": false, + "line_number": 920 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7c832e5288c3cd8f714e3b57d31c7fe05ad0b98b", + "is_verified": false, + "line_number": 921 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "58383e090cd1cdfdbd494f46d533d7be96c3d16f", + "is_verified": false, + "line_number": 922 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "964063ef09c1114c0b89c4a8bdc6fb9a5238b75b", + "is_verified": false, + "line_number": 923 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0f70be8ee00fb5491a86ff2b185e193bed8147d2", + "is_verified": false, + "line_number": 924 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "eade9c861e70446d1a4057306ea14bcbb105515a", + "is_verified": false, + "line_number": 925 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "645a4a4787c20dbf7d23af52b6b66e963a79701d", + "is_verified": false, + "line_number": 926 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "952b79bc3f47f661ffd882f2cac342d761c7ee89", + "is_verified": false, + "line_number": 927 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "325ae8750d58cb76ba5b471c776b575c6dd8f7de", + "is_verified": false, + "line_number": 928 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c848e0ebbd67aadd99f498bf457fe74377e2dee9", + "is_verified": false, + "line_number": 929 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "938a394aacb5f28860f2a21dc11c2143dfda6609", + "is_verified": false, + "line_number": 930 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6f7cc320c863e5e4d854df9f1d9343408b316152", + "is_verified": false, + "line_number": 931 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bca601976f824d572c9829820d04ef78f0aa89f2", + "is_verified": false, + "line_number": 932 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8f436a87f64990bcc5bba342e4614ba240cb4001", + "is_verified": false, + "line_number": 933 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3c41d19e585a5d6932fbedfe9a9970b2be5be662", + "is_verified": false, + "line_number": 934 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "11c444922d1367a8d844b4f265dd34234145b4e1", + "is_verified": false, + "line_number": 935 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4b5b8766a87bdfe9e72b205635cf3202579c294e", + "is_verified": false, + "line_number": 936 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a8c32045952ca987aa668c54161b8313d4e27d06", + "is_verified": false, + "line_number": 937 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7280d2d3abaeaa0b8c09b30184cfa8e9d96f16f9", + "is_verified": false, + "line_number": 938 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d353aeb68a062440b13bc25906bc19450808c33f", + "is_verified": false, + "line_number": 939 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c06ff020b6c003435cd543d7c094df946d5cee8a", + "is_verified": false, + "line_number": 940 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6c846e552b2bae1eb5fb1ee603bd35dbcf43f8e1", + "is_verified": false, + "line_number": 941 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9526db9835d636a82d4c7843dcb4b1a97f0cd41a", + "is_verified": false, + "line_number": 942 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c0d1d341758862cd2d243425d7e0e638ccde2be9", + "is_verified": false, + "line_number": 943 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "168f03ae12ec1b265302c9be39275b3ff886f0ba", + "is_verified": false, + "line_number": 944 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d4431e65831239ecb46c60b109b3cdf3d90413e4", + "is_verified": false, + "line_number": 945 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6065a318efbc35fa8bfa8179ea00d139aa8ac5f8", + "is_verified": false, + "line_number": 946 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ca8eb4ab2a13fd9c8009f64e9a57a9698da2af08", + "is_verified": false, + "line_number": 947 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "076d36e09e412d1baffcfe20e235b32e766d9d37", + "is_verified": false, + "line_number": 948 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8a96b1bb17e8fc8048721963a8944f194e0d6383", + "is_verified": false, + "line_number": 949 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "036334bc532f791df9f17a922a6b282468e3a32d", + "is_verified": false, + "line_number": 950 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2e9e4798ee11ce742834d80c2103c846b8a7daa8", + "is_verified": false, + "line_number": 951 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b34309d4e552ffa204cbf7632dd06376f7cfe925", + "is_verified": false, + "line_number": 952 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "eb323c2dabc2fe8fe9d73e355e24554f45a097ef", + "is_verified": false, + "line_number": 953 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "eeb750c5480e76e5b075a1cc415007182d5a84a5", + "is_verified": false, + "line_number": 954 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "baa82df8fe62f21e4a9bd056515d279b5f4bf296", + "is_verified": false, + "line_number": 955 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7ed197e47d75c92a2bb9fa469ce2584338ae7978", + "is_verified": false, + "line_number": 956 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "eacb84eb412e97afee8329c534ea5822025d2f34", + "is_verified": false, + "line_number": 957 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1a7e7d49835c298874d24cf9434a7c249f71811c", + "is_verified": false, + "line_number": 958 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "71124a16113f0bfca8f71090445ea96115e92c3b", + "is_verified": false, + "line_number": 959 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "eb6fed65dc17090a731ba790be1c1e913ed43696", + "is_verified": false, + "line_number": 960 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ff488edfba52bda0a9d4ef548f4e848e1bc407c1", + "is_verified": false, + "line_number": 961 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d58ebcc9017888fd12d9eee6a1dbb7a1e5d8bf72", + "is_verified": false, + "line_number": 962 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4db9b98c3dc42567e08ac91e4658c7774eacfddd", + "is_verified": false, + "line_number": 963 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e91ea43a53d83fb4b47e5769b7db51e4f1c0a333", + "is_verified": false, + "line_number": 964 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b8768444a059004aa7d50c73da0c7665e774c8b7", + "is_verified": false, + "line_number": 965 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "52af7be744b7e8e3c9d75db11b3de31693313573", + "is_verified": false, + "line_number": 966 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "169a53ab3aa86b11c6a4fb5064b2cab7b64d260d", + "is_verified": false, + "line_number": 967 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6c29925cd018548844c1b174a4fad45f39ca4d3b", + "is_verified": false, + "line_number": 968 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "793d9bb0e0d7f5e031e367587ecb877881cdd56b", + "is_verified": false, + "line_number": 969 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "709969f024af92b318a5dc3a0315a66c2a024820", + "is_verified": false, + "line_number": 970 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6c66657d4bd785b7c16df241260cd51f8d7e7702", + "is_verified": false, + "line_number": 971 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "54330bf419e7174ab210ac03a0b26bdbb50832e3", + "is_verified": false, + "line_number": 972 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "02bbbfc42d316c59297fe15109e17447512bc76c", + "is_verified": false, + "line_number": 973 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "446f08aead8d20df9ee177b4ee290303cbbfc348", + "is_verified": false, + "line_number": 974 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9b47bd9a70c30307c89348cf7044e66b8eeb604b", + "is_verified": false, + "line_number": 975 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "16799c910c44755b0c3ffa38c27e420439938bb8", + "is_verified": false, + "line_number": 976 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cfba338d2d1c6c8ee47fd7297eae9e346ef33d2c", + "is_verified": false, + "line_number": 977 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "42f730799ccc5f4e3f522abf901ce4a7872f4353", + "is_verified": false, + "line_number": 978 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5669611e63657e7b6d5f10aee1fe08837577dc99", + "is_verified": false, + "line_number": 979 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8b8a1180371e560308a4b3bcbf7d135e4fdce66e", + "is_verified": false, + "line_number": 980 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b5b25fad7a60d76bb8612fe1fe7f4114134b7fe1", + "is_verified": false, + "line_number": 981 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7268358632fc15cc97395c23ac937631427a06da", + "is_verified": false, + "line_number": 982 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "77b14302acab126de73e1960951b4d8862f8996b", + "is_verified": false, + "line_number": 983 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a9f98d55aa73cddda74d878887f9cf7c91ed9622", + "is_verified": false, + "line_number": 984 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7c0abf324bb40af2772baa72ec9eb002674b972d", + "is_verified": false, + "line_number": 985 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ecd7751d16ed66ffbccbc3bc0cdc6767e85c9737", + "is_verified": false, + "line_number": 986 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1829e0ea8aa97dd1c07f83877af61079a0420f0a", + "is_verified": false, + "line_number": 987 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "246e88cdb42b377333a3fb259ca89b8f2927c9f6", + "is_verified": false, + "line_number": 988 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "70c184cc1ba36cc336edff03d3180e16a7b6a8c8", + "is_verified": false, + "line_number": 989 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f3e0f3c62ed74ee4c701d70dbfbf5825e9b153e3", + "is_verified": false, + "line_number": 990 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fceabb5893c16c83a2f75e44a2c969cb6bff4c70", + "is_verified": false, + "line_number": 991 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "dd14309feb249e827dba5ced8ac68b654e7db8cf", + "is_verified": false, + "line_number": 992 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9f675a535ed79052f233c3b6f844eb96368d2d4f", + "is_verified": false, + "line_number": 993 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0e0d26feae012efa3585e895b6fa672005c3434e", + "is_verified": false, + "line_number": 994 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "42a18905f6b1ba2fa6cda2c3b08b43059503926d", + "is_verified": false, + "line_number": 995 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "960330eaa639a3374f20fb3bb1d33c3cb926f9cc", + "is_verified": false, + "line_number": 996 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c676ae0d67843480085f4544a475ccec95b1c942", + "is_verified": false, + "line_number": 997 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "05a62b604c1187eb336526d03642a7c46e6727c3", + "is_verified": false, + "line_number": 998 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cde1211319f593ead3f23c0fac4f0ab48866f5da", + "is_verified": false, + "line_number": 999 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7d12d1e4865212b188c6aefd69096d4f6df8d113", + "is_verified": false, + "line_number": 1000 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "58c2087994575f810e6fb07f476718ac01436189", + "is_verified": false, + "line_number": 1001 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b9b320c5cd52c63f2c7d8df9f7eb8d7ae97ea0c9", + "is_verified": false, + "line_number": 1002 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "94ade2ea50c865df9827f975b66b0ed87f6196b3", + "is_verified": false, + "line_number": 1003 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "399c06fffa9278491e56e25312b94398408888b6", + "is_verified": false, + "line_number": 1004 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f20cde564b4b5821671912b7c6a87f2955fa42e8", + "is_verified": false, + "line_number": 1005 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6f320defd3068726e899c9764628473dfd3552bf", + "is_verified": false, + "line_number": 1006 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2e1374c55dbeb0c445b7cebbcf13b2258776c08b", + "is_verified": false, + "line_number": 1007 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "60d220a965d81b4d93238d90e5f9f6a8cfe4ee1a", + "is_verified": false, + "line_number": 1008 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b6b4a1a8971608d6c5f4612efb7b811612fab847", + "is_verified": false, + "line_number": 1009 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "54d103be76f6e12ddfb2d277d367ce2e78d41c5b", + "is_verified": false, + "line_number": 1010 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "65de6ec76c0fb7685c47bc8c136b9f8e35187a14", + "is_verified": false, + "line_number": 1011 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3e507308114a34a5709c1796bc43132539ecc410", + "is_verified": false, + "line_number": 1012 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6b2d7139a0eb9228a3ee9cce0808e1f8a8790e82", + "is_verified": false, + "line_number": 1013 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7a6e781d3ddf14c6314ee3329b8fec94fb15c29c", + "is_verified": false, + "line_number": 1014 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fee4d49183e2b79df72990acf34d147d86b65df3", + "is_verified": false, + "line_number": 1015 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6f0633cbd3640e2b979a8a1516c9bd394da76fe5", + "is_verified": false, + "line_number": 1016 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "711980892808cca786860a2790796417f526d762", + "is_verified": false, + "line_number": 1017 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "25756983273f8f4a48bb032b07c85104e4fc98cd", + "is_verified": false, + "line_number": 1018 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5726a0328e5579f407bbf03fc3caa06062205ca8", + "is_verified": false, + "line_number": 1019 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e8c6a788cf042a2a2ea8989b33826f1d6423eb29", + "is_verified": false, + "line_number": 1020 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "755577452cdccb63d3e7f1d3176316fe5ef084c8", + "is_verified": false, + "line_number": 1021 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0ec16170fcd97d28c0f5fa919e3c635358935c04", + "is_verified": false, + "line_number": 1022 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0f91ef272eab7567d0f2db99dffc6dbaae2cc084", + "is_verified": false, + "line_number": 1023 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "35e6dad6c44367b5bb860ff5afeb54c8c92cef58", + "is_verified": false, + "line_number": 1024 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "73dcdb9d800fe9776667edb8cde8312a0a768ada", + "is_verified": false, + "line_number": 1025 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b56ea4486eded8635f63a8622a012fb3ee81a3bb", + "is_verified": false, + "line_number": 1026 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b0f4a8c4f6255ea5f66fdb118eba5eeb0829307d", + "is_verified": false, + "line_number": 1027 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "88d9c65e3ce55ba286c8faf8cb105ea6ac39a19b", + "is_verified": false, + "line_number": 1028 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "adc51f3f9a4c42b861f0da4fcc29392bafe2d98e", + "is_verified": false, + "line_number": 1029 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "96b4ea6fc588c3413700405f4d169504240aa637", + "is_verified": false, + "line_number": 1030 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f119079e796b8f2b9d29804daa90877f525cee3a", + "is_verified": false, + "line_number": 1031 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fbf43f6ca18c68df0a478acd09bb465453c9358b", + "is_verified": false, + "line_number": 1032 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d437b203233fd78ffc8630e42a0655f58d2e9f4e", + "is_verified": false, + "line_number": 1033 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6b7f8512ed9b6046476383c6515fc080c63ca508", + "is_verified": false, + "line_number": 1034 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d9f3006796ec72e11dba105176761e360fcf2a3d", + "is_verified": false, + "line_number": 1035 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ad59895b47e8ab566d17c2ef7121c98d469e0559", + "is_verified": false, + "line_number": 1036 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "132f531444b23991fdf797454d8f949e5426ff45", + "is_verified": false, + "line_number": 1037 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "406f3373f38a62e52e8caa4458dfaa68eca20780", + "is_verified": false, + "line_number": 1038 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ce605737729ff998492c8760553bd54393097aac", + "is_verified": false, + "line_number": 1039 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fc42bf79fd0d8179e9f4f9f0190faad588388004", + "is_verified": false, + "line_number": 1040 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "efc0f56dded17fa0c00b58a820fbe74a1e368b63", + "is_verified": false, + "line_number": 1041 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9d450e49c3cbcffcfb559a51d6ab4531f2a645bf", + "is_verified": false, + "line_number": 1042 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8437e864bc114188554fd79b98cfd43f4c588df7", + "is_verified": false, + "line_number": 1043 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "de462d8851d3dc92579a62f39fadecf6b9d6bc22", + "is_verified": false, + "line_number": 1044 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "508fdca9918030fb0b8a8739ba791f611b793112", + "is_verified": false, + "line_number": 1045 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4933bc7d4edeb7116d71e7f1947e5d6ed29760ec", + "is_verified": false, + "line_number": 1046 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4a8bfde12d39966ecc92cc667695767bbdf7366b", + "is_verified": false, + "line_number": 1047 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3dbc1c47b263483e20fa69941a4274cc19f85bc2", + "is_verified": false, + "line_number": 1048 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d1287d92f048a817c6bb27b0993a87aa9560996b", + "is_verified": false, + "line_number": 1049 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "10cb9bc401ea5975fd15188a2b9cc592e513647a", + "is_verified": false, + "line_number": 1050 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f18de35aa597b41bb9d73890f35c8f7704c72ea1", + "is_verified": false, + "line_number": 1051 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "dfe7e4f70a85c9d4d9e5e43b38e6c4afb6af9858", + "is_verified": false, + "line_number": 1052 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d39edd8dd598dfb8918b748d29c25259509675dd", + "is_verified": false, + "line_number": 1053 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5d2721a37cabecbb784a5e45ff9d869e7c90d7f5", + "is_verified": false, + "line_number": 1054 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "60d52adbbee54411db221581b7d93960b772f691", + "is_verified": false, + "line_number": 1055 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "af1320e386741990cf1c7201101f2ae194fc72ca", + "is_verified": false, + "line_number": 1056 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4bbc199707b0d38feb6244d4069391cf4af4b8bb", + "is_verified": false, + "line_number": 1057 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "22023f99a0e352116a61bf566f8af2ab60b5d9c1", + "is_verified": false, + "line_number": 1058 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3f664164c66bb49689d9931436c3d4f57f316eb6", + "is_verified": false, + "line_number": 1059 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9a4a988167abb6a3816d472d4be97cd105a69baf", + "is_verified": false, + "line_number": 1060 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7edf4402503eaf501e23c31ef1306392d5ecacd0", + "is_verified": false, + "line_number": 1061 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "508b4ed03f5a2f09fb22e2641580065ee4c8a372", + "is_verified": false, + "line_number": 1062 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b02f44c26e7091096fa6fcafb832b62869af42a2", + "is_verified": false, + "line_number": 1063 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0f9174e85538561b056727e432773bb69e128278", + "is_verified": false, + "line_number": 1064 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cabc1f10dc737ef7e110172b814966cdad11b159", + "is_verified": false, + "line_number": 1065 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ee5288a3e32b3b55b342ef18051c78ffff012231", + "is_verified": false, + "line_number": 1066 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0a25e259c157bcc1a99d7e001e52b35d0a4ae2b8", + "is_verified": false, + "line_number": 1067 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3c7bdd0b20d6f7c299da33dbb32d99105489f1c4", + "is_verified": false, + "line_number": 1068 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "19b40ca81ef322c1c0028ad1a005654faa9cfe93", + "is_verified": false, + "line_number": 1069 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fc4ff73da4fb03231a38728acf285f405b1b3ce5", + "is_verified": false, + "line_number": 1070 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c4e603285dc95917f8836283bebce03ff4bc11ba", + "is_verified": false, + "line_number": 1071 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e9e498abd308db923d58b1c35ad83467e58a60b3", + "is_verified": false, + "line_number": 1072 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "954161d814c5c2ccf3ce8c3609ebb4157c08b6f7", + "is_verified": false, + "line_number": 1073 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9bcf9c2a4de2db297ac881231955ad39f19a9df1", + "is_verified": false, + "line_number": 1074 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8eafb590298e1d35ed72d88625bd344a427ccc8b", + "is_verified": false, + "line_number": 1075 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "32a3705a4ce42eecec3c45b0bb0a2c36142b6d08", + "is_verified": false, + "line_number": 1076 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5e8a991485e2080c429eab8a5049b3c3bf7c0ba8", + "is_verified": false, + "line_number": 1077 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d9fbae4d79a44395e6eca487062df13d46954053", + "is_verified": false, + "line_number": 1078 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f62a4f64d930b746fbefdad6c48b0d2a2dc07130", + "is_verified": false, + "line_number": 1079 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f7af30387bf7c4ac2cc0b48eef09f350ec43dae8", + "is_verified": false, + "line_number": 1080 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "afb00100d9ca02672c09acc78c7e13b56b049f63", + "is_verified": false, + "line_number": 1081 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "428e0f17cb680f5fc2b3cdc648ef8739b0fc1d87", + "is_verified": false, + "line_number": 1082 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a7846f258d908bca9bdf9120db6b9b370a4143bd", + "is_verified": false, + "line_number": 1083 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "38c581282a5c2d07745c008443cdc545acbf5aca", + "is_verified": false, + "line_number": 1084 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "63f97716fc1f282d6718710c230006611b86be04", + "is_verified": false, + "line_number": 1085 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "57600ce03478249d79dd13c009f7f64b7ae6211c", + "is_verified": false, + "line_number": 1086 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8e96ee931397b82b3f2c330bcfb3cfea3093d5a7", + "is_verified": false, + "line_number": 1087 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c85653058313f125a2438e1cf446cb90bbedd8ed", + "is_verified": false, + "line_number": 1088 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1a54794f5e3a4dd2036cfd120e294e6401f6d227", + "is_verified": false, + "line_number": 1089 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "60f2b36dcf992c96fe61ea001441417f314064ff", + "is_verified": false, + "line_number": 1090 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "939ca981ece9656aebd5b02d02ed33deadb8923b", + "is_verified": false, + "line_number": 1091 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c28c0ae6268f5e6e813f9fe3b119e211473071e6", + "is_verified": false, + "line_number": 1092 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fa66a89cdd91b75a640282d832886514fe6456a1", + "is_verified": false, + "line_number": 1093 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e464c2a1ba37ae51b0f7ff8b3fba06a8ed7108dc", + "is_verified": false, + "line_number": 1094 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8fb023d4933c56bfeb403311ffc3752d2fbc975e", + "is_verified": false, + "line_number": 1095 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8f066fc1693da2a9cfa30bc540bb35f884c62a30", + "is_verified": false, + "line_number": 1096 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "63a7db4c42e5b728324ad5d2c92e6514ab23364a", + "is_verified": false, + "line_number": 1097 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d4b9ba68b048c4c52c65e192dd281c1c203463c0", + "is_verified": false, + "line_number": 1098 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "33e4d896c6a8b4d14cb836f616f03eaafa43018b", + "is_verified": false, + "line_number": 1099 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1a5b72368ecddce420d879781be813c19475c1be", + "is_verified": false, + "line_number": 1100 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0106004ab89b24991e5e01849276a2ed348d1194", + "is_verified": false, + "line_number": 1101 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "54ede800e24d999c54ce14b80d8c56f834d1a570", + "is_verified": false, + "line_number": 1102 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ff58b7f59920c5d3484985e53a686b91d7b183cd", + "is_verified": false, + "line_number": 1103 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "255ac9b7f9fa6a2376b2fc2219ff38f80dc8c655", + "is_verified": false, + "line_number": 1104 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b0b7694dff36d2e9337b1012073d9ab41aec18c6", + "is_verified": false, + "line_number": 1105 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3d675b3354c15f5088cf1581fc9fa052360c8ecf", + "is_verified": false, + "line_number": 1106 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6e11485ed9e411128ab20a54b6d52e4e879e289f", + "is_verified": false, + "line_number": 1107 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "200a78aa828ba2d7cca00e420a85bef9dde6c841", + "is_verified": false, + "line_number": 1108 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "936a30deb66f624c112527914bbe2f09fb1c2ea2", + "is_verified": false, + "line_number": 1109 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "430e0786d83a62119d1ed6bdc8b87efbf7afbc9d", + "is_verified": false, + "line_number": 1110 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f3fd7614d07e21dc15fa385fc2042847610f8259", + "is_verified": false, + "line_number": 1111 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "dddf43eddf77d768ace4901fc5d506ae2c85ec2d", + "is_verified": false, + "line_number": 1112 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ae367707142233fce304a364467337f943952845", + "is_verified": false, + "line_number": 1113 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6b16b9ea707df813fc90c54d7a531cf0f6b754d0", + "is_verified": false, + "line_number": 1114 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cd1dc83b5bd180fb9f5e72361ff34526b2227197", + "is_verified": false, + "line_number": 1115 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2f4400f3ba736cab5d0bf75f249c030724c8d0b7", + "is_verified": false, + "line_number": 1116 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "43d51f653e0a59b1f5988c8b6732b71dc2492bde", + "is_verified": false, + "line_number": 1117 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "32336fe7d0a6638edadafcef1f7355ff5a5043d1", + "is_verified": false, + "line_number": 1118 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4915df89c72bb9de93ba1cf88de251db9ebb05ec", + "is_verified": false, + "line_number": 1119 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3f1343a17f1e3d24a58df03d29a1330994239874", + "is_verified": false, + "line_number": 1120 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a240e2ccfb08d02d3d54ce913d120af2b4a68a19", + "is_verified": false, + "line_number": 1121 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ac1f2ad12e871b6e5818be4e7f23f90f0b655c65", + "is_verified": false, + "line_number": 1122 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3b792af94a90899b8cfb1cc44605d4de5c0eab7a", + "is_verified": false, + "line_number": 1123 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d6d3294546ce3a4df35269a80497b35d3d97851c", + "is_verified": false, + "line_number": 1124 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "04992ccff77891f14f3dca8bb59cc30534ae31f3", + "is_verified": false, + "line_number": 1125 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bbb54a9a3169f76822f3c8de4c5c33c12138a8ed", + "is_verified": false, + "line_number": 1126 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "64419f894e06d7b0ab1236d60034a5410006f422", + "is_verified": false, + "line_number": 1127 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f58a6063b0ce4ccf2630215d7ab442eb3a6cc154", + "is_verified": false, + "line_number": 1128 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "80fa5cbedc3d970f28652338cbd1da179a4b24f5", + "is_verified": false, + "line_number": 1129 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "904d8f8daa11159afe547828d6da112ec785fc9e", + "is_verified": false, + "line_number": 1130 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "62e23442e30718968242cf6397ceaf835e2b6758", + "is_verified": false, + "line_number": 1131 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8ce675cce57b21a3cf664029ff539107da67583b", + "is_verified": false, + "line_number": 1132 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "64098f0a9449c43a8f071d2052c6066940e75ee8", + "is_verified": false, + "line_number": 1133 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "876250d35eaa0e8f788304e6f47bfb9ecf4aa1f4", + "is_verified": false, + "line_number": 1134 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7aac80369e7b76f53ae0de0d94dfbaa21a130d32", + "is_verified": false, + "line_number": 1135 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "65df2537b97ebdb84c0dc6afa37f140811294e57", + "is_verified": false, + "line_number": 1136 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f6ed524b021390fe734f26cac66fcf1e6a6c455e", + "is_verified": false, + "line_number": 1137 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8fdc365a4e50f09aa482d72bba1974df3b6c9859", + "is_verified": false, + "line_number": 1138 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "36890040b0afedd15fdd9eb87459a4165fcbe2a3", + "is_verified": false, + "line_number": 1139 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9df5cbdfba97fabe10d94f771bcd7ca889c87b2d", + "is_verified": false, + "line_number": 1140 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "de65594f00e0098e7ab3312414faf191bbc3e3c1", + "is_verified": false, + "line_number": 1141 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "37247ab05766ecc1ac7fae19a77b31f7116cce38", + "is_verified": false, + "line_number": 1142 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "13d8923244df4b3025c5d2dd405a22a757628f8d", + "is_verified": false, + "line_number": 1143 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9eef15e4a145e31f7c74235731b69dba5207b237", + "is_verified": false, + "line_number": 1144 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "746b63eabaddeed7ab5dbe3b1fe4e41f89e9f21e", + "is_verified": false, + "line_number": 1145 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f9512226d4044bb241d77988dac046b05effb4f3", + "is_verified": false, + "line_number": 1146 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "de168aa5d99ff80498b7552c850db5d42cb425f9", + "is_verified": false, + "line_number": 1147 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2367ab77f144da2b2349cdbfdc4500d429754353", + "is_verified": false, + "line_number": 1148 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d6a619ebb4b2766bce83fa5bfb6118a9d8ba3212", + "is_verified": false, + "line_number": 1149 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "35fe8489533c677b657cfee61474bab7f268a495", + "is_verified": false, + "line_number": 1150 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e58be566894c228cb922e434d34416a473f0dc28", + "is_verified": false, + "line_number": 1151 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "18f33c6db138875913acb6ad887ed80ca3dc317f", + "is_verified": false, + "line_number": 1152 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1e8a66cfa6671b1771e5874f29bfd96e47b4ad76", + "is_verified": false, + "line_number": 1153 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "284301d7ef66a6721a4b76a02c274419de91a437", + "is_verified": false, + "line_number": 1154 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6694d586f66b50c0162e1cff4b1f133e2c8a9423", + "is_verified": false, + "line_number": 1155 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c712802905f08891cac2e68e6d8f5f6d85e4cf60", + "is_verified": false, + "line_number": 1156 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cd5f0c85968b392a77596cb5143de81f6f109bcd", + "is_verified": false, + "line_number": 1157 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e158eb64d577c9904690ff67584f2b0090792139", + "is_verified": false, + "line_number": 1158 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "62cef2983d23c372ffd1175683e2cf0489a0a93c", + "is_verified": false, + "line_number": 1159 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0039a393f63d3b522516a90354354b6477765b06", + "is_verified": false, + "line_number": 1160 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5c91012c71d492f7e5bc5607f71e1d3337562f9b", + "is_verified": false, + "line_number": 1161 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "83fd266255474e467fcc3f1ca61b0371bf6933eb", + "is_verified": false, + "line_number": 1162 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "44dc9bc4f3a32681036d3328bf2e2c298c94c5b3", + "is_verified": false, + "line_number": 1163 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c077db4aab559fcc23cecde6c8dce6f58a86c7ba", + "is_verified": false, + "line_number": 1164 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f2e728ed22184e3a7bf3b34308c53815d811687d", + "is_verified": false, + "line_number": 1165 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9d653c4cd2f63ba627e1f7eb557b793e7eb50f3a", + "is_verified": false, + "line_number": 1166 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "33e0029ea6c1f2989bf2b5b86f6c4acc03fd7b10", + "is_verified": false, + "line_number": 1167 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "139c8a653e6827e2b29b75c31d27eba181977579", + "is_verified": false, + "line_number": 1168 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e34424070b48aeaee9eeeb88a1a928d2ce1f5517", + "is_verified": false, + "line_number": 1169 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c4db39ccd7c06e68ada50b294aa53f947559a99a", + "is_verified": false, + "line_number": 1170 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0636d970e79e781a5159068c6fe7f0411698b596", + "is_verified": false, + "line_number": 1171 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0bc38af13c57dafb7f18b33b86e5bcbe1292bc2e", + "is_verified": false, + "line_number": 1172 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "02d9eabf8b61d1e62425eac9c7b39385e602ddad", + "is_verified": false, + "line_number": 1173 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3ba33420b436dd34da6f45fdbdbb26a87c99e811", + "is_verified": false, + "line_number": 1174 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2965a6a5b73c3edfdc11d9a979bb085546d63d1f", + "is_verified": false, + "line_number": 1175 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8b15da0afbed8313d1daec67d4bca7958949484d", + "is_verified": false, + "line_number": 1176 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4bf0c8b08ddcb81f5ac2457580003197ff4782dd", + "is_verified": false, + "line_number": 1177 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9e3822884cf25511703c4fbfce1ddacc0d19d021", + "is_verified": false, + "line_number": 1178 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "26fd6e63721168b064c7825415fda7da4c17cd36", + "is_verified": false, + "line_number": 1179 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "82db110822969249eff39d4b7e6830ee919c4b8e", + "is_verified": false, + "line_number": 1180 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e81523785f6e5efeb372a665059ab959c7911c37", + "is_verified": false, + "line_number": 1181 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4c8056fa1e16e63e4da13f329a0f0ba8c3d875eb", + "is_verified": false, + "line_number": 1182 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "63a9faac8e9440b425905da27052de51aa69b937", + "is_verified": false, + "line_number": 1183 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e0ad9315e82b5f80b7b02ce12ba3e686c9a637a5", + "is_verified": false, + "line_number": 1184 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "176ca3d77737c23c86a524235e4281df3a64a573", + "is_verified": false, + "line_number": 1185 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e8b4a7abb0c1178809eb5f5703ed43d558083a2d", + "is_verified": false, + "line_number": 1186 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7e0ad9ba810350bcd8da9180615fd964827c14ef", + "is_verified": false, + "line_number": 1187 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "39c3357766171faf88e70eea0dccb00239f273c5", + "is_verified": false, + "line_number": 1188 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d17aa49aceeaf925527404fa57a4e17668de8596", + "is_verified": false, + "line_number": 1189 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2a6b75b5576df53c3219112e7daff1dc142702d1", + "is_verified": false, + "line_number": 1190 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b75fa52e7d8ecfb8e7e9ff3dc2c37b73abcf7e2c", + "is_verified": false, + "line_number": 1191 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c551bfc4af7eb1fd5daa4f05fd58a2d4d65b85fe", + "is_verified": false, + "line_number": 1192 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a8d858cd02dcd5038dc3e76ac76b2da91f8dbccd", + "is_verified": false, + "line_number": 1193 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1bf631baf29fc48072c20ebfdd321964066f9f08", + "is_verified": false, + "line_number": 1194 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c6eb53905cd7e0253f4e69f34295cb6a50f58e08", + "is_verified": false, + "line_number": 1195 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7bbb8b2539588d170a6c26e9f61ae0800f9d8f2d", + "is_verified": false, + "line_number": 1196 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "26caefb3dca46d7afafdcf0010c67b9e9fccc92b", + "is_verified": false, + "line_number": 1197 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2cb19ac1427a96db3d380729bf039e5349ef63be", + "is_verified": false, + "line_number": 1198 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9e2aa480ce341383cbca0c207198d483e20322bd", + "is_verified": false, + "line_number": 1199 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "be742ba9f651b96a51823045433f3a1948d7eced", + "is_verified": false, + "line_number": 1200 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "317bd6bc5bcc732a1db7e57d0371aa9257f8df00", + "is_verified": false, + "line_number": 1201 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7c80c0ebf44179e49cf0e5a3d0408cc76aee83de", + "is_verified": false, + "line_number": 1202 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7858b77e2046951eadc43758c07104d777668eb7", + "is_verified": false, + "line_number": 1203 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "85a09b9fd03c47f1b036cf44c4909bc73ddd6cad", + "is_verified": false, + "line_number": 1204 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1718e46e064b47cec903bad3b0e9d6ef1da2f11b", + "is_verified": false, + "line_number": 1205 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0c1ee8a96d538ba8b4fa8b05db03563fd7ef8973", + "is_verified": false, + "line_number": 1206 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2017b3f2be44d213be17940140c168a5fba7561d", + "is_verified": false, + "line_number": 1207 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b083a5002d8fe4f2a66696aa0814e03ffa6d1837", + "is_verified": false, + "line_number": 1208 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ff42555f72300b656e47db4ed191f5df0ac07560", + "is_verified": false, + "line_number": 1209 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2ef2cf7195a65a890efa0632dd212ef8220aa1c6", + "is_verified": false, + "line_number": 1210 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "69cb36505922753131885b4a08c707f81ac66a47", + "is_verified": false, + "line_number": 1211 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "069b86c3a9114bd673eef998e22656df1fcaddd8", + "is_verified": false, + "line_number": 1212 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "70c8686a1be4b67a602a59a873ddbede2cd4da7e", + "is_verified": false, + "line_number": 1213 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "523d5a3e6d4fbf64c23594663c7e4687ae9c2be3", + "is_verified": false, + "line_number": 1214 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "16e86f176fd3cd4f7a58f0ffb8dc5791f3f95a86", + "is_verified": false, + "line_number": 1215 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ed84afa53dc05329a7991f5bf5cd2cae1fd77ffc", + "is_verified": false, + "line_number": 1216 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f1289b7119566377ed28ab9dd62af0fd09ed9fe2", + "is_verified": false, + "line_number": 1217 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a4f904b0556d1681ef00ea1813f2f94e28b797eb", + "is_verified": false, + "line_number": 1218 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0949c112813b58b0da6912740cf8bcbb85226c34", + "is_verified": false, + "line_number": 1219 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1bbf17622cda5702d35e14ba66df075a7bb57913", + "is_verified": false, + "line_number": 1220 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8e3a03cec08874a64bccc6d6d425f0afe79533a1", + "is_verified": false, + "line_number": 1221 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1aafc9018c54c7198cf74db22feb0319707898b6", + "is_verified": false, + "line_number": 1222 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e7b49f254a6e2de711e659bd28ad158691e30fce", + "is_verified": false, + "line_number": 1223 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fbc11861a047faba2041e2b6c715d8ca60803c8e", + "is_verified": false, + "line_number": 1224 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "44c990c1ce572f1e8f1ab851427e3a42ce71242a", + "is_verified": false, + "line_number": 1225 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4d3640532de6af408ed943d63ed3e3c2689e9c5f", + "is_verified": false, + "line_number": 1226 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a523fffc0ede19e1deeda09652de2b7a018cf8b4", + "is_verified": false, + "line_number": 1227 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4a995d1758da7e7154ba4acbec5b5b403742b7e1", + "is_verified": false, + "line_number": 1228 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "de4be8856b30e21fc713dc10f8988539feea7023", + "is_verified": false, + "line_number": 1229 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "fb1c0866f73c66412d08391f3ce4878af73aa639", + "is_verified": false, + "line_number": 1230 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a702fefff9cdbe1f95ab8827ddec5ba8efc30892", + "is_verified": false, + "line_number": 1231 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "724b47ffa7a9db1bbaf712b3d9d2b76898db0ea5", + "is_verified": false, + "line_number": 1232 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e0f16906358b6b058b6d986929a05521b6901f68", + "is_verified": false, + "line_number": 1233 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4332f528fff4a967c90c89db64aa58e23393bfed", + "is_verified": false, + "line_number": 1234 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "451a10712041218c61b0cc3787311943dab42dc6", + "is_verified": false, + "line_number": 1235 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "6a1be9deb76862f934fd8a9197069f4609ef70b5", + "is_verified": false, + "line_number": 1236 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2b1256a86a2fb02c20dc58e47774d30baed60f62", + "is_verified": false, + "line_number": 1237 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "74d000f3ede09a41df362d509537a2ac5f1fa07b", + "is_verified": false, + "line_number": 1238 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "43f8293d7eda52b663063cd56e5a3e394f193642", + "is_verified": false, + "line_number": 1239 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "51352b84bafc3573024540c543cc95922a764ef0", + "is_verified": false, + "line_number": 1240 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0ece3e42bfed9840f907fa700d5d29f0087985db", + "is_verified": false, + "line_number": 1241 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3b91d6d99ae8c482392adc042654bd076573cd8a", + "is_verified": false, + "line_number": 1242 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ab529305822e1642ed7c7d3acd9ba80dabc55108", + "is_verified": false, + "line_number": 1243 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3cf4744d88fd85b0fcb0fbf0425c5b50eae93b3e", + "is_verified": false, + "line_number": 1244 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "228fe53a555785f979a20a0159c96ef7d8d057c7", + "is_verified": false, + "line_number": 1245 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8d21215aa0a8f29d068ff316fc09ea6ae9e766c7", + "is_verified": false, + "line_number": 1246 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d63d3d63396c5e88f1fd8cdab9116331080cd2e2", + "is_verified": false, + "line_number": 1247 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d4fe6d5f06c2860ed38ebb02079bb2ebfcbfb093", + "is_verified": false, + "line_number": 1248 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5e1d352485a30350ac108f66da7ac3ce62b1ea4f", + "is_verified": false, + "line_number": 1249 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c682e7af6638379e4edf52c36995c3454ea1b149", + "is_verified": false, + "line_number": 1250 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "bb193ef1c9bcbc39ed64689f474af29719df489e", + "is_verified": false, + "line_number": 1251 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "01c34073e2e61552f4fd0ba64139be0ccabcdb8a", + "is_verified": false, + "line_number": 1252 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "cc47b8620102a6216f098eb7f9ea841c3c2a5f22", + "is_verified": false, + "line_number": 1253 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8f070c859fe84c5502e45b84a274308bbc0a7744", + "is_verified": false, + "line_number": 1254 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5f3061dc64135be12c1eaef23ab8e02f1826f24d", + "is_verified": false, + "line_number": 1255 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "9238be5963618c3501e919ebd4c13992a4bea3b4", + "is_verified": false, + "line_number": 1256 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "68c1365f209fa103e65c4da375b42d5656575940", + "is_verified": false, + "line_number": 1257 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "384be6402a8d31d62cb35fefaec77b06c8211f59", + "is_verified": false, + "line_number": 1258 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "360329c0a8cb6053168e61758688b85104fc86ff", + "is_verified": false, + "line_number": 1259 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7cd87f59db950306302a74b81e8f926df1577397", + "is_verified": false, + "line_number": 1260 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "553b2380d863621a9e4ab7c7a97fdec425ebab25", + "is_verified": false, + "line_number": 1261 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "43562265e7cf90c28221c2b7dbfcafa8f62843dc", + "is_verified": false, + "line_number": 1262 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ed7e495370ef7882b13866c332dff00ef7c361a6", + "is_verified": false, + "line_number": 1263 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7123453c9f62fc6c33951aa2595f1714b23d583a", + "is_verified": false, + "line_number": 1264 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e941c0eb1694570c999ca3fe548f76f6daaca83c", + "is_verified": false, + "line_number": 1265 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "85018e48b287ca7323192ff38ebe9411e61b38e2", + "is_verified": false, + "line_number": 1266 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "814d7edca30e0262ab0b07c6baf47d20738c823b", + "is_verified": false, + "line_number": 1267 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5dca59fe14f949e763116aef3968af2662926895", + "is_verified": false, + "line_number": 1268 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "ee86abd29ecfab79519c1efc033546d2c477477f", + "is_verified": false, + "line_number": 1269 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5878ed0ebded462f8d2461fe18061aa18d1000fd", + "is_verified": false, + "line_number": 1270 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4dd683cc3993e43d00b1b5f9e4e57895bb56e8e5", + "is_verified": false, + "line_number": 1271 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "a8a20da925fd5126d24df7d8baf68ac1fa23a184", + "is_verified": false, + "line_number": 1272 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "137f68b2d3f03ddd81ed8602ff19218c71df55fb", + "is_verified": false, + "line_number": 1273 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b32f2f31a868ddf0e3f013465c72527f62057e44", + "is_verified": false, + "line_number": 1274 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f5425542a9e9183a33dd16d559c92182f35f44a8", + "is_verified": false, + "line_number": 1275 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "77e8234c8ff852ec820384cd8f9284cde00e34a9", + "is_verified": false, + "line_number": 1276 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "be6e0ac8ab7d8ac8d7f7a4fc86b123392c09374e", + "is_verified": false, + "line_number": 1277 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3063130919857912b6373c6182853095d60ca18b", + "is_verified": false, + "line_number": 1278 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "607c9f8efafb2de11157fefd103f9f1cda4f347b", + "is_verified": false, + "line_number": 1279 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "2c301b0126a15e8150d92a84d8a49ab1eb9b4282", + "is_verified": false, + "line_number": 1280 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "84737ddb75ed5806c645ba66e122402be971389a", + "is_verified": false, + "line_number": 1281 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5a9adaee2ecb6e99992aa263eda966061c9acac0", + "is_verified": false, + "line_number": 1282 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0c09b49e14a5a35d3f26420994f8b786035166e6", + "is_verified": false, + "line_number": 1283 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0ef06e9fe84d92197ae053067b3f3d5051070690", + "is_verified": false, + "line_number": 1284 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b249743c201079e983e03d0afeb3c140342fc9d0", + "is_verified": false, + "line_number": 1285 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "82d624e2d36bf5346e60dd14806ff782bb2a4334", + "is_verified": false, + "line_number": 1286 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "88850db69d81a7ece67fb1d9b286c2d951b70819", + "is_verified": false, + "line_number": 1287 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "e49afb46bf458312000f8f9660ae81ff47bdc199", + "is_verified": false, + "line_number": 1288 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1cbdad16e84903fc3b9b6388a089a067dea2a3d2", + "is_verified": false, + "line_number": 1289 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "82feda736f248ac86d376891de516d9d1824a27c", + "is_verified": false, + "line_number": 1290 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "4a71f468c1364aff801b9120b1f5d529078048e9", + "is_verified": false, + "line_number": 1291 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f091998ff0fee46909f88aa7fd4f3cc73a3d3c9a", + "is_verified": false, + "line_number": 1292 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "29eaffa6f6f8a37758a5f7b32907b3dc5b691896", + "is_verified": false, + "line_number": 1293 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "44f681b1a58ce0c6df53676cc0808013e97ea9f4", + "is_verified": false, + "line_number": 1294 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "962dfd74b7253ac6cd612a6e748f2e95efb79f51", + "is_verified": false, + "line_number": 1295 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c86ef7132a2306cf87224e55cb204e6d2e8e7828", + "is_verified": false, + "line_number": 1296 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c4eb42c72ecfdf7810202a43d54548f7d2bff62d", + "is_verified": false, + "line_number": 1297 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "19383a628b845b1cbb1c0444832b0afbe8ab5064", + "is_verified": false, + "line_number": 1298 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b34bf28a1f7465a72772787a147d434d923c8d1b", + "is_verified": false, + "line_number": 1299 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "288ba78781c2ed007a423cb65cb1bf2306c3fd95", + "is_verified": false, + "line_number": 1300 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f3ceb3cc25a1228a6c53b4e215d7568d36e757a6", + "is_verified": false, + "line_number": 1301 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "87996bb1e32b4a0ecc22ac1d13cea8e0190b350b", + "is_verified": false, + "line_number": 1302 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "790704b8f93fe5aca8ac2ecfcb68f1584dad2647", + "is_verified": false, + "line_number": 1303 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "86223a1c42e86aae0a1ed4fa7d40eb2d059c4dd5", + "is_verified": false, + "line_number": 1304 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1673e79621b9dddf3b29a9b1ddf8d2ec0aad4bdc", + "is_verified": false, + "line_number": 1305 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "35b29b6e62d70ae4822318a19d0a46658eddd34f", + "is_verified": false, + "line_number": 1306 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b3cb65216294e3c0b3981e2db721954bafc3b23a", + "is_verified": false, + "line_number": 1307 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5dbca02e62ce0d208d12a1da12ba317344d8c6cc", + "is_verified": false, + "line_number": 1308 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "83acef9b2863c05447dea16c378025f007bc8c34", + "is_verified": false, + "line_number": 1309 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "524ef34b587ca7240673b9607b4314f3f37cd2a8", + "is_verified": false, + "line_number": 1310 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c76948814be7ef0455d6d9ff65aeae688b7bec24", + "is_verified": false, + "line_number": 1311 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5604fd630dabf095466a6c854750348059dbb1aa", + "is_verified": false, + "line_number": 1312 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "0b5772a512bb087fa1d6e34a062c7eec75f6e744", + "is_verified": false, + "line_number": 1313 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "7d3fa248843c7c76c909ee18b0dd773bbb5741e7", + "is_verified": false, + "line_number": 1314 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d7d16ac0dbd0bb5e98c6cb1d8508ff0132bbcbb0", + "is_verified": false, + "line_number": 1315 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d33b30cdcf982839a7cb6ae4e04b74deb2bd8f28", + "is_verified": false, + "line_number": 1316 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "281ca8a981dae1cebcb05b90cde4c895f3c59525", + "is_verified": false, + "line_number": 1317 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "712b5a91ad8f25eaaae3afccd7b41c6215102f70", + "is_verified": false, + "line_number": 1318 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "5fbc83376379b2201ae51f28039f87cb1ca14649", + "is_verified": false, + "line_number": 1319 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "d7497697fc350ef28cc0682526233a7846bfbf7f", + "is_verified": false, + "line_number": 1320 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "3f3d0b8308dfa23ce4c75abcfdd3840cab33de8b", + "is_verified": false, + "line_number": 1321 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "71a4936bbf172bf22c55b532a505a2c33f04ef2a", + "is_verified": false, + "line_number": 1322 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "f76a3c0087070143222761d33c9496d10ec5645a", + "is_verified": false, + "line_number": 1323 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "b8e837e18bc28489da6d38ac38370bd4a7757770", + "is_verified": false, + "line_number": 1324 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "af90bf5453dacd36dd205811a40eda42d5496cb5", + "is_verified": false, + "line_number": 1325 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "1fd5a47605b1192ee40beb9203beaafe8e53e13c", + "is_verified": false, + "line_number": 1326 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "c286938c2542589cd0fbed6acb6326d3c9efeb77", + "is_verified": false, + "line_number": 1327 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "73cfd5a17466838726c63386a3e5cccdf722a9d8", + "is_verified": false, + "line_number": 1328 + }, + { + "type": "Hex High Entropy String", + "filename": "docs/.i18n/zh-CN.tm.jsonl", + "hashed_secret": "8bb0680522ae015a5b71c1e7d24ec4641960c322", + "is_verified": false, + "line_number": 1329 + } + ], + "docs/brave-search.md": [ + { + "type": "Secret Keyword", + "filename": "docs/brave-search.md", + "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", + "is_verified": false, + "line_number": 27 + } + ], + "docs/channels/bluebubbles.md": [ + { + "type": "Secret Keyword", + "filename": "docs/channels/bluebubbles.md", + "hashed_secret": "555da20df20d4172e00f1b73d7c3943802055270", + "is_verified": false, + "line_number": 37 + } + ], + "docs/channels/feishu.md": [ + { + "type": "Secret Keyword", + "filename": "docs/channels/feishu.md", + "hashed_secret": "b60d121b438a380c343d5ec3c2037564b82ffef3", + "is_verified": false, + "line_number": 187 + }, + { + "type": "Secret Keyword", + "filename": "docs/channels/feishu.md", + "hashed_secret": "186154712b2d5f6791d85b9a0987b98fa231779c", + "is_verified": false, + "line_number": 435 + } + ], + "docs/channels/irc.md": [ + { + "type": "Secret Keyword", + "filename": "docs/channels/irc.md", + "hashed_secret": "d54831b8e4b461d85e32ea82156d2fb5ce5cb624", + "is_verified": false, + "line_number": 191 + } + ], + "docs/channels/line.md": [ + { + "type": "Secret Keyword", + "filename": "docs/channels/line.md", + "hashed_secret": "83661b43df128631f891767fbfc5b049af3dce86", + "is_verified": false, + "line_number": 61 + } + ], + "docs/channels/matrix.md": [ + { + "type": "Secret Keyword", + "filename": "docs/channels/matrix.md", + "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", + "is_verified": false, + "line_number": 60 + } + ], + "docs/channels/nextcloud-talk.md": [ + { + "type": "Secret Keyword", + "filename": "docs/channels/nextcloud-talk.md", + "hashed_secret": "76ed0a056aa77060de25754586440cff390791d0", + "is_verified": false, + "line_number": 56 + } + ], + "docs/channels/nostr.md": [ + { + "type": "Secret Keyword", + "filename": "docs/channels/nostr.md", + "hashed_secret": "edeb23e25a619c434d22bb7f1c3ca4841166b4e8", + "is_verified": false, + "line_number": 67 + } + ], + "docs/channels/slack.md": [ + { + "type": "Secret Keyword", + "filename": "docs/channels/slack.md", + "hashed_secret": "3f4800fb7c1fb79a9a48bfd562d90bc6b2e2b718", + "is_verified": false, + "line_number": 104 + } + ], + "docs/channels/twitch.md": [ + { + "type": "Secret Keyword", + "filename": "docs/channels/twitch.md", + "hashed_secret": "0d1ba0da3e84e54f29846c93c43182eede365858", + "is_verified": false, + "line_number": 138 + }, + { + "type": "Secret Keyword", + "filename": "docs/channels/twitch.md", + "hashed_secret": "7cb4c5b8b81e266d08d4f106799af98d748bceb9", + "is_verified": false, + "line_number": 324 + } + ], + "docs/concepts/memory.md": [ + { + "type": "Secret Keyword", + "filename": "docs/concepts/memory.md", + "hashed_secret": "39d711243bfcee9fec8299b204e1aa9c3430fa12", + "is_verified": false, + "line_number": 281 + }, + { + "type": "Secret Keyword", + "filename": "docs/concepts/memory.md", + "hashed_secret": "1a8abbf465c52363ab4c9c6ad945b8e857cbea55", + "is_verified": false, + "line_number": 305 + }, + { + "type": "Secret Keyword", + "filename": "docs/concepts/memory.md", + "hashed_secret": "b9f640d6095b9f6b5a65983f7b76dbbb254e0044", + "is_verified": false, + "line_number": 706 + } + ], + "docs/concepts/model-providers.md": [ + { + "type": "Secret Keyword", + "filename": "docs/concepts/model-providers.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 178 + }, + { + "type": "Secret Keyword", + "filename": "docs/concepts/model-providers.md", + "hashed_secret": "6a4a6c8f2406f4f0843a0a1aae6a320f92f9d6ae", + "is_verified": false, + "line_number": 274 + }, + { + "type": "Secret Keyword", + "filename": "docs/concepts/model-providers.md", + "hashed_secret": "ef83ad68b9b66e008727b7c417c6a8f618b5177e", + "is_verified": false, + "line_number": 305 + } + ], + "docs/gateway/configuration-examples.md": [ + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-examples.md", + "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "is_verified": false, + "line_number": 57 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-examples.md", + "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "is_verified": false, + "line_number": 59 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-examples.md", + "hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27", + "is_verified": false, + "line_number": 332 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-examples.md", + "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", + "is_verified": false, + "line_number": 431 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-examples.md", + "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "is_verified": false, + "line_number": 596 + } + ], + "docs/gateway/configuration-reference.md": [ + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-reference.md", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 149 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-reference.md", + "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", + "is_verified": false, + "line_number": 1267 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-reference.md", + "hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770", + "is_verified": false, + "line_number": 1283 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-reference.md", + "hashed_secret": "7f8aaf142ce0552c260f2e546dda43ddd7c9aef3", + "is_verified": false, + "line_number": 1461 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-reference.md", + "hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27", + "is_verified": false, + "line_number": 1603 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-reference.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 1631 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-reference.md", + "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", + "is_verified": false, + "line_number": 1862 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-reference.md", + "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", + "is_verified": false, + "line_number": 1966 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-reference.md", + "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "is_verified": false, + "line_number": 2202 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration-reference.md", + "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "is_verified": false, + "line_number": 2204 + } + ], + "docs/gateway/configuration.md": [ + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration.md", + "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "is_verified": false, + "line_number": 434 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/configuration.md", + "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "is_verified": false, + "line_number": 435 + } + ], + "docs/gateway/local-models.md": [ + { + "type": "Secret Keyword", + "filename": "docs/gateway/local-models.md", + "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "is_verified": false, + "line_number": 34 + }, + { + "type": "Secret Keyword", + "filename": "docs/gateway/local-models.md", + "hashed_secret": "49fd535e63175a827aab3eff9ac58a9e82460ac9", + "is_verified": false, + "line_number": 124 + } + ], + "docs/gateway/tailscale.md": [ + { + "type": "Secret Keyword", + "filename": "docs/gateway/tailscale.md", + "hashed_secret": "9cb0dc5383312aa15b9dc6745645bde18ff5ade9", + "is_verified": false, + "line_number": 81 + } + ], + "docs/help/environment.md": [ + { + "type": "Secret Keyword", + "filename": "docs/help/environment.md", + "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "is_verified": false, + "line_number": 31 + }, + { + "type": "Secret Keyword", + "filename": "docs/help/environment.md", + "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "is_verified": false, + "line_number": 33 + } + ], + "docs/help/faq.md": [ + { + "type": "Secret Keyword", + "filename": "docs/help/faq.md", + "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", + "is_verified": false, + "line_number": 1412 + }, + { + "type": "Secret Keyword", + "filename": "docs/help/faq.md", + "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "is_verified": false, + "line_number": 1689 + }, + { + "type": "Secret Keyword", + "filename": "docs/help/faq.md", + "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "is_verified": false, + "line_number": 1690 + }, + { + "type": "Secret Keyword", + "filename": "docs/help/faq.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 2118 + }, + { + "type": "Secret Keyword", + "filename": "docs/help/faq.md", + "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", + "is_verified": false, + "line_number": 2398 + } + ], + "docs/install/macos-vm.md": [ + { + "type": "Secret Keyword", + "filename": "docs/install/macos-vm.md", + "hashed_secret": "8dd3bcd07c9ee927e6921c98b4dc6e94e2cc10a9", + "is_verified": false, + "line_number": 217 + } + ], "docs/nodes/talk.md": [ { "type": "Secret Keyword", - "filename": "docs/nodes/talk.md", - "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", + "filename": "docs/nodes/talk.md", + "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", + "is_verified": false, + "line_number": 58 + } + ], + "docs/perplexity.md": [ + { + "type": "Secret Keyword", + "filename": "docs/perplexity.md", + "hashed_secret": "6b26c117c66a0c030e239eef595c1e18865132a8", + "is_verified": false, + "line_number": 36 + } + ], + "docs/plugins/voice-call.md": [ + { + "type": "Secret Keyword", + "filename": "docs/plugins/voice-call.md", + "hashed_secret": "cb46980ce5532f18440dff4bbbe097896a8c08c8", + "is_verified": false, + "line_number": 239 + } + ], + "docs/providers/anthropic.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/anthropic.md", + "hashed_secret": "c7a8c334eef5d1749fface7d42c66f9ae5e8cf36", + "is_verified": false, + "line_number": 33 + } + ], + "docs/providers/claude-max-api-proxy.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/claude-max-api-proxy.md", + "hashed_secret": "b5c2827eb65bf13b87130e7e3c424ba9ff07cd67", + "is_verified": false, + "line_number": 80 + } + ], + "docs/providers/glm.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/glm.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 24 + } + ], + "docs/providers/litellm.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/litellm.md", + "hashed_secret": "b907cadbe5a060ca6c6b78fee4c1953f34c64c32", + "is_verified": false, + "line_number": 40 + }, + { + "type": "Secret Keyword", + "filename": "docs/providers/litellm.md", + "hashed_secret": "651702a4fa521c0c493a3171cfba79c3c49eeaec", + "is_verified": false, + "line_number": 52 + } + ], + "docs/providers/minimax.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/minimax.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 71 + }, + { + "type": "Secret Keyword", + "filename": "docs/providers/minimax.md", + "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "is_verified": false, + "line_number": 140 + } + ], + "docs/providers/moonshot.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/moonshot.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 43 + } + ], + "docs/providers/nvidia.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/nvidia.md", + "hashed_secret": "2083c49ad8d63838a4d18f1de0c419f06eb464db", + "is_verified": false, + "line_number": 18 + } + ], + "docs/providers/ollama.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/ollama.md", + "hashed_secret": "e774aaeac31c6272107ba89080295e277050fa7c", + "is_verified": false, + "line_number": 33 + } + ], + "docs/providers/openai.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/openai.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 31 + } + ], + "docs/providers/opencode.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/opencode.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 27 + } + ], + "docs/providers/openrouter.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/openrouter.md", + "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "is_verified": false, + "line_number": 24 + } + ], + "docs/providers/synthetic.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/synthetic.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 33 + } + ], + "docs/providers/venice.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/venice.md", + "hashed_secret": "0b1b9301d9cd541620de4e3865d4a8f54f42fa89", + "is_verified": false, + "line_number": 55 + }, + { + "type": "Secret Keyword", + "filename": "docs/providers/venice.md", + "hashed_secret": "c179fe46776696372a90218532dc0d67267f2f04", + "is_verified": false, + "line_number": 236 + } + ], + "docs/providers/vllm.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/vllm.md", + "hashed_secret": "6a4a6c8f2406f4f0843a0a1aae6a320f92f9d6ae", + "is_verified": false, + "line_number": 26 + } + ], + "docs/providers/xiaomi.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/xiaomi.md", + "hashed_secret": "6d9c68c603e465077bdd49c62347fe54717f83a3", + "is_verified": false, + "line_number": 34 + }, + { + "type": "Secret Keyword", + "filename": "docs/providers/xiaomi.md", + "hashed_secret": "2369ac9988d706e53899168280d126c81c33bcd2", + "is_verified": false, + "line_number": 42 + } + ], + "docs/providers/zai.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/zai.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 27 + } + ], + "docs/tools/browser.md": [ + { + "type": "Basic Auth Credentials", + "filename": "docs/tools/browser.md", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", + "is_verified": false, + "line_number": 140 + } + ], + "docs/tools/firecrawl.md": [ + { + "type": "Secret Keyword", + "filename": "docs/tools/firecrawl.md", + "hashed_secret": "674397e2c0c2faaa85961c708d2a96a7cc7af217", + "is_verified": false, + "line_number": 29 + } + ], + "docs/tools/skills-config.md": [ + { + "type": "Secret Keyword", + "filename": "docs/tools/skills-config.md", + "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", + "is_verified": false, + "line_number": 29 + } + ], + "docs/tools/skills.md": [ + { + "type": "Secret Keyword", + "filename": "docs/tools/skills.md", + "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", + "is_verified": false, + "line_number": 198 + } + ], + "docs/tools/web.md": [ + { + "type": "Secret Keyword", + "filename": "docs/tools/web.md", + "hashed_secret": "6b26c117c66a0c030e239eef595c1e18865132a8", + "is_verified": false, + "line_number": 62 + }, + { + "type": "Secret Keyword", + "filename": "docs/tools/web.md", + "hashed_secret": "96c682c88ed551f22fe76d206c2dfb7df9221ad9", + "is_verified": false, + "line_number": 113 + }, + { + "type": "Secret Keyword", + "filename": "docs/tools/web.md", + "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", + "is_verified": false, + "line_number": 161 + }, + { + "type": "Secret Keyword", + "filename": "docs/tools/web.md", + "hashed_secret": "674397e2c0c2faaa85961c708d2a96a7cc7af217", + "is_verified": false, + "line_number": 235 + } + ], + "docs/tts.md": [ + { + "type": "Secret Keyword", + "filename": "docs/tts.md", + "hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770", + "is_verified": false, + "line_number": 95 + }, + { + "type": "Secret Keyword", + "filename": "docs/tts.md", + "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", + "is_verified": false, + "line_number": 100 + } + ], + "docs/zh-CN/brave-search.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/brave-search.md", + "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", + "is_verified": false, + "line_number": 34 + } + ], + "docs/zh-CN/channels/bluebubbles.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/channels/bluebubbles.md", + "hashed_secret": "555da20df20d4172e00f1b73d7c3943802055270", + "is_verified": false, + "line_number": 43 + } + ], + "docs/zh-CN/channels/feishu.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/channels/feishu.md", + "hashed_secret": "b60d121b438a380c343d5ec3c2037564b82ffef3", + "is_verified": false, + "line_number": 195 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/channels/feishu.md", + "hashed_secret": "186154712b2d5f6791d85b9a0987b98fa231779c", + "is_verified": false, + "line_number": 445 + } + ], + "docs/zh-CN/channels/line.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/channels/line.md", + "hashed_secret": "83661b43df128631f891767fbfc5b049af3dce86", + "is_verified": false, + "line_number": 62 + } + ], + "docs/zh-CN/channels/matrix.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/channels/matrix.md", + "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", + "is_verified": false, + "line_number": 62 + } + ], + "docs/zh-CN/channels/nextcloud-talk.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/channels/nextcloud-talk.md", + "hashed_secret": "76ed0a056aa77060de25754586440cff390791d0", + "is_verified": false, + "line_number": 61 + } + ], + "docs/zh-CN/channels/nostr.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/channels/nostr.md", + "hashed_secret": "edeb23e25a619c434d22bb7f1c3ca4841166b4e8", + "is_verified": false, + "line_number": 74 + } + ], + "docs/zh-CN/channels/slack.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/channels/slack.md", + "hashed_secret": "3f4800fb7c1fb79a9a48bfd562d90bc6b2e2b718", + "is_verified": false, + "line_number": 153 + } + ], + "docs/zh-CN/channels/twitch.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/channels/twitch.md", + "hashed_secret": "0d1ba0da3e84e54f29846c93c43182eede365858", + "is_verified": false, + "line_number": 145 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/channels/twitch.md", + "hashed_secret": "7cb4c5b8b81e266d08d4f106799af98d748bceb9", + "is_verified": false, + "line_number": 330 + } + ], + "docs/zh-CN/concepts/memory.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/concepts/memory.md", + "hashed_secret": "39d711243bfcee9fec8299b204e1aa9c3430fa12", + "is_verified": false, + "line_number": 127 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/concepts/memory.md", + "hashed_secret": "1a8abbf465c52363ab4c9c6ad945b8e857cbea55", + "is_verified": false, + "line_number": 150 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/concepts/memory.md", + "hashed_secret": "b9f640d6095b9f6b5a65983f7b76dbbb254e0044", + "is_verified": false, + "line_number": 398 + } + ], + "docs/zh-CN/concepts/model-providers.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/concepts/model-providers.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 181 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/concepts/model-providers.md", + "hashed_secret": "ef83ad68b9b66e008727b7c417c6a8f618b5177e", + "is_verified": false, + "line_number": 282 + } + ], + "docs/zh-CN/gateway/configuration-examples.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration-examples.md", + "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "is_verified": false, + "line_number": 64 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration-examples.md", + "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "is_verified": false, + "line_number": 66 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration-examples.md", + "hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27", + "is_verified": false, + "line_number": 329 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration-examples.md", + "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", + "is_verified": false, + "line_number": 424 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration-examples.md", + "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "is_verified": false, + "line_number": 563 + } + ], + "docs/zh-CN/gateway/configuration.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration.md", + "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "is_verified": false, + "line_number": 289 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration.md", + "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "is_verified": false, + "line_number": 291 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration.md", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 1092 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration.md", + "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", + "is_verified": false, + "line_number": 1570 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration.md", + "hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770", + "is_verified": false, + "line_number": 1586 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration.md", + "hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27", + "is_verified": false, + "line_number": 2398 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 2476 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration.md", + "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", + "is_verified": false, + "line_number": 2768 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/configuration.md", + "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", + "is_verified": false, + "line_number": 2967 + } + ], + "docs/zh-CN/gateway/local-models.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/local-models.md", + "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "is_verified": false, + "line_number": 41 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/local-models.md", + "hashed_secret": "49fd535e63175a827aab3eff9ac58a9e82460ac9", + "is_verified": false, + "line_number": 131 + } + ], + "docs/zh-CN/gateway/tailscale.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/gateway/tailscale.md", + "hashed_secret": "9cb0dc5383312aa15b9dc6745645bde18ff5ade9", + "is_verified": false, + "line_number": 80 + } + ], + "docs/zh-CN/help/environment.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/help/environment.md", + "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "is_verified": false, + "line_number": 38 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/help/environment.md", + "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "is_verified": false, + "line_number": 40 + } + ], + "docs/zh-CN/help/faq.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/help/faq.md", + "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", + "is_verified": false, + "line_number": 1277 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/help/faq.md", + "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "is_verified": false, + "line_number": 1524 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/help/faq.md", + "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", + "is_verified": false, + "line_number": 1525 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/help/faq.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 1916 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/help/faq.md", + "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", + "is_verified": false, + "line_number": 2191 + } + ], + "docs/zh-CN/install/macos-vm.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/install/macos-vm.md", + "hashed_secret": "8dd3bcd07c9ee927e6921c98b4dc6e94e2cc10a9", + "is_verified": false, + "line_number": 224 + } + ], + "docs/zh-CN/nodes/talk.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/nodes/talk.md", + "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", + "is_verified": false, + "line_number": 65 + } + ], + "docs/zh-CN/perplexity.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/perplexity.md", + "hashed_secret": "6b26c117c66a0c030e239eef595c1e18865132a8", + "is_verified": false, + "line_number": 42 + } + ], + "docs/zh-CN/plugins/voice-call.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/plugins/voice-call.md", + "hashed_secret": "cb46980ce5532f18440dff4bbbe097896a8c08c8", + "is_verified": false, + "line_number": 167 + } + ], + "docs/zh-CN/providers/anthropic.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/anthropic.md", + "hashed_secret": "c7a8c334eef5d1749fface7d42c66f9ae5e8cf36", + "is_verified": false, + "line_number": 40 + } + ], + "docs/zh-CN/providers/claude-max-api-proxy.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/claude-max-api-proxy.md", + "hashed_secret": "b5c2827eb65bf13b87130e7e3c424ba9ff07cd67", + "is_verified": false, + "line_number": 87 + } + ], + "docs/zh-CN/providers/glm.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/glm.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 30 + } + ], + "docs/zh-CN/providers/minimax.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/minimax.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 72 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/minimax.md", + "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "is_verified": false, + "line_number": 140 + } + ], + "docs/zh-CN/providers/moonshot.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/moonshot.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 47 + } + ], + "docs/zh-CN/providers/ollama.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/ollama.md", + "hashed_secret": "e774aaeac31c6272107ba89080295e277050fa7c", + "is_verified": false, + "line_number": 38 + } + ], + "docs/zh-CN/providers/openai.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/openai.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 37 + } + ], + "docs/zh-CN/providers/opencode.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/opencode.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 32 + } + ], + "docs/zh-CN/providers/openrouter.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/openrouter.md", + "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "is_verified": false, + "line_number": 30 + } + ], + "docs/zh-CN/providers/synthetic.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/synthetic.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 39 + } + ], + "docs/zh-CN/providers/venice.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/venice.md", + "hashed_secret": "0b1b9301d9cd541620de4e3865d4a8f54f42fa89", + "is_verified": false, + "line_number": 62 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/venice.md", + "hashed_secret": "c179fe46776696372a90218532dc0d67267f2f04", + "is_verified": false, + "line_number": 243 + } + ], + "docs/zh-CN/providers/xiaomi.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/xiaomi.md", + "hashed_secret": "6d9c68c603e465077bdd49c62347fe54717f83a3", + "is_verified": false, + "line_number": 38 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/xiaomi.md", + "hashed_secret": "2369ac9988d706e53899168280d126c81c33bcd2", + "is_verified": false, + "line_number": 46 + } + ], + "docs/zh-CN/providers/zai.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/providers/zai.md", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "is_verified": false, + "line_number": 32 + } + ], + "docs/zh-CN/tools/browser.md": [ + { + "type": "Basic Auth Credentials", + "filename": "docs/zh-CN/tools/browser.md", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", + "is_verified": false, + "line_number": 137 + } + ], + "docs/zh-CN/tools/firecrawl.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/tools/firecrawl.md", + "hashed_secret": "674397e2c0c2faaa85961c708d2a96a7cc7af217", + "is_verified": false, + "line_number": 36 + } + ], + "docs/zh-CN/tools/skills-config.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/tools/skills-config.md", + "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", "is_verified": false, - "line_number": 50 + "line_number": 36 } ], - "docs/perplexity.md": [ + "docs/zh-CN/tools/skills.md": [ { "type": "Secret Keyword", - "filename": "docs/perplexity.md", + "filename": "docs/zh-CN/tools/skills.md", + "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", + "is_verified": false, + "line_number": 183 + } + ], + "docs/zh-CN/tools/web.md": [ + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/tools/web.md", "hashed_secret": "6b26c117c66a0c030e239eef595c1e18865132a8", "is_verified": false, - "line_number": 35 + "line_number": 67 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/tools/web.md", + "hashed_secret": "96c682c88ed551f22fe76d206c2dfb7df9221ad9", + "is_verified": false, + "line_number": 112 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/tools/web.md", + "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", + "is_verified": false, + "line_number": 159 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/tools/web.md", + "hashed_secret": "674397e2c0c2faaa85961c708d2a96a7cc7af217", + "is_verified": false, + "line_number": 229 } ], - "docs/providers/anthropic.md": [ + "docs/zh-CN/tts.md": [ { "type": "Secret Keyword", - "filename": "docs/providers/anthropic.md", - "hashed_secret": "c7a8c334eef5d1749fface7d42c66f9ae5e8cf36", + "filename": "docs/zh-CN/tts.md", + "hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770", "is_verified": false, - "line_number": 32 + "line_number": 89 + }, + { + "type": "Secret Keyword", + "filename": "docs/zh-CN/tts.md", + "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", + "is_verified": false, + "line_number": 94 } ], - "docs/providers/glm.md": [ + "extensions/bluebubbles/src/actions.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/providers/glm.md", - "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "filename": "extensions/bluebubbles/src/actions.test.ts", + "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", "is_verified": false, - "line_number": 22 + "line_number": 86 } ], - "docs/providers/minimax.md": [ + "extensions/bluebubbles/src/attachments.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/providers/minimax.md", - "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "filename": "extensions/bluebubbles/src/attachments.test.ts", + "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", "is_verified": false, - "line_number": 49 + "line_number": 21 }, { "type": "Secret Keyword", - "filename": "docs/providers/minimax.md", - "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "filename": "extensions/bluebubbles/src/attachments.test.ts", + "hashed_secret": "db1530e1ea43af094d3d75b8dbaf19a4a182a318", "is_verified": false, - "line_number": 118 + "line_number": 85 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/attachments.test.ts", + "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "is_verified": false, + "line_number": 103 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/attachments.test.ts", + "hashed_secret": "052f076c732648ab32d2fcde9fe255319bfa0c7b", + "is_verified": false, + "line_number": 215 } ], - "docs/providers/moonshot.md": [ + "extensions/bluebubbles/src/chat.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/providers/moonshot.md", - "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "filename": "extensions/bluebubbles/src/chat.test.ts", + "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "is_verified": false, - "line_number": 39 + "line_number": 19 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/chat.test.ts", + "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", + "is_verified": false, + "line_number": 54 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/chat.test.ts", + "hashed_secret": "5c5a15a8b0b3e154d77746945e563ba40100681b", + "is_verified": false, + "line_number": 82 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/chat.test.ts", + "hashed_secret": "faacad0ce4ea1c19b46e128fd79679d37d3d331d", + "is_verified": false, + "line_number": 131 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/chat.test.ts", + "hashed_secret": "4dcc26a1d99532846fedf1265df4f40f4e0005b8", + "is_verified": false, + "line_number": 227 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/chat.test.ts", + "hashed_secret": "fd2a721f7be1ee3d691a011affcdb11d0ca365a8", + "is_verified": false, + "line_number": 290 } ], - "docs/providers/openai.md": [ + "extensions/bluebubbles/src/monitor.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/providers/openai.md", - "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "filename": "extensions/bluebubbles/src/monitor.test.ts", + "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", "is_verified": false, - "line_number": 31 + "line_number": 278 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/monitor.test.ts", + "hashed_secret": "1ae0af3fe72b3ba394f9fa95a6cffc090d726c23", + "is_verified": false, + "line_number": 552 } ], - "docs/providers/opencode.md": [ + "extensions/bluebubbles/src/reactions.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/providers/opencode.md", - "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "filename": "extensions/bluebubbles/src/reactions.test.ts", + "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "is_verified": false, - "line_number": 25 + "line_number": 37 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/reactions.test.ts", + "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", + "is_verified": false, + "line_number": 178 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/reactions.test.ts", + "hashed_secret": "a4a05c9a6449eb9d6cdac81dd7edc49230e327e6", + "is_verified": false, + "line_number": 209 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/reactions.test.ts", + "hashed_secret": "a2833da9f0a16f09994754d0a31749cecf8c8c77", + "is_verified": false, + "line_number": 315 } ], - "docs/providers/openrouter.md": [ + "extensions/bluebubbles/src/send.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/providers/openrouter.md", - "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", + "filename": "extensions/bluebubbles/src/send.test.ts", + "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "is_verified": false, - "line_number": 22 + "line_number": 55 + }, + { + "type": "Secret Keyword", + "filename": "extensions/bluebubbles/src/send.test.ts", + "hashed_secret": "faacad0ce4ea1c19b46e128fd79679d37d3d331d", + "is_verified": false, + "line_number": 692 } ], - "docs/providers/synthetic.md": [ + "extensions/bluebubbles/src/targets.test.ts": [ + { + "type": "Hex High Entropy String", + "filename": "extensions/bluebubbles/src/targets.test.ts", + "hashed_secret": "a3af2fb0c1e2a30bb038049e1e4b401593af6225", + "is_verified": false, + "line_number": 61 + } + ], + "extensions/bluebubbles/src/targets.ts": [ + { + "type": "Hex High Entropy String", + "filename": "extensions/bluebubbles/src/targets.ts", + "hashed_secret": "a3af2fb0c1e2a30bb038049e1e4b401593af6225", + "is_verified": false, + "line_number": 265 + } + ], + "extensions/copilot-proxy/index.ts": [ { "type": "Secret Keyword", - "filename": "docs/providers/synthetic.md", - "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "filename": "extensions/copilot-proxy/index.ts", + "hashed_secret": "50f013532a9770a2c2cfdc38b7581dd01df69b70", "is_verified": false, - "line_number": 31 + "line_number": 9 } ], - "docs/providers/zai.md": [ + "extensions/feishu/skills/feishu-doc/SKILL.md": [ + { + "type": "Hex High Entropy String", + "filename": "extensions/feishu/skills/feishu-doc/SKILL.md", + "hashed_secret": "8a2256bca273bb01a4e09ae6555b1e6652d9ff8c", + "is_verified": false, + "line_number": 20 + } + ], + "extensions/feishu/skills/feishu-wiki/SKILL.md": [ + { + "type": "Hex High Entropy String", + "filename": "extensions/feishu/skills/feishu-wiki/SKILL.md", + "hashed_secret": "8a2256bca273bb01a4e09ae6555b1e6652d9ff8c", + "is_verified": false, + "line_number": 40 + } + ], + "extensions/feishu/src/channel.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/providers/zai.md", - "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", + "filename": "extensions/feishu/src/channel.test.ts", + "hashed_secret": "8437d84cae482d10a2b9fd3f555d45006979e4be", "is_verified": false, - "line_number": 25 + "line_number": 21 } ], - "docs/tools/browser.md": [ + "extensions/feishu/src/docx.test.ts": [ { - "type": "Basic Auth Credentials", - "filename": "docs/tools/browser.md", - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", + "type": "Secret Keyword", + "filename": "extensions/feishu/src/docx.test.ts", + "hashed_secret": "f49922d511d666848f250663c4fca84074b856a8", + "is_verified": false, + "line_number": 97 + } + ], + "extensions/feishu/src/media.test.ts": [ + { + "type": "Secret Keyword", + "filename": "extensions/feishu/src/media.test.ts", + "hashed_secret": "f49922d511d666848f250663c4fca84074b856a8", + "is_verified": false, + "line_number": 45 + } + ], + "extensions/feishu/src/reply-dispatcher.test.ts": [ + { + "type": "Secret Keyword", + "filename": "extensions/feishu/src/reply-dispatcher.test.ts", + "hashed_secret": "f49922d511d666848f250663c4fca84074b856a8", + "is_verified": false, + "line_number": 48 + } + ], + "extensions/google-antigravity-auth/index.ts": [ + { + "type": "Base64 High Entropy String", + "filename": "extensions/google-antigravity-auth/index.ts", + "hashed_secret": "709d0f232b6ac4f8d24dec3e4fabfdb14257174f", + "is_verified": false, + "line_number": 14 + } + ], + "extensions/google-gemini-cli-auth/oauth.test.ts": [ + { + "type": "Secret Keyword", + "filename": "extensions/google-gemini-cli-auth/oauth.test.ts", + "hashed_secret": "021343c1f561d7bcbc3b513df45cc3a6baf67b43", + "is_verified": false, + "line_number": 30 + } + ], + "extensions/irc/src/accounts.ts": [ + { + "type": "Secret Keyword", + "filename": "extensions/irc/src/accounts.ts", + "hashed_secret": "920f8f5815b381ea692e9e7c2f7119f2b1aa620a", + "is_verified": false, + "line_number": 19 + } + ], + "extensions/irc/src/client.test.ts": [ + { + "type": "Secret Keyword", + "filename": "extensions/irc/src/client.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 8 + }, + { + "type": "Secret Keyword", + "filename": "extensions/irc/src/client.test.ts", + "hashed_secret": "b1cc3814a07fc3d7094f4cc181df7b57b51d165b", "is_verified": false, - "line_number": 163 + "line_number": 39 } ], - "docs/tools/firecrawl.md": [ + "extensions/line/src/channel.startup.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/tools/firecrawl.md", - "hashed_secret": "674397e2c0c2faaa85961c708d2a96a7cc7af217", + "filename": "extensions/line/src/channel.startup.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 28 + "line_number": 103 } ], - "docs/tools/skills-config.md": [ + "extensions/matrix/src/matrix/accounts.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/tools/skills-config.md", - "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", + "filename": "extensions/matrix/src/matrix/accounts.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 30 + "line_number": 74 } ], - "docs/tools/skills.md": [ + "extensions/matrix/src/matrix/client.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/tools/skills.md", - "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", + "filename": "extensions/matrix/src/matrix/client.test.ts", + "hashed_secret": "fe7fcdaea49ece14677acd32374d2f1225819d5c", "is_verified": false, - "line_number": 160 + "line_number": 13 + }, + { + "type": "Secret Keyword", + "filename": "extensions/matrix/src/matrix/client.test.ts", + "hashed_secret": "3dc927d80543dc0f643940b70d066bd4b4c4b78e", + "is_verified": false, + "line_number": 23 } ], - "docs/tools/web.md": [ + "extensions/matrix/src/matrix/client/storage.ts": [ { "type": "Secret Keyword", - "filename": "docs/tools/web.md", - "hashed_secret": "6b26c117c66a0c030e239eef595c1e18865132a8", + "filename": "extensions/matrix/src/matrix/client/storage.ts", + "hashed_secret": "7505d64a54e061b7acd54ccd58b49dc43500b635", "is_verified": false, - "line_number": 61 - }, + "line_number": 8 + } + ], + "extensions/memory-lancedb/config.ts": [ { "type": "Secret Keyword", - "filename": "docs/tools/web.md", - "hashed_secret": "96c682c88ed551f22fe76d206c2dfb7df9221ad9", + "filename": "extensions/memory-lancedb/config.ts", + "hashed_secret": "ecb252044b5ea0f679ee78ec1a12904739e2904d", "is_verified": false, - "line_number": 112 - }, + "line_number": 101 + } + ], + "extensions/memory-lancedb/index.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/tools/web.md", - "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", + "filename": "extensions/memory-lancedb/index.test.ts", + "hashed_secret": "ed65c049bb2f78ee4f703b2158ba9cc6ea31fb7e", "is_verified": false, - "line_number": 160 - }, + "line_number": 71 + } + ], + "extensions/msteams/src/probe.test.ts": [ { "type": "Secret Keyword", - "filename": "docs/tools/web.md", - "hashed_secret": "674397e2c0c2faaa85961c708d2a96a7cc7af217", + "filename": "extensions/msteams/src/probe.test.ts", + "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", "is_verified": false, - "line_number": 223 + "line_number": 35 } ], - "docs/tts.md": [ + "extensions/nextcloud-talk/src/accounts.ts": [ { "type": "Secret Keyword", - "filename": "docs/tts.md", - "hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770", + "filename": "extensions/nextcloud-talk/src/accounts.ts", + "hashed_secret": "920f8f5815b381ea692e9e7c2f7119f2b1aa620a", "is_verified": false, - "line_number": 72 + "line_number": 22 }, { "type": "Secret Keyword", - "filename": "docs/tts.md", - "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", + "filename": "extensions/nextcloud-talk/src/accounts.ts", + "hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb", "is_verified": false, - "line_number": 77 + "line_number": 151 } ], - "extensions/bluebubbles/src/actions.test.ts": [ + "extensions/nextcloud-talk/src/channel.ts": [ { "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/actions.test.ts", - "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", + "filename": "extensions/nextcloud-talk/src/channel.ts", + "hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb", "is_verified": false, - "line_number": 73 + "line_number": 396 } ], - "extensions/bluebubbles/src/attachments.test.ts": [ + "extensions/nostr/README.md": [ { "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/attachments.test.ts", - "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", + "filename": "extensions/nostr/README.md", + "hashed_secret": "edeb23e25a619c434d22bb7f1c3ca4841166b4e8", "is_verified": false, - "line_number": 35 + "line_number": 46 + } + ], + "extensions/nostr/src/channel.test.ts": [ + { + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/channel.test.ts", + "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", + "is_verified": false, + "line_number": 48 }, { "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/attachments.test.ts", - "hashed_secret": "db1530e1ea43af094d3d75b8dbaf19a4a182a318", + "filename": "extensions/nostr/src/channel.test.ts", + "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", "is_verified": false, - "line_number": 99 + "line_number": 48 + } + ], + "extensions/nostr/src/nostr-bus.fuzz.test.ts": [ + { + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/nostr-bus.fuzz.test.ts", + "hashed_secret": "2b4489606a23fb31fcdc849fa7e577ba90f6d39a", + "is_verified": false, + "line_number": 193 }, { - "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/attachments.test.ts", - "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/nostr-bus.fuzz.test.ts", + "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", "is_verified": false, - "line_number": 117 + "line_number": 194 }, { - "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/attachments.test.ts", - "hashed_secret": "052f076c732648ab32d2fcde9fe255319bfa0c7b", + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/nostr-bus.fuzz.test.ts", + "hashed_secret": "b84cb0c3925d34496e6c8b0e55b8c1664a438035", "is_verified": false, - "line_number": 229 + "line_number": 199 } ], - "extensions/bluebubbles/src/chat.test.ts": [ - { - "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/chat.test.ts", - "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", - "is_verified": false, - "line_number": 33 - }, + "extensions/nostr/src/nostr-bus.test.ts": [ { - "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/chat.test.ts", - "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/nostr-bus.test.ts", + "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", "is_verified": false, - "line_number": 68 + "line_number": 11 }, { - "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/chat.test.ts", - "hashed_secret": "5c5a15a8b0b3e154d77746945e563ba40100681b", + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/nostr-bus.test.ts", + "hashed_secret": "7258e28563f03fb4c5994e8402e6f610d1f0f110", "is_verified": false, - "line_number": 85 + "line_number": 33 }, { - "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/chat.test.ts", - "hashed_secret": "faacad0ce4ea1c19b46e128fd79679d37d3d331d", + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/nostr-bus.test.ts", + "hashed_secret": "2b4489606a23fb31fcdc849fa7e577ba90f6d39a", "is_verified": false, - "line_number": 134 + "line_number": 101 }, { - "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/chat.test.ts", - "hashed_secret": "4dcc26a1d99532846fedf1265df4f40f4e0005b8", + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/nostr-bus.test.ts", + "hashed_secret": "ef717286343f6da3f4e6f68c6de02a5148a801c4", "is_verified": false, - "line_number": 219 + "line_number": 106 }, { - "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/chat.test.ts", - "hashed_secret": "fd2a721f7be1ee3d691a011affcdb11d0ca365a8", + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/nostr-bus.test.ts", + "hashed_secret": "98b35fe4c45011220f509ebb5546d3889b55a891", "is_verified": false, - "line_number": 282 + "line_number": 111 } ], - "extensions/bluebubbles/src/monitor.test.ts": [ + "extensions/nostr/src/nostr-profile.fuzz.test.ts": [ { - "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/monitor.test.ts", - "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/nostr-profile.fuzz.test.ts", + "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", "is_verified": false, - "line_number": 187 - }, + "line_number": 11 + } + ], + "extensions/nostr/src/nostr-profile.test.ts": [ { - "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/monitor.test.ts", - "hashed_secret": "1ae0af3fe72b3ba394f9fa95a6cffc090d726c23", + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/nostr-profile.test.ts", + "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", "is_verified": false, - "line_number": 394 + "line_number": 14 } ], - "extensions/bluebubbles/src/reactions.test.ts": [ + "extensions/nostr/src/types.test.ts": [ { - "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/reactions.test.ts", - "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "type": "Hex High Entropy String", + "filename": "extensions/nostr/src/types.test.ts", + "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", "is_verified": false, - "line_number": 38 + "line_number": 4 }, { "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/reactions.test.ts", - "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", + "filename": "extensions/nostr/src/types.test.ts", + "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", "is_verified": false, - "line_number": 179 + "line_number": 4 }, { "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/reactions.test.ts", - "hashed_secret": "a4a05c9a6449eb9d6cdac81dd7edc49230e327e6", + "filename": "extensions/nostr/src/types.test.ts", + "hashed_secret": "3bee216ebc256d692260fc3adc765050508fef5e", "is_verified": false, - "line_number": 210 - }, + "line_number": 123 + } + ], + "extensions/open-prose/skills/prose/SKILL.md": [ + { + "type": "Basic Auth Credentials", + "filename": "extensions/open-prose/skills/prose/SKILL.md", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", + "is_verified": false, + "line_number": 204 + } + ], + "extensions/open-prose/skills/prose/state/postgres.md": [ { "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/reactions.test.ts", - "hashed_secret": "a2833da9f0a16f09994754d0a31749cecf8c8c77", + "filename": "extensions/open-prose/skills/prose/state/postgres.md", + "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", "is_verified": false, - "line_number": 316 + "line_number": 77 + }, + { + "type": "Basic Auth Credentials", + "filename": "extensions/open-prose/skills/prose/state/postgres.md", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", + "is_verified": false, + "line_number": 200 } ], - "extensions/bluebubbles/src/send.test.ts": [ + "extensions/twitch/src/onboarding.test.ts": [ { "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/send.test.ts", - "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "filename": "extensions/twitch/src/onboarding.test.ts", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_verified": false, - "line_number": 38 + "line_number": 239 }, { "type": "Secret Keyword", - "filename": "extensions/bluebubbles/src/send.test.ts", - "hashed_secret": "faacad0ce4ea1c19b46e128fd79679d37d3d331d", + "filename": "extensions/twitch/src/onboarding.test.ts", + "hashed_secret": "c8d8f8140951794fa875ea2c2d010c4382f36566", + "is_verified": false, + "line_number": 249 + } + ], + "extensions/twitch/src/status.test.ts": [ + { + "type": "Secret Keyword", + "filename": "extensions/twitch/src/status.test.ts", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_verified": false, - "line_number": 675 + "line_number": 122 } ], - "extensions/bluebubbles/src/targets.test.ts": [ + "extensions/voice-call/README.md": [ { - "type": "Hex High Entropy String", - "filename": "extensions/bluebubbles/src/targets.test.ts", - "hashed_secret": "a3af2fb0c1e2a30bb038049e1e4b401593af6225", + "type": "Secret Keyword", + "filename": "extensions/voice-call/README.md", + "hashed_secret": "48004f85d79e636cfd408c3baddcb1f0bbdd611a", "is_verified": false, - "line_number": 62 + "line_number": 49 } ], - "extensions/bluebubbles/src/targets.ts": [ + "extensions/voice-call/src/config.test.ts": [ { - "type": "Hex High Entropy String", - "filename": "extensions/bluebubbles/src/targets.ts", - "hashed_secret": "a3af2fb0c1e2a30bb038049e1e4b401593af6225", + "type": "Secret Keyword", + "filename": "extensions/voice-call/src/config.test.ts", + "hashed_secret": "62207a469ec2fdcfc7d66b04c2980ac1501acbf0", "is_verified": false, - "line_number": 214 + "line_number": 129 } ], - "extensions/copilot-proxy/index.ts": [ + "extensions/voice-call/src/providers/telnyx.test.ts": [ { "type": "Secret Keyword", - "filename": "extensions/copilot-proxy/index.ts", - "hashed_secret": "50f013532a9770a2c2cfdc38b7581dd01df69b70", + "filename": "extensions/voice-call/src/providers/telnyx.test.ts", + "hashed_secret": "62207a469ec2fdcfc7d66b04c2980ac1501acbf0", "is_verified": false, - "line_number": 4 + "line_number": 30 } ], - "extensions/google-antigravity-auth/index.ts": [ + "extensions/zalo/README.md": [ { - "type": "Base64 High Entropy String", - "filename": "extensions/google-antigravity-auth/index.ts", - "hashed_secret": "709d0f232b6ac4f8d24dec3e4fabfdb14257174f", + "type": "Secret Keyword", + "filename": "extensions/zalo/README.md", + "hashed_secret": "f51aaee16a4a756d287f126b99c081b73cba7f15", "is_verified": false, - "line_number": 9 + "line_number": 41 } ], - "extensions/matrix/src/matrix/accounts.test.ts": [ + "extensions/zalo/src/monitor.webhook.test.ts": [ { "type": "Secret Keyword", - "filename": "extensions/matrix/src/matrix/accounts.test.ts", + "filename": "extensions/zalo/src/monitor.webhook.test.ts", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 75 + "line_number": 40 } ], - "extensions/matrix/src/matrix/client.test.ts": [ + "skills/1password/references/cli-examples.md": [ { "type": "Secret Keyword", - "filename": "extensions/matrix/src/matrix/client.test.ts", - "hashed_secret": "fe7fcdaea49ece14677acd32374d2f1225819d5c", + "filename": "skills/1password/references/cli-examples.md", + "hashed_secret": "9dda0987cc3054773a2df97e352d4f64d233ef10", "is_verified": false, - "line_number": 14 - }, + "line_number": 17 + } + ], + "skills/openai-whisper-api/SKILL.md": [ { "type": "Secret Keyword", - "filename": "extensions/matrix/src/matrix/client.test.ts", - "hashed_secret": "3dc927d80543dc0f643940b70d066bd4b4c4b78e", + "filename": "skills/openai-whisper-api/SKILL.md", + "hashed_secret": "1077361f94d70e1ddcc7c6dc581a489532a81d03", "is_verified": false, - "line_number": 24 + "line_number": 48 } ], - "extensions/matrix/src/matrix/client/storage.ts": [ + "skills/trello/SKILL.md": [ { "type": "Secret Keyword", - "filename": "extensions/matrix/src/matrix/client/storage.ts", - "hashed_secret": "7505d64a54e061b7acd54ccd58b49dc43500b635", + "filename": "skills/trello/SKILL.md", + "hashed_secret": "11fa7c37d697f30e6aee828b4426a10f83ab2380", "is_verified": false, - "line_number": 9 + "line_number": 22 } ], - "extensions/memory-lancedb/config.ts": [ + "src/agents/compaction.tool-result-details.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "extensions/memory-lancedb/config.ts", - "hashed_secret": "ecb252044b5ea0f679ee78ec1a12904739e2904d", + "filename": "src/agents/compaction.tool-result-details.e2e.test.ts", + "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "is_verified": false, - "line_number": 70 + "line_number": 50 } ], - "extensions/memory-lancedb/index.test.ts": [ + "src/agents/memory-search.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "extensions/memory-lancedb/index.test.ts", - "hashed_secret": "ed65c049bb2f78ee4f703b2158ba9cc6ea31fb7e", + "filename": "src/agents/memory-search.e2e.test.ts", + "hashed_secret": "a1b49d68a91fdf9c9217773f3fac988d77fa0f50", "is_verified": false, - "line_number": 70 + "line_number": 189 } ], - "extensions/msteams/src/probe.test.ts": [ + "src/agents/minimax-vlm.normalizes-api-key.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "extensions/msteams/src/probe.test.ts", - "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", + "filename": "src/agents/minimax-vlm.normalizes-api-key.e2e.test.ts", + "hashed_secret": "8a8461b67e3fe515f248ac2610fd7b1f4fc3b412", "is_verified": false, - "line_number": 34 + "line_number": 28 } ], - "extensions/nextcloud-talk/src/accounts.ts": [ + "src/agents/model-auth.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "extensions/nextcloud-talk/src/accounts.ts", - "hashed_secret": "920f8f5815b381ea692e9e7c2f7119f2b1aa620a", + "filename": "src/agents/model-auth.e2e.test.ts", + "hashed_secret": "07a6b9cec637c806195e8aa7e5c0851ab03dc35e", "is_verified": false, - "line_number": 26 + "line_number": 228 }, { "type": "Secret Keyword", - "filename": "extensions/nextcloud-talk/src/accounts.ts", - "hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb", + "filename": "src/agents/model-auth.e2e.test.ts", + "hashed_secret": "21f296583ccd80c5ab9b3330a8b0d47e4a409fb9", "is_verified": false, - "line_number": 139 - } - ], - "extensions/nextcloud-talk/src/channel.ts": [ + "line_number": 254 + }, { "type": "Secret Keyword", - "filename": "extensions/nextcloud-talk/src/channel.ts", - "hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb", + "filename": "src/agents/model-auth.e2e.test.ts", + "hashed_secret": "b65888424ecafcc98bfd803b24817e4dadf821f8", "is_verified": false, - "line_number": 390 - } - ], - "extensions/nostr/README.md": [ + "line_number": 275 + }, { "type": "Secret Keyword", - "filename": "extensions/nostr/README.md", - "hashed_secret": "edeb23e25a619c434d22bb7f1c3ca4841166b4e8", + "filename": "src/agents/model-auth.e2e.test.ts", + "hashed_secret": "77e991e9f56e6fa4ed1a908208048421f1214c07", "is_verified": false, - "line_number": 43 - } - ], - "extensions/nostr/src/channel.test.ts": [ + "line_number": 296 + }, { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/channel.test.ts", - "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", + "type": "Secret Keyword", + "filename": "src/agents/model-auth.e2e.test.ts", + "hashed_secret": "dff6d4ff5dc357cf451d1855ab9cbda562645c9f", "is_verified": false, - "line_number": 48 + "line_number": 319 }, { "type": "Secret Keyword", - "filename": "extensions/nostr/src/channel.test.ts", - "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", + "filename": "src/agents/model-auth.e2e.test.ts", + "hashed_secret": "b43be360db55d89ec6afd74d6ed8f82002fe4982", "is_verified": false, - "line_number": 48 + "line_number": 374 + }, + { + "type": "Secret Keyword", + "filename": "src/agents/model-auth.e2e.test.ts", + "hashed_secret": "5b850e9dc678446137ff6d905ebd78634d687fdd", + "is_verified": false, + "line_number": 395 } ], - "extensions/nostr/src/nostr-bus.fuzz.test.ts": [ + "src/agents/model-auth.ts": [ { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/nostr-bus.fuzz.test.ts", - "hashed_secret": "2b4489606a23fb31fcdc849fa7e577ba90f6d39a", + "type": "Secret Keyword", + "filename": "src/agents/model-auth.ts", + "hashed_secret": "8956265d216d474a080edaa97880d37fc1386f33", "is_verified": false, - "line_number": 202 - }, + "line_number": 25 + } + ], + "src/agents/models-config.e2e-harness.ts": [ { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/nostr-bus.fuzz.test.ts", - "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", + "type": "Secret Keyword", + "filename": "src/agents/models-config.e2e-harness.ts", + "hashed_secret": "7cf31e8b6cda49f70c31f1f25af05d46f924142d", "is_verified": false, - "line_number": 203 + "line_number": 110 + } + ], + "src/agents/models-config.fills-missing-provider-apikey-from-env-var.e2e.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/agents/models-config.fills-missing-provider-apikey-from-env-var.e2e.test.ts", + "hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c", + "is_verified": false, + "line_number": 19 }, { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/nostr-bus.fuzz.test.ts", - "hashed_secret": "b84cb0c3925d34496e6c8b0e55b8c1664a438035", + "type": "Secret Keyword", + "filename": "src/agents/models-config.fills-missing-provider-apikey-from-env-var.e2e.test.ts", + "hashed_secret": "3a81eb091f80c845232225be5663d270e90dacb7", "is_verified": false, - "line_number": 208 + "line_number": 73 } ], - "extensions/nostr/src/nostr-bus.test.ts": [ + "src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.e2e.test.ts": [ { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/nostr-bus.test.ts", - "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", + "type": "Secret Keyword", + "filename": "src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.e2e.test.ts", + "hashed_secret": "980d02eb9335ae7c9e9984f6c8ad432352a0d2ac", "is_verified": false, - "line_number": 11 - }, + "line_number": 20 + } + ], + "src/agents/models-config.providers.nvidia.test.ts": [ { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/nostr-bus.test.ts", - "hashed_secret": "7258e28563f03fb4c5994e8402e6f610d1f0f110", + "type": "Secret Keyword", + "filename": "src/agents/models-config.providers.nvidia.test.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 33 + "line_number": 13 }, { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/nostr-bus.test.ts", - "hashed_secret": "2b4489606a23fb31fcdc849fa7e577ba90f6d39a", + "type": "Secret Keyword", + "filename": "src/agents/models-config.providers.nvidia.test.ts", + "hashed_secret": "be1a7be9d4d5af417882b267f4db6dddc08507bd", "is_verified": false, - "line_number": 101 - }, + "line_number": 27 + } + ], + "src/agents/models-config.providers.ollama.e2e.test.ts": [ { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/nostr-bus.test.ts", - "hashed_secret": "ef717286343f6da3f4e6f68c6de02a5148a801c4", + "type": "Secret Keyword", + "filename": "src/agents/models-config.providers.ollama.e2e.test.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 106 - }, + "line_number": 37 + } + ], + "src/agents/models-config.providers.qianfan.e2e.test.ts": [ { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/nostr-bus.test.ts", - "hashed_secret": "98b35fe4c45011220f509ebb5546d3889b55a891", + "type": "Secret Keyword", + "filename": "src/agents/models-config.providers.qianfan.e2e.test.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 111 + "line_number": 12 } ], - "extensions/nostr/src/nostr-profile.fuzz.test.ts": [ + "src/agents/models-config.skips-writing-models-json-no-env-token.e2e.test.ts": [ { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/nostr-profile.fuzz.test.ts", - "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", + "type": "Secret Keyword", + "filename": "src/agents/models-config.skips-writing-models-json-no-env-token.e2e.test.ts", + "hashed_secret": "4c7bac93427c83bcc3beeceebfa54f16f801b78f", "is_verified": false, - "line_number": 12 + "line_number": 100 + }, + { + "type": "Secret Keyword", + "filename": "src/agents/models-config.skips-writing-models-json-no-env-token.e2e.test.ts", + "hashed_secret": "4f2b3ddc953da005a97d825652080fe6eff21520", + "is_verified": false, + "line_number": 113 } ], - "extensions/nostr/src/nostr-profile.test.ts": [ + "src/agents/openai-responses.reasoning-replay.test.ts": [ { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/nostr-profile.test.ts", - "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", + "type": "Secret Keyword", + "filename": "src/agents/openai-responses.reasoning-replay.test.ts", + "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "is_verified": false, - "line_number": 14 + "line_number": 55 } ], - "extensions/nostr/src/types.test.ts": [ + "src/agents/pi-embedded-runner.e2e.test.ts": [ { - "type": "Hex High Entropy String", - "filename": "extensions/nostr/src/types.test.ts", - "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", + "type": "Secret Keyword", + "filename": "src/agents/pi-embedded-runner.e2e.test.ts", + "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", "is_verified": false, - "line_number": 8 + "line_number": 127 }, { "type": "Secret Keyword", - "filename": "extensions/nostr/src/types.test.ts", - "hashed_secret": "ce4303f6b22257d9c9cf314ef1dee4707c6e1c13", + "filename": "src/agents/pi-embedded-runner.e2e.test.ts", + "hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c", "is_verified": false, - "line_number": 8 - }, + "line_number": 238 + } + ], + "src/agents/pi-embedded-runner/model.ts": [ { "type": "Secret Keyword", - "filename": "extensions/nostr/src/types.test.ts", - "hashed_secret": "3bee216ebc256d692260fc3adc765050508fef5e", + "filename": "src/agents/pi-embedded-runner/model.ts", + "hashed_secret": "e774aaeac31c6272107ba89080295e277050fa7c", "is_verified": false, - "line_number": 127 + "line_number": 118 } ], - "extensions/open-prose/skills/prose/SKILL.md": [ + "src/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.ts": [ { - "type": "Basic Auth Credentials", - "filename": "extensions/open-prose/skills/prose/SKILL.md", - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", + "type": "Secret Keyword", + "filename": "src/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 200 + "line_number": 86 } ], - "extensions/open-prose/skills/prose/state/postgres.md": [ + "src/agents/pi-tools.safe-bins.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "extensions/open-prose/skills/prose/state/postgres.md", - "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", + "filename": "src/agents/pi-tools.safe-bins.e2e.test.ts", + "hashed_secret": "3ea88a727641fd5571b5e126ce87032377be1e7f", "is_verified": false, - "line_number": 75 - }, + "line_number": 126 + } + ], + "src/agents/sanitize-for-prompt.test.ts": [ { - "type": "Basic Auth Credentials", - "filename": "extensions/open-prose/skills/prose/state/postgres.md", - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", + "type": "Base64 High Entropy String", + "filename": "src/agents/sanitize-for-prompt.test.ts", + "hashed_secret": "9c62d3aa77c19e170c44b18129f967e2041fda41", "is_verified": false, - "line_number": 198 + "line_number": 28 } ], - "extensions/zalo/README.md": [ + "src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "extensions/zalo/README.md", - "hashed_secret": "f51aaee16a4a756d287f126b99c081b73cba7f15", + "filename": "src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.e2e.test.ts", + "hashed_secret": "7a85f4764bbd6daf1c3545efbbf0f279a6dc0beb", "is_verified": false, - "line_number": 41 + "line_number": 103 } ], - "extensions/zalo/src/monitor.webhook.test.ts": [ + "src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "extensions/zalo/src/monitor.webhook.test.ts", - "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "filename": "src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.e2e.test.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 43 + "line_number": 147 } ], - "skills/1password/references/cli-examples.md": [ + "src/agents/skills.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "skills/1password/references/cli-examples.md", - "hashed_secret": "9dda0987cc3054773a2df97e352d4f64d233ef10", + "filename": "src/agents/skills.e2e.test.ts", + "hashed_secret": "5df3a673d724e8a1eb673a8baf623e183940804d", "is_verified": false, - "line_number": 17 - } - ], - "skills/local-places/SERVER_README.md": [ + "line_number": 250 + }, { "type": "Secret Keyword", - "filename": "skills/local-places/SERVER_README.md", - "hashed_secret": "6d9c68c603e465077bdd49c62347fe54717f83a3", + "filename": "src/agents/skills.e2e.test.ts", + "hashed_secret": "8921daaa546693e52bc1f9c40bdcf15e816e0448", "is_verified": false, - "line_number": 28 + "line_number": 277 } ], - "skills/openai-whisper-api/SKILL.md": [ + "src/agents/tools/web-fetch.firecrawl-api-key-normalization.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "skills/openai-whisper-api/SKILL.md", - "hashed_secret": "1077361f94d70e1ddcc7c6dc581a489532a81d03", + "filename": "src/agents/tools/web-fetch.firecrawl-api-key-normalization.e2e.test.ts", + "hashed_secret": "9da08ab1e27fe0ae2ba6101aea30edcec02d21a4", "is_verified": false, - "line_number": 39 + "line_number": 45 } ], - "skills/trello/SKILL.md": [ + "src/agents/tools/web-fetch.ssrf.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "skills/trello/SKILL.md", - "hashed_secret": "11fa7c37d697f30e6aee828b4426a10f83ab2380", + "filename": "src/agents/tools/web-fetch.ssrf.e2e.test.ts", + "hashed_secret": "5ce8e9d54c77266fff990194d2219a708c59b76c", "is_verified": false, - "line_number": 18 + "line_number": 73 } ], - "src/agents/memory-search.test.ts": [ + "src/agents/tools/web-search.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/memory-search.test.ts", - "hashed_secret": "a1b49d68a91fdf9c9217773f3fac988d77fa0f50", + "filename": "src/agents/tools/web-search.e2e.test.ts", + "hashed_secret": "c8d313eac6d38274ccfc0fa7935c68bd61d5bc2f", "is_verified": false, - "line_number": 164 + "line_number": 129 } ], - "src/agents/model-auth.test.ts": [ + "src/agents/tools/web-search.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/model-auth.test.ts", - "hashed_secret": "07a6b9cec637c806195e8aa7e5c0851ab03dc35e", + "filename": "src/agents/tools/web-search.ts", + "hashed_secret": "dfba7aade0868074c2861c98e2a9a92f3178a51b", "is_verified": false, - "line_number": 211 + "line_number": 97 }, { "type": "Secret Keyword", - "filename": "src/agents/model-auth.test.ts", - "hashed_secret": "21f296583ccd80c5ab9b3330a8b0d47e4a409fb9", + "filename": "src/agents/tools/web-search.ts", + "hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb", "is_verified": false, - "line_number": 240 + "line_number": 285 }, { "type": "Secret Keyword", - "filename": "src/agents/model-auth.test.ts", - "hashed_secret": "77e991e9f56e6fa4ed1a908208048421f1214c07", + "filename": "src/agents/tools/web-search.ts", + "hashed_secret": "c4865ff9250aca23b0d98eb079dad70ebec1cced", "is_verified": false, - "line_number": 264 + "line_number": 295 }, { "type": "Secret Keyword", - "filename": "src/agents/model-auth.test.ts", - "hashed_secret": "dff6d4ff5dc357cf451d1855ab9cbda562645c9f", + "filename": "src/agents/tools/web-search.ts", + "hashed_secret": "527ee41f36386e85fa932ef09471ca017f3c95c8", "is_verified": false, - "line_number": 295 + "line_number": 298 } ], - "src/agents/model-auth.ts": [ + "src/agents/tools/web-tools.enabled-defaults.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/model-auth.ts", - "hashed_secret": "8956265d216d474a080edaa97880d37fc1386f33", + "filename": "src/agents/tools/web-tools.enabled-defaults.e2e.test.ts", + "hashed_secret": "47b249a75ca78fdb578d0f28c33685e27ea82684", "is_verified": false, - "line_number": 22 - } - ], - "src/agents/models-config.auto-injects-github-copilot-provider-token-is.test.ts": [ + "line_number": 181 + }, { "type": "Secret Keyword", - "filename": "src/agents/models-config.auto-injects-github-copilot-provider-token-is.test.ts", - "hashed_secret": "7cf31e8b6cda49f70c31f1f25af05d46f924142d", + "filename": "src/agents/tools/web-tools.enabled-defaults.e2e.test.ts", + "hashed_secret": "d0ffd81d6d7ad1bc3c365660fe8882480c9a986e", "is_verified": false, - "line_number": 16 + "line_number": 187 } ], - "src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.test.ts": [ + "src/agents/tools/web-tools.fetch.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.test.ts", - "hashed_secret": "7cf31e8b6cda49f70c31f1f25af05d46f924142d", + "filename": "src/agents/tools/web-tools.fetch.e2e.test.ts", + "hashed_secret": "5ce8e9d54c77266fff990194d2219a708c59b76c", "is_verified": false, - "line_number": 16 + "line_number": 246 } ], - "src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts": [ - { - "type": "Secret Keyword", - "filename": "src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts", - "hashed_secret": "7cf31e8b6cda49f70c31f1f25af05d46f924142d", - "is_verified": false, - "line_number": 16 - }, + "src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts", - "hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c", + "filename": "src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.e2e.test.ts", + "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", "is_verified": false, - "line_number": 50 + "line_number": 56 }, { "type": "Secret Keyword", - "filename": "src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts", - "hashed_secret": "3a81eb091f80c845232225be5663d270e90dacb7", + "filename": "src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.e2e.test.ts", + "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", "is_verified": false, - "line_number": 108 + "line_number": 62 } ], - "src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts": [ + "src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts", - "hashed_secret": "7cf31e8b6cda49f70c31f1f25af05d46f924142d", + "filename": "src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.e2e.test.ts", + "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", "is_verified": false, - "line_number": 16 + "line_number": 42 }, { "type": "Secret Keyword", - "filename": "src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts", - "hashed_secret": "980d02eb9335ae7c9e9984f6c8ad432352a0d2ac", + "filename": "src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.e2e.test.ts", + "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", "is_verified": false, - "line_number": 57 + "line_number": 149 } ], - "src/agents/models-config.skips-writing-models-json-no-env-token.test.ts": [ + "src/auto-reply/status.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/models-config.skips-writing-models-json-no-env-token.test.ts", - "hashed_secret": "7cf31e8b6cda49f70c31f1f25af05d46f924142d", + "filename": "src/auto-reply/status.test.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 16 - }, + "line_number": 36 + } + ], + "src/browser/bridge-server.auth.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/models-config.skips-writing-models-json-no-env-token.test.ts", - "hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c", + "filename": "src/browser/bridge-server.auth.test.ts", + "hashed_secret": "6af3c121ed4a752936c297cddfb7b00394eabf10", "is_verified": false, - "line_number": 112 + "line_number": 66 + } + ], + "src/browser/browser-utils.test.ts": [ + { + "type": "Hex High Entropy String", + "filename": "src/browser/browser-utils.test.ts", + "hashed_secret": "4e126c049580d66ca1549fa534d95a7263f27f46", + "is_verified": false, + "line_number": 38 }, { - "type": "Secret Keyword", - "filename": "src/agents/models-config.skips-writing-models-json-no-env-token.test.ts", - "hashed_secret": "94c4be5a1976115e8152960c21e04400a4fccdf6", + "type": "Basic Auth Credentials", + "filename": "src/browser/browser-utils.test.ts", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_verified": false, - "line_number": 146 + "line_number": 159 } ], - "src/agents/models-config.uses-first-github-copilot-profile-env-tokens.test.ts": [ + "src/browser/cdp.test.ts": [ { - "type": "Secret Keyword", - "filename": "src/agents/models-config.uses-first-github-copilot-profile-env-tokens.test.ts", - "hashed_secret": "7cf31e8b6cda49f70c31f1f25af05d46f924142d", + "type": "Basic Auth Credentials", + "filename": "src/browser/cdp.test.ts", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_verified": false, - "line_number": 16 + "line_number": 186 } ], - "src/agents/openai-responses.reasoning-replay.test.ts": [ + "src/channels/plugins/plugins-channel.test.ts": [ { - "type": "Secret Keyword", - "filename": "src/agents/openai-responses.reasoning-replay.test.ts", - "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "type": "Hex High Entropy String", + "filename": "src/channels/plugins/plugins-channel.test.ts", + "hashed_secret": "99c962e8c62296bdc9a17f5caf91ce9bb4c7e0e6", "is_verified": false, - "line_number": 124 + "line_number": 46 } ], - "src/agents/pi-embedded-runner.applygoogleturnorderingfix.test.ts": [ + "src/cli/program.smoke.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/pi-embedded-runner.applygoogleturnorderingfix.test.ts", - "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", + "filename": "src/cli/program.smoke.e2e.test.ts", + "hashed_secret": "8689a958b58e4a6f7da6211e666da8e17651697c", "is_verified": false, - "line_number": 58 + "line_number": 215 } ], - "src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts": [ + "src/cli/update-cli.test.ts": [ { - "type": "Secret Keyword", - "filename": "src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts", - "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", + "type": "Hex High Entropy String", + "filename": "src/cli/update-cli.test.ts", + "hashed_secret": "e4f91dd323bac5bfc4f60a6e433787671dc2421d", "is_verified": false, - "line_number": 57 + "line_number": 239 } ], - "src/agents/pi-embedded-runner.createsystempromptoverride.test.ts": [ + "src/commands/auth-choice.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/pi-embedded-runner.createsystempromptoverride.test.ts", - "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", + "filename": "src/commands/auth-choice.e2e.test.ts", + "hashed_secret": "2480500ff391183070fe22ba8665a8be19350833", "is_verified": false, - "line_number": 56 + "line_number": 454 + }, + { + "type": "Secret Keyword", + "filename": "src/commands/auth-choice.e2e.test.ts", + "hashed_secret": "844ae5308654406d80db6f2b3d0beb07d616f9e1", + "is_verified": false, + "line_number": 487 + }, + { + "type": "Secret Keyword", + "filename": "src/commands/auth-choice.e2e.test.ts", + "hashed_secret": "77e991e9f56e6fa4ed1a908208048421f1214c07", + "is_verified": false, + "line_number": 549 + }, + { + "type": "Secret Keyword", + "filename": "src/commands/auth-choice.e2e.test.ts", + "hashed_secret": "266e955b27b5fc2c2f532e446f2e71c3667a4cd9", + "is_verified": false, + "line_number": 584 + }, + { + "type": "Secret Keyword", + "filename": "src/commands/auth-choice.e2e.test.ts", + "hashed_secret": "1b4d8423b11d32dd0c466428ac81de84a4a9442b", + "is_verified": false, + "line_number": 726 + }, + { + "type": "Secret Keyword", + "filename": "src/commands/auth-choice.e2e.test.ts", + "hashed_secret": "c24e00b94c972ed497d5961212ac96f0dffb4f7a", + "is_verified": false, + "line_number": 798 } ], - "src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts": [ + "src/commands/auth-choice.preferred-provider.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts", - "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", + "filename": "src/commands/auth-choice.preferred-provider.ts", + "hashed_secret": "c03a8d10174dd7eb2b3288b570a5a74fdd9ae05d", "is_verified": false, - "line_number": 56 + "line_number": 8 } ], - "src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts": [ + "src/commands/configure.gateway-auth.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts", - "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", + "filename": "src/commands/configure.gateway-auth.e2e.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 56 + "line_number": 21 + }, + { + "type": "Secret Keyword", + "filename": "src/commands/configure.gateway-auth.e2e.test.ts", + "hashed_secret": "d5d4cd07616a542891b7ec2d0257b3a24b69856e", + "is_verified": false, + "line_number": 62 } ], - "src/agents/pi-embedded-runner.limithistoryturns.test.ts": [ + "src/commands/daemon-install-helpers.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/pi-embedded-runner.limithistoryturns.test.ts", - "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", + "filename": "src/commands/daemon-install-helpers.e2e.test.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 57 + "line_number": 128 } ], - "src/agents/pi-embedded-runner.resolvesessionagentids.test.ts": [ + "src/commands/doctor-memory-search.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/pi-embedded-runner.resolvesessionagentids.test.ts", - "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", + "filename": "src/commands/doctor-memory-search.test.ts", + "hashed_secret": "2e07956ffc9bc4fd624064c40b7495c85d5f1467", "is_verified": false, - "line_number": 56 + "line_number": 38 } ], - "src/agents/pi-embedded-runner.splitsdktools.test.ts": [ + "src/commands/model-picker.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/pi-embedded-runner.splitsdktools.test.ts", - "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", + "filename": "src/commands/model-picker.e2e.test.ts", + "hashed_secret": "5b924ca5330ede58702a5b0e414207b90fb1aef3", "is_verified": false, - "line_number": 57 + "line_number": 127 } ], - "src/agents/pi-embedded-runner.test.ts": [ + "src/commands/models/list.status.e2e.test.ts": [ + { + "type": "Base64 High Entropy String", + "filename": "src/commands/models/list.status.e2e.test.ts", + "hashed_secret": "d6ae2508a78a232d5378ef24b85ce40cbb4d7ff0", + "is_verified": false, + "line_number": 12 + }, + { + "type": "Base64 High Entropy String", + "filename": "src/commands/models/list.status.e2e.test.ts", + "hashed_secret": "2d8012102440ea97852b3152239218f00579bafa", + "is_verified": false, + "line_number": 19 + }, + { + "type": "Base64 High Entropy String", + "filename": "src/commands/models/list.status.e2e.test.ts", + "hashed_secret": "51848e2be4b461a549218d3167f19c01be6b98b8", + "is_verified": false, + "line_number": 51 + }, { "type": "Secret Keyword", - "filename": "src/agents/pi-embedded-runner.test.ts", - "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", + "filename": "src/commands/models/list.status.e2e.test.ts", + "hashed_secret": "51848e2be4b461a549218d3167f19c01be6b98b8", "is_verified": false, - "line_number": 117 + "line_number": 51 }, { "type": "Secret Keyword", - "filename": "src/agents/pi-embedded-runner.test.ts", - "hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c", + "filename": "src/commands/models/list.status.e2e.test.ts", + "hashed_secret": "1c1e381bfb72d3b7bfca9437053d9875356680f0", "is_verified": false, - "line_number": 178 + "line_number": 57 } ], - "src/agents/skills.applyskillenvoverrides.test.ts": [ + "src/commands/onboard-auth.config-minimax.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/skills.applyskillenvoverrides.test.ts", - "hashed_secret": "5df3a673d724e8a1eb673a8baf623e183940804d", + "filename": "src/commands/onboard-auth.config-minimax.ts", + "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", "is_verified": false, - "line_number": 54 + "line_number": 36 }, { "type": "Secret Keyword", - "filename": "src/agents/skills.applyskillenvoverrides.test.ts", - "hashed_secret": "8921daaa546693e52bc1f9c40bdcf15e816e0448", + "filename": "src/commands/onboard-auth.config-minimax.ts", + "hashed_secret": "ddcb713196b974770575a9bea5a4e7d46361f8e9", "is_verified": false, - "line_number": 80 + "line_number": 78 } ], - "src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts": [ + "src/commands/onboard-auth.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts", - "hashed_secret": "7a85f4764bbd6daf1c3545efbbf0f279a6dc0beb", + "filename": "src/commands/onboard-auth.e2e.test.ts", + "hashed_secret": "e184b402822abc549b37689c84e8e0e33c39a1f1", "is_verified": false, - "line_number": 124 + "line_number": 272 } ], - "src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts": [ + "src/commands/onboard-custom.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts", - "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", + "filename": "src/commands/onboard-custom.e2e.test.ts", + "hashed_secret": "62e6748c6bb4c4a0f785a28cdd7d41ef212c0091", "is_verified": false, - "line_number": 102 + "line_number": 238 } ], - "src/agents/tools/web-fetch.ssrf.test.ts": [ + "src/commands/onboard-non-interactive.provider-auth.e2e.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/commands/onboard-non-interactive.provider-auth.e2e.test.ts", + "hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c", + "is_verified": false, + "line_number": 153 + }, + { + "type": "Secret Keyword", + "filename": "src/commands/onboard-non-interactive.provider-auth.e2e.test.ts", + "hashed_secret": "07a6b9cec637c806195e8aa7e5c0851ab03dc35e", + "is_verified": false, + "line_number": 191 + }, + { + "type": "Secret Keyword", + "filename": "src/commands/onboard-non-interactive.provider-auth.e2e.test.ts", + "hashed_secret": "77e991e9f56e6fa4ed1a908208048421f1214c07", + "is_verified": false, + "line_number": 234 + }, + { + "type": "Secret Keyword", + "filename": "src/commands/onboard-non-interactive.provider-auth.e2e.test.ts", + "hashed_secret": "65547299f940eca3dc839f3eac85e8a78a6deb05", + "is_verified": false, + "line_number": 282 + }, + { + "type": "Secret Keyword", + "filename": "src/commands/onboard-non-interactive.provider-auth.e2e.test.ts", + "hashed_secret": "2833d098c110602e4c8d577fbfdb423a9ffd58e9", + "is_verified": false, + "line_number": 304 + }, { "type": "Secret Keyword", - "filename": "src/agents/tools/web-fetch.ssrf.test.ts", - "hashed_secret": "5ce8e9d54c77266fff990194d2219a708c59b76c", + "filename": "src/commands/onboard-non-interactive.provider-auth.e2e.test.ts", + "hashed_secret": "266e955b27b5fc2c2f532e446f2e71c3667a4cd9", "is_verified": false, - "line_number": 55 - } - ], - "src/agents/tools/web-search.ts": [ + "line_number": 338 + }, { "type": "Secret Keyword", - "filename": "src/agents/tools/web-search.ts", - "hashed_secret": "dfba7aade0868074c2861c98e2a9a92f3178a51b", + "filename": "src/commands/onboard-non-interactive.provider-auth.e2e.test.ts", + "hashed_secret": "995b80728ee01edb90ddfed07870bbab405df19f", "is_verified": false, - "line_number": 85 + "line_number": 366 }, { "type": "Secret Keyword", - "filename": "src/agents/tools/web-search.ts", - "hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb", + "filename": "src/commands/onboard-non-interactive.provider-auth.e2e.test.ts", + "hashed_secret": "b65888424ecafcc98bfd803b24817e4dadf821f8", "is_verified": false, - "line_number": 190 + "line_number": 383 }, { "type": "Secret Keyword", - "filename": "src/agents/tools/web-search.ts", - "hashed_secret": "c4865ff9250aca23b0d98eb079dad70ebec1cced", + "filename": "src/commands/onboard-non-interactive.provider-auth.e2e.test.ts", + "hashed_secret": "62e6748c6bb4c4a0f785a28cdd7d41ef212c0091", "is_verified": false, - "line_number": 198 + "line_number": 402 }, { "type": "Secret Keyword", - "filename": "src/agents/tools/web-search.ts", - "hashed_secret": "527ee41f36386e85fa932ef09471ca017f3c95c8", + "filename": "src/commands/onboard-non-interactive.provider-auth.e2e.test.ts", + "hashed_secret": "8818d3b7c102fd6775af9e1390e5ed3a128473fb", "is_verified": false, - "line_number": 199 + "line_number": 447 } ], - "src/agents/tools/web-tools.enabled-defaults.test.ts": [ + "src/commands/onboard-non-interactive/api-keys.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/tools/web-tools.enabled-defaults.test.ts", - "hashed_secret": "47b249a75ca78fdb578d0f28c33685e27ea82684", + "filename": "src/commands/onboard-non-interactive/api-keys.ts", + "hashed_secret": "112f3a99b283a4e1788dedd8e0e5d35375c33747", "is_verified": false, - "line_number": 213 - }, + "line_number": 11 + } + ], + "src/commands/status.update.test.ts": [ { - "type": "Secret Keyword", - "filename": "src/agents/tools/web-tools.enabled-defaults.test.ts", - "hashed_secret": "d0ffd81d6d7ad1bc3c365660fe8882480c9a986e", + "type": "Hex High Entropy String", + "filename": "src/commands/status.update.test.ts", + "hashed_secret": "33c76f70af66754ca47d19b17da8dc232e125253", "is_verified": false, - "line_number": 242 + "line_number": 74 } ], - "src/agents/tools/web-tools.fetch.test.ts": [ + "src/commands/vllm-setup.ts": [ { "type": "Secret Keyword", - "filename": "src/agents/tools/web-tools.fetch.test.ts", - "hashed_secret": "5ce8e9d54c77266fff990194d2219a708c59b76c", + "filename": "src/commands/vllm-setup.ts", + "hashed_secret": "5b924ca5330ede58702a5b0e414207b90fb1aef3", "is_verified": false, - "line_number": 101 + "line_number": 60 } ], - "src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.e2e.test.ts": [ + "src/commands/zai-endpoint-detect.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.e2e.test.ts", + "filename": "src/commands/zai-endpoint-detect.e2e.test.ts", "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", "is_verified": false, - "line_number": 90 - }, + "line_number": 24 + } + ], + "src/config/config-misc.test.ts": [ { "type": "Secret Keyword", - "filename": "src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.e2e.test.ts", - "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "filename": "src/config/config-misc.test.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 96 + "line_number": 62 } ], - "src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.e2e.test.ts": [ + "src/config/config.env-vars.test.ts": [ { "type": "Secret Keyword", - "filename": "src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.e2e.test.ts", - "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", + "filename": "src/config/config.env-vars.test.ts", + "hashed_secret": "a24ef9c1a27cac44823571ceef2e8262718eee36", "is_verified": false, - "line_number": 87 + "line_number": 13 }, { "type": "Secret Keyword", - "filename": "src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.e2e.test.ts", - "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "filename": "src/config/config.env-vars.test.ts", + "hashed_secret": "29d5f92e9ee44d4854d6dfaeefc3dc27d779fdf3", "is_verified": false, - "line_number": 228 - } - ], - "src/auto-reply/status.test.ts": [ + "line_number": 19 + }, { "type": "Secret Keyword", - "filename": "src/auto-reply/status.test.ts", - "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", + "filename": "src/config/config.env-vars.test.ts", + "hashed_secret": "1672b6a1e7956c6a70f45d699aa42a351b1f8b80", "is_verified": false, - "line_number": 20 + "line_number": 27 } ], - "src/browser/cdp.helpers.test.ts": [ + "src/config/config.irc.test.ts": [ { - "type": "Basic Auth Credentials", - "filename": "src/browser/cdp.helpers.test.ts", - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", + "type": "Secret Keyword", + "filename": "src/config/config.irc.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 22 + "line_number": 92 } ], - "src/browser/cdp.test.ts": [ + "src/config/config.talk-api-key-fallback.test.ts": [ { - "type": "Basic Auth Credentials", - "filename": "src/browser/cdp.test.ts", - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", + "type": "Secret Keyword", + "filename": "src/config/config.talk-api-key-fallback.test.ts", + "hashed_secret": "bea2f7b64fab8d1d414d0449530b1e088d36d5b1", "is_verified": false, - "line_number": 172 + "line_number": 33 } ], - "src/browser/target-id.test.ts": [ + "src/config/env-preserve-io.test.ts": [ { - "type": "Hex High Entropy String", - "filename": "src/browser/target-id.test.ts", - "hashed_secret": "4e126c049580d66ca1549fa534d95a7263f27f46", + "type": "Secret Keyword", + "filename": "src/config/env-preserve-io.test.ts", + "hashed_secret": "85639f0560fd9bf8704f52e01c5e764c9ed5a6aa", "is_verified": false, - "line_number": 13 - } - ], - "src/cli/update-cli.test.ts": [ + "line_number": 59 + }, { - "type": "Hex High Entropy String", - "filename": "src/cli/update-cli.test.ts", - "hashed_secret": "e4f91dd323bac5bfc4f60a6e433787671dc2421d", + "type": "Secret Keyword", + "filename": "src/config/env-preserve-io.test.ts", + "hashed_secret": "996650087ab48bdb1ca80f0842c97d4fbb6f1c71", "is_verified": false, - "line_number": 112 + "line_number": 86 } ], - "src/commands/auth-choice.preferred-provider.ts": [ + "src/config/env-preserve.test.ts": [ { "type": "Secret Keyword", - "filename": "src/commands/auth-choice.preferred-provider.ts", - "hashed_secret": "c03a8d10174dd7eb2b3288b570a5a74fdd9ae05d", + "filename": "src/config/env-preserve.test.ts", + "hashed_secret": "f6067ac4599b1cd5176f34897bb556a1a1eaf049", "is_verified": false, - "line_number": 8 + "line_number": 6 + }, + { + "type": "Secret Keyword", + "filename": "src/config/env-preserve.test.ts", + "hashed_secret": "5a41c5061e7279cec0566b3ef52cbe042e831192", + "is_verified": false, + "line_number": 7 + }, + { + "type": "Secret Keyword", + "filename": "src/config/env-preserve.test.ts", + "hashed_secret": "53d407242b91f07138abcf30ee0e6b71f304b87f", + "is_verified": false, + "line_number": 19 + }, + { + "type": "Secret Keyword", + "filename": "src/config/env-preserve.test.ts", + "hashed_secret": "c1b24294f00e281605f9dd6a298612e3060062b4", + "is_verified": false, + "line_number": 82 } ], - "src/commands/auth-choice.test.ts": [ + "src/config/env-substitution.test.ts": [ { "type": "Secret Keyword", - "filename": "src/commands/auth-choice.test.ts", - "hashed_secret": "2480500ff391183070fe22ba8665a8be19350833", + "filename": "src/config/env-substitution.test.ts", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_verified": false, - "line_number": 289 + "line_number": 37 }, { "type": "Secret Keyword", - "filename": "src/commands/auth-choice.test.ts", - "hashed_secret": "77e991e9f56e6fa4ed1a908208048421f1214c07", + "filename": "src/config/env-substitution.test.ts", + "hashed_secret": "ec417f567082612f8fd6afafe1abcab831fca840", "is_verified": false, - "line_number": 350 + "line_number": 68 }, { "type": "Secret Keyword", - "filename": "src/commands/auth-choice.test.ts", - "hashed_secret": "1b4d8423b11d32dd0c466428ac81de84a4a9442b", + "filename": "src/config/env-substitution.test.ts", + "hashed_secret": "520bd69c3eb1646d9a78181ecb4c90c51fdf428d", "is_verified": false, - "line_number": 528 + "line_number": 69 + }, + { + "type": "Secret Keyword", + "filename": "src/config/env-substitution.test.ts", + "hashed_secret": "f136444bf9b3d01a9f9b772b80ac6bf7b6a43ef0", + "is_verified": false, + "line_number": 227 } ], - "src/commands/configure.gateway-auth.test.ts": [ + "src/config/io.write-config.test.ts": [ { "type": "Secret Keyword", - "filename": "src/commands/configure.gateway-auth.test.ts", - "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "filename": "src/config/io.write-config.test.ts", + "hashed_secret": "13951588fd3325e25ed1e3b116d7009fb221c85e", "is_verified": false, - "line_number": 8 + "line_number": 65 } ], - "src/commands/models/list.status.test.ts": [ + "src/config/model-alias-defaults.test.ts": [ { - "type": "Base64 High Entropy String", - "filename": "src/commands/models/list.status.test.ts", - "hashed_secret": "d6ae2508a78a232d5378ef24b85ce40cbb4d7ff0", + "type": "Secret Keyword", + "filename": "src/config/model-alias-defaults.test.ts", + "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", "is_verified": false, - "line_number": 11 - }, + "line_number": 66 + } + ], + "src/config/redact-snapshot.test.ts": [ { "type": "Base64 High Entropy String", - "filename": "src/commands/models/list.status.test.ts", - "hashed_secret": "2d8012102440ea97852b3152239218f00579bafa", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "3732e17b2d11ed6c64fef02c341958007af154e7", "is_verified": false, - "line_number": 18 + "line_number": 77 }, { - "type": "Base64 High Entropy String", - "filename": "src/commands/models/list.status.test.ts", - "hashed_secret": "51848e2be4b461a549218d3167f19c01be6b98b8", + "type": "Secret Keyword", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "3732e17b2d11ed6c64fef02c341958007af154e7", "is_verified": false, - "line_number": 46 + "line_number": 77 }, { "type": "Secret Keyword", - "filename": "src/commands/models/list.status.test.ts", - "hashed_secret": "51848e2be4b461a549218d3167f19c01be6b98b8", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "7f413afd37447cd321d79286be0f58d7a9875d9b", "is_verified": false, - "line_number": 46 + "line_number": 89 }, { "type": "Secret Keyword", - "filename": "src/commands/models/list.status.test.ts", - "hashed_secret": "1c1e381bfb72d3b7bfca9437053d9875356680f0", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "c21afa950dee2a70f3e0f6ffdfbc87f8edb90262", "is_verified": false, - "line_number": 52 - } - ], - "src/commands/onboard-auth.config-minimax.ts": [ + "line_number": 99 + }, { "type": "Secret Keyword", - "filename": "src/commands/onboard-auth.config-minimax.ts", - "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "83a9937c6de261ffda22304834f30fe6c8f97926", "is_verified": false, - "line_number": 30 + "line_number": 110 }, { "type": "Secret Keyword", - "filename": "src/commands/onboard-auth.config-minimax.ts", - "hashed_secret": "ddcb713196b974770575a9bea5a4e7d46361f8e9", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "87ac76dfc9cba93bead43c191e31bd099a97cc11", "is_verified": false, - "line_number": 85 - } - ], - "src/commands/onboard-auth.test.ts": [ + "line_number": 198 + }, { "type": "Secret Keyword", - "filename": "src/commands/onboard-auth.test.ts", - "hashed_secret": "666c100dab549a6f56da7da546bd848ed5086541", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "abb1aabcd0e49019c2873944a40671a80ccd64c7", "is_verified": false, - "line_number": 230 + "line_number": 309 + }, + { + "type": "Base64 High Entropy String", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "8e22880b4e96bab354e1da6c91d2f58dabde3555", + "is_verified": false, + "line_number": 321 }, { "type": "Secret Keyword", - "filename": "src/commands/onboard-auth.test.ts", - "hashed_secret": "e184b402822abc549b37689c84e8e0e33c39a1f1", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "8e22880b4e96bab354e1da6c91d2f58dabde3555", "is_verified": false, - "line_number": 262 - } - ], - "src/commands/onboard-non-interactive.ai-gateway.test.ts": [ + "line_number": 321 + }, { "type": "Secret Keyword", - "filename": "src/commands/onboard-non-interactive.ai-gateway.test.ts", - "hashed_secret": "77e991e9f56e6fa4ed1a908208048421f1214c07", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "a9c732e05044a08c760cce7f6d142cd0d35a19e5", "is_verified": false, - "line_number": 50 - } - ], - "src/commands/onboard-non-interactive/api-keys.ts": [ + "line_number": 375 + }, { "type": "Secret Keyword", - "filename": "src/commands/onboard-non-interactive/api-keys.ts", - "hashed_secret": "112f3a99b283a4e1788dedd8e0e5d35375c33747", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "50843dd5651cfafbe7c5611c1eed195c63e6e3fd", "is_verified": false, - "line_number": 10 - } - ], - "src/config/config.env-vars.test.ts": [ + "line_number": 691 + }, { "type": "Secret Keyword", - "filename": "src/config/config.env-vars.test.ts", - "hashed_secret": "a24ef9c1a27cac44823571ceef2e8262718eee36", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "927e7cdedcb8f71af399a49fb90a381df8b8df28", "is_verified": false, - "line_number": 15 + "line_number": 808 }, { "type": "Secret Keyword", - "filename": "src/config/config.env-vars.test.ts", - "hashed_secret": "29d5f92e9ee44d4854d6dfaeefc3dc27d779fdf3", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "1996cc327bd39dad69cd8feb24250dafd51e7c08", "is_verified": false, - "line_number": 47 + "line_number": 814 }, { "type": "Secret Keyword", - "filename": "src/config/config.env-vars.test.ts", - "hashed_secret": "1672b6a1e7956c6a70f45d699aa42a351b1f8b80", + "filename": "src/config/redact-snapshot.test.ts", + "hashed_secret": "a5c0a65a4fa8874a486aa5072671927ceba82a90", "is_verified": false, - "line_number": 63 + "line_number": 838 } ], - "src/config/config.talk-api-key-fallback.test.ts": [ + "src/config/schema.help.ts": [ { "type": "Secret Keyword", - "filename": "src/config/config.talk-api-key-fallback.test.ts", - "hashed_secret": "bea2f7b64fab8d1d414d0449530b1e088d36d5b1", + "filename": "src/config/schema.help.ts", + "hashed_secret": "9f4cda226d3868676ac7f86f59e4190eb94bd208", "is_verified": false, - "line_number": 42 - } - ], - "src/config/config.web-search-provider.test.ts": [ + "line_number": 109 + }, { "type": "Secret Keyword", - "filename": "src/config/config.web-search-provider.test.ts", - "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", + "filename": "src/config/schema.help.ts", + "hashed_secret": "01822c8bbf6a8b136944b14182cb885100ec2eae", "is_verified": false, - "line_number": 14 + "line_number": 130 + }, + { + "type": "Secret Keyword", + "filename": "src/config/schema.help.ts", + "hashed_secret": "bb7dfd9746e660e4a4374951ec5938ef0e343255", + "is_verified": false, + "line_number": 187 } ], - "src/config/env-substitution.test.ts": [ + "src/config/schema.irc.ts": [ { "type": "Secret Keyword", - "filename": "src/config/env-substitution.test.ts", - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", + "filename": "src/config/schema.irc.ts", + "hashed_secret": "de18cf01737148de8ff7cb33fd38dd4d3e226384", "is_verified": false, - "line_number": 38 + "line_number": 6 }, { "type": "Secret Keyword", - "filename": "src/config/env-substitution.test.ts", - "hashed_secret": "ec417f567082612f8fd6afafe1abcab831fca840", + "filename": "src/config/schema.irc.ts", + "hashed_secret": "b362522192a2259c5d10ecb89fe728a66d6015e9", "is_verified": false, - "line_number": 69 + "line_number": 7 }, { "type": "Secret Keyword", - "filename": "src/config/env-substitution.test.ts", - "hashed_secret": "520bd69c3eb1646d9a78181ecb4c90c51fdf428d", + "filename": "src/config/schema.irc.ts", + "hashed_secret": "383088054f9b38c21ec29db239e3fccb7eb0a485", "is_verified": false, - "line_number": 70 + "line_number": 20 }, { "type": "Secret Keyword", - "filename": "src/config/env-substitution.test.ts", - "hashed_secret": "f136444bf9b3d01a9f9b772b80ac6bf7b6a43ef0", + "filename": "src/config/schema.irc.ts", + "hashed_secret": "a3484eea8ccb96dd79f50edc14b8fbf2867a9180", "is_verified": false, - "line_number": 228 + "line_number": 21 } ], - "src/config/schema.ts": [ + "src/config/schema.labels.ts": [ { "type": "Secret Keyword", - "filename": "src/config/schema.ts", + "filename": "src/config/schema.labels.ts", "hashed_secret": "e73c9fcad85cd4eecc74181ec4bdb31064d68439", "is_verified": false, - "line_number": 184 + "line_number": 104 }, { "type": "Secret Keyword", - "filename": "src/config/schema.ts", + "filename": "src/config/schema.labels.ts", "hashed_secret": "2eda7cd978f39eebec3bf03e4410a40e14167fff", "is_verified": false, - "line_number": 220 - }, + "line_number": 145 + } + ], + "src/config/slack-http-config.test.ts": [ { "type": "Secret Keyword", - "filename": "src/config/schema.ts", - "hashed_secret": "9f4cda226d3868676ac7f86f59e4190eb94bd208", + "filename": "src/config/slack-http-config.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 418 - }, + "line_number": 10 + } + ], + "src/config/telegram-webhook-secret.test.ts": [ { "type": "Secret Keyword", - "filename": "src/config/schema.ts", - "hashed_secret": "01822c8bbf6a8b136944b14182cb885100ec2eae", + "filename": "src/config/telegram-webhook-secret.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 437 - }, + "line_number": 10 + } + ], + "src/docker-setup.test.ts": [ { - "type": "Secret Keyword", - "filename": "src/config/schema.ts", - "hashed_secret": "bb7dfd9746e660e4a4374951ec5938ef0e343255", + "type": "Base64 High Entropy String", + "filename": "src/docker-setup.test.ts", + "hashed_secret": "32ac33b537769e97787f70ef85576cc243fab934", "is_verified": false, - "line_number": 487 + "line_number": 131 } ], - "src/config/slack-http-config.test.ts": [ + "src/gateway/auth-rate-limit.ts": [ { "type": "Secret Keyword", - "filename": "src/config/slack-http-config.test.ts", - "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "filename": "src/gateway/auth-rate-limit.ts", + "hashed_secret": "76ed0a056aa77060de25754586440cff390791d0", "is_verified": false, - "line_number": 11 + "line_number": 37 } ], "src/gateway/auth.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/gateway/auth.test.ts", + "hashed_secret": "db5543cd7440bbdc4c5aaf8aa363715c31dd5a27", + "is_verified": false, + "line_number": 32 + }, + { + "type": "Secret Keyword", + "filename": "src/gateway/auth.test.ts", + "hashed_secret": "d51f846285cbc6d1dd76677a0fd588c8df44e506", + "is_verified": false, + "line_number": 48 + }, { "type": "Secret Keyword", "filename": "src/gateway/auth.test.ts", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 43 + "line_number": 95 }, { "type": "Secret Keyword", "filename": "src/gateway/auth.test.ts", "hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c", "is_verified": false, - "line_number": 51 + "line_number": 103 } ], "src/gateway/call.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/gateway/call.test.ts", + "hashed_secret": "db5543cd7440bbdc4c5aaf8aa363715c31dd5a27", + "is_verified": false, + "line_number": 357 + }, + { + "type": "Secret Keyword", + "filename": "src/gateway/call.test.ts", + "hashed_secret": "de1c41e8ece73f5d5c259bb37eccb59a542b91dc", + "is_verified": false, + "line_number": 361 + }, { "type": "Secret Keyword", "filename": "src/gateway/call.test.ts", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 285 + "line_number": 398 }, { "type": "Secret Keyword", "filename": "src/gateway/call.test.ts", "hashed_secret": "e493f561d90c6638c1f51c5a8a069c3b129b79ed", "is_verified": false, - "line_number": 295 + "line_number": 408 }, { "type": "Secret Keyword", "filename": "src/gateway/call.test.ts", "hashed_secret": "2e07956ffc9bc4fd624064c40b7495c85d5f1467", "is_verified": false, - "line_number": 300 + "line_number": 413 }, { "type": "Secret Keyword", "filename": "src/gateway/call.test.ts", "hashed_secret": "bddc29032de580fb53b3a9a0357dd409086db800", "is_verified": false, - "line_number": 313 + "line_number": 426 + }, + { + "type": "Secret Keyword", + "filename": "src/gateway/call.test.ts", + "hashed_secret": "6255675480f681df08c1704b7b3cd2c49917f0e2", + "is_verified": false, + "line_number": 463 } ], - "src/gateway/client.test.ts": [ + "src/gateway/client.e2e.test.ts": [ { "type": "Private Key", - "filename": "src/gateway/client.test.ts", + "filename": "src/gateway/client.e2e.test.ts", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 83 + "line_number": 85 } ], "src/gateway/gateway-cli-backend.live.test.ts": [ @@ -1887,16 +12591,25 @@ "filename": "src/gateway/gateway-models.profiles.live.test.ts", "hashed_secret": "3e2fd4a90d5afbd27974730c4d6a9592fe300825", "is_verified": false, - "line_number": 219 + "line_number": 242 } ], - "src/gateway/gateway.e2e.test.ts": [ + "src/gateway/server-methods/skills.update.normalizes-api-key.test.ts": [ { "type": "Secret Keyword", - "filename": "src/gateway/gateway.e2e.test.ts", - "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "filename": "src/gateway/server-methods/skills.update.normalizes-api-key.test.ts", + "hashed_secret": "c17b6f497b392e2efc655e8b646b3455f4b28e58", "is_verified": false, - "line_number": 73 + "line_number": 29 + } + ], + "src/gateway/server-methods/talk.ts": [ + { + "type": "Secret Keyword", + "filename": "src/gateway/server-methods/talk.ts", + "hashed_secret": "e478a5eeba4907d2f12a68761996b9de745d826d", + "is_verified": false, + "line_number": 13 } ], "src/gateway/server.auth.e2e.test.ts": [ @@ -1905,14 +12618,32 @@ "filename": "src/gateway/server.auth.e2e.test.ts", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 179 + "line_number": 460 }, { "type": "Secret Keyword", "filename": "src/gateway/server.auth.e2e.test.ts", "hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c", "is_verified": false, - "line_number": 197 + "line_number": 478 + } + ], + "src/gateway/server.skills-status.e2e.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/gateway/server.skills-status.e2e.test.ts", + "hashed_secret": "1cc6bff0f84efb2d3ff4fa1347f3b2bc173aaff0", + "is_verified": false, + "line_number": 13 + } + ], + "src/gateway/server.talk-config.e2e.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/gateway/server.talk-config.e2e.test.ts", + "hashed_secret": "3c310634864babb081f0b617c14bc34823d7e369", + "is_verified": false, + "line_number": 13 } ], "src/gateway/session-utils.test.ts": [ @@ -1921,16 +12652,16 @@ "filename": "src/gateway/session-utils.test.ts", "hashed_secret": "bb9a5d9483409d2c60b28268a0efcb93324d4cda", "is_verified": false, - "line_number": 156 + "line_number": 280 } ], - "src/gateway/tools-invoke-http.test.ts": [ + "src/gateway/test-openai-responses-model.ts": [ { "type": "Secret Keyword", - "filename": "src/gateway/tools-invoke-http.test.ts", - "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "filename": "src/gateway/test-openai-responses-model.ts", + "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "is_verified": false, - "line_number": 56 + "line_number": 17 } ], "src/gateway/ws-log.test.ts": [ @@ -1948,14 +12679,14 @@ "filename": "src/infra/env.test.ts", "hashed_secret": "df98a117ddabf85991b9fe0e268214dc0e1254dc", "is_verified": false, - "line_number": 10 + "line_number": 9 }, { "type": "Secret Keyword", "filename": "src/infra/env.test.ts", "hashed_secret": "6d811dc1f59a55ca1a3d38b5042a062b9f79e8ec", "is_verified": false, - "line_number": 25 + "line_number": 30 } ], "src/infra/outbound/message-action-runner.test.ts": [ @@ -1964,23 +12695,46 @@ "filename": "src/infra/outbound/message-action-runner.test.ts", "hashed_secret": "804ec071803318791b835cffd6e509c8d32239db", "is_verified": false, - "line_number": 88 + "line_number": 129 }, { "type": "Secret Keyword", "filename": "src/infra/outbound/message-action-runner.test.ts", "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", "is_verified": false, - "line_number": 385 + "line_number": 435 } ], - "src/infra/outbound/outbound-policy.test.ts": [ + "src/infra/outbound/outbound.test.ts": [ { "type": "Hex High Entropy String", - "filename": "src/infra/outbound/outbound-policy.test.ts", + "filename": "src/infra/outbound/outbound.test.ts", "hashed_secret": "804ec071803318791b835cffd6e509c8d32239db", "is_verified": false, - "line_number": 33 + "line_number": 631 + } + ], + "src/infra/provider-usage.auth.normalizes-keys.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/infra/provider-usage.auth.normalizes-keys.test.ts", + "hashed_secret": "45c7365e3b542cdb4fae6ec10c2ff149224d7656", + "is_verified": false, + "line_number": 80 + }, + { + "type": "Secret Keyword", + "filename": "src/infra/provider-usage.auth.normalizes-keys.test.ts", + "hashed_secret": "b67074884ab7ef7c7a8cd6a3da9565d96c792248", + "is_verified": false, + "line_number": 81 + }, + { + "type": "Secret Keyword", + "filename": "src/infra/provider-usage.auth.normalizes-keys.test.ts", + "hashed_secret": "d4d8027e64f9cf4180d3aecfe31ea409368022ee", + "is_verified": false, + "line_number": 82 } ], "src/infra/shell-env.test.ts": [ @@ -1989,21 +12743,89 @@ "filename": "src/infra/shell-env.test.ts", "hashed_secret": "65c10dc3549fe07424148a8a4790a3341ecbc253", "is_verified": false, - "line_number": 27 + "line_number": 26 }, { "type": "Secret Keyword", "filename": "src/infra/shell-env.test.ts", "hashed_secret": "e013ffda590d2178607c16d11b1ea42f75ceb0e7", "is_verified": false, - "line_number": 59 + "line_number": 58 }, { "type": "Base64 High Entropy String", "filename": "src/infra/shell-env.test.ts", "hashed_secret": "be6ee9a6bf9f2dad84a5a67d6c0576a5bacc391e", "is_verified": false, - "line_number": 61 + "line_number": 60 + } + ], + "src/line/accounts.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/line/accounts.test.ts", + "hashed_secret": "fe1bae27cb7c1fb823f496f286e78f1d2ae87734", + "is_verified": false, + "line_number": 30 + }, + { + "type": "Secret Keyword", + "filename": "src/line/accounts.test.ts", + "hashed_secret": "8a8281cec699f5e51330e21dd7fab3531af6ef0c", + "is_verified": false, + "line_number": 48 + }, + { + "type": "Secret Keyword", + "filename": "src/line/accounts.test.ts", + "hashed_secret": "b4924d9834a1126714643ac231fb6623c14c3449", + "is_verified": false, + "line_number": 74 + } + ], + "src/line/bot-handlers.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/line/bot-handlers.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 106 + } + ], + "src/line/bot-message-context.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/line/bot-message-context.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 18 + } + ], + "src/line/monitor.fail-closed.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/line/monitor.fail-closed.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 22 + } + ], + "src/line/webhook-node.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/line/webhook-node.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 28 + } + ], + "src/line/webhook.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/line/webhook.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 23 } ], "src/logging/redact.test.ts": [ @@ -2012,37 +12834,37 @@ "filename": "src/logging/redact.test.ts", "hashed_secret": "dd7754662b89333191ff45e8257a3e6d3fcd3990", "is_verified": false, - "line_number": 9 + "line_number": 8 }, { "type": "Private Key", "filename": "src/logging/redact.test.ts", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 64 + "line_number": 73 }, { "type": "Hex High Entropy String", "filename": "src/logging/redact.test.ts", "hashed_secret": "7992945213f7d76889fa83ff0f2be352409c837e", "is_verified": false, - "line_number": 65 + "line_number": 74 }, { "type": "Base64 High Entropy String", "filename": "src/logging/redact.test.ts", "hashed_secret": "063995ecb4fa5afe2460397d322925cd867b7d74", "is_verified": false, - "line_number": 79 + "line_number": 88 } ], - "src/media-understanding/apply.test.ts": [ + "src/media-understanding/apply.e2e.test.ts": [ { "type": "Secret Keyword", - "filename": "src/media-understanding/apply.test.ts", + "filename": "src/media-understanding/apply.e2e.test.ts", "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 14 + "line_number": 12 } ], "src/media-understanding/providers/deepgram/audio.test.ts": [ @@ -2051,7 +12873,7 @@ "filename": "src/media-understanding/providers/deepgram/audio.test.ts", "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 31 + "line_number": 27 } ], "src/media-understanding/providers/google/video.test.ts": [ @@ -2060,7 +12882,7 @@ "filename": "src/media-understanding/providers/google/video.test.ts", "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 28 + "line_number": 64 } ], "src/media-understanding/providers/openai/audio.test.ts": [ @@ -2069,7 +12891,7 @@ "filename": "src/media-understanding/providers/openai/audio.test.ts", "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 26 + "line_number": 22 } ], "src/media-understanding/runner.auto-audio.test.ts": [ @@ -2078,7 +12900,7 @@ "filename": "src/media-understanding/runner.auto-audio.test.ts", "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 42 + "line_number": 40 } ], "src/media-understanding/runner.deepgram.test.ts": [ @@ -2087,7 +12909,23 @@ "filename": "src/media-understanding/runner.deepgram.test.ts", "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 46 + "line_number": 44 + } + ], + "src/memory/embeddings-voyage.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/memory/embeddings-voyage.test.ts", + "hashed_secret": "7c2020578bbe5e2e3f78d7f954eb2ad8ab5b0403", + "is_verified": false, + "line_number": 33 + }, + { + "type": "Secret Keyword", + "filename": "src/memory/embeddings-voyage.test.ts", + "hashed_secret": "8afdb3da9b79c8957ae35978ea8f33fbc3bfdf60", + "is_verified": false, + "line_number": 77 } ], "src/memory/embeddings.test.ts": [ @@ -2096,21 +12934,21 @@ "filename": "src/memory/embeddings.test.ts", "hashed_secret": "a47110e348a3063541fb1f1f640d635d457181a0", "is_verified": false, - "line_number": 32 + "line_number": 45 }, { "type": "Secret Keyword", "filename": "src/memory/embeddings.test.ts", "hashed_secret": "c734e47630dda71619c696d88381f06f7511bd78", "is_verified": false, - "line_number": 149 + "line_number": 160 }, { "type": "Secret Keyword", "filename": "src/memory/embeddings.test.ts", "hashed_secret": "56e1d57b8db262b08bc73c60ed08d8c92e59503f", "is_verified": false, - "line_number": 179 + "line_number": 189 } ], "src/pairing/pairing-store.ts": [ @@ -2119,37 +12957,64 @@ "filename": "src/pairing/pairing-store.ts", "hashed_secret": "f8c6f1ff98c5ee78c27d34a3ca68f35ad79847af", "is_verified": false, - "line_number": 12 + "line_number": 13 } ], - "src/security/audit.test.ts": [ + "src/pairing/setup-code.test.ts": [ { - "type": "Hex High Entropy String", - "filename": "src/security/audit.test.ts", - "hashed_secret": "b1775a785f09a6ebaf2dc33d6eaeb98974d9cdb8", + "type": "Base64 High Entropy String", + "filename": "src/pairing/setup-code.test.ts", + "hashed_secret": "4914c103484773b5a8e18448b11919bb349cbff8", "is_verified": false, - "line_number": 180 + "line_number": 22 }, { - "type": "Hex High Entropy String", - "filename": "src/security/audit.test.ts", - "hashed_secret": "fa8da98a5bdb77b4902cbb4338e6e94ea825300e", + "type": "Secret Keyword", + "filename": "src/pairing/setup-code.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 209 - }, + "line_number": 96 + } + ], + "src/security/audit.test.ts": [ { "type": "Secret Keyword", "filename": "src/security/audit.test.ts", "hashed_secret": "21f688ab56f76a99e5c6ed342291422f4e57e47f", "is_verified": false, - "line_number": 1046 + "line_number": 2063 }, { "type": "Secret Keyword", "filename": "src/security/audit.test.ts", "hashed_secret": "3dc927d80543dc0f643940b70d066bd4b4c4b78e", "is_verified": false, - "line_number": 1077 + "line_number": 2094 + } + ], + "src/telegram/monitor.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/telegram/monitor.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 205 + }, + { + "type": "Secret Keyword", + "filename": "src/telegram/monitor.test.ts", + "hashed_secret": "5934c4d4a4fa5d66ddb3d3fc0bba84996c17a5b7", + "is_verified": false, + "line_number": 233 + } + ], + "src/telegram/webhook.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/telegram/webhook.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 42 } ], "src/tts/tts.test.ts": [ @@ -2158,34 +13023,82 @@ "filename": "src/tts/tts.test.ts", "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", "is_verified": false, - "line_number": 33 + "line_number": 36 }, { "type": "Hex High Entropy String", "filename": "src/tts/tts.test.ts", "hashed_secret": "b214f706bb602c1cc2adc5c6165e73622305f4bb", "is_verified": false, - "line_number": 68 + "line_number": 98 + }, + { + "type": "Secret Keyword", + "filename": "src/tts/tts.test.ts", + "hashed_secret": "75ddfb45216fe09680dfe70eda4f559a910c832c", + "is_verified": false, + "line_number": 397 + }, + { + "type": "Secret Keyword", + "filename": "src/tts/tts.test.ts", + "hashed_secret": "e29af93630aa18cc3457cb5b13937b7ab7c99c9b", + "is_verified": false, + "line_number": 413 + }, + { + "type": "Secret Keyword", + "filename": "src/tts/tts.test.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", + "is_verified": false, + "line_number": 447 + } + ], + "src/tui/gateway-chat.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/tui/gateway-chat.test.ts", + "hashed_secret": "6255675480f681df08c1704b7b3cd2c49917f0e2", + "is_verified": false, + "line_number": 85 } ], - "src/web/qr-image.test.ts": [ + "src/web/login.test.ts": [ { "type": "Hex High Entropy String", - "filename": "src/web/qr-image.test.ts", + "filename": "src/web/login.test.ts", "hashed_secret": "564666dc1ca6e7318b2d5feeb1ce7b5bf717411e", "is_verified": false, - "line_number": 12 + "line_number": 60 } ], - "test/provider-timeout.e2e.test.ts": [ + "ui/src/i18n/locales/en.ts": [ { "type": "Secret Keyword", - "filename": "test/provider-timeout.e2e.test.ts", - "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "filename": "ui/src/i18n/locales/en.ts", + "hashed_secret": "de0ff6b974d6910aca8d6b830e1b761f076d8fe6", "is_verified": false, - "line_number": 182 + "line_number": 60 + } + ], + "ui/src/i18n/locales/pt-BR.ts": [ + { + "type": "Secret Keyword", + "filename": "ui/src/i18n/locales/pt-BR.ts", + "hashed_secret": "ef7b6f95faca2d7d3a5aa5a6434c89530c6dd243", + "is_verified": false, + "line_number": 60 + } + ], + "vendor/a2ui/README.md": [ + { + "type": "Secret Keyword", + "filename": "vendor/a2ui/README.md", + "hashed_secret": "2619a5397a5d054dab3fe24e6a8da1fbd76ec3a6", + "is_verified": false, + "line_number": 123 } ] }, - "generated_at": "2026-01-25T10:55:04Z" + "generated_at": "2026-02-17T13:34:38Z" } diff --git a/AGENTS.md b/AGENTS.md index 8a48c0402431b..5e589d336dd68 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,6 +70,10 @@ - Language: TypeScript (ESM). Prefer strict typing; avoid `any`. - Formatting/linting via Oxlint and Oxfmt; run `pnpm check` before commits. +- Never add `@ts-nocheck` and do not disable `no-explicit-any`; fix root causes and update Oxlint/Oxfmt config only when required. +- Never share class behavior via prototype mutation (`applyPrototypeMixins`, `Object.defineProperty` on `.prototype`, or exporting `Class.prototype` for merges). Use explicit inheritance/composition (`A extends B extends C`) or helper composition so TypeScript can typecheck. +- If this pattern is needed, stop and get explicit approval before shipping; default behavior is to split/refactor into an explicit class hierarchy and keep members strongly typed. +- In tests, prefer per-instance stubs over prototype mutation (`SomeClass.prototype.method = ...`) unless a test explicitly documents why prototype-level patching is required. - Add brief code comments for tricky or non-obvious logic. - Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`. - Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability. @@ -110,6 +114,7 @@ ## Git Notes - If `git branch -d/-D ` is policy-blocked, delete the local ref directly: `git update-ref -d refs/heads/`. +- Bulk PR close/reopen safety: if a close action would affect more than 5 PRs, first ask for explicit user confirmation with the exact PR count and target scope/query. ## Security & Configuration Tips @@ -119,6 +124,20 @@ - Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples. - Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them. +## GHSA (Repo Advisory) Patch/Publish + +- Before reviewing security advisories, read `SECURITY.md`. +- Fetch: `gh api /repos/openclaw/openclaw/security-advisories/` +- Latest npm: `npm view openclaw version --userconfig "$(mktemp)"` +- Private fork PRs must be closed: + `fork=$(gh api /repos/openclaw/openclaw/security-advisories/ | jq -r .private_fork.full_name)` + `gh pr list -R "$fork" --state open` (must be empty) +- Description newline footgun: write Markdown via heredoc to `/tmp/ghsa.desc.md` (no `"\\n"` strings) +- Build patch JSON via jq: `jq -n --rawfile desc /tmp/ghsa.desc.md '{summary,severity,description:$desc,vulnerabilities:[...]}' > /tmp/ghsa.patch.json` +- Patch + publish: `gh api -X PATCH /repos/openclaw/openclaw/security-advisories/ --input /tmp/ghsa.patch.json` (publish = include `"state":"published"`; no `/publish` endpoint) +- If publish fails (HTTP 422): missing `severity`/`description`/`vulnerabilities[]`, or private fork has open PRs +- Verify: re-fetch; ensure `state=published`, `published_at` set; `jq -r .description | rg '\\\\n'` returns nothing + ## Troubleshooting - Rebrand/migration issues or legacy config/service warnings: run `openclaw doctor` (see `docs/gateway/doctor.md`). @@ -182,3 +201,39 @@ - Publish: `npm publish --access public --otp=""` (run from the package dir). - Verify without local npmrc side effects: `npm view version --userconfig "$(mktemp)"`. - Kill the tmux session after publish. + +## Plugin Release Fast Path (no core `openclaw` publish) + +- Release only already-on-npm plugins. Source list is in `docs/reference/RELEASING.md` under "Current npm plugin list". +- Run all CLI `op` calls and `npm publish` inside tmux to avoid hangs/interruption: + - `tmux new -d -s release-plugins-$(date +%Y%m%d-%H%M%S)` + - `eval "$(op signin --account my.1password.com)"` +- 1Password helpers: + - password used by `npm login`: + `op item get Npmjs --format=json | jq -r '.fields[] | select(.id=="password").value'` + - OTP: + `op read 'op://Private/Npmjs/one-time password?attribute=otp'` +- Fast publish loop (local helper script in `/tmp` is fine; keep repo clean): + - compare local plugin `version` to `npm view version` + - only run `npm publish --access public --otp=""` when versions differ + - skip if package is missing on npm or version already matches. +- Keep `openclaw` untouched: never run publish from repo root unless explicitly requested. +- Post-check for each release: + - per-plugin: `npm view @openclaw/ version --userconfig "$(mktemp)"` should be `2026.2.17` + - core guard: `npm view openclaw version --userconfig "$(mktemp)"` should stay at previous version unless explicitly requested. + +## Changelog Release Notes + +- When cutting a mac release with beta GitHub prerelease: + - Tag `vYYYY.M.D-beta.N` from the release commit (example: `v2026.2.15-beta.1`). + - Create prerelease with title `openclaw YYYY.M.D-beta.N`. + - Use release notes from `CHANGELOG.md` version section (`Changes` + `Fixes`, no title duplicate). + - Attach at least `OpenClaw-YYYY.M.D.zip` and `OpenClaw-YYYY.M.D.dSYM.zip`; include `.dmg` if available. + +- Keep top version entries in `CHANGELOG.md` sorted by impact: + - `### Changes` first. + - `### Fixes` deduped and ranked with user-facing fixes first. +- Before tagging/publishing, run: + - `node --import tsx scripts/release-check.ts` + - `pnpm release:check` + - `pnpm test:install:smoke` or `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke` for non-root smoke path. diff --git a/CHANGELOG.md b/CHANGELOG.md index a67f6817aa79a..eafd5468e4ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,61 +2,483 @@ Docs: https://docs.openclaw.ai -## Unreleased +## 2026.2.19 (Unreleased) ### Changes +- iOS/Watch: add an Apple Watch companion MVP with watch inbox UI, watch notification relay handling, and gateway command surfaces for watch status/send flows. (#20054) Thanks @mbelinky. +- iOS/Gateway: wake disconnected iOS nodes via APNs before `nodes.invoke` and auto-reconnect gateway sessions on silent push wake to reduce invoke failures while the app is backgrounded. (#20332) Thanks @mbelinky. +- Gateway/CLI: add paired-device hygiene flows with `device.pair.remove`, plus `openclaw devices remove` and guarded `openclaw devices clear --yes [--pending]` commands for removing paired entries and optionally rejecting pending requests. (#20057) Thanks @mbelinky. +- iOS/APNs: add push registration and notification-signing configuration for node delivery. (#20308) Thanks @mbelinky. +- Gateway/APNs: add a push-test pipeline for APNs delivery validation in gateway flows. (#20307) Thanks @mbelinky. +- Security/Audit: add `gateway.http.no_auth` findings when `gateway.auth.mode="none"` leaves Gateway HTTP APIs reachable, with loopback warning and remote-exposure critical severity, plus regression coverage and docs updates. +- Skills: harden coding-agent skill guidance by removing shell-command examples that interpolate untrusted issue text directly into command strings. +- Dev tooling: align `oxfmt` local/CI formatting behavior. (#12579) Thanks @vincentkoc. + +### Fixes + +- Agents/Streaming: keep assistant partial streaming active during reasoning streams, handle native `thinking_*` stream events consistently, dedupe mixed reasoning-end signals, and clear stale mutating tool errors after same-target retry success. (#20635) Thanks @obviyus. +- iOS/Screen: move `WKWebView` lifecycle ownership into `ScreenWebView` coordinator and explicit attach/detach flow to reduce gesture/lifecycle crash risk (`__NSArrayM insertObject:atIndex:` paths) during screen tab updates. (#20366) Thanks @ngutman. +- iOS/Onboarding: prevent pairing-status flicker during auto-resume by keeping resumed state transitions stable. (#20310) Thanks @mbelinky. +- iOS/Onboarding: stabilize pairing and reconnect behavior by resetting stale pairing request state on manual retry, disconnecting both operator and node gateways on operator failure, and avoiding duplicate pairing loops from operator transport identity attachment. (#20056) Thanks @mbelinky. +- iOS/Signing: restore local auto-selected signing-team overrides during iOS project generation by wiring `.local-signing.xcconfig` into the active signing config and emitting `OPENCLAW_DEVELOPMENT_TEAM` in local signing setup. (#19993) Thanks @ngutman. +- Telegram: unify message-like inbound handling so `message` and `channel_post` share the same dedupe/access/media pipeline and remain behaviorally consistent. (#20591) Thanks @obviyus. +- Telegram/Agents: gate exec/bash tool-failure warnings behind verbose mode so default Telegram replies stay clean while verbose sessions still surface diagnostics. (#20560) Thanks @obviyus. +- Telegram/Cron/Heartbeat: honor explicit Telegram topic targets in cron and heartbeat delivery (`:topic:`) so scheduled sends land in the configured topic instead of the last active thread. (#19367) Thanks @Lukavyi. +- Gateway/Daemon: forward `TMPDIR` into installed service environments so macOS LaunchAgent gateway runs can open SQLite temp/journal files reliably instead of failing with `SQLITE_CANTOPEN`. (#20512) Thanks @Clawborn. +- Agents/Billing: include the active model that produced a billing error in user-facing billing messages (for example, `OpenAI (gpt-5.3)`) across payload, failover, and lifecycle error paths, so users can identify exactly which key needs credits. (#20510) Thanks @echoVic. +- Gateway/TUI: honor `agents.defaults.blockStreamingDefault` for `chat.send` by removing the hardcoded block-streaming disable override, so replies can use configured block-mode delivery. (#19693) Thanks @neipor. +- UI/Sessions: accept the canonical main session-key alias in Chat UI flows so main-session routing stays consistent. (#20311) Thanks @mbelinky. +- OpenClawKit/Protocol: preserve JSON boolean literals (`true`/`false`) when bridging through `AnyCodable` so Apple client RPC params no longer re-encode booleans as `1`/`0`. Thanks @mbelinky. +- Commands/Doctor: skip embedding-provider warnings when `memory.backend` is `qmd`, because QMD manages embeddings internally and does not require `memorySearch` providers. (#17263) Thanks @miloudbelarebia. +- Canvas/A2UI: improve bundled-asset resolution and empty-state handling so UI fallbacks render reliably. (#20312) Thanks @mbelinky. +- Commands/Doctor: avoid rewriting invalid configs with new `gateway.auth.token` defaults during repair and only write when real config changes are detected, preventing accidental token duplication and backup churn. +- Gateway/Auth: default unresolved gateway auth to token mode with startup auto-generation/persistence of `gateway.auth.token`, while allowing explicit `gateway.auth.mode: "none"` for intentional open loopback setups. (#20686) thanks @gumadeiras. +- Channels/Matrix: fix mention detection for `formatted_body` Matrix-to links by handling matrix.to mention formats consistently. (#16941) Thanks @zerone0x. +- Heartbeat/Cron: skip interval heartbeats when `HEARTBEAT.md` is missing or empty and no tagged cron events are queued, while preserving cron-event fallback for queued tagged reminders. (#20461) thanks @vikpos. +- Browser/Relay: reuse an already-running extension relay when the relay port is occupied by another OpenClaw process, while still failing on non-relay port collisions to avoid masking unrelated listeners. (#20035) Thanks @mbelinky. +- Scripts: update clawdock helper command support to include `docker-compose.extra.yml` where available. (#17094) Thanks @zerone0x. +- Lobster/Config: remove Lobster executable-path overrides (`lobsterPath`), require PATH-based execution, and add focused Windows wrapper-resolution tests to keep shell-free behavior stable. +- Gateway/WebChat: block `sessions.patch` and `sessions.delete` for WebChat clients so session-store mutations stay restricted to non-WebChat operator flows. Thanks @allsmog for reporting. +- Gateway: clarify launchctl GUI domain bootstrap failure on macOS. (#13795) Thanks @vincentkoc. +- Lobster/CI: fix flaky test Windows cmd shim script resolution. (#20833) Thanks @vincentkoc. +- Browser/Relay: require gateway-token auth on both `/extension` and `/cdp`, and align Chrome extension setup to use a single `gateway.auth.token` input for relay authentication. Thanks @tdjackey for reporting. +- Gateway/Hooks: run BOOT.md startup checks per configured agent scope, including per-agent session-key resolution, startup-hook regression coverage, and non-success boot outcome logging for diagnosability. (#20569) thanks @mcaxtr. +- Protocol/Apple: regenerate Swift gateway models for `push.test` so `pnpm protocol:check` stays green on main. Thanks @mbelinky. +- Sandbox/Registry: serialize container and browser registry writes with shared file locks and atomic replacement to prevent lost updates and delete rollback races from desyncing `sandbox list`, `prune`, and `recreate --all`. Thanks @kexinoh. +- OTEL/diagnostics-otel: complete OpenTelemetry v2 API migration. (#12897) Thanks @vincentkoc. +- Cron/Webhooks: protect cron webhook POST delivery with SSRF-guarded outbound fetch (`fetchWithSsrFGuard`) to block private/metadata destinations before request dispatch. Thanks @Adam55A-code. +- Security/Voice Call: harden `voice-call` telephony TTS override merging by blocking unsafe deep-merge keys (`__proto__`, `prototype`, `constructor`) and add regression coverage for top-level and nested prototype-pollution payloads. +- Security/Windows Daemon: harden Scheduled Task `gateway.cmd` generation by quoting cmd metacharacter arguments, escaping `%`/`!` expansions, and rejecting CR/LF in arguments, descriptions, and environment assignments (`set "KEY=VALUE"`), preventing command injection in Windows daemon startup scripts. This ships in the next npm release. Thanks @tdjackey for reporting. +- Security/Gateway/Canvas: replace shared-IP fallback auth with node-scoped session capability URLs for `/__openclaw__/canvas/*` and `/__openclaw__/a2ui/*`, fail closed when trusted-proxy requests omit forwarded client headers, and add IPv6/proxy-header regression coverage. This ships in the next npm release. Thanks @aether-ai-agent for reporting. +- Security/Net: enforce strict dotted-decimal IPv4 literals in SSRF checks and fail closed on unsupported legacy forms (octal/hex/short/packed, for example `0177.0.0.1`, `127.1`, `2130706433`) before DNS lookup. +- Security/Discord: enforce trusted-sender guild permission checks for moderation actions (`timeout`, `kick`, `ban`) and ignore untrusted `senderUserId` params to prevent privilege escalation in tool-driven flows. Thanks @aether-ai-agent for reporting. +- Security/ACP+Exec: add `openclaw acp --token-file/--password-file` secret-file support (with inline secret flag warnings), redact ACP working-directory prefixes to `~` home-relative paths, constrain exec script preflight file inspection to the effective `workdir` boundary, and add security-audit warnings when `tools.exec.host="sandbox"` is configured while sandbox mode is off. +- Security/Plugins/Hooks: enforce runtime/package path containment with realpath checks so `openclaw.extensions`, `openclaw.hooks`, and hook handler modules cannot escape their trusted roots via traversal or symlinks. +- Security/Discord: centralize trusted sender checks for moderation actions in message-action dispatch, share moderation command parsing across handlers, and clarify permission helpers with explicit any/all semantics. +- Security/ACP: harden ACP bridge session management with duplicate-session refresh, idle-session reaping, oldest-idle soft-cap eviction, and burst rate limiting on session creation to reduce local DoS risk without disrupting normal IDE usage. +- Security/ACP: bound ACP prompt text payloads to 2 MiB before gateway forwarding, account for join separator bytes during pre-concatenation size checks, and avoid stale active-run session state when oversized prompts are rejected. Thanks @aether-ai-agent for reporting. +- Security/Plugins/Hooks: add optional `--pin` for npm plugin/hook installs, persist resolved npm metadata (`name`, `version`, `spec`, integrity, shasum, timestamp), warn/confirm on integrity drift during updates, and extend `openclaw security audit` to flag unpinned specs, missing integrity metadata, and install-record version drift. +- Security/Plugins: harden plugin discovery by blocking unsafe candidates (root escapes, world-writable paths, suspicious ownership), add startup warnings when `plugins.allow` is empty with discoverable non-bundled plugins, and warn on loaded plugins without install/load-path provenance. +- Security/Gateway: rate-limit control-plane write RPCs (`config.apply`, `config.patch`, `update.run`) to 3 requests per minute per `deviceId+clientIp`, add restart single-flight coalescing plus a 30-second restart cooldown, and log actor/device/ip with changed-path audit details for config/update-triggered restarts. +- Security/Webhooks: harden Feishu and Zalo webhook ingress with webhook-mode token preconditions, loopback-default Feishu bind host, JSON content-type enforcement, per-path rate limiting, replay dedupe for Zalo events, constant-time Zalo secret comparison, and anomaly status counters. +- Security/Plugins: for the next npm release, clarify plugin trust boundary and keep `runtime.system.runCommandWithTimeout` available by default for trusted in-process plugins. Thanks @markmusson for reporting. +- Security/Skills: for the next npm release, reject symlinks during skill packaging to prevent external file inclusion in distributed `.skill` archives. Thanks @aether-ai-agent for reporting. +- Security/Gateway: fail startup when `hooks.token` matches `gateway.auth.token` so hooks and gateway token reuse is rejected at boot. (#20813) Thanks @coygeek. +- Security/Network: block plaintext `ws://` connections to non-loopback hosts and require secure websocket transport elsewhere. (#20803) Thanks @jscaldwell55. +- Security/Config: parse frontmatter YAML using the YAML 1.2 core schema to avoid implicit coercion of `on`/`off`-style values. (#20857) Thanks @davidrudduck. +- Security/Discord: escape backticks in exec-approval embed content to prevent markdown formatting injection via command text. (#20854) Thanks @davidrudduck. +- Security/Agents: replace shell-based `execSync` usage with `execFileSync` in command lookup helpers to eliminate shell argument interpolation risk. (#20655) Thanks @mahanandhi. +- Security/Media: use `crypto.randomBytes()` for temp file names and set owner-only permissions for TTS temp files. (#20654) Thanks @mahanandhi. +- Security/Gateway: set baseline security headers (`X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`) on gateway HTTP responses. (#10526) Thanks @abdelsfane. +- Security/iMessage: harden remote attachment SSH/SCP handling by requiring strict host-key verification, validating `channels.imessage.remoteHost` as `host`/`user@host`, and rejecting unsafe host tokens from config or auto-detection. Thanks @allsmog for reporting. +- Security/Feishu: prevent path traversal in Feishu inbound media temp-file writes by replacing key-derived temp filenames with UUID-based names. Thanks @allsmog for reporting. +- Security/Feishu: escape mention regex metacharacters in `stripBotMention` so crafted mention metadata cannot trigger regex injection or ReDoS during inbound message parsing. (#20916) Thanks @orlyjamie for the fix and @allsmog for reporting. +- LINE/Security: harden inbound media temp-file naming by using UUID-based temp paths for downloaded media instead of external message IDs. (#20792) Thanks @mbelinky. +- Security/Media: harden local media ingestion against TOCTOU/symlink swap attacks by pinning reads to a single file descriptor with symlink rejection and inode/device verification in `saveMediaSource`. Thanks @dorjoos for reporting. +- Security/Lobster (Windows): for the next npm release, remove shell-based fallback when launching Lobster wrappers (`.cmd`/`.bat`) and switch to explicit argv execution with wrapper entrypoint resolution, preventing command injection while preserving Windows wrapper compatibility. Thanks @allsmog for reporting. +- Security/Exec: require `tools.exec.safeBins` binaries to resolve from trusted bin directories (system defaults plus gateway startup `PATH`) so PATH-hijacked trojan binaries cannot bypass allowlist checks. Thanks @jackhax for reporting. +- Security/Exec: remove file-existence oracle behavior from `tools.exec.safeBins` by using deterministic argv-only stdin-safe validation and blocking file-oriented flags (for example `sort -o`, `jq -f`, `grep -f`) so allow/deny results no longer disclose host file presence. This ships in the next npm release. Thanks @nedlir for reporting. +- Security/Browser: route browser URL navigation through one SSRF-guarded validation path for tab-open/CDP-target/Playwright navigation flows and block private/metadata destinations by default (configurable via `browser.ssrfPolicy`). This ships in the next npm release. Thanks @dorjoos for reporting. +- Security/Exec: for the next npm release, harden safe-bin stdin-only enforcement by blocking output/recursive flags (`sort -o/--output`, grep recursion) and tightening default safe bins to remove `sort`/`grep`, preventing safe-bin allowlist bypass for file writes/recursive reads. Thanks @nedlir for reporting. +- Security/Gateway/Agents: remove implicit admin scopes from agent tool gateway calls by classifying methods to least-privilege operator scopes, and enforce owner-only tooling (`cron`, `gateway`, `whatsapp_login`) through centralized tool-policy wrappers plus tool metadata to prevent non-owner DM privilege escalation. Ships in the next npm release. Thanks @Adam55A-code for reporting. +- Security/Gateway: centralize gateway method-scope authorization and default non-CLI gateway callers to least-privilege method scopes, with explicit CLI scope handling, full core-handler scope classification coverage, and regression guards to prevent scope drift. +- Security/Net: block SSRF bypass via NAT64 (`64:ff9b::/96`, `64:ff9b:1::/48`), 6to4 (`2002::/16`), and Teredo (`2001:0000::/32`) IPv6 transition addresses, and fail closed on IPv6 parse errors. Thanks @jackhax. +- Security/OTEL: sanitize OTLP endpoint URL resolution. (#13791) Thanks @vincentkoc. +- Security: patch Dependabot security issues in pnpm lock. (#20832) Thanks @vincentkoc. +- Security: migrate request dependencies to `@cypress/request`. (#20836) Thanks @vincentkoc. + +## 2026.2.17 + +### Changes + +- Agents/Anthropic: add opt-in 1M context beta header support for Opus/Sonnet via model `params.context1m: true` (maps to `anthropic-beta: context-1m-2025-08-07`). +- Agents/Models: support Anthropic Sonnet 4.6 (`anthropic/claude-sonnet-4-6`) across aliases/defaults with forward-compat fallback when upstream catalogs still only expose Sonnet 4.5. +- Commands/Subagents: add `/subagents spawn` for deterministic subagent activation from chat commands. (#18218) Thanks @JoshuaLelon. +- Agents/Subagents: add an accepted response note for `sessions_spawn` explaining polling subagents are disabled for one-off calls. Thanks @tyler6204. +- Agents/Subagents: prefix spawned subagent task messages with context to preserve source information in downstream handling. Thanks @tyler6204. +- iOS/Share: add an iOS share extension that forwards shared URL/text/image content directly to gateway `agent.request`, with delivery-route fallback and optional receipt acknowledgements. (#19424) Thanks @mbelinky. +- iOS/Talk: add a `Background Listening` toggle that keeps Talk Mode active while the app is backgrounded (off by default for battery safety). Thanks @zeulewan. +- iOS/Talk: add a `Voice Directive Hint` toggle for Talk Mode prompts so users can disable ElevenLabs voice-switching instructions to save tokens when not needed. (#18250) Thanks @zeulewan. +- iOS/Talk: harden barge-in behavior by disabling interrupt-on-speech when output route is built-in speaker/receiver, reducing false interruptions from local TTS bleed-through. Thanks @zeulewan. +- Slack: add native single-message text streaming with Slack `chat.startStream`/`appendStream`/`stopStream`; keep reply threading aligned with `replyToMode`, default streaming to enabled, and fall back to normal delivery when streaming fails. (#9972) Thanks @natedenh. +- Slack: add configurable streaming modes for draft previews. (#18555) Thanks @Solvely-Colin. +- Telegram/Agents: add inline button `style` support (`primary|success|danger`) across message tool schema, Telegram action parsing, send pipeline, and runtime prompt guidance. (#18241) Thanks @obviyus. +- Telegram: surface user message reactions as system events, with configurable `channels.telegram.reactionNotifications` scope. (#10075) Thanks @Glucksberg. +- iMessage: support `replyToId` on outbound text/media sends and normalize leading `[[reply_to:]]` tags so replies target the intended iMessage. Thanks @tyler6204. +- Tool Display/Web UI: add intent-first tool detail views and exec summaries. (#18592) Thanks @xdLawless2. +- Discord: expose native `/exec` command options (host/security/ask/node) so Discord slash commands get autocomplete and structured inputs. Thanks @thewilloftheshadow. +- Discord: allow reusable interactive components with `components.reusable=true` so buttons, selects, and forms can be used multiple times before expiring. Thanks @thewilloftheshadow. +- Discord: add per-button `allowedUsers` allowlist for interactive components to restrict who can click buttons. Thanks @thewilloftheshadow. +- Cron/Gateway: separate per-job webhook delivery (`delivery.mode = "webhook"`) from announce delivery, enforce valid HTTP(S) webhook URLs, and keep a temporary legacy `notify + cron.webhook` fallback for stored jobs. (#17901) Thanks @advaitpaliwal. +- Cron/CLI: add deterministic default stagger for recurring top-of-hour cron schedules (including 6-field seconds cron), auto-migrate existing jobs to persisted `schedule.staggerMs`, and add `openclaw cron add/edit --stagger ` plus `--exact` overrides for per-job timing control. +- Cron: log per-run model/provider usage telemetry in cron run logs/webhooks and add a local usage report script for aggregating token usage by job. (#18172) Thanks @HankAndTheCrew. +- Tools/Web: add URL allowlists for `web_search` and `web_fetch`. (#18584) Thanks @smartprogrammer93. +- Browser: add `extraArgs` config for custom Chrome launch arguments. (#18443) Thanks @JayMishra-source. +- Voice Call: pre-cache inbound greeting TTS for faster first playback. (#18447) Thanks @JayMishra-source. +- Skills: compact skill file `` paths in the system prompt by replacing home-directory prefixes with `~`, and add targeted compaction tests for prompt serialization behavior. (#14776) Thanks @bitfish3. +- Skills: refine skill-description routing boundaries with explicit "Use when"/"NOT for" guidance for coding-agent/github/weather, and clarify PTY/browser fallback wording. (#14577) Thanks @DylanWoodAkers. +- Auto-reply/Prompts: include trusted inbound `message_id` in conversation metadata payloads for downstream targeting workflows. Thanks @tyler6204. +- Auto-reply: include `sender_id` in trusted inbound metadata so moderation workflows can target the sender without relying on untrusted text. (#18303) Thanks @crimeacs. +- UI/Sessions: avoid duplicating typed session prefixes in display names (for example `Subagent Subagent ...`). Thanks @tyler6204. +- Agents/Z.AI: enable `tool_stream` by default for real-time tool call streaming, with opt-out via `params.tool_stream: false`. (#18173) Thanks @tianxiao1430-jpg. +- Plugins: add `before_agent_start` model/provider overrides before resolution. (#18568) Thanks @natefikru. +- Mattermost: add emoji reaction actions plus reaction event notifications, including an explicit boolean `remove` flag to avoid accidental removals. (#18608) Thanks @echo931. +- Memory/Search: add FTS fallback plus query expansion for memory search. (#18304) Thanks @irchelper. +- Agents/Models: support per-model `thinkingDefault` overrides in model config. (#18152) Thanks @wu-tian807. +- Agents: enable `llms.txt` discovery in default behavior. (#18158) Thanks @yolo-maxi. +- Extensions/Auth: add OpenAI Codex CLI auth provider integration. (#18009) Thanks @jiteshdhamaniya. +- Feishu: add Bitable create-app/create-field tools for automation workflows. (#17963) Thanks @gaowanqi08141999. +- Docker: add optional `OPENCLAW_INSTALL_BROWSER` build arg to preinstall Chromium + Xvfb in the Docker image, avoiding runtime Playwright installs. (#18449) + +### Fixes + +- Tests/Telegram: add regression coverage for command-menu sync that asserts all `setMyCommands` entries are Telegram-safe and hyphen-normalized across native/custom/plugin command sources. (#19703) Thanks @obviyus. +- Agents/Image: collapse resize diagnostics to one line per image and include visible pixel/byte size details in the log message for faster triage. +- Agents/Subagents: preemptively guard accumulated tool-result context before model calls by truncating oversized outputs and compacting oldest tool-result messages to avoid context-window overflow crashes. Thanks @tyler6204. +- Agents/Subagents/CLI: fail `sessions_spawn` when subagent model patching is rejected, allow subagent model patch defaults from `subagents.model`, and keep `sessions list`/`status` model reporting aligned to runtime model resolution. (#18660) Thanks @robbyczgw-cla. +- Agents/Subagents: add explicit subagent guidance to recover from `[compacted: tool output removed to free context]` / `[truncated: output exceeded context limit]` markers by re-reading with smaller chunks instead of full-file `cat`. Thanks @tyler6204. +- Agents/Tools: make `read` auto-page across chunks (when no explicit `limit` is provided) and scale its per-call output budget from model `contextWindow`, so larger contexts can read more before context guards kick in. Thanks @tyler6204. +- Agents/Tools: strip duplicated `read` truncation payloads from tool-result `details` and make pre-call context guarding account for heavy tool-result metadata, so repeated `read` calls no longer bypass compaction and overflow model context windows. Thanks @tyler6204. +- Reply threading: keep reply context sticky across streamed/split chunks and preserve `replyToId` on all chunk sends across shared and channel-specific delivery paths (including iMessage, BlueBubbles, Telegram, Discord, and Matrix), so follow-up bubbles stay attached to the same referenced message. Thanks @tyler6204. +- Gateway/Agent: defer transient lifecycle `error` snapshots with a short grace window so `agent.wait` does not resolve early during retry/failover. Thanks @tyler6204. +- Gateway/Presence: centralize presence snapshot broadcasts and unify runtime version precedence (`OPENCLAW_VERSION` > `OPENCLAW_SERVICE_VERSION` > `npm_package_version`) so self-presence and websocket `hello-ok` report consistent versions. +- Hooks/Automation: bridge outbound/inbound message lifecycle into internal hook events (`message:received`, `message:sent`) with session-key correlation guards, while keeping per-payload success/error reporting accurate for chunked and best-effort deliveries. (PR #9387) +- Media understanding: honor `agents.defaults.imageModel` during auto-discovery so implicit image analysis uses configured primary/fallback image models. (PR #7607) +- iOS/Onboarding: stop auth Step 3 retry-loop churn by pausing reconnect attempts on unauthorized/missing-token gateway errors and keeping auth/pairing issue state sticky during manual retry. (#19153) Thanks @mbelinky. +- Voice-call: auto-end calls when media streams disconnect to prevent stuck active calls. (#18435) Thanks @JayMishra-source. +- Voice call/Gateway: prevent overlapping closed-loop turn races with per-call turn locking, route transcript dedupe via source-aware fingerprints with strict cache eviction bounds, and harden `voicecall latency` stats for large logs without spread-operator stack overflow. (#19140) Thanks @mbelinky. +- iOS/Chat: route ChatSheet RPCs through the operator session instead of the node session to avoid node-role authorization failures for `chat.history`, `chat.send`, and `sessions.list`. (#19320) Thanks @mbelinky. +- macOS/Update: correct the Sparkle appcast version for 2026.2.15 so updates are offered again. (#18201) +- Gateway/Auth: clear stale device-auth tokens after device token mismatch errors so re-paired clients can re-auth. (#18201) +- Telegram: enable DM voice-note transcription with CLI fallback handling. (#18564) Thanks @thhuang. +- Telegram/Polls: restore Telegram poll action wiring in channel handlers. (#18122) Thanks @akyourowngames. +- WebChat: strip reply/audio directive tags from rendered chat output. (#18093) Thanks @aldoeliacim. +- Discord: honor configured HTTP proxy for app-id and allowlist REST resolution. (#17958) Thanks @k2009. +- BlueBubbles: add fallback path to recover outbound `message_id` from `fromMe` webhooks when platform message IDs are missing. Thanks @tyler6204. +- BlueBubbles: match outbound message-id fallback recovery by chat identifier as well as account context. Thanks @tyler6204. +- BlueBubbles: include sender identifier in untrusted conversation metadata for conversation info payloads. Thanks @tyler6204. +- Security/Exec: fix the OC-09 credential-theft path via environment-variable injection. (#18048) Thanks @aether-ai-agent. +- Security/Config: confine `$include` resolution to the top-level config directory, harden traversal/symlink checks with cross-platform-safe path containment, and add doctor hints for invalid escaped include paths. (#18652) Thanks @aether-ai-agent. +- Security/Net: block SSRF bypass via ISATAP embedded IPv4 transition addresses and centralize hostname/IP blocking checks across URL safety validators. Thanks @zpbrent for reporting. +- Providers: improve error messaging for unconfigured local `ollama`/`vllm` providers. (#18183) Thanks @arosstale. +- TTS: surface all provider errors instead of only the last error in aggregated failures. (#17964) Thanks @ikari-pl. +- CLI/Doctor/Configure: skip gateway auth checks for loopback-only setups. (#18407) Thanks @sggolakiya. +- CLI/Doctor: reconcile gateway service-token drift after re-pair flows. (#18525) Thanks @norunners. +- Process/Windows: disable detached spawn in exec runs to prevent empty command output. (#18067) Thanks @arosstale. +- Process: gracefully terminate process trees with SIGTERM before SIGKILL. (#18626) Thanks @sauerdaniel. +- Sessions/Windows: use atomic session-store writes to prevent context loss on Windows. (#18347) Thanks @twcwinston. +- Agents/Image: validate base64 image payloads before provider submission. (#18263) Thanks @sriram369. +- Models CLI: validate catalog entries in `openclaw models set`. (#18129) Thanks @carrotRakko. +- Usage: isolate last-turn totals in token usage reporting to avoid mixed-turn totals. (#18052) Thanks @arosstale. +- Cron: resolve `accountId` from agent bindings in isolated sessions. (#17996) Thanks @simonemacario. +- Gateway/HTTP: preserve unbracketed IPv6 `Host` headers when normalizing requests. (#18061) Thanks @Clawborn. +- Sandbox: fix workspace-directory orphaning during SHA-1 -> SHA-256 slug migration. (#18523) Thanks @yinghaosang. +- Ollama/Qwen: handle Qwen 3 reasoning field format in Ollama responses. (#18631) Thanks @mr-sk. +- OpenAI/Transcripts: always drop orphaned reasoning blocks from transcript repair. (#18632) Thanks @TySabs. +- Fix types in all tests. Typecheck the whole repository. +- Gateway/Channels: wire `gateway.channelHealthCheckMinutes` into strict config validation, treat implicit account status as managed for health checks, and harden channel auto-restart flow (preserve restart-attempt caps across crash loops, propagate enabled/configured runtime flags, and stop pending restart backoff after manual stop). Thanks @steipete. +- Gateway/WebChat: hard-cap `chat.history` oversized payloads by truncating high-cost fields and replacing over-budget entries with placeholders, so history fetches stay within configured byte limits and avoid chat UI freezes. (#18505) +- UI/Usage: replace lingering undefined `var(--text-muted)` usage with `var(--muted)` in usage date-range and chart styles to keep muted text visible across themes. (#17975) Thanks @jogelin. +- UI/Usage: preserve selected-range totals when timeline data is downsampled by bucket-aggregating timeseries points (instead of dropping intermediate points), so filtered tokens/cost stay accurate. (#17959) Thanks @jogelin. +- UI/Sessions: refresh the sessions table only after successful deletes and preserve delete errors on cancel/failure paths, so deleted sessions disappear automatically without masking delete failures. (#18507) +- Scripts/UI/Windows: fix `pnpm ui:*` spawn `EINVAL` failures by restoring shell-backed launch for `.cmd`/`.bat` runners, narrowing shell usage to launcher types that require it, and rejecting unsafe forwarded shell metacharacters in UI script args. (#18594) +- Hooks/Session-memory: recover `/new` conversation summaries when session pointers are reset-path or missing `sessionFile`, and consistently prefer the newest `.jsonl.reset.*` transcript candidate for fallback extraction. (#18088) +- Auto-reply/Sessions: prevent stale thread ID leakage into non-thread sessions so replies stay in the main DM after topic interactions. (#18528) Thanks @j2h4u. +- Slack: restrict forwarded-attachment ingestion to explicit shared-message attachments and skip non-Slack forwarded `image_url` fetches, preventing non-forward attachment unfurls from polluting inbound agent context while preserving forwarded message handling. +- Feishu: detect bot mentions in post messages with embedded docs when `message.mentions` is empty. (#18074) Thanks @popomore. +- Agents/Sessions: align session lock watchdog hold windows with run and compaction timeout budgets (plus grace), preventing valid long-running turns from being force-unlocked mid-run while still recovering hung lock owners. (#18060) +- Cron: preserve default model fallbacks for cron agent runs when only `model.primary` is overridden, so failover still follows configured fallbacks unless explicitly cleared with `fallbacks: []`. (#18210) Thanks @mahsumaktas. +- Cron: route text-only announce output through the main session announce flow via runSubagentAnnounceFlow so cron text-only output remains visible to the initiating session. Thanks @tyler6204. +- Cron: treat `timeoutSeconds: 0` as no-timeout (not clamped to 1), ensuring long-running cron runs are not prematurely terminated. Thanks @tyler6204. +- Cron announce injection now targets the session determined by delivery config (`to` + channel) instead of defaulting to the current session. Thanks @tyler6204. +- Cron/Heartbeat: canonicalize session-scoped reminder `sessionKey` routing and preserve explicit flat `sessionKey` cron tool inputs, preventing enqueue/wake namespace drift for session-targeted reminders. (#18637) Thanks @vignesh07. +- Cron/Webhooks: reuse existing session IDs for webhook/cron runs when the session key is stable and still fresh, preserving conversation history. (#18031) Thanks @Operative-001. +- Cron: prevent spin loops when cron jobs complete within the scheduled second by advancing the next run and enforcing a minimum refire gap. (#18073) Thanks @widingmarcus-cyber. +- OpenClawKit/iOS ChatUI: accept canonical session-key completion events for local pending runs and preserve message IDs across history refreshes, preventing stuck "thinking" state and message flicker after gateway replies. (#18165) Thanks @mbelinky. +- iOS/Onboarding: add QR-first onboarding wizard with setup-code deep link support, pairing/auth issue guidance, and device-pair QR generation improvements for Telegram/Web/TUI fallback flows. (#18162) Thanks @mbelinky and @Marvae. +- iOS/Gateway: stabilize connect/discovery state handling, add onboarding reset recovery in Settings, and fix iOS gateway-controller coverage for command-surface and last-connection persistence behavior. (#18164) Thanks @mbelinky. +- iOS/Talk: harden mobile talk config handling by ignoring redacted/env-placeholder API keys, support secure local keychain override, improve accessibility motion/contrast behavior in status UI, and tighten ATS to local-network allowance. (#18163) Thanks @mbelinky. +- iOS/Location: restore the significant location monitor implementation (service hooks + protocol surface + ATS key alignment) after merge drift so iOS builds compile again. (#18260) Thanks @ngutman. +- iOS/Signing: auto-select local Apple Development team during iOS project generation/build, prefer the canonical OpenClaw team when available, and support local per-machine signing overrides without committing team IDs. (#18421) Thanks @ngutman. +- Discord/Telegram: make per-account message action gates effective for both action listing and execution, and preserve top-level gate restrictions when account overrides only specify a subset of `actions` keys (account key -> base key -> default fallback). (#18494) +- Telegram: keep DM-topic replies and draft previews in the originating private-chat topic by preserving positive `message_thread_id` values for DM threads. (#18586) Thanks @sebslight. +- Telegram: preserve private-chat topic `message_thread_id` on outbound sends (message/sticker/poll), keep thread-not-found retry fallback, and avoid masking `chat not found` routing errors. (#18993) Thanks @obviyus. +- Discord: prevent duplicate media delivery when the model uses the `message send` tool with media, by skipping media extraction from messaging tool results since the tool already sent the message directly. (#18270) +- Discord: route `audioAsVoice` auto-replies through the voice message API so opt-in audio renders as voice messages. (#18041) Thanks @zerone0x. +- Discord: skip auto-thread creation in forum/media/voice/stage channels and keep group session last-route metadata fresh to avoid invalid thread API errors and lost follow-up sends. (#18098) Thanks @Clawborn. +- Discord/Commands: normalize `commands.allowFrom` entries with `user:`/`discord:`/`pk:` prefixes and `<@id>` mentions so command authorization matches Discord allowlist behavior. (#18042) +- Telegram: keep draft-stream preview replies attached to the user message for `replyToMode: "all"` in groups and DMs, preserving threaded reply context from preview through finalization. (#17880) Thanks @yinghaosang. +- Telegram: prevent streaming final replies from being overwritten by later final/error payloads, and suppress fallback tool-error warnings when a recovered assistant answer already exists after tool calls. (#17883) Thanks @Marvae and @obviyus. +- Telegram: debounce the first draft-stream preview update (30-char threshold) and finalize short responses by editing the stop-time preview message, improving first push notifications and avoiding duplicate final sends. (#18148) Thanks @Marvae. +- Telegram: disable block streaming when `channels.telegram.streamMode` is `off`, preventing newline/content-block replies from splitting into multiple messages. (#17679) Thanks @saivarunk. +- Telegram: keep `streamMode: "partial"` draft previews in a single message across assistant-message/reasoning boundaries, preventing duplicate preview bubbles during partial-mode tool-call turns. (#18956) Thanks @obviyus. +- Telegram: normalize native command names for Telegram menu registration (`-` -> `_`) to avoid `BOT_COMMAND_INVALID` command-menu wipeouts, and log failed command syncs instead of silently swallowing them. (#19257) Thanks @akramcodez. +- Telegram: route non-abort slash commands on the normal chat/topic sequential lane while keeping true abort requests (`/stop`, `stop`) on the control lane, preventing command/reply race conditions from control-lane bypass. (#17899) Thanks @obviyus. +- Telegram: ignore `` placeholder lines when extracting `MEDIA:` tool-result paths, preventing false local-file reads and dropped replies. (#18510) Thanks @yinghaosang. +- Telegram: skip retries when inbound media `getFile` fails with Telegram's 20MB limit and continue processing message text, avoiding dropped messages for oversized attachments. (#18531) Thanks @brandonwise. +- Telegram: clear stored polling offsets when bot tokens change or accounts are deleted, preventing stale offsets after token rotations. (#18233) +- Telegram: enable `autoSelectFamily` by default on Node.js 22+ so IPv4 fallback works on broken IPv6 networks. (#18272) Thanks @nacho9900. +- Auto-reply/TTS: keep tool-result media delivery enabled in group chats and native command sessions (while still suppressing tool summary text) so `NO_REPLY` follow-ups do not drop successful TTS audio. (#17991) Thanks @zerone0x. +- Agents/Tools: deliver tool-result media even when verbose tool output is off so media attachments are not dropped. (#16679) +- Discord: optimize reaction notification handling to skip unnecessary message fetches in `off`/`all`/`allowlist` modes, streamline reaction routing, and improve reaction emoji formatting. (#18248) Thanks @thewilloftheshadow and @victorGPT. +- CLI/Pairing: make `openclaw qr --remote` prefer `gateway.remote.url` over tailscale/public URL resolution and register the `openclaw clawbot qr` legacy alias path. (#18091) +- CLI/QR: restore fail-fast validation for `openclaw qr --remote` when neither `gateway.remote.url` nor tailscale `serve`/`funnel` is configured, preventing unusable remote pairing QR flows. (#18166) Thanks @mbelinky. +- CLI: fix parent/subcommand option collisions across gateway, daemon, update, ACP, and browser command flows, while preserving legacy `browser set headers --json ` compatibility. +- CLI/Doctor: ensure `openclaw doctor --fix --non-interactive --yes` exits promptly after completion so one-shot automation no longer hangs. (#18502) +- CLI/Doctor: auto-repair `dmPolicy="open"` configs missing wildcard allowlists and write channel-correct repair paths (including `channels.googlechat.dm.allowFrom`) so `openclaw doctor --fix` no longer leaves Google Chat configs invalid after attempted repair. (#18544) +- CLI/Doctor: detect gateway service token drift when the gateway token is only provided via environment variables, keeping service repairs aligned after token rotation. +- Gateway/Update: prevent restart crash loops after failed self-updates by restarting only on successful updates, stopping early on failed install/build steps, and running `openclaw doctor --fix` during updates to sanitize config. (#18131) Thanks @RamiNoodle733. +- Gateway/Update: preserve update.run restart delivery context so post-update status replies route back to the initiating channel/thread. (#18267) Thanks @yinghaosang. +- CLI/Update: run a standalone restart helper after updates, honoring service-name overrides and reporting restart initiation separately from confirmed restarts. (#18050) +- CLI/Daemon: warn when a gateway restart sees a stale service token so users can reinstall with `openclaw gateway install --force`, and skip drift warnings for non-gateway service restarts. (#18018) +- CLI/Daemon: prefer the active version-manager Node when installing daemons and include macOS version-manager bin directories in the service PATH so launchd services resolve user-managed runtimes. +- CLI/Status: fix `openclaw status --all` token summaries for bot-token-only channels so Mattermost/Zalo no longer show a bot+app warning. (#18527) Thanks @echo931. +- CLI/Configure: make the `/model picker` allowlist prompt searchable with tokenized matching in `openclaw configure` so users can filter huge model lists by typing terms like `gpt-5.2 openai/`. (#19010) Thanks @bjesuiter. +- CLI/Message: preserve `--components` JSON payloads in `openclaw message send` so Discord component payloads are no longer dropped. (#18222) Thanks @saurabhchopade. +- Voice Call: add an optional stale call reaper (`staleCallReaperSeconds`) to end stuck calls when enabled. (#18437) +- Auto-reply/Subagents: propagate group context (`groupId`, `groupChannel`, `space`) when spawning via `/subagents spawn`, matching tool-triggered subagent spawn behavior. +- Subagents: route nested announce results back to the parent session after the parent run ends, falling back only when the parent session is deleted. (#18043) Thanks @tyler6204. +- Subagents: cap announce retry loops with max attempts and expiry to prevent infinite retry spam after deferred announces. (#18444) +- Agents/Tools/exec: add a preflight guard that detects likely shell env var injection (e.g. `$DM_JSON`, `$TMPDIR`) in Python/Node scripts before execution, preventing recurring cron failures and wasted tokens when models emit mixed shell+language source. (#12836) +- Agents/Tools/exec: treat normal non-zero exit codes as completed and append the exit code to tool output to avoid false tool-failure warnings. (#18425) +- Agents/Tools: make loop detection progress-aware and phased by hard-blocking known `process(action=poll|log)` no-progress loops, warning on generic identical-call repeats, warning + no-progress-blocking ping-pong alternation loops (10/20), coalescing repeated warning spam into threshold buckets (including canonical ping-pong pairs), adding a global circuit breaker at 30 no-progress repeats, and emitting structured diagnostic `tool.loop` warning/error events for loop actions. (#16808) Thanks @akramcodez and @beca-oc. +- Agents/Hooks: preserve the `before_tool_call` wrapped-marker across abort-signal tool wrapping so the hook runs once per tool call in normal agent sessions. (#16852) Thanks @sreuter. +- Agents/Tests: add `before_message_write` persistence regression coverage for block/mutate behavior (including synthetic tool-result flushes) and thrown-hook fallback persistence. (#18197) Thanks @shakkernerd +- Agents/Tools: scope the `message` tool schema to the active channel so Telegram uses `buttons` and Discord uses `components`. (#18215) Thanks @obviyus. +- Agents/Image tool: replace Anthropic-incompatible union schema with explicit `image` (single) and `images` (multi) parameters, keeping tool schemas `anyOf`/`oneOf`/`allOf`-free while preserving multi-image analysis support. (#18551, #18566) Thanks @aldoeliacim. +- Agents/Models: probe the primary model when its auth-profile cooldown is near expiry (with per-provider throttling), so runs recover from temporary rate limits without staying on fallback models until restart. (#17478) Thanks @PlayerGhost. +- Agents/Failover: classify provider abort stop-reason errors (`Unhandled stop reason: abort`, `stop reason: abort`, `reason: abort`) as timeout-class failures so configured model fallback chains trigger instead of surfacing raw abort failures. (#18618) Thanks @sauerdaniel. +- Models/CLI: sync auth-profiles credentials into agent `auth.json` before registry availability checks so `openclaw models list --all` reports auth correctly for API-key/token providers, normalize provider-id aliases when bridging credentials, and skip expired token mirrors. (#18610, #18615) +- Agents/Context: raise default total bootstrap prompt cap from `24000` to `150000` chars (keeping `bootstrapMaxChars` at `20000`), include total-cap visibility in `/context`, and mark truncation from injected-vs-raw sizes so total-cap clipping is reflected accurately. +- Memory/QMD: scope managed collection names per agent and precreate glob-backed collection directories before registration, preventing cross-agent collection clobbering and startup ENOENT failures in fresh workspaces. (#17194) Thanks @jonathanadams96. +- Cron: preserve per-job schedule-error isolation in post-run maintenance recompute so malformed sibling jobs no longer abort persistence of successful runs. (#17852) Thanks @pierreeurope. +- Gateway/Config: prevent `config.patch` object-array merges from falling back to full-array replacement when some patch entries lack `id`, so partial `agents.list` updates no longer drop unrelated agents. (#17989) Thanks @stakeswky. +- Gateway/Auth: trim whitespace around trusted proxy entries before matching so configured proxies with stray spaces still authorize. (#18084) Thanks @Clawborn. +- Config/Discord: require string IDs in Discord allowlists, keep onboarding inputs string-only, and add doctor repair for numeric entries. (#18220) Thanks @thewilloftheshadow. +- Security/Sessions: create new session transcript JSONL files with user-only (`0o600`) permissions and extend `openclaw security audit --fix` to remediate existing transcript file permissions. +- Sessions/Maintenance: archive transcripts when pruning stale sessions, clean expired media in subdirectories, and purge `.deleted` transcript archives after the prune window to prevent disk leaks. (#18538) +- Infra/Fetch: ensure foreign abort-signal listener cleanup never masks original fetch successes/failures, while still preventing detached-finally unhandled rejection noise in `wrapFetchWithAbortSignal`. Thanks @Jackten. +- Heartbeat: allow suppressing tool error warning payloads during heartbeat runs via a new heartbeat config flag. (#18497) Thanks @thewilloftheshadow. +- Heartbeat: include sender metadata (From/To/Provider) in heartbeat prompts so model context matches the delivery target. (#18532) Thanks @dinakars777. +- Heartbeat/Telegram: strip configured `responsePrefix` before heartbeat ack detection (with boundary-safe matching) so prefixed `HEARTBEAT_OK` replies are correctly suppressed instead of leaking into DMs. (#18602) + +## 2026.2.15 + +### Changes + +- Discord: unlock rich interactive agent prompts with Components v2 (buttons, selects, modals, and attachment-backed file blocks) so for native interaction through Discord. Thanks @thewilloftheshadow. +- Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow. +- Plugins: expose `llm_input` and `llm_output` hook payloads so extensions can observe prompt/input context and model output usage details. (#16724) Thanks @SecondThread. +- Subagents: nested sub-agents (sub-sub-agents) with configurable depth. Set `agents.defaults.subagents.maxSpawnDepth: 2` to allow sub-agents to spawn their own children. Includes `maxChildrenPerAgent` limit (default 5), depth-aware tool policy, and proper announce chain routing. (#14447) Thanks @tyler6204. +- Slack/Discord/Telegram: add per-channel ack reaction overrides (account/channel-level) to support platform-specific emoji formats. (#17092) Thanks @zerone0x. +- Telegram: add `channel_post` inbound support for channel-based bot-to-bot wake/trigger flows, with channel allowlist gating and message/media batching parity. +- Cron/Gateway: add finished-run webhook delivery toggle (`notify`) and dedicated webhook auth token support (`cron.webhookToken`) for outbound cron webhook posts. (#14535) Thanks @advaitpaliwal. +- Channels: deduplicate probe/token resolution base types across core + extensions while preserving per-channel error typing. (#16986) Thanks @iyoda and @thewilloftheshadow. +- Memory: add MMR (Maximal Marginal Relevance) re-ranking for hybrid search diversity. Configurable via `memorySearch.query.hybrid.mmr`. Thanks @rodrigouroz. +- Memory: add opt-in temporal decay for hybrid search scoring, with configurable half-life via `memorySearch.query.hybrid.temporalDecay`. Thanks @rodrigouroz. + +### Fixes + +- Discord: send initial content when creating non-forum threads so `thread-create` content is delivered. (#18117) Thanks @zerone0x. +- Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh. +- Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent. +- Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent. +- Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh. +- Gateway/Security: redact sensitive session/path details from `status` responses for non-admin clients; full details remain available to `operator.admin`. (#8590) Thanks @fr33d3m0n. +- Gateway/Control UI: preserve requested operator scopes for Control UI bypass modes (`allowInsecureAuth` / `dangerouslyDisableDeviceAuth`) when device identity is unavailable, preventing false `missing scope` failures on authenticated LAN/HTTP operator sessions. (#17682) Thanks @leafbird. +- LINE/Security: fail closed on webhook startup when channel token or channel secret is missing, and treat LINE accounts as configured only when both are present. (#17587) Thanks @davidahmann. +- Skills/Security: restrict `download` installer `targetDir` to the per-skill tools directory to prevent arbitrary file writes. Thanks @Adam55A-code. +- Skills/Linux: harden go installer fallback on apt-based systems by handling root/no-sudo environments safely, doing best-effort apt index refresh, and returning actionable errors instead of failing with spawn errors. (#17687) Thanks @mcrolly. +- Web Fetch/Security: cap downloaded response body size before HTML parsing to prevent memory exhaustion from oversized or deeply nested pages. Thanks @xuemian168. +- Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving `passwordFile` path exemptions, preventing accidental redaction of non-secret config values like `maxTokens` and IRC password-file paths. (#16042) Thanks @akramcodez. +- Dev tooling: harden git `pre-commit` hook against option injection from malicious filenames (for example `--force`), preventing accidental staging of ignored files. Thanks @mrthankyou. +- Gateway/Agent: reject malformed `agent:`-prefixed session keys (for example, `agent:main`) in `agent` and `agent.identity.get` instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz. +- Gateway/Chat: harden `chat.send` inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n. +- Gateway/Send: return an actionable error when `send` targets internal-only `webchat`, guiding callers to use `chat.send` or a deliverable channel. (#15703) Thanks @rodrigouroz. +- Gateway/Commands: keep webchat command authorization on the internal `webchat` context instead of inferring another provider from channel allowlists, fixing dropped `/new`/`/status` commands in Control UI when channel allowlists are configured. (#7189) Thanks @karlisbergmanis-lv. +- Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing `script-src 'self'`. Thanks @Adam55A-code. +- Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent. +- Agents/Sandbox: clarify system prompt path guidance so sandbox `bash/exec` uses container paths (for example `/workspace`) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot. +- Agents/Context: apply configured model `contextWindow` overrides after provider discovery so `lookupContextTokens()` honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07. +- Agents/Context: derive `lookupContextTokens()` from auth-available model metadata and keep the smallest discovered context window for duplicate model ids, preventing cross-provider cache collisions from overestimating session context limits. (#17586) Thanks @githabideri and @vignesh07. +- Agents/OpenAI: force `store=true` for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07. +- Memory/FTS: make `buildFtsQuery` Unicode-aware so non-ASCII queries (including CJK) produce keyword tokens instead of falling back to vector-only search. (#17672) Thanks @KinGP5471. +- Auto-reply/Compaction: resolve `memory/YYYY-MM-DD.md` placeholders with timezone-aware runtime dates and append a `Current time:` line to memory-flush turns, preventing wrong-year memory filenames without making the system prompt time-variant. (#17603, #17633) Thanks @nicholaspapadam-wq and @vignesh07. +- Auth/Cooldowns: auto-expire stale auth profile cooldowns when `cooldownUntil` or `disabledUntil` timestamps have passed, and reset `errorCount` so the next transient failure does not immediately escalate to a disproportionately long cooldown. Handles `cooldownUntil` and `disabledUntil` independently. (#3604) Thanks @nabbilkhan. +- Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07. +- Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204. +- Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone. +- Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber. +- Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model. +- Agents/Tools: scope the `message` tool schema to the active channel so Telegram uses `buttons` and Discord uses `components`. (#18215) Thanks @obviyus. +- Telegram: omit `message_thread_id` for DM sends/draft previews and keep forum-topic handling (`id=1` general omitted, non-general kept), preventing DM failures with `400 Bad Request: message thread not found`. (#10942) Thanks @garnetlyx. +- Telegram: replace inbound `` placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023. +- Telegram: retry inbound media `getFile` calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang. +- Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus. +- Discord: preserve channel session continuity when runtime payloads omit `message.channelId` by falling back to event/raw `channel_id` values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as `sessionKey=unknown`. (#17622) Thanks @shakkernerd. +- Discord: dedupe native skill commands by skill name in multi-agent setups to prevent duplicated slash commands with `_2` suffixes. (#17365) Thanks @seewhyme. +- Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu. +- Discord: skip text-based exec approval forwarding in favor of Discord's component-based approval UI. Thanks @thewilloftheshadow. +- Web UI/Agents: hide `BOOTSTRAP.md` in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras. +- Memory/QMD: scope managed collection names per agent and precreate glob-backed collection directories before registration, preventing cross-agent collection clobbering and startup ENOENT failures in fresh workspaces. (#17194) Thanks @jonathanadams96. +- Gateway/Memory: initialize QMD startup sync for every configured agent (not just the default agent), so `memory.qmd.update.onBoot` is effective across multi-agent setups. (#17663) Thanks @HenryLoenwind. +- Auto-reply/WhatsApp/TUI/Web: when a final assistant message is `NO_REPLY` and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show `NO_REPLY` placeholders. (#7010) Thanks @Morrowind-Xie. +- Cron: infer `payload.kind="agentTurn"` for model-only `cron.update` payload patches, so partial agent-turn updates do not fail validation when `kind` is omitted. (#15664) Thanks @rodrigouroz. +- TUI: make searchable-select filtering and highlight rendering ANSI-aware so queries ignore hidden escape codes and no longer corrupt ANSI styling sequences during match highlighting. (#4519) Thanks @bee4come. +- TUI/Windows: coalesce rapid single-line submit bursts in Git Bash into one multiline message as a fallback when bracketed paste is unavailable, preventing pasted multiline text from being split into multiple sends. (#4986) Thanks @adamkane. +- TUI: suppress false `(no output)` placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07. +- TUI: preserve copy-sensitive long tokens (URLs/paths/file-like identifiers) during wrapping and overflow sanitization so wrapped output no longer inserts spaces that corrupt copy/paste values. (#17515, #17466, #17505) Thanks @abe238, @trevorpan, and @JasonCry. +- CLI/Build: make legacy daemon CLI compatibility shim generation tolerant of minimal tsdown daemon export sets, while preserving restart/register compatibility aliases and surfacing explicit errors for unavailable legacy daemon commands. Thanks @vignesh07. + +## 2026.2.14 + +### Changes + +- Telegram: add poll sending via `openclaw message poll` (duration seconds, silent delivery, anonymity controls). (#16209) Thanks @robbyczgw-cla. +- Slack/Discord: add `dmPolicy` + `allowFrom` config aliases for DM access control; legacy `dm.policy` + `dm.allowFrom` keys remain supported and `openclaw doctor --fix` can migrate them. +- Discord: allow exec approval prompts to target channels or both DM+channel via `channels.discord.execApprovals.target`. (#16051) Thanks @leonnardo. - Sandbox: add `sandbox.browser.binds` to configure browser-container bind mounts separately from exec containers. (#16230) Thanks @seheepeak. - Discord: add debug logging for message routing decisions to improve `--debug` tracing. (#16202) Thanks @jayleekr. -- Discord: allow exec approval prompts to target channels or both DM+channel via `channels.discord.execApprovals.target`. (#16051) Thanks @leonnardo. -- Telegram: add poll sending via `openclaw message poll` (duration seconds, silent delivery, anonymity controls). (#16209) Thanks @robbyczgw-cla. +- Agents: add optional `messages.suppressToolErrors` config to hide non-mutating tool-failure warnings from user-facing chat while still surfacing mutating failures. (#16620) Thanks @vai-oro. ### Fixes +- CLI/Installation: fix Docker installation hangs on macOS. (#12972) Thanks @vincentkoc. +- Models: fix antigravity opus 4.6 availability follow-up. (#12845) Thanks @vincentkoc. +- Security/Sessions/Telegram: restrict session tool targeting by default to the current session tree (`tools.sessions.visibility`, default `tree`) with sandbox clamping, and pass configured per-account Telegram webhook secrets in webhook mode when no explicit override is provided. Thanks @aether-ai-agent. +- CLI/Plugins: ensure `openclaw message send` exits after successful delivery across plugin-backed channels so one-shot sends do not hang. (#16491) Thanks @yinghaosang. +- CLI/Plugins: run registered plugin `gateway_stop` hooks before `openclaw message` exits (success and failure paths), so plugin-backed channels can clean up one-shot CLI resources. (#16580) Thanks @gumadeiras. +- WhatsApp: honor per-account `dmPolicy` overrides (account-level settings now take precedence over channel defaults for inbound DMs). (#10082) Thanks @mcaxtr. +- Telegram: when `channels.telegram.commands.native` is `false`, exclude plugin commands from `setMyCommands` menu registration while keeping plugin slash handlers callable. (#15132) Thanks @Glucksberg. +- LINE: return 200 OK for Developers Console "Verify" requests (`{"events":[]}`) without `X-Line-Signature`, while still requiring signatures for real deliveries. (#16582) Thanks @arosstale. +- Cron: deliver text-only output directly when `delivery.to` is set so cron recipients get full output instead of summaries. (#16360) Thanks @thewilloftheshadow. +- Cron/Slack: preserve agent identity (name and icon) when cron jobs deliver outbound messages. (#16242) Thanks @robbyczgw-cla. +- Media: accept `MEDIA:`-prefixed paths (lenient whitespace) when loading outbound media to prevent `ENOENT` for tool-returned local media paths. (#13107) Thanks @mcaxtr. +- Media understanding: treat binary `application/vnd.*`/zip/octet-stream attachments as non-text (while keeping vendor `+json`/`+xml` text-eligible) so Office/ZIP files are not inlined into prompt body text. (#16513) Thanks @rmramsey32. +- Agents: deliver tool result media (screenshots, images, audio) to channels regardless of verbose level. (#11735) Thanks @strelov1. +- Auto-reply/Block streaming: strip leading whitespace from streamed block replies so messages starting with blank lines no longer deliver visible leading empty lines. (#16422) Thanks @mcinteerj. +- Auto-reply/Queue: keep queued followups and overflow summaries when drain attempts fail, then retry delivery instead of dropping messages on transient errors. (#16771) Thanks @mmhzlrj. +- Agents/Image tool: allow workspace-local image paths by including the active workspace directory in local media allowlists, and trust sandbox-validated paths in image loaders to prevent false "not under an allowed directory" rejections. (#15541) +- Agents/Image tool: propagate the effective workspace root into tool wiring so workspace-local image paths are accepted by default when running without an explicit `workspaceDir`. (#16722) - BlueBubbles: include sender identity in group chat envelopes and pass clean message text to the agent prompt, aligning with iMessage/Signal formatting. (#16210) Thanks @zerone0x. -- Security/Node Host: enforce `system.run` rawCommand/argv consistency to prevent allowlist/approval bypass. Thanks @christos-eth. -- Security/Gateway: block `system.execApprovals.*` via `node.invoke` (use `exec.approvals.node.*` instead). Thanks @christos-eth. - CLI: fix lazy core command registration so top-level maintenance commands (`doctor`, `dashboard`, `reset`, `uninstall`) resolve correctly instead of exposing a non-functional `maintenance` placeholder command. -- Security/Agents: scope CLI process cleanup to owned child PIDs to avoid killing unrelated processes on shared hosts. Thanks @aether-ai-agent. -- Security/Agents (macOS): prevent shell injection when writing Claude CLI keychain credentials. (#15924) Thanks @aether-ai-agent. -- Security: fix Chutes manual OAuth login state validation (thanks @aether-ai-agent). (#16058) -- Security/Tlon: harden Urbit URL fetching against SSRF by blocking private/internal hosts by default (opt-in: `channels.tlon.allowPrivateNetwork`). Thanks @p80n-sec. -- Security/Voice Call (Telnyx): require webhook signature verification when receiving inbound events; configs without `telnyx.publicKey` are now rejected unless `skipSignatureVerification` is enabled. Thanks @p80n-sec. -- Security/Discovery: stop treating Bonjour TXT records as authoritative routing (prefer resolved service endpoints) and prevent discovery from overriding stored TLS pins; autoconnect now requires a previously trusted gateway. Thanks @simecek. -- Skills: watch `SKILL.md` only when refreshing skills snapshot to avoid file-descriptor exhaustion in large data trees. (#11325) Thanks @household-bard. -- macOS: hard-limit unkeyed `openclaw://agent` deep links and ignore `deliver` / `to` / `channel` unless a valid unattended key is provided. Thanks @Cillian-Collins. -- Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238. +- CLI/Dashboard: when `gateway.bind=lan`, generate localhost dashboard URLs to satisfy browser secure-context requirements while preserving non-LAN bind behavior. (#16434) Thanks @BinHPdev. +- TUI/Gateway: resolve local gateway target URL from `gateway.bind` mode (tailnet/lan) instead of hardcoded localhost so `openclaw tui` connects when gateway is non-loopback. (#16299) Thanks @cortexuvula. +- TUI: honor explicit `--session ` in `openclaw tui` even when `session.scope` is `global`, so named sessions no longer collapse into shared global history. (#16575) Thanks @cinqu. - TUI: use available terminal width for session name display in searchable select lists. (#16238) Thanks @robbyczgw-cla. -- Security/Voice Call: require valid Twilio webhook signatures even when ngrok free tier loopback compatibility mode is enabled. Thanks @p80n-sec. -- Security/Google Chat: deprecate `users/` allowlists (treat `users/...` as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc. -- Security/Google Chat: reject ambiguous shared-path webhook routing when multiple webhook targets verify successfully (prevents cross-account policy-context misrouting). Thanks @vincentkoc. -- Security/Browser: block cross-origin mutating requests to loopback browser control routes (CSRF hardening). Thanks @vincentkoc. -- Security/Slack: compute command authorization for DM slash commands even when `dmPolicy=open`, preventing unauthorized users from running privileged commands via DM. Thanks @christos-eth. -- Security/Nostr: require loopback source and block cross-origin profile mutation/import attempts. Thanks @vincentkoc. -- Security/Archive: enforce archive extraction entry/size limits to prevent resource exhaustion from high-expansion ZIP/TAR archives. Thanks @vincentkoc. -- Security/Media: reject oversized base64-backed input media before decoding to avoid large allocations. Thanks @vincentkoc. -- Security/Gateway: reject oversized base64 chat attachments before decoding to avoid large allocations. Thanks @vincentkoc. -- Security/Gateway: stop returning raw resolved config values in `skills.status` requirement checks (prevents operator.read clients from reading secrets). Thanks @simecek. -- Security/Zalo: reject ambiguous shared-path webhook routing when multiple webhook targets match the same secret. -- Security/BlueBubbles: reject ambiguous shared-path webhook routing when multiple webhook targets match the same guid/password. -- Security/BlueBubbles: require explicit `mediaLocalRoots` allowlists for local outbound media path reads to prevent local file disclosure. (#16322) Thanks @mbelinky. -- Cron/Slack: preserve agent identity (name and icon) when cron jobs deliver outbound messages. (#16242) Thanks @robbyczgw-cla. +- TUI: preserve in-flight streaming replies when a different run finalizes concurrently (avoid clearing active run or reloading history mid-stream). (#10704) Thanks @axschr73. +- TUI: keep pre-tool streamed text visible when later tool-boundary deltas temporarily omit earlier text blocks. (#6958) Thanks @KrisKind75. +- TUI: sanitize ANSI/control-heavy history text, redact binary-like lines, and split pathological long unbroken tokens before rendering to prevent startup crashes on binary attachment history. (#13007) Thanks @wilkinspoe. +- TUI: harden render-time sanitizer for narrow terminals by chunking moderately long unbroken tokens and adding fast-path sanitization guards to reduce overhead on normal text. (#5355) Thanks @tingxueren. +- TUI: render assistant body text in terminal default foreground (instead of fixed light ANSI color) so contrast remains readable on light themes such as Solarized Light. (#16750) Thanks @paymog. +- TUI/Hooks: pass explicit reset reason (`new` vs `reset`) through `sessions.reset` and emit internal command hooks for gateway-triggered resets so `/new` hook workflows fire in TUI/webchat. +- Gateway/Agent: route bare `/new` and `/reset` through `sessions.reset` before running the fresh-session greeting prompt, so reset commands clear the current session in-place instead of falling through to normal agent runs. (#16732) Thanks @kdotndot and @vignesh07. +- Cron: prevent `cron list`/`cron status` from silently skipping past-due recurring jobs by using maintenance recompute semantics. (#16156) Thanks @zerone0x. - Cron: repair missing/corrupt `nextRunAtMs` for the updated job without globally recomputing unrelated due jobs during `cron update`. (#15750) +- Cron: treat persisted jobs with missing `enabled` as enabled by default across update/list/timer due-path checks, and add regression coverage for missing-`enabled` store records. (#15433) Thanks @eternauta1337. +- Cron: skip missed-job replay on startup for jobs interrupted mid-run (stale `runningAtMs` markers), preventing restart loops for self-restarting jobs such as update tasks. (#16694) Thanks @sbmilburn. +- Heartbeat/Cron: treat cron-tagged queued system events as cron reminders even on interval wakes, so isolated cron announce summaries no longer run under the default heartbeat prompt. (#14947) Thanks @archedark-ada and @vignesh07. - Discord: prefer gateway guild id when logging inbound messages so cached-miss guilds do not appear as `guild=dm`. Thanks @thewilloftheshadow. -- TUI: refactor searchable select list description layout and add regression coverage for ANSI-highlight width bounds. - -## 2026.2.14 - -### Fixes - +- Discord: treat empty per-guild `channels: {}` config maps as no channel allowlist (not deny-all), so `groupPolicy: "open"` guilds without explicit channel entries continue to receive messages. (#16714) Thanks @xqliu. +- Models/CLI: guard `models status` string trimming paths to prevent crashes from malformed non-string config values. (#16395) Thanks @BinHPdev. +- Gateway/Subagents: preserve queued announce items and summary state on delivery errors, retry failed announce drains, and avoid dropping unsent announcements on timeout/failure. (#16729) Thanks @Clawdette-Workspace. +- Gateway/Config: make `config.patch` merge object arrays by `id` (for example `agents.list`) instead of replacing the whole array, so partial agent updates do not silently delete unrelated agents. (#6766) Thanks @lightclient. +- Webchat/Prompts: stop injecting direct-chat `conversation_label` into inbound untrusted metadata context blocks, preventing internal label noise from leaking into visible chat replies. (#16556) Thanks @nberardi. +- Auto-reply/Prompts: include trusted inbound `message_id`, `chat_id`, `reply_to_id`, and optional `message_id_full` metadata fields so action tools (for example reactions) can target the triggering message without relying on user text. (#17662) Thanks @MaikiMolto. +- Gateway/Sessions: abort active embedded runs and clear queued session work before `sessions.reset`, returning unavailable if the run does not stop in time. (#16576) Thanks @Grynn. +- Sessions/Agents: harden transcript path resolution for mismatched agent context by preserving explicit store roots and adding safe absolute-path fallback to the correct agent sessions directory. (#16288) Thanks @robbyczgw-cla. +- Agents: add a safety timeout around embedded `session.compact()` to ensure stalled compaction runs settle and release blocked session lanes. (#16331) Thanks @BinHPdev. +- Agents/Tools: make required-parameter validation errors list missing fields and instruct: "Supply correct parameters before retrying," reducing repeated invalid tool-call loops (for example `read({})`). (#14729) +- Agents: keep unresolved mutating tool failures visible until the same action retry succeeds, scope mutation-error surfacing to mutating calls (including `session_status` model changes), and dedupe duplicate failure warnings in outbound replies. (#16131) Thanks @Swader. +- Agents/Process/Bootstrap: preserve unbounded `process log` offset-only pagination (default tail applies only when both `offset` and `limit` are omitted) and enforce strict `bootstrapTotalMaxChars` budgeting across injected bootstrap content (including markers), skipping additional injection when remaining budget is too small. (#16539) Thanks @CharlieGreenman. +- Agents/Workspace: persist bootstrap onboarding state so partially initialized workspaces recover missing `BOOTSTRAP.md` once, while completed onboarding keeps BOOTSTRAP deleted even if runtime files are later recreated. Thanks @gumadeiras. +- Agents/Workspace: create `BOOTSTRAP.md` when core workspace files are seeded in partially initialized workspaces, while keeping BOOTSTRAP one-shot after onboarding deletion. (#16457) Thanks @robbyczgw-cla. +- Agents: classify external timeout aborts during compaction the same as internal timeouts, preventing unnecessary auth-profile rotation and preserving compaction-timeout snapshot fallback behavior. (#9855) Thanks @mverrilli. +- Agents: treat empty-stream provider failures (`request ended without sending any chunks`) as timeout-class failover signals, enabling auth-profile rotation/fallback and showing a friendly timeout message instead of raw provider errors. (#10210) Thanks @zenchantlive. +- Agents: treat `read` tool `file_path` arguments as valid in tool-start diagnostics to avoid false “read tool called without path” warnings when alias parameters are used. (#16717) Thanks @Stache73. +- Agents/Transcript: drop malformed tool-call blocks with blank required fields (`id`/`name` or missing `input`/`arguments`) during session transcript repair to prevent persistent tool-call corruption on future turns. (#15485) Thanks @mike-zachariades. +- Tools/Write/Edit: normalize structured text-block arguments for `content`/`oldText`/`newText` before filesystem edits, preventing JSON-like file corruption and false “exact text not found” misses from block-form params. (#16778) Thanks @danielpipernz. +- Ollama/Agents: avoid forcing `` tag enforcement for Ollama models, which could suppress all output as `(no output)`. (#16191) Thanks @Glucksberg. +- Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238. +- Agents/Process: supervise PTY/child process lifecycles with explicit ownership, cancellation, timeouts, and deterministic cleanup, preventing Codex/Pi PTY sessions from dying or stalling on resume. (#14257) Thanks @onutc. +- Skills: watch `SKILL.md` only when refreshing skills snapshot to avoid file-descriptor exhaustion in large data trees. (#11325) Thanks @household-bard. +- Memory/QMD: make `memory status` read-only by skipping QMD boot update/embed side effects for status-only manager checks. +- Memory/QMD: keep original QMD failures when builtin fallback initialization fails (for example missing embedding API keys), instead of replacing them with fallback init errors. +- Memory/Builtin: keep `memory status` dirty reporting stable across invocations by deriving status-only manager dirty state from persisted index metadata instead of process-start defaults. (#10863) Thanks @BarryYangi. +- Memory/QMD: cap QMD command output buffering to prevent memory exhaustion from pathological `qmd` command output. +- Memory/QMD: parse qmd scope keys once per request to avoid repeated parsing in scope checks. +- Memory/QMD: query QMD index using exact docid matches before falling back to prefix lookup for better recall correctness and index efficiency. +- Memory/QMD: pass result limits to `search`/`vsearch` commands so QMD can cap results earlier. +- Memory/QMD: avoid reading full markdown files when a `from/lines` window is requested in QMD reads. +- Memory/QMD: skip rewriting unchanged session export markdown files during sync to reduce disk churn. +- Memory/QMD: make QMD result JSON parsing resilient to noisy command output by extracting the first JSON array from noisy `stdout`. +- Memory/QMD: treat prefixed `no results found` marker output as an empty result set in qmd JSON parsing. (#11302) Thanks @blazerui. +- Memory/QMD: avoid multi-collection `query` ranking corruption by running one `qmd query -c ` per managed collection and merging by best score (also used for `search`/`vsearch` fallback-to-query). (#16740) Thanks @volarian-vai. +- Memory/QMD: rebind managed collections when existing collection metadata drifts (including sessions name-only listings), preventing non-default agents from reusing another agent's `sessions` collection path. (#17194) Thanks @jonathanadams96. +- Memory/QMD: make `openclaw memory index` verify and print the active QMD index file path/size, and fail when QMD leaves a missing or zero-byte index artifact after an update. (#16775) Thanks @Shunamxiao. +- Memory/QMD: detect null-byte `ENOTDIR` update failures, rebuild managed collections once, and retry update to self-heal corrupted collection metadata. (#12919) Thanks @jorgejhms. +- Memory/QMD/Security: add `rawKeyPrefix` support for QMD scope rules and preserve legacy `keyPrefix: "agent:..."` matching, preventing scoped deny bypass when operators match agent-prefixed session keys. +- Memory/Builtin: narrow memory watcher targets to markdown globs and ignore dependency/venv directories to reduce file-descriptor pressure during memory sync startup. (#11721) Thanks @rex05ai. +- Security/Memory-LanceDB: treat recalled memories as untrusted context (escape injected memory text + explicit non-instruction framing), skip likely prompt-injection payloads during auto-capture, and restrict auto-capture to user messages to reduce memory-poisoning risk. (#12524) Thanks @davidschmid24. +- Security/Memory-LanceDB: require explicit `autoCapture: true` opt-in (default is now disabled) to prevent automatic PII capture unless operators intentionally enable it. (#12552) Thanks @fr33d3m0n. +- Diagnostics/Memory: prune stale diagnostic session state entries and cap tracked session states to prevent unbounded in-memory growth on long-running gateways. (#5136) Thanks @coygeek and @vignesh07. +- Gateway/Memory: clean up `agentRunSeq` tracking on run completion/abort and enforce maintenance-time cap pruning to prevent unbounded sequence-map growth over long uptimes. (#6036) Thanks @coygeek and @vignesh07. +- Auto-reply/Memory: bound `ABORT_MEMORY` growth by evicting oldest entries and deleting reset (`false`) flags so abort state tracking cannot grow unbounded over long uptimes. (#6629) Thanks @coygeek and @vignesh07. +- Slack/Memory: bound thread-starter cache growth with TTL + max-size pruning to prevent long-running Slack gateways from accumulating unbounded thread cache state. (#5258) Thanks @coygeek and @vignesh07. +- Outbound/Memory: bound directory cache growth with max-size eviction and proactive TTL pruning to prevent long-running gateways from accumulating unbounded directory entries. (#5140) Thanks @coygeek and @vignesh07. +- Skills/Memory: remove disconnected nodes from remote-skills cache to prevent stale node metadata from accumulating over long uptimes. (#6760) Thanks @coygeek. +- Sandbox/Tools: make sandbox file tools bind-mount aware (including absolute container paths) and enforce read-only bind semantics for writes. (#16379) Thanks @tasaankaeris. +- Sandbox/Prompts: show the sandbox container workdir as the prompt working directory and clarify host-path usage for file tools, preventing host-path `exec` failures in sandbox sessions. (#16790) Thanks @carrotRakko. +- Media/Security: allow local media reads from OpenClaw state `workspace/` and `sandboxes/` roots by default so generated workspace media can be delivered without unsafe global path bypasses. (#15541) Thanks @lanceji. +- Media/Security: harden local media allowlist bypasses by requiring an explicit `readFile` override when callers mark paths as validated, and reject filesystem-root `localRoots` entries. (#16739) +- Media/Security: allow outbound local media reads from the active agent workspace (including `workspace-`) via agent-scoped local roots, avoiding broad global allowlisting of all per-agent workspaces. (#17136) Thanks @MisterGuy420. +- Outbound/Media: thread explicit `agentId` through core `sendMessage` direct-delivery path so agent-scoped local media roots apply even when mirror metadata is absent. (#17268) Thanks @gumadeiras. +- Discord/Security: harden voice message media loading (SSRF + allowed-local-root checks) so tool-supplied paths/URLs cannot be used to probe internal URLs or read arbitrary local files. +- Security/BlueBubbles: require explicit `mediaLocalRoots` allowlists for local outbound media path reads to prevent local file disclosure. (#16322) Thanks @mbelinky. +- Security/BlueBubbles: reject ambiguous shared-path webhook routing when multiple webhook targets match the same guid/password. +- Security/BlueBubbles: harden BlueBubbles webhook auth behind reverse proxies by only accepting passwordless webhooks for direct localhost loopback requests (forwarded/proxied requests now require a password). Thanks @simecek. - Feishu/Security: harden media URL fetching against SSRF and local file disclosure. (#16285) Thanks @mbelinky. -- Telegram/Security: require numeric Telegram sender IDs for allowlist authorization (reject `@username` principals), auto-resolve `@username` to IDs in `openclaw doctor --fix` (when possible), and warn in `openclaw security audit` when legacy configs contain usernames. Thanks @vincentkoc. -- Security/Skills: harden archive extraction for download-installed skills to prevent path traversal outside the target directory. Thanks @markmusson. -- Security/Media: stream and bound URL-backed input media fetches to prevent memory exhaustion from oversized responses. Thanks @vincentkoc. +- Security/Zalo: reject ambiguous shared-path webhook routing when multiple webhook targets match the same secret. +- Security/Nostr: require loopback source and block cross-origin profile mutation/import attempts. Thanks @vincentkoc. - Security/Signal: harden signal-cli archive extraction during install to prevent path traversal outside the install root. - Security/Hooks: restrict hook transform modules to `~/.openclaw/hooks/transforms` (prevents path traversal/escape module loads via config). Config note: `hooks.transformsDir` must now be within that directory. Thanks @akhmittra. - Security/Hooks: ignore hook package manifest entries that point outside the package directory (prevents out-of-tree handler loads during hook discovery). -- Ollama/Agents: avoid forcing `` tag enforcement for Ollama models, which could suppress all output as `(no output)`. (#16191) Thanks @Glucksberg. +- Security/Archive: enforce archive extraction entry/size limits to prevent resource exhaustion from high-expansion ZIP/TAR archives. Thanks @vincentkoc. +- Security/Media: reject oversized base64-backed input media before decoding to avoid large allocations. Thanks @vincentkoc. +- Security/Media: stream and bound URL-backed input media fetches to prevent memory exhaustion from oversized responses. Thanks @vincentkoc. +- Security/Skills: harden archive extraction for download-installed skills to prevent path traversal outside the target directory. Thanks @markmusson. +- Security/Slack: compute command authorization for DM slash commands even when `dmPolicy=open`, preventing unauthorized users from running privileged commands via DM. Thanks @christos-eth. +- Security/Pairing: scope pairing allowlist writes/reads to channel accounts (for example `telegram:yy`), and propagate account-aware pairing approvals so multi-account channels do not share a single per-channel pairing allowFrom store. (#17631) Thanks @crazytan. +- Security/iMessage: keep DM pairing-store identities out of group allowlist authorization (prevents cross-context command authorization). Thanks @vincentkoc. +- Security/Google Chat: deprecate `users/` allowlists (treat `users/...` as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc. +- Security/Google Chat: reject ambiguous shared-path webhook routing when multiple webhook targets verify successfully (prevents cross-account policy-context misrouting). Thanks @vincentkoc. +- Telegram/Security: require numeric Telegram sender IDs for allowlist authorization (reject `@username` principals), auto-resolve `@username` to IDs in `openclaw doctor --fix` (when possible), and warn in `openclaw security audit` when legacy configs contain usernames. Thanks @vincentkoc. +- Telegram/Security: reject Telegram webhook startup when `webhookSecret` is missing or empty (prevents unauthenticated webhook request forgery). Thanks @yueyueL. +- Security/Windows: avoid shell invocation when spawning child processes to prevent cmd.exe metacharacter injection via untrusted CLI arguments (e.g. agent prompt text). +- Telegram: set webhook callback timeout handling to `onTimeout: "return"` (10s) so long-running update processing no longer emits webhook 500s and retry storms. (#16763) Thanks @chansearrington. +- Signal: preserve case-sensitive `group:` target IDs during normalization so mixed-case group IDs no longer fail with `Group not found`. (#16748) Thanks @repfigit. +- Security/Agents: scope CLI process cleanup to owned child PIDs to avoid killing unrelated processes on shared hosts. Thanks @aether-ai-agent. +- Security/Agents: enforce workspace-root path bounds for `apply_patch` in non-sandbox mode to block traversal and symlink escape writes. Thanks @p80n-sec. +- Security/Agents: enforce symlink-escape checks for `apply_patch` delete hunks under `workspaceOnly`, while still allowing deleting the symlink itself. Thanks @p80n-sec. +- Security/Agents (macOS): prevent shell injection when writing Claude CLI keychain credentials. (#15924) Thanks @aether-ai-agent. +- macOS: hard-limit unkeyed `openclaw://agent` deep links and ignore `deliver` / `to` / `channel` unless a valid unattended key is provided. Thanks @Cillian-Collins. +- Scripts/Security: validate GitHub logins and avoid shell invocation in `scripts/update-clawtributors.ts` to prevent command injection via malicious commit records. Thanks @scanleale. +- Security: fix Chutes manual OAuth login state validation by requiring the full redirect URL (reject code-only pastes) (thanks @aether-ai-agent). +- Security/Gateway: harden tool-supplied `gatewayUrl` overrides by restricting them to loopback or the configured `gateway.remote.url`. Thanks @p80n-sec. +- Security/Gateway: block `system.execApprovals.*` via `node.invoke` (use `exec.approvals.node.*` instead). Thanks @christos-eth. +- Security/Gateway: reject oversized base64 chat attachments before decoding to avoid large allocations. Thanks @vincentkoc. +- Security/Gateway: stop returning raw resolved config values in `skills.status` requirement checks (prevents operator.read clients from reading secrets). Thanks @simecek. +- Security/Net: fix SSRF guard bypass via full-form IPv4-mapped IPv6 literals (blocks loopback/private/metadata access). Thanks @yueyueL. +- Security/Browser: harden browser control file upload + download helpers to prevent path traversal / local file disclosure. Thanks @1seal. +- Security/Browser: block cross-origin mutating requests to loopback browser control routes (CSRF hardening). Thanks @vincentkoc. +- Security/Node Host: enforce `system.run` rawCommand/argv consistency to prevent allowlist/approval bypass. Thanks @christos-eth. +- Security/Exec approvals: prevent safeBins allowlist bypass via shell expansion (host exec allowlist mode only; not enabled by default). Thanks @christos-eth. +- Security/Exec: harden PATH handling by disabling project-local `node_modules/.bin` bootstrapping by default, disallowing node-host `PATH` overrides, and spawning ACP servers via the current executable by default. Thanks @akhmittra. +- Security/Tlon: harden Urbit URL fetching against SSRF by blocking private/internal hosts by default (opt-in: `channels.tlon.allowPrivateNetwork`). Thanks @p80n-sec. +- Security/Voice Call (Telnyx): require webhook signature verification when receiving inbound events; configs without `telnyx.publicKey` are now rejected unless `skipSignatureVerification` is enabled. Thanks @p80n-sec. +- Security/Voice Call: require valid Twilio webhook signatures even when ngrok free tier loopback compatibility mode is enabled. Thanks @p80n-sec. +- Security/Discovery: stop treating Bonjour TXT records as authoritative routing (prefer resolved service endpoints) and prevent discovery from overriding stored TLS pins; autoconnect now requires a previously trusted gateway. Thanks @simecek. ## 2026.2.13 @@ -82,6 +504,7 @@ Docs: https://docs.openclaw.ai - Docs/Hooks: update hooks documentation URLs to the new `/automation/hooks` location. (#16165) Thanks @nicholascyh. - Security/Audit: warn when `gateway.tools.allow` re-enables default-denied tools over HTTP `POST /tools/invoke`, since this can increase RCE blast radius if the gateway is reachable. - Security/Plugins/Hooks: harden npm-based installs by restricting specs to registry packages only, passing `--ignore-scripts` to `npm pack`, and cleaning up temp install directories. +- Security/Sessions: preserve inter-session input provenance for routed prompts so delegated/internal sessions are not treated as direct external user instructions. Thanks @anbecker. - Feishu: stop persistent Typing reaction on NO_REPLY/suppressed runs by wiring reply-dispatcher cleanup to remove typing indicators. (#15464) Thanks @arosstale. - Agents: strip leading empty lines from `sanitizeUserFacingText` output and normalize whitespace-only outputs to empty text. (#16158) Thanks @mcinteerj. - BlueBubbles: gracefully degrade when Private API is disabled by filtering private-only actions, skipping private-only reactions/reply effects, and avoiding private reply markers so non-private flows remain usable. (#16002) Thanks @L-U-C-K-Y. @@ -99,6 +522,7 @@ Docs: https://docs.openclaw.ai - Telegram: scope skill commands to the resolved agent for default accounts so `setMyCommands` no longer triggers `BOT_COMMANDS_TOO_MUCH` when multiple agents are configured. (#15599) - Discord: avoid misrouting numeric guild allowlist entries to `/channels/` by prefixing guild-only inputs with `guild:` during resolution. (#12326) Thanks @headswim. - Memory/QMD: default `memory.qmd.searchMode` to `search` for faster CPU-only recall and always scope `search`/`vsearch` requests to managed collections (auto-falling back to `query` when required). (#16047) Thanks @togotago. +- Memory/LanceDB: add configurable `captureMaxChars` for auto-capture while keeping the legacy 500-char default. (#16641) Thanks @ciberponk. - MS Teams: preserve parsed mention entities/text when appending OneDrive fallback file links, and accept broader real-world Teams mention ID formats (`29:...`, `8:orgid:...`) while still rejecting placeholder patterns. (#15436) Thanks @hyojin. - Media: classify `text/*` MIME types as documents in media-kind routing so text attachments are no longer treated as unknown. (#12237) Thanks @arosstale. - Inbound/Web UI: preserve literal `\n` sequences when normalizing inbound text so Windows paths like `C:\\Work\\nxxx\\README.md` are not corrupted. (#11547) Thanks @mcaxtr. @@ -111,7 +535,7 @@ Docs: https://docs.openclaw.ai - OpenAI Codex/Auth: bridge OpenClaw OAuth profiles into `pi` `auth.json` so model discovery and models-list registry resolution can use Codex OAuth credentials. (#15184) Thanks @loiie45e. - Auth/OpenAI Codex: share OAuth login handling across onboarding and `models auth login --provider openai-codex`, keep onboarding alive when OAuth fails, and surface a direct OAuth help note instead of terminating the wizard. (#15406, follow-up to #14552) Thanks @zhiluo20. - Onboarding/Providers: add vLLM as an onboarding provider with model discovery, auth profile wiring, and non-interactive auth-choice validation. (#12577) Thanks @gejifeng. -- Onboarding/CLI: restore terminal state without resuming paused `stdin`, so onboarding exits cleanly after choosing Web UI and the installer returns instead of appearing stuck. +- Onboarding/CLI: restore terminal state without resuming paused `stdin`, so onboarding exits cleanly (including Docker TTY installs that would otherwise hang). (#12972) Thanks @vincentkoc. - Signal/Install: auto-install `signal-cli` via Homebrew on non-x64 Linux architectures, avoiding x86_64 native binary `Exec format error` failures on arm64/arm hosts. (#15443) Thanks @jogvan-k. - macOS Voice Wake: fix a crash in trigger trimming for CJK/Unicode transcripts by matching and slicing on original-string ranges instead of transformed-string indices. (#11052) Thanks @Flash-LHR. - Mattermost (plugin): retry websocket monitor connections with exponential backoff and abort-aware teardown so transient connect failures no longer permanently stop monitoring. (#14962) Thanks @mcaxtr. @@ -172,7 +596,9 @@ Docs: https://docs.openclaw.ai - Tools/web_search: support `freshness` for the Perplexity provider by mapping `pd`/`pw`/`pm`/`py` to Perplexity `search_recency_filter` values and including freshness in the Perplexity cache key. (#15343) Thanks @echoVic. - Clawdock: avoid Zsh readonly variable collisions in helper scripts. (#15501) Thanks @nkelner. - Memory: switch default local embedding model to the QAT `embeddinggemma-300m-qat-Q8_0` variant for better quality at the same footprint. (#15429) Thanks @azade-c. +- Docs/Discord: expand quick setup and clarify guild workspace guidance. (#20088) Thanks @pejmanjohn, @thewilloftheshadow. - Docs/Mermaid: remove hardcoded Mermaid init theme blocks from four docs diagrams so dark mode inherits readable theme defaults. (#15157) Thanks @heytulsiprasad. +- Security/Pairing: generate 256-bit base64url device and node pairing tokens and use byte-safe constant-time verification to avoid token-compare edge-case failures. (#16535) Thanks @FaizanKolega, @gumadeiras. ## 2026.2.12 @@ -192,6 +618,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Gateway/OpenResponses: harden URL-based `input_file`/`input_image` handling with explicit SSRF deny policy, hostname allowlists (`files.urlAllowlist` / `images.urlAllowlist`), per-request URL input caps (`maxUrlParts`), blocked-fetch audit logging, and regression coverage/docs updates. +- Sessions: guard `withSessionStoreLock` against undefined `storePath` to prevent `path.dirname` crash. (#14717) - Security: fix unauthenticated Nostr profile API remote config tampering. (#13719) Thanks @coygeek. - Security: remove bundled soul-evil hook. (#14757) Thanks @Imccccc. - Security/Audit: add hook session-routing hardening checks (`hooks.defaultSessionKey`, `hooks.allowRequestSessionKey`, and prefix allowlists), and warn when HTTP API endpoints allow explicit session-key routing. @@ -209,6 +636,7 @@ Docs: https://docs.openclaw.ai - Configure/Gateway: reject literal `"undefined"`/`"null"` token input and validate gateway password prompt values to avoid invalid password-mode configs. (#13767) Thanks @omair445. - Gateway: handle async `EPIPE` on stdout/stderr during shutdown. (#13414) Thanks @keshav55. - Gateway/Control UI: resolve missing dashboard assets when `openclaw` is installed globally via symlink-based Node managers (nvm/fnm/n/Homebrew). (#14919) Thanks @aynorica. +- Gateway/Control UI: keep partial assistant output visible when runs are aborted, and persist aborted partials to session transcripts for follow-up context. - Cron: use requested `agentId` for isolated job auth resolution. (#13983) Thanks @0xRaini. - Cron: prevent cron jobs from skipping execution when `nextRunAtMs` advances. (#14068) Thanks @WalterSumbon. - Cron: pass `agentId` to `runHeartbeatOnce` for main-session jobs. (#14140) Thanks @ishikawa-pro. @@ -238,6 +666,7 @@ Docs: https://docs.openclaw.ai - Browser: add Chrome launch flag `--disable-blink-features=AutomationControlled` to reduce `navigator.webdriver` automation detection issues on reCAPTCHA-protected sites. (#10735) Thanks @Milofax. - Heartbeat: filter noise-only system events so scheduled reminder notifications do not fire when cron runs carry only heartbeat markers. (#13317) Thanks @pvtclawn. - Signal: render mention placeholders as `@uuid`/`@phone` so mention gating and Clawdbot targeting work. (#2013) Thanks @alexgleason. +- Agents/Reminders: guard reminder promises by appending a note when no `cron.add` succeeded in the turn, so users know nothing was scheduled. (#18588) Thanks @vignesh07. - Discord: omit empty content fields for media-only messages while preserving caption whitespace. (#9507) Thanks @leszekszpunar. - Onboarding/Providers: add Z.AI endpoint-specific auth choices (`zai-coding-global`, `zai-coding-cn`, `zai-global`, `zai-cn`) and expand default Z.AI model wiring. (#13456) Thanks @tomsun28. - Onboarding/Providers: update MiniMax API default/recommended models from M2.1 to M2.5, add M2.5/M2.5-Lightning model entries, and include `minimax-m2.5` in modern model filtering. (#14865) Thanks @adao-max. @@ -254,13 +683,6 @@ Docs: https://docs.openclaw.ai - Media: strip `MEDIA:` lines with local paths instead of leaking as visible text. (#14399) Thanks @0xRaini. - Config/Cron: exclude `maxTokens` from config redaction and honor `deleteAfterRun` on skipped cron jobs. (#13342) Thanks @niceysam. - Config: ignore `meta` field changes in config file watcher. (#13460) Thanks @brandonwise. -- Cron: use requested `agentId` for isolated job auth resolution. (#13983) Thanks @0xRaini. -- Cron: pass `agentId` to `runHeartbeatOnce` for main-session jobs. (#14140) Thanks @ishikawa-pro. -- Cron: prevent cron jobs from skipping execution when `nextRunAtMs` advances. (#14068) Thanks @WalterSumbon. -- Cron: re-arm timers when `onTimer` fires while a job is still executing. (#14233) Thanks @tomron87. -- Cron: prevent duplicate fires when multiple jobs trigger simultaneously. (#14256) Thanks @xinhuagu. -- Cron: isolate scheduler errors so one bad job does not break all jobs. (#14385) Thanks @MarvinDontPanic. -- Cron: prevent one-shot `at` jobs from re-firing on restart after skipped/errored runs. (#13878) Thanks @lailoo. - Daemon: suppress `EPIPE` error when restarting LaunchAgent. (#14343) Thanks @0xRaini. - Antigravity: add opus 4.6 forward-compat model and bypass thinking signature sanitization. (#14218) Thanks @jg-noncelogic. - Agents: prevent file descriptor leaks in child process cleanup. (#13565) Thanks @KyleChen26. @@ -281,6 +703,7 @@ Docs: https://docs.openclaw.ai - Commands: add `commands.allowFrom` config for separate command authorization, allowing operators to restrict slash commands to specific users while keeping chat open to others. (#12430) Thanks @thewilloftheshadow. - Docker: add ClawDock shell helpers for Docker workflows. (#12817) Thanks @Olshansk. +- Gateway: periodic channel health monitor auto-restarts stuck, crashed, or silently-stopped channels. Configurable via `gateway.channelHealthCheckMinutes` (default: 5, set to 0 to disable). (#7053, #4302) - iOS: alpha node app + setup-code onboarding. (#11756) Thanks @mbelinky. - Channels: comprehensive BlueBubbles and channel cleanup. (#11093) Thanks @tyler6204. - Channels: IRC first-class channel support. (#11482) Thanks @vignesh07. @@ -344,6 +767,7 @@ Docs: https://docs.openclaw.ai - Thinking: allow xhigh for `github-copilot/gpt-5.2-codex` and `github-copilot/gpt-5.2`. (#11646) Thanks @LatencyTDH. - Thinking: honor `/think off` for reasoning-capable models. (#9564) Thanks @liuy. - Discord: support forum/media thread-create starter messages, wire `message thread create --message`, and harden routing. (#10062) Thanks @jarvis89757. +- Discord: download attachments from forwarded messages. (#17049) Thanks @pip-nomel, @thewilloftheshadow. - Paths: structurally resolve `OPENCLAW_HOME`-derived home paths and fix Windows drive-letter handling in tool meta shortening. (#12125) Thanks @mcaxtr. - Memory: set Voyage embeddings `input_type` for improved retrieval. (#10818) Thanks @mcinteerj. - Memory: disable async batch embeddings by default for memory indexing (opt-in via `agents.defaults.memorySearch.remote.batch.enabled`). (#13069) Thanks @mcinteerj. @@ -504,23 +928,17 @@ Docs: https://docs.openclaw.ai - Telegram: recover from grammY long-poll timed out errors. (#7466) Thanks @macmimi23. - Media understanding: skip binary media from file text extraction. (#7475) Thanks @AlexZhangji. - Security: enforce access-group gating for Slack slash commands when channel type lookup fails. -- Security: require validated shared-secret auth before skipping device identity on gateway connect. +- Security: require validated shared-secret auth before skipping device identity on gateway connect. Thanks @simecek. - Security: guard skill installer downloads with SSRF checks (block private/localhost URLs). +- Security/Gateway: require `operator.approvals` for in-chat `/approve` when invoked from gateway clients. Thanks @yueyueL. - Security: harden Windows exec allowlist; block cmd.exe bypass via single &. Thanks @simecek. - Discord: route autoThread replies to existing threads instead of the root channel. (#8302) Thanks @gavinbmoore, @thewilloftheshadow. - Media understanding: apply SSRF guardrails to provider fetches; allow private baseUrl overrides explicitly. - fix(voice-call): harden inbound allowlist; reject anonymous callers; require Telnyx publicKey for allowlist; token-gate Twilio media streams; cap webhook body size (thanks @simecek) -- fix(webchat): respect user scroll position during streaming and refresh (#7226) (thanks @marcomarandiz) -- Telegram: recover from grammY long-poll timed out errors. (#7466) Thanks @macmimi23. -- Agents: repair malformed tool calls and session transcripts. (#7473) Thanks @justinhuangcode. -- fix(agents): validate AbortSignal instances before calling AbortSignal.any() (#7277) (thanks @Elarwei001) -- Media understanding: skip binary media from file text extraction. (#7475) Thanks @AlexZhangji. - Onboarding: keep TUI flow exclusive (skip completion prompt + background Web UI seed); completion prompt now handled by install/update. -- TUI: block onboarding output while TUI is active and restore terminal state on exit. - CLI/Zsh completion: cache scripts in state dir and escape option descriptions to avoid invalid option errors. - fix(ui): resolve Control UI asset path correctly. - fix(ui): refresh agent files after external edits. -- Docs: finish renaming the QMD memory docs to reference the OpenClaw state dir. - Tests: stub SSRF DNS pinning in web auto-reply + Gemini video coverage. (#6619) Thanks @joshp123. ## 2026.2.1 @@ -545,7 +963,7 @@ Docs: https://docs.openclaw.ai - Security: guard remote media fetches with SSRF protections (block private/localhost, DNS pinning). - Updates: clean stale global install rename dirs and extend gateway update timeouts to avoid npm ENOTEMPTY failures. -- Plugins: validate plugin/hook install paths and reject traversal-like names. +- Security/Plugins/Hooks: validate install paths and reject traversal-like names (prevents path traversal outside the state dir). Thanks @logicx24. - Telegram: add download timeouts for file fetches. (#6914) Thanks @hclsys. - Telegram: enforce thread specs for DM vs forum sends. (#6833) Thanks @obviyus. - Streaming: flush block streaming on paragraph boundaries for newline chunking. (#7014) @@ -1383,7 +1801,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Heartbeat: tighten prompt guidance + suppress duplicate alerts for 24h. (#980) — thanks @voidserf. - Repo: ignore local identity files to avoid accidental commits. (#1001) — thanks @gerardward2007. - Sessions/Security: add `session.dmScope` for multi-user DM isolation and audit warnings. (#948) — thanks @Alphonse-arianee. -- Plugins: add provider auth registry + `openclaw models auth login` for plugin-driven OAuth/API key flows. - Onboarding: switch channels setup to a single-select loop with per-channel actions and disabled hints in the picker. - TUI: show provider/model labels for the active session and default model. - Heartbeat: add per-agent heartbeat configuration and multi-agent docs example. @@ -1805,6 +2222,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Tests/Agents: add regression coverage for workspace tool path resolution and bash cwd defaults. - iOS/Android: enable stricter concurrency/lint checks; fix Swift 6 strict concurrency issues + Android lint errors (ExifInterface, obsolete SDK check). (#662) — thanks @KristijanJovanovski. - Auth: read Codex CLI keychain tokens on macOS before falling back to `~/.codex/auth.json`, preventing stale refresh tokens from breaking gateway live tests. +- Security/Exec approvals: reject shell command substitution (`$()` and backticks) inside double quotes to prevent exec allowlist bypass when exec allowlist mode is explicitly enabled (the default configuration does not use this mode). Thanks @simecek. - iOS/macOS: share `AsyncTimeout`, require explicit `bridgeStableID` on connect, and harden tool display defaults (avoids missing-resource label fallbacks). - Telegram: serialize media-group processing to avoid missed albums under load. - Signal: handle `dataMessage.reaction` events (signal-cli SSE) to avoid broken attachment errors. (#637) — thanks @neist. @@ -1918,7 +2336,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Skills additions (Himalaya email, CodexBar, 1Password). - Dependency refreshes (pi-\* stack, Slack SDK, discord-api-types, file-type, zod, Biome, Vite). -- Refactors: centralized group allowlist/mention policy; lint/import cleanup; switch tsx → bun for TS execution. ## 2026.1.5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5e9164a94d51..eddb13348eec6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,7 @@ Welcome to the lobster tank! 🦞 ## Quick Links - **GitHub:** https://github.com/openclaw/openclaw +- **Vision:** [`VISION.md`](VISION.md) - **Discord:** https://discord.gg/qkhbAGHRBT - **X/Twitter:** [@steipete](https://x.com/steipete) / [@openclaw](https://x.com/openclaw) @@ -13,24 +14,33 @@ Welcome to the lobster tank! 🦞 - **Peter Steinberger** - Benevolent Dictator - GitHub: [@steipete](https://github.com/steipete) · X: [@steipete](https://x.com/steipete) -- **Shadow** - Discord + Slack subsystem +- **Shadow** - Discord subsystem, Discord admin, Clawhub, all community moderation - GitHub: [@thewilloftheshadow](https://github.com/thewilloftheshadow) · X: [@4shad0wed](https://x.com/4shad0wed) -- **Vignesh** - Memory (QMD), formal modeling, TUI, and Lobster +- **Vignesh** - Memory (QMD), formal modeling, TUI, IRC, and Lobster - GitHub: [@vignesh07](https://github.com/vignesh07) · X: [@\_vgnsh](https://x.com/_vgnsh) - **Jos** - Telegram, API, Nix mode - GitHub: [@joshp123](https://github.com/joshp123) · X: [@jjpcodes](https://x.com/jjpcodes) +- **Ayaan Zaidi** - Telegram subsystem, iOS app + - GitHub: [@obviyus](https://github.com/obviyus) · X: [@0bviyus](https://x.com/0bviyus) + +- **Tyler Yust** - Agents/subagents, cron, BlueBubbles, macOS app + - GitHub: [@tyler6204](https://github.com/tyler6204) · X: [@tyleryust](https://x.com/tyleryust) + +- **Mariano Belinky** - iOS app, Security + - GitHub: [@mbelinky](https://github.com/mbelinky) · X: [@belimad](https://x.com/belimad) + +- **Seb Slight** - Docs, Agent Reliability, Runtime Hardening + - GitHub: [@sebslight](https://github.com/sebslight) · X: [@sebslig](https://x.com/sebslig) + - **Christoph Nakazawa** - JS Infra - GitHub: [@cpojer](https://github.com/cpojer) · X: [@cnakazawa](https://x.com/cnakazawa) - **Gustavo Madeira Santana** - Multi-agents, CLI, web UI - GitHub: [@gumadeiras](https://github.com/gumadeiras) · X: [@gumadeiras](https://x.com/gumadeiras) -- **Maximilian Nussbaumer** - DevOps, CI, Code Sanity - - GitHub: [@quotentiroler](https://github.com/quotentiroler) · X: [@quotentiroler](https://x.com/quotentiroler) - ## How to Contribute 1. **Bugs & small fixes** → Open a PR! @@ -42,7 +52,7 @@ Welcome to the lobster tank! 🦞 - Test locally with your OpenClaw instance - Run tests: `pnpm build && pnpm check && pnpm test` - Ensure CI checks pass -- Keep PRs focused (one thing per PR) +- Keep PRs focused (one thing per PR; do not mix unrelated concerns) - Describe what & why ## Control UI Decorators @@ -84,6 +94,26 @@ We are currently prioritizing: Check the [GitHub Issues](https://github.com/openclaw/openclaw/issues) for "good first issue" labels! +## Maintainers + +We're selectively expanding the maintainer team. +If you're an experienced contributor who wants to help shape OpenClaw's direction — whether through code, docs, or community — we'd like to hear from you. + +Being a maintainer is a responsibility, not an honorary title. We expect active, consistent involvement — triaging issues, reviewing PRs, and helping move the project forward. + +Still interested? Email contributing@openclaw.ai with: + +- Links to your PRs on OpenClaw (if you don't have any, start there first) +- Links to open source projects you maintain or actively contribute to +- Your GitHub, Discord, and X/Twitter handles +- A brief intro: background, experience, and areas of interest +- Languages you speak and where you're based +- How much time you can realistically commit + +We welcome people across all skill sets — engineering, documentation, community management, and more. +We review every human-only-written application carefully and add maintainers slowly and deliberately. +Please allow a few weeks for a response. + ## Report a Vulnerability We take security reports seriously. Report vulnerabilities directly to the repository where the issue lives: diff --git a/Dockerfile b/Dockerfile index 716ab2099f736..2ead5c51fcd3a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,19 @@ COPY scripts ./scripts RUN pnpm install --frozen-lockfile +# Optionally install Chromium and Xvfb for browser automation. +# Build with: docker build --build-arg OPENCLAW_INSTALL_BROWSER=1 ... +# Adds ~300MB but eliminates the 60-90s Playwright install on every container start. +# Must run after pnpm install so playwright-core is available in node_modules. +ARG OPENCLAW_INSTALL_BROWSER="" +RUN if [ -n "$OPENCLAW_INSTALL_BROWSER" ]; then \ + apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb && \ + node /app/node_modules/playwright-core/cli.js install --with-deps chromium && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \ + fi + COPY . . RUN pnpm build # Force pnpm for UI build (Bun may fail on ARM/Synology architectures) diff --git a/Dockerfile.sandbox-common b/Dockerfile.sandbox-common new file mode 100644 index 0000000000000..71f80070adf0c --- /dev/null +++ b/Dockerfile.sandbox-common @@ -0,0 +1,45 @@ +ARG BASE_IMAGE=openclaw-sandbox:bookworm-slim +FROM ${BASE_IMAGE} + +USER root + +ENV DEBIAN_FRONTEND=noninteractive + +ARG PACKAGES="curl wget jq coreutils grep nodejs npm python3 git ca-certificates golang-go rustc cargo unzip pkg-config libasound2-dev build-essential file" +ARG INSTALL_PNPM=1 +ARG INSTALL_BUN=1 +ARG BUN_INSTALL_DIR=/opt/bun +ARG INSTALL_BREW=1 +ARG BREW_INSTALL_DIR=/home/linuxbrew/.linuxbrew +ARG FINAL_USER=sandbox + +ENV BUN_INSTALL=${BUN_INSTALL_DIR} +ENV HOMEBREW_PREFIX=${BREW_INSTALL_DIR} +ENV HOMEBREW_CELLAR=${BREW_INSTALL_DIR}/Cellar +ENV HOMEBREW_REPOSITORY=${BREW_INSTALL_DIR}/Homebrew +ENV PATH=${BUN_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/sbin:${PATH} + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ${PACKAGES} \ + && rm -rf /var/lib/apt/lists/* + +RUN if [ "${INSTALL_PNPM}" = "1" ]; then npm install -g pnpm; fi + +RUN if [ "${INSTALL_BUN}" = "1" ]; then \ + curl -fsSL https://bun.sh/install | bash; \ + ln -sf "${BUN_INSTALL_DIR}/bin/bun" /usr/local/bin/bun; \ +fi + +RUN if [ "${INSTALL_BREW}" = "1" ]; then \ + if ! id -u linuxbrew >/dev/null 2>&1; then useradd -m -s /bin/bash linuxbrew; fi; \ + mkdir -p "${BREW_INSTALL_DIR}"; \ + chown -R linuxbrew:linuxbrew "$(dirname "${BREW_INSTALL_DIR}")"; \ + su - linuxbrew -c "NONINTERACTIVE=1 CI=1 /bin/bash -c '$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)'"; \ + if [ ! -e "${BREW_INSTALL_DIR}/Library" ]; then ln -s "${BREW_INSTALL_DIR}/Homebrew/Library" "${BREW_INSTALL_DIR}/Library"; fi; \ + if [ ! -x "${BREW_INSTALL_DIR}/bin/brew" ]; then echo \"brew install failed\"; exit 1; fi; \ + ln -sf "${BREW_INSTALL_DIR}/bin/brew" /usr/local/bin/brew; \ +fi + +# Default is sandbox, but allow BASE_IMAGE overrides to select another final user. +USER ${FINAL_USER} + diff --git a/README.md b/README.md index b1a3b407a0ea8..fe96768c95fb1 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ It answers you on the channels you already use (WhatsApp, Telegram, Slack, Disco If you want a personal, single-user assistant that feels local, fast, and always-on, this is it. -[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/start/faq) · [Wizard](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd) +[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/start/faq) · [Wizard](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd) Preferred setup: run the onboarding wizard (`openclaw onboard`) in your terminal. The wizard guides you step by step through setting up the gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**. @@ -112,9 +112,9 @@ Full security guide: [Security](https://docs.openclaw.ai/gateway/security) Default behavior on Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Google Chat/Slack: -- **DM pairing** (`dmPolicy="pairing"` / `channels.discord.dm.policy="pairing"` / `channels.slack.dm.policy="pairing"`): unknown senders receive a short pairing code and the bot does not process their message. +- **DM pairing** (`dmPolicy="pairing"` / `channels.discord.dmPolicy="pairing"` / `channels.slack.dmPolicy="pairing"`; legacy: `channels.discord.dm.policy`, `channels.slack.dm.policy`): unknown senders receive a short pairing code and the bot does not process their message. - Approve with: `openclaw pairing approve ` (then the sender is added to a local allowlist store). -- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.dm.allowFrom` / `channels.slack.dm.allowFrom`). +- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.allowFrom` / `channels.slack.allowFrom`; legacy: `channels.discord.dm.allowFrom`, `channels.slack.dm.allowFrom`). Run `openclaw doctor` to surface risky/misconfigured DM policies. @@ -360,7 +360,7 @@ Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker ### [Discord](https://docs.openclaw.ai/channels/discord) - Set `DISCORD_BOT_TOKEN` or `channels.discord.token` (env wins). -- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.dm.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed. +- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed. ```json5 { @@ -546,4 +546,5 @@ Thanks to all clawtributors: 0xJonHoldsCrypto aaronn Alphonse-arianee atalovesyou Azade carlulsoe ddyo Erik jiulingyun latitudeki5223 Manuel Maly minghinmatthewlam Mourad Boustani odrobnik pcty-nextgen-ios-builder Quentin rafaelreis-r Randy Torres rhjoh Rolf Fredheim ronak-guliani William Stock + Akash Kobal

diff --git a/SECURITY.md b/SECURITY.md index f4ccac868cc7a..d02b9fb80103a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -39,18 +39,36 @@ Reports without reproduction steps, demonstrated impact, and remediation advice OpenClaw is a labor of love. There is no bug bounty program and no budget for paid reports. Please still disclose responsibly so we can fix issues quickly. The best way to help the project right now is by sending PRs. +## Maintainers: GHSA Updates via CLI + +When patching a GHSA via `gh api`, include `X-GitHub-Api-Version: 2022-11-28` (or newer). Without it, some fields (notably CVSS) may not persist even if the request returns 200. + ## Out of Scope - Public Internet Exposure - Using OpenClaw in ways that the docs recommend not to - Prompt injection attacks +## Plugin Trust Boundary + +Plugins/extensions are loaded **in-process** with the Gateway and are treated as trusted code. + +- Plugins can execute with the same OS privileges as the OpenClaw process. +- Runtime helpers (for example `runtime.system.runCommandWithTimeout`) are convenience APIs, not a sandbox boundary. +- Only install plugins you trust, and prefer `plugins.allow` to pin explicit trusted plugin ids. + ## Operational Guidance For threat model + hardening guidance (including `openclaw security audit --deep` and `--fix`), see: - `https://docs.openclaw.ai/gateway/security` +### Tool filesystem hardening + +- `tools.exec.applyPatch.workspaceOnly: true` (recommended): keeps `apply_patch` writes/deletes within the configured workspace directory. +- `tools.fs.workspaceOnly: true` (optional): restricts `read`/`write`/`edit`/`apply_patch` paths to the workspace directory. +- Avoid setting `tools.exec.applyPatch.workspaceOnly: false` unless you fully trust who can trigger tool execution. + ### Web Interface Safety OpenClaw's web interface (Gateway Control UI + HTTP endpoints) is intended for **local use only**. @@ -58,6 +76,10 @@ OpenClaw's web interface (Gateway Control UI + HTTP endpoints) is intended for * - Recommended: keep the Gateway **loopback-only** (`127.0.0.1` / `::1`). - Config: `gateway.bind="loopback"` (default). - CLI: `openclaw gateway run --bind loopback`. +- Canvas host note: network-visible canvas is **intentional** for trusted node scenarios (LAN/tailnet). + - Expected setup: non-loopback bind + Gateway auth (token/password/trusted-proxy) + firewall/tailnet controls. + - Expected routes: `/__openclaw__/canvas/`, `/__openclaw__/a2ui/`. + - This deployment model alone is not a security vulnerability. - Do **not** expose it to the public internet (no direct bind to `0.0.0.0`, no public reverse proxy). It is not hardened for public exposure. - If you need remote access, prefer an SSH tunnel or Tailscale serve/funnel (so the Gateway still binds to loopback), plus strong Gateway auth. - The Gateway HTTP surface includes the canvas host (`/__openclaw__/canvas/`, `/__openclaw__/a2ui/`). Treat canvas content as sensitive/untrusted and avoid exposing it beyond loopback unless you understand the risk. diff --git a/VISION.md b/VISION.md new file mode 100644 index 0000000000000..4ff70189ab892 --- /dev/null +++ b/VISION.md @@ -0,0 +1,110 @@ +## OpenClaw Vision + +OpenClaw is the AI that actually does things. +It runs on your devices, in your channels, with your rules. + +This document explains the current state and direction of the project. +We are still early, so iteration is fast. +Project overview and developer docs: [`README.md`](README.md) +Contribution guide: [`CONTRIBUTING.md`](CONTRIBUTING.md) + +OpenClaw started as a personal playground to learn AI and build something genuinely useful: +an assistant that can run real tasks on a real computer. +It evolved through several names and shells: Warelay -> Clawdbot -> Moltbot -> OpenClaw. + +The goal: a personal assistant that is easy to use, supports a wide range of platforms, and respects privacy and security. + +The current focus is: + +Priority: + +- Security and safe defaults +- Bug fixes and stability +- Setup reliability and first-run UX + +Next priorities: + +- Supporting all major model providers +- Improving support for major messaging channels (and adding a few high-demand ones) +- Performance and test infrastructure +- Better computer-use and agent harness capabilities +- Ergonomics across CLI and web frontend +- Companion apps on macOS, iOS, Android, Windows, and Linux + +Contribution rules: + +- One PR = one issue/topic. Do not bundle multiple unrelated fixes/features. +- PRs over ~5,000 changed lines are reviewed only in exceptional circumstances. +- Do not open large batches of tiny PRs at once; each PR has review cost. +- For very small related fixes, grouping into one focused PR is encouraged. + +## Security + +Security in OpenClaw is a deliberate tradeoff: strong defaults without killing capability. +The goal is to stay powerful for real work while making risky paths explicit and operator-controlled. + +Canonical security policy and reporting: + +- [`SECURITY.md`](SECURITY.md) + +We prioritize secure defaults, but also expose clear knobs for trusted high-power workflows. + +## Plugins & Memory + +OpenClaw has an extensive plugin API. +Core stays lean; optional capability should usually ship as plugins. + +Preferred plugin path is npm package distribution plus local extension loading for development. +If you build a plugin, host and maintain it in your own repository. +The bar for adding optional plugins to core is intentionally high. +Plugin docs: [`docs/tools/plugin.md`](docs/tools/plugin.md) +Community plugin listing + PR bar: https://docs.openclaw.ai/plugins/community + +Memory is a special plugin slot where only one memory plugin can be active at a time. +Today we ship multiple memory options; over time we plan to converge on one recommended default path. + +### Skills + +We still ship some bundled skills for baseline UX. +New skills should be published to ClawHub first (`clawhub.ai`), not added to core by default. +Core skill additions should be rare and require a strong product or security reason. + +### MCP Support + +OpenClaw supports MCP through `mcporter`: https://github.com/steipete/mcporter + +This keeps MCP integration flexible and decoupled from core runtime: + +- add or change MCP servers without restarting the gateway +- keep core tool/context surface lean +- reduce MCP churn impact on core stability and security + +For now, we prefer this bridge model over building first-class MCP runtime into core. +If there is an MCP server or feature `mcporter` does not support yet, please open an issue there. + +### Setup + +OpenClaw is currently terminal-first by design. +This keeps setup explicit: users see docs, auth, permissions, and security posture up front. + +Long term, we want easier onboarding flows as hardening matures. +We do not want convenience wrappers that hide critical security decisions from users. + +### Why TypeScript? + +OpenClaw is primarily an orchestration system: prompts, tools, protocols, and integrations. +TypeScript was chosen to keep OpenClaw hackable by default. +It is widely known, fast to iterate in, and easy to read, modify, and extend. + +## What We Will Not Merge (For Now) + +- New core skills when they can live on ClawHub +- Full-doc translation sets for all docs (deferred; we plan AI-generated translations later) +- Commercial service integrations that do not clearly fit the model-provider category +- Wrapper channels around already supported channels without a clear capability or security gap +- First-class MCP runtime in core when `mcporter` already provides the integration path +- Agent-hierarchy frameworks (manager-of-managers / nested planner trees) as a default architecture +- Heavy orchestration layers that duplicate existing agent and tool infrastructure + +This list is a roadmap guardrail, not a law of physics. +Strong user demand and strong technical rationale can change it. diff --git a/appcast.xml b/appcast.xml index 469e66f994c60..3318fbaf86b61 100644 --- a/appcast.xml +++ b/appcast.xml @@ -2,6 +2,212 @@ OpenClaw + + 2026.2.14 + Sun, 15 Feb 2026 04:24:34 +0100 + https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml + 202602140 + 2026.2.14 + 15.0 + OpenClaw 2026.2.14 +

Changes

+
    +
  • Telegram: add poll sending via openclaw message poll (duration seconds, silent delivery, anonymity controls). (#16209) Thanks @robbyczgw-cla.
  • +
  • Slack/Discord: add dmPolicy + allowFrom config aliases for DM access control; legacy dm.policy + dm.allowFrom keys remain supported and openclaw doctor --fix can migrate them.
  • +
  • Discord: allow exec approval prompts to target channels or both DM+channel via channels.discord.execApprovals.target. (#16051) Thanks @leonnardo.
  • +
  • Sandbox: add sandbox.browser.binds to configure browser-container bind mounts separately from exec containers. (#16230) Thanks @seheepeak.
  • +
  • Discord: add debug logging for message routing decisions to improve --debug tracing. (#16202) Thanks @jayleekr.
  • +
+

Fixes

+
    +
  • CLI/Plugins: ensure openclaw message send exits after successful delivery across plugin-backed channels so one-shot sends do not hang. (#16491) Thanks @yinghaosang.
  • +
  • CLI/Plugins: run registered plugin gateway_stop hooks before openclaw message exits (success and failure paths), so plugin-backed channels can clean up one-shot CLI resources. (#16580) Thanks @gumadeiras.
  • +
  • WhatsApp: honor per-account dmPolicy overrides (account-level settings now take precedence over channel defaults for inbound DMs). (#10082) Thanks @mcaxtr.
  • +
  • Telegram: when channels.telegram.commands.native is false, exclude plugin commands from setMyCommands menu registration while keeping plugin slash handlers callable. (#15132) Thanks @Glucksberg.
  • +
  • LINE: return 200 OK for Developers Console "Verify" requests ({"events":[]}) without X-Line-Signature, while still requiring signatures for real deliveries. (#16582) Thanks @arosstale.
  • +
  • Cron: deliver text-only output directly when delivery.to is set so cron recipients get full output instead of summaries. (#16360) Thanks @thewilloftheshadow.
  • +
  • Cron/Slack: preserve agent identity (name and icon) when cron jobs deliver outbound messages. (#16242) Thanks @robbyczgw-cla.
  • +
  • Media: accept MEDIA:-prefixed paths (lenient whitespace) when loading outbound media to prevent ENOENT for tool-returned local media paths. (#13107) Thanks @mcaxtr.
  • +
  • Agents: deliver tool result media (screenshots, images, audio) to channels regardless of verbose level. (#11735) Thanks @strelov1.
  • +
  • Agents/Image tool: allow workspace-local image paths by including the active workspace directory in local media allowlists, and trust sandbox-validated paths in image loaders to prevent false "not under an allowed directory" rejections. (#15541)
  • +
  • Agents/Image tool: propagate the effective workspace root into tool wiring so workspace-local image paths are accepted by default when running without an explicit workspaceDir. (#16722)
  • +
  • BlueBubbles: include sender identity in group chat envelopes and pass clean message text to the agent prompt, aligning with iMessage/Signal formatting. (#16210) Thanks @zerone0x.
  • +
  • CLI: fix lazy core command registration so top-level maintenance commands (doctor, dashboard, reset, uninstall) resolve correctly instead of exposing a non-functional maintenance placeholder command.
  • +
  • CLI/Dashboard: when gateway.bind=lan, generate localhost dashboard URLs to satisfy browser secure-context requirements while preserving non-LAN bind behavior. (#16434) Thanks @BinHPdev.
  • +
  • TUI/Gateway: resolve local gateway target URL from gateway.bind mode (tailnet/lan) instead of hardcoded localhost so openclaw tui connects when gateway is non-loopback. (#16299) Thanks @cortexuvula.
  • +
  • TUI: honor explicit --session in openclaw tui even when session.scope is global, so named sessions no longer collapse into shared global history. (#16575) Thanks @cinqu.
  • +
  • TUI: use available terminal width for session name display in searchable select lists. (#16238) Thanks @robbyczgw-cla.
  • +
  • TUI: refactor searchable select list description layout and add regression coverage for ANSI-highlight width bounds.
  • +
  • TUI: preserve in-flight streaming replies when a different run finalizes concurrently (avoid clearing active run or reloading history mid-stream). (#10704) Thanks @axschr73.
  • +
  • TUI: keep pre-tool streamed text visible when later tool-boundary deltas temporarily omit earlier text blocks. (#6958) Thanks @KrisKind75.
  • +
  • TUI: sanitize ANSI/control-heavy history text, redact binary-like lines, and split pathological long unbroken tokens before rendering to prevent startup crashes on binary attachment history. (#13007) Thanks @wilkinspoe.
  • +
  • TUI: harden render-time sanitizer for narrow terminals by chunking moderately long unbroken tokens and adding fast-path sanitization guards to reduce overhead on normal text. (#5355) Thanks @tingxueren.
  • +
  • TUI: render assistant body text in terminal default foreground (instead of fixed light ANSI color) so contrast remains readable on light themes such as Solarized Light. (#16750) Thanks @paymog.
  • +
  • TUI/Hooks: pass explicit reset reason (new vs reset) through sessions.reset and emit internal command hooks for gateway-triggered resets so /new hook workflows fire in TUI/webchat.
  • +
  • Cron: prevent cron list/cron status from silently skipping past-due recurring jobs by using maintenance recompute semantics. (#16156) Thanks @zerone0x.
  • +
  • Cron: repair missing/corrupt nextRunAtMs for the updated job without globally recomputing unrelated due jobs during cron update. (#15750)
  • +
  • Cron: skip missed-job replay on startup for jobs interrupted mid-run (stale runningAtMs markers), preventing restart loops for self-restarting jobs such as update tasks. (#16694) Thanks @sbmilburn.
  • +
  • Discord: prefer gateway guild id when logging inbound messages so cached-miss guilds do not appear as guild=dm. Thanks @thewilloftheshadow.
  • +
  • Discord: treat empty per-guild channels: {} config maps as no channel allowlist (not deny-all), so groupPolicy: "open" guilds without explicit channel entries continue to receive messages. (#16714) Thanks @xqliu.
  • +
  • Models/CLI: guard models status string trimming paths to prevent crashes from malformed non-string config values. (#16395) Thanks @BinHPdev.
  • +
  • Gateway/Subagents: preserve queued announce items and summary state on delivery errors, retry failed announce drains, and avoid dropping unsent announcements on timeout/failure. (#16729) Thanks @Clawdette-Workspace.
  • +
  • Gateway/Sessions: abort active embedded runs and clear queued session work before sessions.reset, returning unavailable if the run does not stop in time. (#16576) Thanks @Grynn.
  • +
  • Sessions/Agents: harden transcript path resolution for mismatched agent context by preserving explicit store roots and adding safe absolute-path fallback to the correct agent sessions directory. (#16288) Thanks @robbyczgw-cla.
  • +
  • Agents: add a safety timeout around embedded session.compact() to ensure stalled compaction runs settle and release blocked session lanes. (#16331) Thanks @BinHPdev.
  • +
  • Agents: keep unresolved mutating tool failures visible until the same action retry succeeds, scope mutation-error surfacing to mutating calls (including session_status model changes), and dedupe duplicate failure warnings in outbound replies. (#16131) Thanks @Swader.
  • +
  • Agents/Process/Bootstrap: preserve unbounded process log offset-only pagination (default tail applies only when both offset and limit are omitted) and enforce strict bootstrapTotalMaxChars budgeting across injected bootstrap content (including markers), skipping additional injection when remaining budget is too small. (#16539) Thanks @CharlieGreenman.
  • +
  • Agents/Workspace: persist bootstrap onboarding state so partially initialized workspaces recover missing BOOTSTRAP.md once, while completed onboarding keeps BOOTSTRAP deleted even if runtime files are later recreated. Thanks @gumadeiras.
  • +
  • Agents/Workspace: create BOOTSTRAP.md when core workspace files are seeded in partially initialized workspaces, while keeping BOOTSTRAP one-shot after onboarding deletion. (#16457) Thanks @robbyczgw-cla.
  • +
  • Agents: classify external timeout aborts during compaction the same as internal timeouts, preventing unnecessary auth-profile rotation and preserving compaction-timeout snapshot fallback behavior. (#9855) Thanks @mverrilli.
  • +
  • Agents: treat empty-stream provider failures (request ended without sending any chunks) as timeout-class failover signals, enabling auth-profile rotation/fallback and showing a friendly timeout message instead of raw provider errors. (#10210) Thanks @zenchantlive.
  • +
  • Agents: treat read tool file_path arguments as valid in tool-start diagnostics to avoid false “read tool called without path” warnings when alias parameters are used. (#16717) Thanks @Stache73.
  • +
  • Ollama/Agents: avoid forcing tag enforcement for Ollama models, which could suppress all output as (no output). (#16191) Thanks @Glucksberg.
  • +
  • Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238.
  • +
  • Skills: watch SKILL.md only when refreshing skills snapshot to avoid file-descriptor exhaustion in large data trees. (#11325) Thanks @household-bard.
  • +
  • Memory/QMD: make memory status read-only by skipping QMD boot update/embed side effects for status-only manager checks.
  • +
  • Memory/QMD: keep original QMD failures when builtin fallback initialization fails (for example missing embedding API keys), instead of replacing them with fallback init errors.
  • +
  • Memory/Builtin: keep memory status dirty reporting stable across invocations by deriving status-only manager dirty state from persisted index metadata instead of process-start defaults. (#10863) Thanks @BarryYangi.
  • +
  • Memory/QMD: cap QMD command output buffering to prevent memory exhaustion from pathological qmd command output.
  • +
  • Memory/QMD: parse qmd scope keys once per request to avoid repeated parsing in scope checks.
  • +
  • Memory/QMD: query QMD index using exact docid matches before falling back to prefix lookup for better recall correctness and index efficiency.
  • +
  • Memory/QMD: pass result limits to search/vsearch commands so QMD can cap results earlier.
  • +
  • Memory/QMD: avoid reading full markdown files when a from/lines window is requested in QMD reads.
  • +
  • Memory/QMD: skip rewriting unchanged session export markdown files during sync to reduce disk churn.
  • +
  • Memory/QMD: make QMD result JSON parsing resilient to noisy command output by extracting the first JSON array from noisy stdout.
  • +
  • Memory/QMD: treat prefixed no results found marker output as an empty result set in qmd JSON parsing. (#11302) Thanks @blazerui.
  • +
  • Memory/QMD: avoid multi-collection query ranking corruption by running one qmd query -c per managed collection and merging by best score (also used for search/vsearch fallback-to-query). (#16740) Thanks @volarian-vai.
  • +
  • Memory/QMD: detect null-byte ENOTDIR update failures, rebuild managed collections once, and retry update to self-heal corrupted collection metadata. (#12919) Thanks @jorgejhms.
  • +
  • Memory/QMD/Security: add rawKeyPrefix support for QMD scope rules and preserve legacy keyPrefix: "agent:..." matching, preventing scoped deny bypass when operators match agent-prefixed session keys.
  • +
  • Memory/Builtin: narrow memory watcher targets to markdown globs and ignore dependency/venv directories to reduce file-descriptor pressure during memory sync startup. (#11721) Thanks @rex05ai.
  • +
  • Security/Memory-LanceDB: treat recalled memories as untrusted context (escape injected memory text + explicit non-instruction framing), skip likely prompt-injection payloads during auto-capture, and restrict auto-capture to user messages to reduce memory-poisoning risk. (#12524) Thanks @davidschmid24.
  • +
  • Security/Memory-LanceDB: require explicit autoCapture: true opt-in (default is now disabled) to prevent automatic PII capture unless operators intentionally enable it. (#12552) Thanks @fr33d3m0n.
  • +
  • Diagnostics/Memory: prune stale diagnostic session state entries and cap tracked session states to prevent unbounded in-memory growth on long-running gateways. (#5136) Thanks @coygeek and @vignesh07.
  • +
  • Gateway/Memory: clean up agentRunSeq tracking on run completion/abort and enforce maintenance-time cap pruning to prevent unbounded sequence-map growth over long uptimes. (#6036) Thanks @coygeek and @vignesh07.
  • +
  • Auto-reply/Memory: bound ABORT_MEMORY growth by evicting oldest entries and deleting reset (false) flags so abort state tracking cannot grow unbounded over long uptimes. (#6629) Thanks @coygeek and @vignesh07.
  • +
  • Slack/Memory: bound thread-starter cache growth with TTL + max-size pruning to prevent long-running Slack gateways from accumulating unbounded thread cache state. (#5258) Thanks @coygeek and @vignesh07.
  • +
  • Outbound/Memory: bound directory cache growth with max-size eviction and proactive TTL pruning to prevent long-running gateways from accumulating unbounded directory entries. (#5140) Thanks @coygeek and @vignesh07.
  • +
  • Skills/Memory: remove disconnected nodes from remote-skills cache to prevent stale node metadata from accumulating over long uptimes. (#6760) Thanks @coygeek.
  • +
  • Sandbox/Tools: make sandbox file tools bind-mount aware (including absolute container paths) and enforce read-only bind semantics for writes. (#16379) Thanks @tasaankaeris.
  • +
  • Media/Security: allow local media reads from OpenClaw state workspace/ and sandboxes/ roots by default so generated workspace media can be delivered without unsafe global path bypasses. (#15541) Thanks @lanceji.
  • +
  • Media/Security: harden local media allowlist bypasses by requiring an explicit readFile override when callers mark paths as validated, and reject filesystem-root localRoots entries. (#16739)
  • +
  • Discord/Security: harden voice message media loading (SSRF + allowed-local-root checks) so tool-supplied paths/URLs cannot be used to probe internal URLs or read arbitrary local files.
  • +
  • Security/BlueBubbles: require explicit mediaLocalRoots allowlists for local outbound media path reads to prevent local file disclosure. (#16322) Thanks @mbelinky.
  • +
  • Security/BlueBubbles: reject ambiguous shared-path webhook routing when multiple webhook targets match the same guid/password.
  • +
  • Security/BlueBubbles: harden BlueBubbles webhook auth behind reverse proxies by only accepting passwordless webhooks for direct localhost loopback requests (forwarded/proxied requests now require a password). Thanks @simecek.
  • +
  • Feishu/Security: harden media URL fetching against SSRF and local file disclosure. (#16285) Thanks @mbelinky.
  • +
  • Security/Zalo: reject ambiguous shared-path webhook routing when multiple webhook targets match the same secret.
  • +
  • Security/Nostr: require loopback source and block cross-origin profile mutation/import attempts. Thanks @vincentkoc.
  • +
  • Security/Signal: harden signal-cli archive extraction during install to prevent path traversal outside the install root.
  • +
  • Security/Hooks: restrict hook transform modules to ~/.openclaw/hooks/transforms (prevents path traversal/escape module loads via config). Config note: hooks.transformsDir must now be within that directory. Thanks @akhmittra.
  • +
  • Security/Hooks: ignore hook package manifest entries that point outside the package directory (prevents out-of-tree handler loads during hook discovery).
  • +
  • Security/Archive: enforce archive extraction entry/size limits to prevent resource exhaustion from high-expansion ZIP/TAR archives. Thanks @vincentkoc.
  • +
  • Security/Media: reject oversized base64-backed input media before decoding to avoid large allocations. Thanks @vincentkoc.
  • +
  • Security/Media: stream and bound URL-backed input media fetches to prevent memory exhaustion from oversized responses. Thanks @vincentkoc.
  • +
  • Security/Skills: harden archive extraction for download-installed skills to prevent path traversal outside the target directory. Thanks @markmusson.
  • +
  • Security/Slack: compute command authorization for DM slash commands even when dmPolicy=open, preventing unauthorized users from running privileged commands via DM. Thanks @christos-eth.
  • +
  • Security/iMessage: keep DM pairing-store identities out of group allowlist authorization (prevents cross-context command authorization). Thanks @vincentkoc.
  • +
  • Security/Google Chat: deprecate users/ allowlists (treat users/... as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc.
  • +
  • Security/Google Chat: reject ambiguous shared-path webhook routing when multiple webhook targets verify successfully (prevents cross-account policy-context misrouting). Thanks @vincentkoc.
  • +
  • Telegram/Security: require numeric Telegram sender IDs for allowlist authorization (reject @username principals), auto-resolve @username to IDs in openclaw doctor --fix (when possible), and warn in openclaw security audit when legacy configs contain usernames. Thanks @vincentkoc.
  • +
  • Telegram/Security: reject Telegram webhook startup when webhookSecret is missing or empty (prevents unauthenticated webhook request forgery). Thanks @yueyueL.
  • +
  • Security/Windows: avoid shell invocation when spawning child processes to prevent cmd.exe metacharacter injection via untrusted CLI arguments (e.g. agent prompt text).
  • +
  • Telegram: set webhook callback timeout handling to onTimeout: "return" (10s) so long-running update processing no longer emits webhook 500s and retry storms. (#16763) Thanks @chansearrington.
  • +
  • Signal: preserve case-sensitive group: target IDs during normalization so mixed-case group IDs no longer fail with Group not found. (#16748) Thanks @repfigit.
  • +
  • Feishu/Security: harden media URL fetching against SSRF and local file disclosure. (#16285) Thanks @mbelinky.
  • +
  • Security/Agents: scope CLI process cleanup to owned child PIDs to avoid killing unrelated processes on shared hosts. Thanks @aether-ai-agent.
  • +
  • Security/Agents: enforce workspace-root path bounds for apply_patch in non-sandbox mode to block traversal and symlink escape writes. Thanks @p80n-sec.
  • +
  • Security/Agents: enforce symlink-escape checks for apply_patch delete hunks under workspaceOnly, while still allowing deleting the symlink itself. Thanks @p80n-sec.
  • +
  • Security/Agents (macOS): prevent shell injection when writing Claude CLI keychain credentials. (#15924) Thanks @aether-ai-agent.
  • +
  • macOS: hard-limit unkeyed openclaw://agent deep links and ignore deliver / to / channel unless a valid unattended key is provided. Thanks @Cillian-Collins.
  • +
  • Scripts/Security: validate GitHub logins and avoid shell invocation in scripts/update-clawtributors.ts to prevent command injection via malicious commit records. Thanks @scanleale.
  • +
  • Security: fix Chutes manual OAuth login state validation by requiring the full redirect URL (reject code-only pastes) (thanks @aether-ai-agent).
  • +
  • Security/Gateway: harden tool-supplied gatewayUrl overrides by restricting them to loopback or the configured gateway.remote.url. Thanks @p80n-sec.
  • +
  • Security/Gateway: block system.execApprovals.* via node.invoke (use exec.approvals.node.* instead). Thanks @christos-eth.
  • +
  • Security/Gateway: reject oversized base64 chat attachments before decoding to avoid large allocations. Thanks @vincentkoc.
  • +
  • Security/Gateway: stop returning raw resolved config values in skills.status requirement checks (prevents operator.read clients from reading secrets). Thanks @simecek.
  • +
  • Security/Net: fix SSRF guard bypass via full-form IPv4-mapped IPv6 literals (blocks loopback/private/metadata access). Thanks @yueyueL.
  • +
  • Security/Browser: harden browser control file upload + download helpers to prevent path traversal / local file disclosure. Thanks @1seal.
  • +
  • Security/Browser: block cross-origin mutating requests to loopback browser control routes (CSRF hardening). Thanks @vincentkoc.
  • +
  • Security/Node Host: enforce system.run rawCommand/argv consistency to prevent allowlist/approval bypass. Thanks @christos-eth.
  • +
  • Security/Exec approvals: prevent safeBins allowlist bypass via shell expansion (host exec allowlist mode only; not enabled by default). Thanks @christos-eth.
  • +
  • Security/Exec: harden PATH handling by disabling project-local node_modules/.bin bootstrapping by default, disallowing node-host PATH overrides, and spawning ACP servers via the current executable by default. Thanks @akhmittra.
  • +
  • Security/Tlon: harden Urbit URL fetching against SSRF by blocking private/internal hosts by default (opt-in: channels.tlon.allowPrivateNetwork). Thanks @p80n-sec.
  • +
  • Security/Voice Call (Telnyx): require webhook signature verification when receiving inbound events; configs without telnyx.publicKey are now rejected unless skipSignatureVerification is enabled. Thanks @p80n-sec.
  • +
  • Security/Voice Call: require valid Twilio webhook signatures even when ngrok free tier loopback compatibility mode is enabled. Thanks @p80n-sec.
  • +
  • Security/Discovery: stop treating Bonjour TXT records as authoritative routing (prefer resolved service endpoints) and prevent discovery from overriding stored TLS pins; autoconnect now requires a previously trusted gateway. Thanks @simecek.
  • +
+

View full changelog

+]]>
+ +
+ + 2026.2.15 + Mon, 16 Feb 2026 05:04:34 +0100 + https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml + 202602150 + 2026.2.15 + 15.0 + OpenClaw 2026.2.15 +

Changes

+
    +
  • Discord: unlock rich interactive agent prompts with Components v2 (buttons, selects, modals, and attachment-backed file blocks) so for native interaction through Discord. Thanks @thewilloftheshadow.
  • +
  • Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow.
  • +
  • Plugins: expose llm_input and llm_output hook payloads so extensions can observe prompt/input context and model output usage details. (#16724) Thanks @SecondThread.
  • +
  • Subagents: nested sub-agents (sub-sub-agents) with configurable depth. Set agents.defaults.subagents.maxSpawnDepth: 2 to allow sub-agents to spawn their own children. Includes maxChildrenPerAgent limit (default 5), depth-aware tool policy, and proper announce chain routing. (#14447) Thanks @tyler6204.
  • +
  • Slack/Discord/Telegram: add per-channel ack reaction overrides (account/channel-level) to support platform-specific emoji formats. (#17092) Thanks @zerone0x.
  • +
  • Cron/Gateway: add finished-run webhook delivery toggle (notify) and dedicated webhook auth token support (cron.webhookToken) for outbound cron webhook posts. (#14535) Thanks @advaitpaliwal.
  • +
  • Channels: deduplicate probe/token resolution base types across core + extensions while preserving per-channel error typing. (#16986) Thanks @iyoda and @thewilloftheshadow.
  • +
+

Fixes

+
    +
  • Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh.
  • +
  • Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent.
  • +
  • Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent.
  • +
  • Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh.
  • +
  • Gateway/Security: redact sensitive session/path details from status responses for non-admin clients; full details remain available to operator.admin. (#8590) Thanks @fr33d3m0n.
  • +
  • Gateway/Control UI: preserve requested operator scopes for Control UI bypass modes (allowInsecureAuth / dangerouslyDisableDeviceAuth) when device identity is unavailable, preventing false missing scope failures on authenticated LAN/HTTP operator sessions. (#17682) Thanks @leafbird.
  • +
  • LINE/Security: fail closed on webhook startup when channel token or channel secret is missing, and treat LINE accounts as configured only when both are present. (#17587) Thanks @davidahmann.
  • +
  • Skills/Security: restrict download installer targetDir to the per-skill tools directory to prevent arbitrary file writes. Thanks @Adam55A-code.
  • +
  • Skills/Linux: harden go installer fallback on apt-based systems by handling root/no-sudo environments safely, doing best-effort apt index refresh, and returning actionable errors instead of failing with spawn errors. (#17687) Thanks @mcrolly.
  • +
  • Web Fetch/Security: cap downloaded response body size before HTML parsing to prevent memory exhaustion from oversized or deeply nested pages. Thanks @xuemian168.
  • +
  • Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving passwordFile path exemptions, preventing accidental redaction of non-secret config values like maxTokens and IRC password-file paths. (#16042) Thanks @akramcodez.
  • +
  • Dev tooling: harden git pre-commit hook against option injection from malicious filenames (for example --force), preventing accidental staging of ignored files. Thanks @mrthankyou.
  • +
  • Gateway/Agent: reject malformed agent:-prefixed session keys (for example, agent:main) in agent and agent.identity.get instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz.
  • +
  • Gateway/Chat: harden chat.send inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.
  • +
  • Gateway/Send: return an actionable error when send targets internal-only webchat, guiding callers to use chat.send or a deliverable channel. (#15703) Thanks @rodrigouroz.
  • +
  • Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing script-src 'self'. Thanks @Adam55A-code.
  • +
  • Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent.
  • +
  • Agents/Sandbox: clarify system prompt path guidance so sandbox bash/exec uses container paths (for example /workspace) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot.
  • +
  • Agents/Context: apply configured model contextWindow overrides after provider discovery so lookupContextTokens() honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.
  • +
  • Agents/Context: derive lookupContextTokens() from auth-available model metadata and keep the smallest discovered context window for duplicate model ids, preventing cross-provider cache collisions from overestimating session context limits. (#17586) Thanks @githabideri and @vignesh07.
  • +
  • Agents/OpenAI: force store=true for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.
  • +
  • Memory/FTS: make buildFtsQuery Unicode-aware so non-ASCII queries (including CJK) produce keyword tokens instead of falling back to vector-only search. (#17672) Thanks @KinGP5471.
  • +
  • Auto-reply/Compaction: resolve memory/YYYY-MM-DD.md placeholders with timezone-aware runtime dates and append a Current time: line to memory-flush turns, preventing wrong-year memory filenames without making the system prompt time-variant. (#17603, #17633) Thanks @nicholaspapadam-wq and @vignesh07.
  • +
  • Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.
  • +
  • Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204.
  • +
  • Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone.
  • +
  • Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.
  • +
  • Subagents/Models: preserve agents.defaults.model.fallbacks when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.
  • +
  • Telegram: omit message_thread_id for DM sends/draft previews and keep forum-topic handling (id=1 general omitted, non-general kept), preventing DM failures with 400 Bad Request: message thread not found. (#10942) Thanks @garnetlyx.
  • +
  • Telegram: replace inbound placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.
  • +
  • Telegram: retry inbound media getFile calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.
  • +
  • Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.
  • +
  • Discord: preserve channel session continuity when runtime payloads omit message.channelId by falling back to event/raw channel_id values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as sessionKey=unknown. (#17622) Thanks @shakkernerd.
  • +
  • Discord: dedupe native skill commands by skill name in multi-agent setups to prevent duplicated slash commands with _2 suffixes. (#17365) Thanks @seewhyme.
  • +
  • Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu.
  • +
  • Web UI/Agents: hide BOOTSTRAP.md in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.
  • +
  • Auto-reply/WhatsApp/TUI/Web: when a final assistant message is NO_REPLY and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show NO_REPLY placeholders. (#7010) Thanks @Morrowind-Xie.
  • +
  • Cron: infer payload.kind="agentTurn" for model-only cron.update payload patches, so partial agent-turn updates do not fail validation when kind is omitted. (#15664) Thanks @rodrigouroz.
  • +
  • TUI: make searchable-select filtering and highlight rendering ANSI-aware so queries ignore hidden escape codes and no longer corrupt ANSI styling sequences during match highlighting. (#4519) Thanks @bee4come.
  • +
  • TUI/Windows: coalesce rapid single-line submit bursts in Git Bash into one multiline message as a fallback when bracketed paste is unavailable, preventing pasted multiline text from being split into multiple sends. (#4986) Thanks @adamkane.
  • +
  • TUI: suppress false (no output) placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07.
  • +
  • TUI: preserve copy-sensitive long tokens (URLs/paths/file-like identifiers) during wrapping and overflow sanitization so wrapped output no longer inserts spaces that corrupt copy/paste values. (#17515, #17466, #17505) Thanks @abe238, @trevorpan, and @JasonCry.
  • +
  • CLI/Build: make legacy daemon CLI compatibility shim generation tolerant of minimal tsdown daemon export sets, while preserving restart/register compatibility aliases and surfacing explicit errors for unavailable legacy daemon commands. Thanks @vignesh07.
  • +
+

View full changelog

+]]>
+ +
2026.2.13 Sat, 14 Feb 2026 04:30:23 +0100 @@ -103,157 +309,5 @@ ]]> - - 2026.2.12 - Fri, 13 Feb 2026 03:17:54 +0100 - https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml - 9500 - 2026.2.12 - 15.0 - OpenClaw 2026.2.12 -

Changes

-
    -
  • CLI: add openclaw logs --local-time to display log timestamps in local timezone. (#13818) Thanks @xialonglee.
  • -
  • Telegram: render blockquotes as native
    tags instead of stripping them. (#14608)
  • -
  • Config: avoid redacting maxTokens-like fields during config snapshot redaction, preventing round-trip validation failures in /config. (#14006) Thanks @constansino.
  • -
-

Breaking

-
    -
  • Hooks: POST /hooks/agent now rejects payload sessionKey overrides by default. To keep fixed hook context, set hooks.defaultSessionKey (recommended with hooks.allowedSessionKeyPrefixes: ["hook:"]). If you need legacy behavior, explicitly set hooks.allowRequestSessionKey: true. Thanks @alpernae for reporting.
  • -
-

Fixes

-
    -
  • Gateway/OpenResponses: harden URL-based input_file/input_image handling with explicit SSRF deny policy, hostname allowlists (files.urlAllowlist / images.urlAllowlist), per-request URL input caps (maxUrlParts), blocked-fetch audit logging, and regression coverage/docs updates.
  • -
  • Security: fix unauthenticated Nostr profile API remote config tampering. (#13719) Thanks @coygeek.
  • -
  • Security: remove bundled soul-evil hook. (#14757) Thanks @Imccccc.
  • -
  • Security/Audit: add hook session-routing hardening checks (hooks.defaultSessionKey, hooks.allowRequestSessionKey, and prefix allowlists), and warn when HTTP API endpoints allow explicit session-key routing.
  • -
  • Security/Sandbox: confine mirrored skill sync destinations to the sandbox skills/ root and stop using frontmatter-controlled skill names as filesystem destination paths. Thanks @1seal.
  • -
  • Security/Web tools: treat browser/web content as untrusted by default (wrapped outputs for browser snapshot/tabs/console and structured external-content metadata for web tools), and strip toolResult.details from model-facing transcript/compaction inputs to reduce prompt-injection replay risk.
  • -
  • Security/Hooks: harden webhook and device token verification with shared constant-time secret comparison, and add per-client auth-failure throttling for hook endpoints (429 + Retry-After). Thanks @akhmittra.
  • -
  • Security/Browser: require auth for loopback browser control HTTP routes, auto-generate gateway.auth.token when browser control starts without auth, and add a security-audit check for unauthenticated browser control. Thanks @tcusolle.
  • -
  • Sessions/Gateway: harden transcript path resolution and reject unsafe session IDs/file paths so session operations stay within agent sessions directories. Thanks @akhmittra.
  • -
  • Gateway: raise WS payload/buffer limits so 5,000,000-byte image attachments work reliably. (#14486) Thanks @0xRaini.
  • -
  • Logging/CLI: use local timezone timestamps for console prefixing, and include ±HH:MM offsets when using openclaw logs --local-time to avoid ambiguity. (#14771) Thanks @0xRaini.
  • -
  • Gateway: drain active turns before restart to prevent message loss. (#13931) Thanks @0xRaini.
  • -
  • Gateway: auto-generate auth token during install to prevent launchd restart loops. (#13813) Thanks @cathrynlavery.
  • -
  • Gateway: prevent undefined/missing token in auth config. (#13809) Thanks @asklee-klawd.
  • -
  • Gateway: handle async EPIPE on stdout/stderr during shutdown. (#13414) Thanks @keshav55.
  • -
  • Gateway/Control UI: resolve missing dashboard assets when openclaw is installed globally via symlink-based Node managers (nvm/fnm/n/Homebrew). (#14919) Thanks @aynorica.
  • -
  • Cron: use requested agentId for isolated job auth resolution. (#13983) Thanks @0xRaini.
  • -
  • Cron: prevent cron jobs from skipping execution when nextRunAtMs advances. (#14068) Thanks @WalterSumbon.
  • -
  • Cron: pass agentId to runHeartbeatOnce for main-session jobs. (#14140) Thanks @ishikawa-pro.
  • -
  • Cron: re-arm timers when onTimer fires while a job is still executing. (#14233) Thanks @tomron87.
  • -
  • Cron: prevent duplicate fires when multiple jobs trigger simultaneously. (#14256) Thanks @xinhuagu.
  • -
  • Cron: isolate scheduler errors so one bad job does not break all jobs. (#14385) Thanks @MarvinDontPanic.
  • -
  • Cron: prevent one-shot at jobs from re-firing on restart after skipped/errored runs. (#13878) Thanks @lailoo.
  • -
  • Heartbeat: prevent scheduler stalls on unexpected run errors and avoid immediate rerun loops after requests-in-flight skips. (#14901) Thanks @joeykrug.
  • -
  • Cron: honor stored session model overrides for isolated-agent runs while preserving hooks.gmail.model precedence for Gmail hook sessions. (#14983) Thanks @shtse8.
  • -
  • Logging/Browser: fall back to os.tmpdir()/openclaw for default log, browser trace, and browser download temp paths when /tmp/openclaw is unavailable.
  • -
  • WhatsApp: convert Markdown bold/strikethrough to WhatsApp formatting. (#14285) Thanks @Raikan10.
  • -
  • WhatsApp: allow media-only sends and normalize leading blank payloads. (#14408) Thanks @karimnaguib.
  • -
  • WhatsApp: default MIME type for voice messages when Baileys omits it. (#14444) Thanks @mcaxtr.
  • -
  • Telegram: handle no-text message in model picker editMessageText. (#14397) Thanks @0xRaini.
  • -
  • Telegram: surface REACTION_INVALID as non-fatal warning. (#14340) Thanks @0xRaini.
  • -
  • BlueBubbles: fix webhook auth bypass via loopback proxy trust. (#13787) Thanks @coygeek.
  • -
  • Slack: change default replyToMode from "off" to "all". (#14364) Thanks @nm-de.
  • -
  • Slack: detect control commands when channel messages start with bot mention prefixes (for example, @Bot /new). (#14142) Thanks @beefiker.
  • -
  • Signal: enforce E.164 validation for the Signal bot account prompt so mistyped numbers are caught early. (#15063) Thanks @Duartemartins.
  • -
  • Discord: process DM reactions instead of silently dropping them. (#10418) Thanks @mcaxtr.
  • -
  • Discord: respect replyToMode in threads. (#11062) Thanks @cordx56.
  • -
  • Heartbeat: filter noise-only system events so scheduled reminder notifications do not fire when cron runs carry only heartbeat markers. (#13317) Thanks @pvtclawn.
  • -
  • Signal: render mention placeholders as @uuid/@phone so mention gating and Clawdbot targeting work. (#2013) Thanks @alexgleason.
  • -
  • Discord: omit empty content fields for media-only messages while preserving caption whitespace. (#9507) Thanks @leszekszpunar.
  • -
  • Onboarding/Providers: add Z.AI endpoint-specific auth choices (zai-coding-global, zai-coding-cn, zai-global, zai-cn) and expand default Z.AI model wiring. (#13456) Thanks @tomsun28.
  • -
  • Onboarding/Providers: update MiniMax API default/recommended models from M2.1 to M2.5, add M2.5/M2.5-Lightning model entries, and include minimax-m2.5 in modern model filtering. (#14865) Thanks @adao-max.
  • -
  • Ollama: use configured models.providers.ollama.baseUrl for model discovery and normalize /v1 endpoints to the native Ollama API root. (#14131) Thanks @shtse8.
  • -
  • Voice Call: pass Twilio stream auth token via instead of query string. (#14029) Thanks @mcwigglesmcgee.
  • -
  • Feishu: pass Buffer directly to the Feishu SDK upload APIs instead of Readable.from(...) to avoid form-data upload failures. (#10345) Thanks @youngerstyle.
  • -
  • Feishu: trigger mention-gated group handling only when the bot itself is mentioned (not just any mention). (#11088) Thanks @openperf.
  • -
  • Feishu: probe status uses the resolved account context for multi-account credential checks. (#11233) Thanks @onevcat.
  • -
  • Feishu DocX: preserve top-level converted block order using firstLevelBlockIds when writing/appending documents. (#13994) Thanks @Cynosure159.
  • -
  • Feishu plugin packaging: remove workspace:* openclaw dependency from extensions/feishu and sync lockfile for install compatibility. (#14423) Thanks @jackcooper2015.
  • -
  • CLI/Wizard: exit with code 1 when configure, agents add, or interactive onboard wizards are canceled, so set -e automation stops correctly. (#14156) Thanks @0xRaini.
  • -
  • Media: strip MEDIA: lines with local paths instead of leaking as visible text. (#14399) Thanks @0xRaini.
  • -
  • Config/Cron: exclude maxTokens from config redaction and honor deleteAfterRun on skipped cron jobs. (#13342) Thanks @niceysam.
  • -
  • Config: ignore meta field changes in config file watcher. (#13460) Thanks @brandonwise.
  • -
  • Cron: use requested agentId for isolated job auth resolution. (#13983) Thanks @0xRaini.
  • -
  • Cron: pass agentId to runHeartbeatOnce for main-session jobs. (#14140) Thanks @ishikawa-pro.
  • -
  • Cron: prevent cron jobs from skipping execution when nextRunAtMs advances. (#14068) Thanks @WalterSumbon.
  • -
  • Cron: re-arm timers when onTimer fires while a job is still executing. (#14233) Thanks @tomron87.
  • -
  • Cron: prevent duplicate fires when multiple jobs trigger simultaneously. (#14256) Thanks @xinhuagu.
  • -
  • Cron: isolate scheduler errors so one bad job does not break all jobs. (#14385) Thanks @MarvinDontPanic.
  • -
  • Cron: prevent one-shot at jobs from re-firing on restart after skipped/errored runs. (#13878) Thanks @lailoo.
  • -
  • Daemon: suppress EPIPE error when restarting LaunchAgent. (#14343) Thanks @0xRaini.
  • -
  • Antigravity: add opus 4.6 forward-compat model and bypass thinking signature sanitization. (#14218) Thanks @jg-noncelogic.
  • -
  • Agents: prevent file descriptor leaks in child process cleanup. (#13565) Thanks @KyleChen26.
  • -
  • Agents: prevent double compaction caused by cache TTL bypassing guard. (#13514) Thanks @taw0002.
  • -
  • Agents: use last API call's cache tokens for context display instead of accumulated sum. (#13805) Thanks @akari-musubi.
  • -
  • Agents: keep followup-runner session totalTokens aligned with post-compaction context by using last-call usage and shared token-accounting logic. (#14979) Thanks @shtse8.
  • -
  • Hooks/Plugins: wire 9 previously unwired plugin lifecycle hooks into core runtime paths (session, compaction, gateway, and outbound message hooks). (#14882) Thanks @shtse8.
  • -
  • Hooks/Tools: dispatch before_tool_call and after_tool_call hooks from both tool execution paths with rebased conflict fixes. (#15012) Thanks @Patrick-Barletta, @Takhoffman.
  • -
  • Discord: allow channel-edit to archive/lock threads and set auto-archive duration. (#5542) Thanks @stumct.
  • -
  • Discord tests: use a partial @buape/carbon mock in slash command coverage. (#13262) Thanks @arosstale.
  • -
  • Tests: update thread ID handling in Slack message collection tests. (#14108) Thanks @swizzmagik.
  • -
-

View full changelog

-]]>
- -
- - 2026.2.9 - Mon, 09 Feb 2026 13:23:25 -0600 - https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml - 9194 - 2026.2.9 - 15.0 - OpenClaw 2026.2.9 -

Added

-
    -
  • iOS: alpha node app + setup-code onboarding. (#11756) Thanks @mbelinky.
  • -
  • Channels: comprehensive BlueBubbles and channel cleanup. (#11093) Thanks @tyler6204.
  • -
  • Plugins: device pairing + phone control plugins (Telegram /pair, iOS/Android node controls). (#11755) Thanks @mbelinky.
  • -
  • Tools: add Grok (xAI) as a web_search provider. (#12419) Thanks @tmchow.
  • -
  • Gateway: add agent management RPC methods for the web UI (agents.create, agents.update, agents.delete). (#11045) Thanks @advaitpaliwal.
  • -
  • Web UI: show a Compaction divider in chat history. (#11341) Thanks @Takhoffman.
  • -
  • Agents: include runtime shell in agent envelopes. (#1835) Thanks @Takhoffman.
  • -
  • Paths: add OPENCLAW_HOME for overriding the home directory used by internal path resolution. (#12091) Thanks @sebslight.
  • -
-

Fixes

-
    -
  • Telegram: harden quote parsing; preserve quote context; avoid QUOTE_TEXT_INVALID; avoid nested reply quote misclassification. (#12156) Thanks @rybnikov.
  • -
  • Telegram: recover proactive sends when stale topic thread IDs are used by retrying without message_thread_id. (#11620)
  • -
  • Telegram: render markdown spoilers with HTML tags. (#11543) Thanks @ezhikkk.
  • -
  • Telegram: truncate command registration to 100 entries to avoid BOT_COMMANDS_TOO_MUCH failures on startup. (#12356) Thanks @arosstale.
  • -
  • Telegram: match DM allowFrom against sender user id (fallback to chat id) and clarify pairing logs. (#12779) Thanks @liuxiaopai-ai.
  • -
  • Onboarding: QuickStart now auto-installs shell completion (prompt only in Manual).
  • -
  • Auth: strip embedded line breaks from pasted API keys and tokens before storing/resolving credentials.
  • -
  • Web UI: make chat refresh smoothly scroll to the latest messages and suppress new-messages badge flash during manual refresh.
  • -
  • Tools/web_search: include provider-specific settings in the web search cache key, and pass inlineCitations for Grok. (#12419) Thanks @tmchow.
  • -
  • Tools/web_search: normalize direct Perplexity model IDs while keeping OpenRouter model IDs unchanged. (#12795) Thanks @cdorsey.
  • -
  • Model failover: treat HTTP 400 errors as failover-eligible, enabling automatic model fallback. (#1879) Thanks @orenyomtov.
  • -
  • Errors: prevent false positive context overflow detection when conversation mentions "context overflow" topic. (#2078) Thanks @sbking.
  • -
  • Gateway: no more post-compaction amnesia; injected transcript writes now preserve Pi session parentId chain so agents can remember again. (#12283) Thanks @Takhoffman.
  • -
  • Gateway: fix multi-agent sessions.usage discovery. (#11523) Thanks @Takhoffman.
  • -
  • Agents: recover from context overflow caused by oversized tool results (pre-emptive capping + fallback truncation). (#11579) Thanks @tyler6204.
  • -
  • Subagents/compaction: stabilize announce timing and preserve compaction metrics across retries. (#11664) Thanks @tyler6204.
  • -
  • Cron: share isolated announce flow and harden scheduling/delivery reliability. (#11641) Thanks @tyler6204.
  • -
  • Cron tool: recover flat params when LLM omits the job wrapper for add requests. (#12124) Thanks @tyler6204.
  • -
  • Gateway/CLI: when gateway.bind=lan, use a LAN IP for probe URLs and Control UI links. (#11448) Thanks @AnonO6.
  • -
  • Hooks: fix bundled hooks broken since 2026.2.2 (tsdown migration). (#9295) Thanks @patrickshao.
  • -
  • Routing: refresh bindings per message by loading config at route resolution so binding changes apply without restart. (#11372) Thanks @juanpablodlc.
  • -
  • Exec approvals: render forwarded commands in monospace for safer approval scanning. (#11937) Thanks @sebslight.
  • -
  • Config: clamp maxTokens to contextWindow to prevent invalid model configs. (#5516) Thanks @lailoo.
  • -
  • Thinking: allow xhigh for github-copilot/gpt-5.2-codex and github-copilot/gpt-5.2. (#11646) Thanks @LatencyTDH.
  • -
  • Discord: support forum/media thread-create starter messages, wire message thread create --message, and harden routing. (#10062) Thanks @jarvis89757.
  • -
  • Paths: structurally resolve OPENCLAW_HOME-derived home paths and fix Windows drive-letter handling in tool meta shortening. (#12125) Thanks @mcaxtr.
  • -
  • Memory: set Voyage embeddings input_type for improved retrieval. (#10818) Thanks @mcinteerj.
  • -
  • Memory/QMD: reuse default model cache across agents instead of re-downloading per agent. (#12114) Thanks @tyler6204.
  • -
  • Media understanding: recognize .caf audio attachments for transcription. (#10982) Thanks @succ985.
  • -
  • State dir: honor OPENCLAW_STATE_DIR for default device identity and canvas storage paths. (#4824) Thanks @kossoy.
  • -
-

View full changelog

-]]>
- -
\ No newline at end of file diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts index 7bc18a89bc80d..870aaa59c1bac 100644 --- a/apps/android/app/build.gradle.kts +++ b/apps/android/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "ai.openclaw.android" minSdk = 31 targetSdk = 36 - versionCode = 202602130 - versionName = "2026.2.13" + versionCode = 202602190 + versionName = "2026.2.19" ndk { // Support all major ABIs — native libs are tiny (~47 KB per ABI) abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") @@ -63,7 +63,11 @@ android { } lint { - disable += setOf("IconLauncherShape") + disable += setOf( + "GradleDependency", + "IconLauncherShape", + "NewerVersionAvailable", + ) warningsAsErrors = true } diff --git a/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayTls.kt b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayTls.kt index 1e43804d20e50..0726c94fc9738 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayTls.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayTls.kt @@ -8,6 +8,7 @@ import java.security.MessageDigest import java.security.SecureRandom import java.security.cert.CertificateException import java.security.cert.X509Certificate +import java.util.Locale import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLContext @@ -91,9 +92,11 @@ suspend fun probeGatewayTlsFingerprint( return withContext(Dispatchers.IO) { val trustAll = - @SuppressLint("CustomX509TrustManager") + @SuppressLint("CustomX509TrustManager", "TrustAllX509TrustManager") object : X509TrustManager { + @SuppressLint("TrustAllX509TrustManager") override fun checkClientTrusted(chain: Array, authType: String) {} + @SuppressLint("TrustAllX509TrustManager") override fun checkServerTrusted(chain: Array, authType: String) {} override fun getAcceptedIssuers(): Array = emptyArray() } @@ -144,7 +147,7 @@ private fun sha256Hex(data: ByteArray): String { val digest = MessageDigest.getInstance("SHA-256").digest(data) val out = StringBuilder(digest.size * 2) for (byte in digest) { - out.append(String.format("%02x", byte)) + out.append(String.format(Locale.US, "%02x", byte)) } return out.toString() } @@ -152,5 +155,5 @@ private fun sha256Hex(data: ByteArray): String { private fun normalizeFingerprint(raw: String): String { val stripped = raw.trim() .replace(Regex("^sha-?256\\s*:?\\s*", RegexOption.IGNORE_CASE), "") - return stripped.lowercase().filter { it in '0'..'9' || it in 'a'..'f' } + return stripped.lowercase(Locale.US).filter { it in '0'..'9' || it in 'a'..'f' } } diff --git a/apps/android/app/src/main/java/ai/openclaw/android/node/AppUpdateHandler.kt b/apps/android/app/src/main/java/ai/openclaw/android/node/AppUpdateHandler.kt index 7472544d3172e..e54c846c0fbf7 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/node/AppUpdateHandler.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/node/AppUpdateHandler.kt @@ -187,11 +187,11 @@ class AppUpdateHandler( lastNotifUpdate = now if (contentLength > 0) { val pct = ((totalBytes * 100) / contentLength).toInt() - val mb = String.format("%.1f", totalBytes / 1048576.0) - val totalMb = String.format("%.1f", contentLength / 1048576.0) + val mb = String.format(Locale.US, "%.1f", totalBytes / 1048576.0) + val totalMb = String.format(Locale.US, "%.1f", contentLength / 1048576.0) notifManager.notify(notifId, buildProgressNotif(pct, 100, "$mb / $totalMb MB ($pct%)")) } else { - val mb = String.format("%.1f", totalBytes / 1048576.0) + val mb = String.format(Locale.US, "%.1f", totalBytes / 1048576.0) notifManager.notify(notifId, buildProgressNotif(0, 0, "${mb} MB downloaded")) } } @@ -239,13 +239,15 @@ class AppUpdateHandler( // Use PackageInstaller session API — works from background on API 34+ // The system handles showing the install confirmation dialog notifManager.cancel(notifId) - notifManager.notify(notifId, android.app.Notification.Builder(appContext, channelId) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setContentTitle("Installing Update...") - + notifManager.notify( + notifId, + android.app.Notification.Builder(appContext, channelId) + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setContentTitle("Installing Update...") .setContentIntent(launchPi) - .setContentText("${String.format("%.1f", totalBytes / 1048576.0)} MB downloaded") - .build()) + .setContentText("${String.format(Locale.US, "%.1f", totalBytes / 1048576.0)} MB downloaded") + .build(), + ) val installer = appContext.packageManager.packageInstaller val params = android.content.pm.PackageInstaller.SessionParams( diff --git a/apps/ios/.swiftlint.yml b/apps/ios/.swiftlint.yml index fc8509c83859e..23db4515968b7 100644 --- a/apps/ios/.swiftlint.yml +++ b/apps/ios/.swiftlint.yml @@ -3,3 +3,7 @@ parent_config: ../../.swiftlint.yml included: - Sources - ../shared/ClawdisNodeKit/Sources + +type_body_length: + warning: 900 + error: 1300 diff --git a/apps/ios/Config/Signing.xcconfig b/apps/ios/Config/Signing.xcconfig new file mode 100644 index 0000000000000..e0afd46aa7e06 --- /dev/null +++ b/apps/ios/Config/Signing.xcconfig @@ -0,0 +1,18 @@ +// Shared iOS signing defaults for local development + CI. +OPENCLAW_IOS_DEFAULT_TEAM = Y5PE65HELJ +OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM) +OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios +OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.ios.watchkitapp +OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.ios.watchkitapp.extension + +// Local contributors can override this by running scripts/ios-configure-signing.sh. +// Keep include after defaults: xcconfig is evaluated top-to-bottom. +#include? "../.local-signing.xcconfig" +#include? "../LocalSigning.xcconfig" + +CODE_SIGN_STYLE = Automatic +CODE_SIGN_IDENTITY = Apple Development +DEVELOPMENT_TEAM = $(OPENCLAW_IOS_SELECTED_TEAM) + +// Let Xcode manage provisioning for the selected local team. +PROVISIONING_PROFILE_SPECIFIER = diff --git a/apps/ios/LocalSigning.xcconfig.example b/apps/ios/LocalSigning.xcconfig.example new file mode 100644 index 0000000000000..bfa610fb350bf --- /dev/null +++ b/apps/ios/LocalSigning.xcconfig.example @@ -0,0 +1,14 @@ +// Copy to LocalSigning.xcconfig for personal local signing overrides. +// This file is only an example and should stay committed. + +OPENCLAW_CODE_SIGN_STYLE = Automatic +OPENCLAW_DEVELOPMENT_TEAM = P5Z8X89DJL + +OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios.test.mariano +OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.ios.test.mariano.share +OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.ios.test.mariano.watchkitapp +OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.ios.test.mariano.watchkitapp.extension + +// Leave empty with automatic signing. +OPENCLAW_APP_PROFILE = +OPENCLAW_SHARE_PROFILE = diff --git a/apps/ios/README.md b/apps/ios/README.md index 2e426c18d70bf..b870bdcea583a 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -1,66 +1,110 @@ -# OpenClaw (iOS) +# OpenClaw iOS (Super Alpha) -This is an **alpha** iOS app that connects to an OpenClaw Gateway as a `role: node`. +NO TEST FLIGHT AVAILABLE AT THIS POINT -Expect rough edges: +This iPhone app is super-alpha and internal-use only. It connects to an OpenClaw Gateway as a `role: node`. -- UI and onboarding are changing quickly. -- Background behavior is not stable yet (foreground app is the supported mode right now). -- Permissions are opt-in and the app should be treated as sensitive while we harden it. +## Distribution Status -## What It Does +NO TEST FLIGHT AVAILABLE AT THIS POINT -- Connects to a Gateway over `ws://` / `wss://` -- Pairs a new device (approved from your bot) -- Exposes phone services as node commands (camera, location, photos, calendar, reminders, etc; gated by iOS permissions) -- Provides Talk + Chat surfaces (alpha) +- Current distribution: local/manual deploy from source via Xcode. +- App Store flow is not part of the current internal development path. -## Pairing (Recommended Flow) +## Super-Alpha Disclaimer -If your Gateway has the `device-pair` plugin installed: +- Breaking changes are expected. +- UI and onboarding flows can change without migration guarantees. +- Foreground use is the only reliable mode right now. +- Treat this build as sensitive while permissions and background behavior are still being hardened. -1. In Telegram, message your bot: `/pair` -2. Copy the **setup code** message -3. On iOS: OpenClaw → Settings → Gateway → paste setup code → Connect -4. Back in Telegram: `/pair approve` +## Exact Xcode Manual Deploy Flow -## Build And Run - -Prereqs: - -- Xcode (current stable) -- `pnpm` -- `xcodegen` - -From the repo root: +1. Prereqs: + - Xcode 16+ + - `pnpm` + - `xcodegen` + - Apple Development signing set up in Xcode +2. From repo root: ```bash pnpm install -pnpm ios:open +./scripts/ios-configure-signing.sh +cd apps/ios +xcodegen generate +open OpenClaw.xcodeproj ``` -Then in Xcode: - -1. Select the `OpenClaw` scheme -2. Select a simulator or a connected device -3. Run - -If you're using a personal Apple Development team, you may need to change the bundle identifier in Xcode to a unique value so signing succeeds. +3. In Xcode: + - Scheme: `OpenClaw` + - Destination: connected iPhone (recommended for real behavior) + - Build configuration: `Debug` + - Run (`Product` -> `Run`) +4. If signing fails on a personal team: + - Use unique local bundle IDs via `apps/ios/LocalSigning.xcconfig`. + - Start from `apps/ios/LocalSigning.xcconfig.example`. -## Build From CLI +Shortcut command (same flow + open project): ```bash -pnpm ios:build -``` - -## Tests - -```bash -cd apps/ios -xcodegen generate -xcodebuild test -project OpenClaw.xcodeproj -scheme OpenClaw -destination "platform=iOS Simulator,name=iPhone 17" +pnpm ios:open ``` -## Shared Code - -- `apps/shared/OpenClawKit` contains the shared transport/types used by the iOS app. +## APNs Expectations For Local/Manual Builds + +- The app calls `registerForRemoteNotifications()` at launch. +- `apps/ios/Sources/OpenClaw.entitlements` sets `aps-environment` to `development`. +- APNs token registration to gateway happens only after gateway connection (`push.apns.register`). +- Your selected team/profile must support Push Notifications for the app bundle ID you are signing. +- If push capability or provisioning is wrong, APNs registration fails at runtime (check Xcode logs for `APNs registration failed`). +- Debug builds register as APNs sandbox; Release builds use production. + +## What Works Now (Concrete) + +- Pairing via setup code flow (`/pair` then `/pair approve` in Telegram). +- Gateway connection via discovery or manual host/port with TLS fingerprint trust prompt. +- Chat + Talk surfaces through the operator gateway session. +- iPhone node commands in foreground: camera snap/clip, canvas present/navigate/eval/snapshot, screen record, location, contacts, calendar, reminders, photos, motion, local notifications. +- Share extension deep-link forwarding into the connected gateway session. + +## Known Issues / Limitations / Problems + +- Foreground-first: iOS can suspend sockets in background; reconnect recovery is still being tuned. +- Background command limits are strict: `canvas.*`, `camera.*`, `screen.*`, and `talk.*` are blocked when backgrounded. +- Background location requires `Always` location permission. +- Pairing/auth errors intentionally pause reconnect loops until a human fixes auth/pairing state. +- Voice Wake and Talk contend for the same microphone; Talk suppresses wake capture while active. +- APNs reliability depends on local signing/provisioning/topic alignment. +- Expect rough UX edges and occasional reconnect churn during active development. + +## Current In-Progress Workstream + +Automatic wake/reconnect hardening: + +- improve wake/resume behavior across scene transitions +- reduce dead-socket states after background -> foreground +- tighten node/operator session reconnect coordination +- reduce manual recovery steps after transient network failures + +## Debugging Checklist + +1. Confirm build/signing baseline: + - regenerate project (`xcodegen generate`) + - verify selected team + bundle IDs +2. In app `Settings -> Gateway`: + - confirm status text, server, and remote address + - verify whether status shows pairing/auth gating +3. If pairing is required: + - run `/pair approve` from Telegram, then reconnect +4. If discovery is flaky: + - enable `Discovery Debug Logs` + - inspect `Settings -> Gateway -> Discovery Logs` +5. If network path is unclear: + - switch to manual host/port + TLS in Gateway Advanced settings +6. In Xcode console, filter for subsystem/category signals: + - `ai.openclaw.ios` + - `GatewayDiag` + - `APNs registration failed` +7. Validate background expectations: + - repro in foreground first + - then test background transitions and confirm reconnect on return diff --git a/apps/ios/ShareExtension/Info.plist b/apps/ios/ShareExtension/Info.plist new file mode 100644 index 0000000000000..bc1f60bc24dab --- /dev/null +++ b/apps/ios/ShareExtension/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + OpenClaw Share + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 2026.2.19 + CFBundleVersion + 20260219 + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsImageWithMaxCount + 10 + NSExtensionActivationSupportsMovieWithMaxCount + 1 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShareViewController + + + diff --git a/apps/ios/ShareExtension/ShareViewController.swift b/apps/ios/ShareExtension/ShareViewController.swift new file mode 100644 index 0000000000000..1181641e33097 --- /dev/null +++ b/apps/ios/ShareExtension/ShareViewController.swift @@ -0,0 +1,548 @@ +import Foundation +import OpenClawKit +import os +import UIKit +import UniformTypeIdentifiers + +final class ShareViewController: UIViewController { + private struct ShareAttachment: Codable { + var type: String + var mimeType: String + var fileName: String + var content: String + } + + private struct ExtractedShareContent { + var payload: SharedContentPayload + var attachments: [ShareAttachment] + } + + private let logger = Logger(subsystem: "ai.openclaw.ios", category: "ShareExtension") + private var statusLabel: UILabel? + private let draftTextView = UITextView() + private let sendButton = UIButton(type: .system) + private let cancelButton = UIButton(type: .system) + private var didPrepareDraft = false + private var isSending = false + private var pendingAttachments: [ShareAttachment] = [] + + override func viewDidLoad() { + super.viewDidLoad() + self.preferredContentSize = CGSize(width: UIScreen.main.bounds.width, height: 420) + self.setupUI() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + guard !self.didPrepareDraft else { return } + self.didPrepareDraft = true + Task { await self.prepareDraft() } + } + + private func setupUI() { + self.view.backgroundColor = .systemBackground + + self.draftTextView.translatesAutoresizingMaskIntoConstraints = false + self.draftTextView.font = .preferredFont(forTextStyle: .body) + self.draftTextView.backgroundColor = UIColor.secondarySystemBackground + self.draftTextView.layer.cornerRadius = 10 + self.draftTextView.textContainerInset = UIEdgeInsets(top: 12, left: 10, bottom: 12, right: 10) + + self.sendButton.translatesAutoresizingMaskIntoConstraints = false + self.sendButton.setTitle("Send to OpenClaw", for: .normal) + self.sendButton.titleLabel?.font = .preferredFont(forTextStyle: .headline) + self.sendButton.addTarget(self, action: #selector(self.handleSendTap), for: .touchUpInside) + self.sendButton.isEnabled = false + + self.cancelButton.translatesAutoresizingMaskIntoConstraints = false + self.cancelButton.setTitle("Cancel", for: .normal) + self.cancelButton.addTarget(self, action: #selector(self.handleCancelTap), for: .touchUpInside) + + let buttons = UIStackView(arrangedSubviews: [self.cancelButton, self.sendButton]) + buttons.translatesAutoresizingMaskIntoConstraints = false + buttons.axis = .horizontal + buttons.alignment = .fill + buttons.distribution = .fillEqually + buttons.spacing = 12 + + self.view.addSubview(self.draftTextView) + self.view.addSubview(buttons) + + NSLayoutConstraint.activate([ + self.draftTextView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 14), + self.draftTextView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 14), + self.draftTextView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -14), + self.draftTextView.bottomAnchor.constraint(equalTo: buttons.topAnchor, constant: -12), + + buttons.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 14), + buttons.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -14), + buttons.bottomAnchor.constraint(equalTo: self.view.keyboardLayoutGuide.topAnchor, constant: -8), + buttons.heightAnchor.constraint(equalToConstant: 44), + ]) + } + + private func prepareDraft() async { + let traceId = UUID().uuidString + ShareGatewayRelaySettings.saveLastEvent("Share opened.") + self.showStatus("Preparing share…") + self.logger.info("share begin trace=\(traceId, privacy: .public)") + let extracted = await self.extractSharedContent() + let payload = extracted.payload + self.pendingAttachments = extracted.attachments + self.logger.info( + "share payload trace=\(traceId, privacy: .public) titleChars=\(payload.title?.count ?? 0) textChars=\(payload.text?.count ?? 0) hasURL=\(payload.url != nil) imageAttachments=\(self.pendingAttachments.count)" + ) + let message = self.composeDraft(from: payload) + await MainActor.run { + self.draftTextView.text = message + self.sendButton.isEnabled = true + self.draftTextView.becomeFirstResponder() + } + if message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + ShareGatewayRelaySettings.saveLastEvent("Share ready: waiting for message input.") + self.showStatus("Add a message, then tap Send.") + } else { + ShareGatewayRelaySettings.saveLastEvent("Share ready: draft prepared.") + self.showStatus("Edit text, then tap Send.") + } + } + + @objc + private func handleSendTap() { + guard !self.isSending else { return } + Task { await self.sendCurrentDraft() } + } + + @objc + private func handleCancelTap() { + self.extensionContext?.completeRequest(returningItems: nil) + } + + private func sendCurrentDraft() async { + let message = await MainActor.run { self.draftTextView.text ?? "" } + let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { + ShareGatewayRelaySettings.saveLastEvent("Share blocked: message is empty.") + self.showStatus("Message is empty.") + return + } + + await MainActor.run { + self.isSending = true + self.sendButton.isEnabled = false + self.cancelButton.isEnabled = false + } + self.showStatus("Sending to OpenClaw gateway…") + ShareGatewayRelaySettings.saveLastEvent("Sending to gateway…") + do { + try await self.sendMessageToGateway(trimmed, attachments: self.pendingAttachments) + ShareGatewayRelaySettings.saveLastEvent( + "Sent to gateway (\(trimmed.count) chars, \(self.pendingAttachments.count) attachment(s)).") + self.showStatus("Sent to OpenClaw.") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) { + self.extensionContext?.completeRequest(returningItems: nil) + } + } catch { + self.logger.error("share send failed reason=\(error.localizedDescription, privacy: .public)") + ShareGatewayRelaySettings.saveLastEvent("Send failed: \(error.localizedDescription)") + self.showStatus("Send failed: \(error.localizedDescription)") + await MainActor.run { + self.isSending = false + self.sendButton.isEnabled = true + self.cancelButton.isEnabled = true + } + } + } + + private func sendMessageToGateway(_ message: String, attachments: [ShareAttachment]) async throws { + guard let config = ShareGatewayRelaySettings.loadConfig() else { + throw NSError( + domain: "OpenClawShare", + code: 10, + userInfo: [NSLocalizedDescriptionKey: "OpenClaw is not connected to a gateway yet."]) + } + guard let url = URL(string: config.gatewayURLString) else { + throw NSError( + domain: "OpenClawShare", + code: 11, + userInfo: [NSLocalizedDescriptionKey: "Invalid saved gateway URL."]) + } + + let gateway = GatewayNodeSession() + defer { + Task { await gateway.disconnect() } + } + let makeOptions: (String) -> GatewayConnectOptions = { clientId in + GatewayConnectOptions( + role: "node", + scopes: [], + caps: [], + commands: [], + permissions: [:], + clientId: clientId, + clientMode: "node", + clientDisplayName: "OpenClaw Share", + includeDeviceIdentity: false) + } + + do { + try await gateway.connect( + url: url, + token: config.token, + password: config.password, + connectOptions: makeOptions("openclaw-ios"), + sessionBox: nil, + onConnected: {}, + onDisconnected: { _ in }, + onInvoke: { req in + BridgeInvokeResponse( + id: req.id, + ok: false, + error: OpenClawNodeError( + code: .invalidRequest, + message: "share extension does not support node invoke")) + }) + } catch { + let expectsLegacyClientId = self.shouldRetryWithLegacyClientId(error) + guard expectsLegacyClientId else { throw error } + try await gateway.connect( + url: url, + token: config.token, + password: config.password, + connectOptions: makeOptions("moltbot-ios"), + sessionBox: nil, + onConnected: {}, + onDisconnected: { _ in }, + onInvoke: { req in + BridgeInvokeResponse( + id: req.id, + ok: false, + error: OpenClawNodeError( + code: .invalidRequest, + message: "share extension does not support node invoke")) + }) + } + + struct AgentRequestPayload: Codable { + var message: String + var sessionKey: String? + var thinking: String + var deliver: Bool + var attachments: [ShareAttachment]? + var receipt: Bool + var receiptText: String? + var to: String? + var channel: String? + var timeoutSeconds: Int? + var key: String? + } + + let deliveryChannel = config.deliveryChannel?.trimmingCharacters(in: .whitespacesAndNewlines) + let deliveryTo = config.deliveryTo?.trimmingCharacters(in: .whitespacesAndNewlines) + let canDeliverToRoute = (deliveryChannel?.isEmpty == false) && (deliveryTo?.isEmpty == false) + + let params = AgentRequestPayload( + message: message, + sessionKey: config.sessionKey, + thinking: "low", + deliver: canDeliverToRoute, + attachments: attachments.isEmpty ? nil : attachments, + receipt: canDeliverToRoute, + receiptText: canDeliverToRoute ? "Just received your iOS share + request, working on it." : nil, + to: canDeliverToRoute ? deliveryTo : nil, + channel: canDeliverToRoute ? deliveryChannel : nil, + timeoutSeconds: nil, + key: UUID().uuidString) + let data = try JSONEncoder().encode(params) + guard let json = String(data: data, encoding: .utf8) else { + throw NSError( + domain: "OpenClawShare", + code: 12, + userInfo: [NSLocalizedDescriptionKey: "Failed to encode chat payload."]) + } + struct NodeEventParams: Codable { + var event: String + var payloadJSON: String + } + let eventData = try JSONEncoder().encode(NodeEventParams(event: "agent.request", payloadJSON: json)) + guard let nodeEventParams = String(data: eventData, encoding: .utf8) else { + throw NSError( + domain: "OpenClawShare", + code: 13, + userInfo: [NSLocalizedDescriptionKey: "Failed to encode node event payload."]) + } + _ = try await gateway.request(method: "node.event", paramsJSON: nodeEventParams, timeoutSeconds: 25) + } + + private func shouldRetryWithLegacyClientId(_ error: Error) -> Bool { + if let gatewayError = error as? GatewayResponseError { + let code = gatewayError.code.lowercased() + let message = gatewayError.message.lowercased() + let pathValue = (gatewayError.details["path"]?.value as? String)?.lowercased() ?? "" + let mentionsClientIdPath = + message.contains("/client/id") || message.contains("client id") + || pathValue.contains("/client/id") + let isInvalidConnectParams = + (code.contains("invalid") && code.contains("connect")) + || message.contains("invalid connect params") + if isInvalidConnectParams && mentionsClientIdPath { + return true + } + } + + let text = error.localizedDescription.lowercased() + return text.contains("invalid connect params") + && (text.contains("/client/id") || text.contains("client id")) + } + + private func showStatus(_ text: String) { + DispatchQueue.main.async { + let label: UILabel + if let existing = self.statusLabel { + label = existing + } else { + let newLabel = UILabel() + newLabel.translatesAutoresizingMaskIntoConstraints = false + newLabel.numberOfLines = 0 + newLabel.textAlignment = .center + newLabel.font = .preferredFont(forTextStyle: .body) + newLabel.textColor = .label + newLabel.backgroundColor = UIColor.systemBackground.withAlphaComponent(0.92) + newLabel.layer.cornerRadius = 12 + newLabel.clipsToBounds = true + newLabel.layoutMargins = UIEdgeInsets(top: 12, left: 14, bottom: 12, right: 14) + self.view.addSubview(newLabel) + NSLayoutConstraint.activate([ + newLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 18), + newLabel.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -18), + newLabel.bottomAnchor.constraint(equalTo: self.sendButton.topAnchor, constant: -10), + ]) + self.statusLabel = newLabel + label = newLabel + } + label.text = " \(text) " + } + } + + private func composeDraft(from payload: SharedContentPayload) -> String { + var lines: [String] = [] + let title = self.sanitizeDraftFragment(payload.title) + let text = self.sanitizeDraftFragment(payload.text) + let url = payload.url?.absoluteString.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + + if let title, !title.isEmpty { lines.append(title) } + if let text, !text.isEmpty { lines.append(text) } + if !url.isEmpty { lines.append(url) } + + return lines.joined(separator: "\n\n") + } + + private func sanitizeDraftFragment(_ raw: String?) -> String? { + guard let raw else { return nil } + let banned = [ + "shared from ios.", + "text:", + "shared attachment(s):", + "please help me with this.", + "please help me with this.w", + ] + let cleanedLines = raw + .components(separatedBy: .newlines) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { line in + guard !line.isEmpty else { return false } + let lowered = line.lowercased() + return !banned.contains { lowered == $0 || lowered.hasPrefix($0) } + } + let cleaned = cleanedLines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines) + return cleaned.isEmpty ? nil : cleaned + } + + private func extractSharedContent() async -> ExtractedShareContent { + guard let items = self.extensionContext?.inputItems as? [NSExtensionItem] else { + return ExtractedShareContent( + payload: SharedContentPayload(title: nil, url: nil, text: nil), + attachments: []) + } + + var title: String? + var sharedURL: URL? + var sharedText: String? + var imageCount = 0 + var videoCount = 0 + var fileCount = 0 + var unknownCount = 0 + var attachments: [ShareAttachment] = [] + let maxImageAttachments = 3 + + for item in items { + if title == nil { + title = item.attributedTitle?.string ?? item.attributedContentText?.string + } + + for provider in item.attachments ?? [] { + if sharedURL == nil { + sharedURL = await self.loadURL(from: provider) + } + + if sharedText == nil { + sharedText = await self.loadText(from: provider) + } + + if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { + imageCount += 1 + if attachments.count < maxImageAttachments, + let attachment = await self.loadImageAttachment(from: provider, index: attachments.count) + { + attachments.append(attachment) + } + } else if provider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { + videoCount += 1 + } else if provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) { + fileCount += 1 + } else { + unknownCount += 1 + } + + } + } + + _ = imageCount + _ = videoCount + _ = fileCount + _ = unknownCount + + return ExtractedShareContent( + payload: SharedContentPayload(title: title, url: sharedURL, text: sharedText), + attachments: attachments) + } + + private func loadImageAttachment(from provider: NSItemProvider, index: Int) async -> ShareAttachment? { + let imageUTI = self.preferredImageTypeIdentifier(from: provider) ?? UTType.image.identifier + guard let rawData = await self.loadDataValue(from: provider, typeIdentifier: imageUTI) else { + return nil + } + + let maxBytes = 5_000_000 + guard let image = UIImage(data: rawData), + let data = self.normalizedJPEGData(from: image, maxBytes: maxBytes) + else { + return nil + } + + return ShareAttachment( + type: "image", + mimeType: "image/jpeg", + fileName: "shared-image-\(index + 1).jpg", + content: data.base64EncodedString()) + } + + private func preferredImageTypeIdentifier(from provider: NSItemProvider) -> String? { + for identifier in provider.registeredTypeIdentifiers { + guard let utType = UTType(identifier) else { continue } + if utType.conforms(to: .image) { + return identifier + } + } + return nil + } + + private func normalizedJPEGData(from image: UIImage, maxBytes: Int) -> Data? { + var quality: CGFloat = 0.9 + while quality >= 0.4 { + if let data = image.jpegData(compressionQuality: quality), data.count <= maxBytes { + return data + } + quality -= 0.1 + } + guard let fallback = image.jpegData(compressionQuality: 0.35) else { return nil } + if fallback.count <= maxBytes { return fallback } + return nil + } + + private func loadURL(from provider: NSItemProvider) async -> URL? { + if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { + if let url = await self.loadURLValue( + from: provider, + typeIdentifier: UTType.url.identifier) + { + return url + } + } + + if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) { + if let text = await self.loadTextValue(from: provider, typeIdentifier: UTType.text.identifier), + let url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines)), + url.scheme != nil + { + return url + } + } + + return nil + } + + private func loadText(from provider: NSItemProvider) async -> String? { + if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { + if let text = await self.loadTextValue(from: provider, typeIdentifier: UTType.plainText.identifier) { + return text + } + } + + if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { + if let url = await self.loadURLValue(from: provider, typeIdentifier: UTType.url.identifier) { + return url.absoluteString + } + } + + return nil + } + + private func loadURLValue(from provider: NSItemProvider, typeIdentifier: String) async -> URL? { + await withCheckedContinuation { continuation in + provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in + if let url = item as? URL { + continuation.resume(returning: url) + return + } + if let str = item as? String, let url = URL(string: str) { + continuation.resume(returning: url) + return + } + if let ns = item as? NSString, let url = URL(string: ns as String) { + continuation.resume(returning: url) + return + } + continuation.resume(returning: nil) + } + } + } + + private func loadTextValue(from provider: NSItemProvider, typeIdentifier: String) async -> String? { + await withCheckedContinuation { continuation in + provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in + if let text = item as? String { + continuation.resume(returning: text) + return + } + if let text = item as? NSString { + continuation.resume(returning: text as String) + return + } + if let text = item as? NSAttributedString { + continuation.resume(returning: text.string) + return + } + continuation.resume(returning: nil) + } + } + } + + private func loadDataValue(from provider: NSItemProvider, typeIdentifier: String) async -> Data? { + await withCheckedContinuation { continuation in + provider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { data, _ in + continuation.resume(returning: data) + } + } + } +} diff --git a/apps/ios/Signing.xcconfig b/apps/ios/Signing.xcconfig new file mode 100644 index 0000000000000..f942fc0224ff6 --- /dev/null +++ b/apps/ios/Signing.xcconfig @@ -0,0 +1,17 @@ +// Default signing values for shared/repo builds. +// Auto-selected local team overrides live in .local-signing.xcconfig (git-ignored). +// Manual local overrides can go in LocalSigning.xcconfig (git-ignored). + +OPENCLAW_CODE_SIGN_STYLE = Manual +OPENCLAW_DEVELOPMENT_TEAM = Y5PE65HELJ + +OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios +OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.ios.share + +OPENCLAW_APP_PROFILE = ai.openclaw.ios Development +OPENCLAW_SHARE_PROFILE = ai.openclaw.ios.share Development + +// Keep local includes after defaults: xcconfig is evaluated top-to-bottom, +// so later assignments in local files override the defaults above. +#include? ".local-signing.xcconfig" +#include? "LocalSigning.xcconfig" diff --git a/apps/ios/Sources/Calendar/CalendarService.swift b/apps/ios/Sources/Calendar/CalendarService.swift index 9ac83dd39285b..94b2d9ea3f5ff 100644 --- a/apps/ios/Sources/Calendar/CalendarService.swift +++ b/apps/ios/Sources/Calendar/CalendarService.swift @@ -6,7 +6,7 @@ final class CalendarService: CalendarServicing { func events(params: OpenClawCalendarEventsParams) async throws -> OpenClawCalendarEventsPayload { let store = EKEventStore() let status = EKEventStore.authorizationStatus(for: .event) - let authorized = await Self.ensureAuthorization(store: store, status: status) + let authorized = EventKitAuthorization.allowsRead(status: status) guard authorized else { throw NSError(domain: "Calendar", code: 1, userInfo: [ NSLocalizedDescriptionKey: "CALENDAR_PERMISSION_REQUIRED: grant Calendar permission", @@ -39,7 +39,7 @@ final class CalendarService: CalendarServicing { func add(params: OpenClawCalendarAddParams) async throws -> OpenClawCalendarAddPayload { let store = EKEventStore() let status = EKEventStore.authorizationStatus(for: .event) - let authorized = await Self.ensureWriteAuthorization(store: store, status: status) + let authorized = EventKitAuthorization.allowsWrite(status: status) guard authorized else { throw NSError(domain: "Calendar", code: 2, userInfo: [ NSLocalizedDescriptionKey: "CALENDAR_PERMISSION_REQUIRED: grant Calendar permission", @@ -95,38 +95,6 @@ final class CalendarService: CalendarServicing { return OpenClawCalendarAddPayload(event: payload) } - private static func ensureAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool { - switch status { - case .authorized: - return true - case .notDetermined: - // Don’t prompt during node.invoke; prompts block the invoke and lead to timeouts. - return false - case .restricted, .denied: - return false - case .fullAccess: - return true - case .writeOnly: - return false - @unknown default: - return false - } - } - - private static func ensureWriteAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool { - switch status { - case .authorized, .fullAccess, .writeOnly: - return true - case .notDetermined: - // Don’t prompt during node.invoke; prompts block the invoke and lead to timeouts. - return false - case .restricted, .denied: - return false - @unknown default: - return false - } - } - private static func resolveCalendar( store: EKEventStore, calendarId: String?, diff --git a/apps/ios/Sources/Camera/CameraController.swift b/apps/ios/Sources/Camera/CameraController.swift index e76dbeeabb90e..1e9c10bc44c93 100644 --- a/apps/ios/Sources/Camera/CameraController.swift +++ b/apps/ios/Sources/Camera/CameraController.swift @@ -93,14 +93,10 @@ actor CameraController { } withExtendedLifetime(delegate) {} - let maxPayloadBytes = 5 * 1024 * 1024 - // Base64 inflates payloads by ~4/3; cap encoded bytes so the payload stays under 5MB (API limit). - let maxEncodedBytes = (maxPayloadBytes / 4) * 3 - let res = try JPEGTranscoder.transcodeToJPEG( - imageData: rawData, + let res = try PhotoCapture.transcodeJPEGForGateway( + rawData: rawData, maxWidthPx: maxWidth, - quality: quality, - maxBytes: maxEncodedBytes) + quality: quality) return ( format: format.rawValue, @@ -335,8 +331,8 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat func photoOutput( _ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, - error: Error?) - { + error: Error? + ) { guard !self.didResume else { return } self.didResume = true @@ -364,8 +360,8 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat func photoOutput( _ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, - error: Error?) - { + error: Error? + ) { guard let error else { return } guard !self.didResume else { return } self.didResume = true diff --git a/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift index 3c828551ada0b..9571839059d4f 100644 --- a/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift +++ b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift @@ -2,8 +2,10 @@ import OpenClawChatUI import OpenClawKit import OpenClawProtocol import Foundation +import OSLog struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable { + private static let logger = Logger(subsystem: "ai.openclaw", category: "ios.chat.transport") private let gateway: GatewayNodeSession init(gateway: GatewayNodeSession) { @@ -33,10 +35,8 @@ struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable { } func setActiveSessionKey(_ sessionKey: String) async throws { - struct Subscribe: Codable { var sessionKey: String } - let data = try JSONEncoder().encode(Subscribe(sessionKey: sessionKey)) - let json = String(data: data, encoding: .utf8) - await self.gateway.sendEvent(event: "chat.subscribe", payloadJSON: json) + // Operator clients receive chat events without node-style subscriptions. + // (chat.subscribe is a node event, not an operator RPC method.) } func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload { @@ -54,6 +54,7 @@ struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable { idempotencyKey: String, attachments: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse { + Self.logger.info("chat.send start sessionKey=\(sessionKey, privacy: .public) len=\(message.count, privacy: .public) attachments=\(attachments.count, privacy: .public)") struct Params: Codable { var sessionKey: String var message: String @@ -72,8 +73,15 @@ struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable { idempotencyKey: idempotencyKey) let data = try JSONEncoder().encode(params) let json = String(data: data, encoding: .utf8) - let res = try await self.gateway.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 35) - return try JSONDecoder().decode(OpenClawChatSendResponse.self, from: res) + do { + let res = try await self.gateway.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 35) + let decoded = try JSONDecoder().decode(OpenClawChatSendResponse.self, from: res) + Self.logger.info("chat.send ok runId=\(decoded.runId, privacy: .public)") + return decoded + } catch { + Self.logger.error("chat.send failed \(error.localizedDescription, privacy: .public)") + throw error + } } func requestHealth(timeoutMs: Int) async throws -> Bool { diff --git a/apps/ios/Sources/EventKit/EventKitAuthorization.swift b/apps/ios/Sources/EventKit/EventKitAuthorization.swift new file mode 100644 index 0000000000000..c27e9a3efdef8 --- /dev/null +++ b/apps/ios/Sources/EventKit/EventKitAuthorization.swift @@ -0,0 +1,34 @@ +import EventKit + +enum EventKitAuthorization { + static func allowsRead(status: EKAuthorizationStatus) -> Bool { + switch status { + case .authorized, .fullAccess: + return true + case .writeOnly: + return false + case .notDetermined: + // Don’t prompt during node.invoke; prompts block the invoke and lead to timeouts. + return false + case .restricted, .denied: + return false + @unknown default: + return false + } + } + + static func allowsWrite(status: EKAuthorizationStatus) -> Bool { + switch status { + case .authorized, .fullAccess, .writeOnly: + return true + case .notDetermined: + // Don’t prompt during node.invoke; prompts block the invoke and lead to timeouts. + return false + case .restricted, .denied: + return false + @unknown default: + return false + } + } +} + diff --git a/apps/ios/Sources/Gateway/GatewayConnectionController.swift b/apps/ios/Sources/Gateway/GatewayConnectionController.swift index 995e2f36d048e..92abd996b72c2 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectionController.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionController.swift @@ -72,32 +72,55 @@ final class GatewayConnectionController { } } - func connect(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async { + func allowAutoConnectAgain() { + self.didAutoConnect = false + self.maybeAutoConnect() + } + + func restartDiscovery() { + self.discovery.stop() + self.didAutoConnect = false + self.discovery.start() + self.updateFromDiscovery() + } + + + /// Returns `nil` when a connect attempt was started, otherwise returns a user-facing error. + func connectWithDiagnostics(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async -> String? { await self.connectDiscoveredGateway(gateway) } private func connectDiscoveredGateway( - _ gateway: GatewayDiscoveryModel.DiscoveredGateway) async + _ gateway: GatewayDiscoveryModel.DiscoveredGateway) async -> String? { let instanceId = UserDefaults.standard.string(forKey: "node.instanceId")? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if instanceId.isEmpty { + return "Missing instanceId (node.instanceId). Try restarting the app." + } let token = GatewaySettingsStore.loadGatewayToken(instanceId: instanceId) let password = GatewaySettingsStore.loadGatewayPassword(instanceId: instanceId) // Resolve the service endpoint (SRV/A/AAAA). TXT is unauthenticated; do not route via TXT. - guard let target = await self.resolveServiceEndpoint(gateway.endpoint) else { return } + guard let target = await self.resolveServiceEndpoint(gateway.endpoint) else { + return "Failed to resolve the discovered gateway endpoint." + } let stableID = gateway.stableID // Discovery is a LAN operation; refuse unauthenticated plaintext connects. let tlsRequired = true let stored = GatewayTLSStore.loadFingerprint(stableID: stableID) - guard gateway.tlsEnabled || stored != nil else { return } + guard gateway.tlsEnabled || stored != nil else { + return "Discovered gateway is missing TLS and no trusted fingerprint is stored." + } if tlsRequired, stored == nil { guard let url = self.buildGatewayURL(host: target.host, port: target.port, useTLS: true) - else { return } - guard let fp = await self.probeTLSFingerprint(url: url) else { return } + else { return "Failed to build TLS URL for trust verification." } + guard let fp = await self.probeTLSFingerprint(url: url) else { + return "Failed to read TLS fingerprint from discovered gateway." + } self.pendingTrustConnect = (url: url, stableID: stableID, isManual: false) self.pendingTrustPrompt = TrustPrompt( stableID: stableID, @@ -107,7 +130,7 @@ final class GatewayConnectionController { fingerprintSha256: fp, isManual: false) self.appModel?.gatewayStatusText = "Verify gateway TLS fingerprint" - return + return nil } let tlsParams = stored.map { fp in @@ -118,7 +141,7 @@ final class GatewayConnectionController { host: target.host, port: target.port, useTLS: tlsParams?.required == true) - else { return } + else { return "Failed to build discovered gateway URL." } GatewaySettingsStore.saveLastGatewayConnectionDiscovered(stableID: stableID, useTLS: true) self.didAutoConnect = true self.startAutoConnect( @@ -127,6 +150,11 @@ final class GatewayConnectionController { tls: tlsParams, token: token, password: password) + return nil + } + + func connect(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async { + _ = await self.connectWithDiagnostics(gateway) } func connectManual(host: String, port: Int, useTLS: Bool) async { @@ -490,6 +518,125 @@ final class GatewayConnectionController { } } + private func resolveHostPortFromBonjourEndpoint(_ endpoint: NWEndpoint) async -> (host: String, port: Int)? { + switch endpoint { + case let .hostPort(host, port): + return (host: host.debugDescription, port: Int(port.rawValue)) + case let .service(name, type, domain, _): + return await Self.resolveBonjourServiceToHostPort(name: name, type: type, domain: domain) + default: + return nil + } + } + + private static func resolveBonjourServiceToHostPort( + name: String, + type: String, + domain: String, + timeoutSeconds: TimeInterval = 3.0 + ) async -> (host: String, port: Int)? { + // NetService callbacks are delivered via a run loop. If we resolve from a thread without one, + // we can end up never receiving callbacks, which in turn leaks the continuation and leaves + // the UI stuck "connecting". Keep the whole lifecycle on the main run loop and always + // resume the continuation exactly once (timeout/cancel safe). + @MainActor + final class Resolver: NSObject, @preconcurrency NetServiceDelegate { + private var cont: CheckedContinuation<(host: String, port: Int)?, Never>? + private let service: NetService + private var timeoutTask: Task? + private var finished = false + + init(cont: CheckedContinuation<(host: String, port: Int)?, Never>, service: NetService) { + self.cont = cont + self.service = service + super.init() + } + + func start(timeoutSeconds: TimeInterval) { + self.service.delegate = self + self.service.schedule(in: .main, forMode: .default) + + // NetService has its own timeout, but we keep a manual one as a backstop in case + // callbacks never arrive (e.g. local network permission issues). + self.timeoutTask = Task { @MainActor [weak self] in + guard let self else { return } + let ns = UInt64(max(0.1, timeoutSeconds) * 1_000_000_000) + try? await Task.sleep(nanoseconds: ns) + self.finish(nil) + } + + self.service.resolve(withTimeout: timeoutSeconds) + } + + func netServiceDidResolveAddress(_ sender: NetService) { + self.finish(Self.extractHostPort(sender)) + } + + func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) { + _ = errorDict // currently best-effort; callers surface a generic failure + self.finish(nil) + } + + private func finish(_ result: (host: String, port: Int)?) { + guard !self.finished else { return } + self.finished = true + + self.timeoutTask?.cancel() + self.timeoutTask = nil + + self.service.stop() + self.service.remove(from: .main, forMode: .default) + + let c = self.cont + self.cont = nil + c?.resume(returning: result) + } + + private static func extractHostPort(_ svc: NetService) -> (host: String, port: Int)? { + let port = svc.port + + if let host = svc.hostName?.trimmingCharacters(in: .whitespacesAndNewlines), !host.isEmpty { + return (host: host, port: port) + } + + guard let addrs = svc.addresses else { return nil } + for addrData in addrs { + let host = addrData.withUnsafeBytes { ptr -> String? in + guard let base = ptr.baseAddress, !ptr.isEmpty else { return nil } + var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + + let rc = getnameinfo( + base.assumingMemoryBound(to: sockaddr.self), + socklen_t(ptr.count), + &buffer, + socklen_t(buffer.count), + nil, + 0, + NI_NUMERICHOST) + guard rc == 0 else { return nil } + return String(cString: buffer) + } + + if let host, !host.isEmpty { + return (host: host, port: port) + } + } + + return nil + } + } + + return await withCheckedContinuation { cont in + Task { @MainActor in + let service = NetService(domain: domain, type: type, name: name) + let resolver = Resolver(cont: cont, service: service) + // Keep the resolver alive for the lifetime of the NetService resolve. + objc_setAssociatedObject(service, "resolver", resolver, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + resolver.start(timeoutSeconds: timeoutSeconds) + } + } + } + private func buildGatewayURL(host: String, port: Int, useTLS: Bool) -> URL? { let scheme = useTLS ? "wss" : "ws" var components = URLComponents() @@ -582,6 +729,9 @@ final class GatewayConnectionController { if locationMode != .off { caps.append(OpenClawCapability.location.rawValue) } caps.append(OpenClawCapability.device.rawValue) + if WatchMessagingService.isSupportedOnDevice() { + caps.append(OpenClawCapability.watch.rawValue) + } caps.append(OpenClawCapability.photos.rawValue) caps.append(OpenClawCapability.contacts.rawValue) caps.append(OpenClawCapability.calendar.rawValue) @@ -625,6 +775,10 @@ final class GatewayConnectionController { commands.append(OpenClawDeviceCommand.status.rawValue) commands.append(OpenClawDeviceCommand.info.rawValue) } + if caps.contains(OpenClawCapability.watch.rawValue) { + commands.append(OpenClawWatchCommand.status.rawValue) + commands.append(OpenClawWatchCommand.notify.rawValue) + } if caps.contains(OpenClawCapability.photos.rawValue) { commands.append(OpenClawPhotosCommand.latest.rawValue) } @@ -675,6 +829,12 @@ final class GatewayConnectionController { permissions["motion"] = motionStatus == .authorized || pedometerStatus == .authorized + let watchStatus = WatchMessagingService.currentStatusSnapshot() + permissions["watchSupported"] = watchStatus.supported + permissions["watchPaired"] = watchStatus.paired + permissions["watchAppInstalled"] = watchStatus.appInstalled + permissions["watchReachable"] = watchStatus.reachable + return permissions } diff --git a/apps/ios/Sources/Gateway/GatewayConnectionIssue.swift b/apps/ios/Sources/Gateway/GatewayConnectionIssue.swift new file mode 100644 index 0000000000000..56d490e226bab --- /dev/null +++ b/apps/ios/Sources/Gateway/GatewayConnectionIssue.swift @@ -0,0 +1,71 @@ +import Foundation + +enum GatewayConnectionIssue: Equatable { + case none + case tokenMissing + case unauthorized + case pairingRequired(requestId: String?) + case network + case unknown(String) + + var requestId: String? { + if case let .pairingRequired(requestId) = self { + return requestId + } + return nil + } + + var needsAuthToken: Bool { + switch self { + case .tokenMissing, .unauthorized: + return true + default: + return false + } + } + + var needsPairing: Bool { + if case .pairingRequired = self { return true } + return false + } + + static func detect(from statusText: String) -> Self { + let trimmed = statusText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return .none } + let lower = trimmed.lowercased() + + if lower.contains("pairing required") || lower.contains("not_paired") || lower.contains("not paired") { + return .pairingRequired(requestId: self.extractRequestId(from: trimmed)) + } + if lower.contains("gateway token missing") { + return .tokenMissing + } + if lower.contains("unauthorized") { + return .unauthorized + } + if lower.contains("connection refused") || + lower.contains("timed out") || + lower.contains("network is unreachable") || + lower.contains("cannot find host") || + lower.contains("could not connect") + { + return .network + } + if lower.hasPrefix("gateway error:") { + return .unknown(trimmed) + } + return .none + } + + private static func extractRequestId(from statusText: String) -> String? { + let marker = "requestId:" + guard let range = statusText.range(of: marker) else { return nil } + let suffix = statusText[range.upperBound...] + let trimmed = suffix.trimmingCharacters(in: .whitespacesAndNewlines) + let end = trimmed.firstIndex(where: { ch in + ch == ")" || ch.isWhitespace || ch == "," || ch == ";" + }) ?? trimmed.endIndex + let id = String(trimmed[.. String { diff --git a/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift b/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift new file mode 100644 index 0000000000000..eac92df71e886 --- /dev/null +++ b/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift @@ -0,0 +1,113 @@ +import SwiftUI + +struct GatewayQuickSetupSheet: View { + @Environment(NodeAppModel.self) private var appModel + @Environment(GatewayConnectionController.self) private var gatewayController + @Environment(\.dismiss) private var dismiss + + @AppStorage("onboarding.quickSetupDismissed") private var quickSetupDismissed: Bool = false + @State private var connecting: Bool = false + @State private var connectError: String? + + var body: some View { + NavigationStack { + VStack(alignment: .leading, spacing: 16) { + Text("Connect to a Gateway?") + .font(.title2.bold()) + + if let candidate = self.bestCandidate { + VStack(alignment: .leading, spacing: 6) { + Text(verbatim: candidate.name) + .font(.headline) + Text(verbatim: candidate.debugID) + .font(.footnote) + .foregroundStyle(.secondary) + + VStack(alignment: .leading, spacing: 2) { + // Use verbatim strings so Bonjour-provided values can't be interpreted as + // localized format strings (which can crash with Objective-C exceptions). + Text(verbatim: "Discovery: \(self.gatewayController.discoveryStatusText)") + Text(verbatim: "Status: \(self.appModel.gatewayStatusText)") + Text(verbatim: "Node: \(self.appModel.nodeStatusText)") + Text(verbatim: "Operator: \(self.appModel.operatorStatusText)") + } + .font(.footnote) + .foregroundStyle(.secondary) + } + .padding(12) + .background(.thinMaterial) + .clipShape(RoundedRectangle(cornerRadius: 14)) + + Button { + self.connectError = nil + self.connecting = true + Task { + let err = await self.gatewayController.connectWithDiagnostics(candidate) + await MainActor.run { + self.connecting = false + self.connectError = err + // If we kicked off a connect, leave the sheet up so the user can see status evolve. + } + } + } label: { + Group { + if self.connecting { + HStack(spacing: 8) { + ProgressView().progressViewStyle(.circular) + Text("Connecting…") + } + } else { + Text("Connect") + } + } + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .disabled(self.connecting) + + if let connectError { + Text(connectError) + .font(.footnote) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } + + Button { + self.dismiss() + } label: { + Text("Not now") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .disabled(self.connecting) + + Toggle("Don’t show this again", isOn: self.$quickSetupDismissed) + .padding(.top, 4) + } else { + Text("No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled.") + .foregroundStyle(.secondary) + } + + Spacer() + } + .padding() + .navigationTitle("Quick Setup") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + self.quickSetupDismissed = true + self.dismiss() + } label: { + Text("Close") + } + } + } + } + } + + private var bestCandidate: GatewayDiscoveryModel.DiscoveredGateway? { + // Prefer whatever discovery says is first; the list is already name-sorted. + self.gatewayController.gateways.first + } +} diff --git a/apps/ios/Sources/Gateway/GatewaySettingsStore.swift b/apps/ios/Sources/Gateway/GatewaySettingsStore.swift index 11fbbc5f0ca56..3ff57ad2e6746 100644 --- a/apps/ios/Sources/Gateway/GatewaySettingsStore.swift +++ b/apps/ios/Sources/Gateway/GatewaySettingsStore.swift @@ -4,6 +4,7 @@ import os enum GatewaySettingsStore { private static let gatewayService = "ai.openclaw.gateway" private static let nodeService = "ai.openclaw.node" + private static let talkService = "ai.openclaw.talk" private static let instanceIdDefaultsKey = "node.instanceId" private static let preferredGatewayStableIDDefaultsKey = "gateway.preferredStableID" @@ -24,6 +25,7 @@ enum GatewaySettingsStore { private static let instanceIdAccount = "instanceId" private static let preferredGatewayStableIDAccount = "preferredStableID" private static let lastDiscoveredGatewayStableIDAccount = "lastDiscoveredStableID" + private static let talkElevenLabsApiKeyAccount = "elevenlabs.apiKey" static func bootstrapPersistence() { self.ensureStableInstanceID() @@ -143,6 +145,27 @@ enum GatewaySettingsStore { case discovered } + static func loadTalkElevenLabsApiKey() -> String? { + let value = KeychainStore.loadString( + service: self.talkService, + account: self.talkElevenLabsApiKeyAccount)? + .trimmingCharacters(in: .whitespacesAndNewlines) + if value?.isEmpty == false { return value } + return nil + } + + static func saveTalkElevenLabsApiKey(_ apiKey: String?) { + let trimmed = apiKey?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if trimmed.isEmpty { + _ = KeychainStore.delete(service: self.talkService, account: self.talkElevenLabsApiKeyAccount) + return + } + _ = KeychainStore.saveString( + trimmed, + service: self.talkService, + account: self.talkElevenLabsApiKeyAccount) + } + static func saveLastGatewayConnectionManual(host: String, port: Int, useTLS: Bool, stableID: String) { let defaults = UserDefaults.standard defaults.set(LastGatewayKind.manual.rawValue, forKey: self.lastGatewayKindDefaultsKey) @@ -184,6 +207,25 @@ enum GatewaySettingsStore { return .manual(host: host, port: port, useTLS: useTLS, stableID: stableID) } + static func clearLastGatewayConnection(defaults: UserDefaults = .standard) { + defaults.removeObject(forKey: self.lastGatewayKindDefaultsKey) + defaults.removeObject(forKey: self.lastGatewayHostDefaultsKey) + defaults.removeObject(forKey: self.lastGatewayPortDefaultsKey) + defaults.removeObject(forKey: self.lastGatewayTlsDefaultsKey) + defaults.removeObject(forKey: self.lastGatewayStableIDDefaultsKey) + } + + static func deleteGatewayCredentials(instanceId: String) { + let trimmed = instanceId.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return } + _ = KeychainStore.delete( + service: self.gatewayService, + account: self.gatewayTokenAccount(instanceId: trimmed)) + _ = KeychainStore.delete( + service: self.gatewayService, + account: self.gatewayPasswordAccount(instanceId: trimmed)) + } + static func loadGatewayClientIdOverride(stableID: String) -> String? { let trimmedID = stableID.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedID.isEmpty else { return nil } diff --git a/apps/ios/Sources/Gateway/GatewaySetupCode.swift b/apps/ios/Sources/Gateway/GatewaySetupCode.swift new file mode 100644 index 0000000000000..8ccbab42da73a --- /dev/null +++ b/apps/ios/Sources/Gateway/GatewaySetupCode.swift @@ -0,0 +1,42 @@ +import Foundation + +struct GatewaySetupPayload: Codable { + var url: String? + var host: String? + var port: Int? + var tls: Bool? + var token: String? + var password: String? +} + +enum GatewaySetupCode { + static func decode(raw: String) -> GatewaySetupPayload? { + if let payload = decodeFromJSON(raw) { + return payload + } + if let decoded = decodeBase64Payload(raw), + let payload = decodeFromJSON(decoded) + { + return payload + } + return nil + } + + private static func decodeFromJSON(_ json: String) -> GatewaySetupPayload? { + guard let data = json.data(using: .utf8) else { return nil } + return try? JSONDecoder().decode(GatewaySetupPayload.self, from: data) + } + + private static func decodeBase64Payload(_ raw: String) -> String? { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + let normalized = trimmed + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + let padding = normalized.count % 4 + let padded = padding == 0 ? normalized : normalized + String(repeating: "=", count: 4 - padding) + guard let data = Data(base64Encoded: padded) else { return nil } + return String(data: data, encoding: .utf8) + } +} + diff --git a/apps/ios/Sources/Gateway/GatewayTrustPromptAlert.swift b/apps/ios/Sources/Gateway/GatewayTrustPromptAlert.swift index f117ad9ea46f0..eff6b71bad543 100644 --- a/apps/ios/Sources/Gateway/GatewayTrustPromptAlert.swift +++ b/apps/ios/Sources/Gateway/GatewayTrustPromptAlert.swift @@ -6,10 +6,10 @@ struct GatewayTrustPromptAlert: ViewModifier { private var promptBinding: Binding { Binding( get: { self.gatewayController.pendingTrustPrompt }, - set: { newValue in - if newValue == nil { - self.gatewayController.clearPendingTrustPrompt() - } + set: { _ in + // Keep pending trust state until explicit user action. + // `alert(item:)` may set the binding to nil during dismissal, which can race with + // the button handler and cause accept to no-op. }) } @@ -39,4 +39,3 @@ extension View { self.modifier(GatewayTrustPromptAlert()) } } - diff --git a/apps/ios/Sources/Gateway/TCPProbe.swift b/apps/ios/Sources/Gateway/TCPProbe.swift new file mode 100644 index 0000000000000..e22da96298f86 --- /dev/null +++ b/apps/ios/Sources/Gateway/TCPProbe.swift @@ -0,0 +1,43 @@ +import Foundation +import Network +import os + +enum TCPProbe { + static func probe(host: String, port: Int, timeoutSeconds: Double, queueLabel: String) async -> Bool { + guard port >= 1, port <= 65535 else { return false } + guard let nwPort = NWEndpoint.Port(rawValue: UInt16(port)) else { return false } + + let endpointHost = NWEndpoint.Host(host) + let connection = NWConnection(host: endpointHost, port: nwPort, using: .tcp) + + return await withCheckedContinuation { cont in + let queue = DispatchQueue(label: queueLabel) + let finished = OSAllocatedUnfairLock(initialState: false) + let finish: @Sendable (Bool) -> Void = { ok in + let shouldResume = finished.withLock { flag -> Bool in + if flag { return false } + flag = true + return true + } + guard shouldResume else { return } + connection.cancel() + cont.resume(returning: ok) + } + + connection.stateUpdateHandler = { state in + switch state { + case .ready: + finish(true) + case .failed, .cancelled: + finish(false) + default: + break + } + } + + connection.start(queue: queue) + queue.asyncAfter(deadline: .now() + timeoutSeconds) { finish(false) } + } + } +} + diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index fe3c9ba4ed8ed..fe086049a8f3c 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -17,13 +17,24 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - APPL - CFBundleShortVersionString - 2026.2.13 - CFBundleVersion - 20260213 - NSAppTransportSecurity + APPL + CFBundleShortVersionString + 2026.2.19 + CFBundleURLTypes + + CFBundleURLName + ai.openclaw.ios + CFBundleURLSchemes + + openclaw + + + + CFBundleVersion + 20260219 + NSAppTransportSecurity + NSAllowsArbitraryLoadsInWebContent @@ -51,6 +62,7 @@ UIBackgroundModes audio + remote-notification UILaunchScreen diff --git a/apps/ios/Sources/Location/LocationService.swift b/apps/ios/Sources/Location/LocationService.swift index 99265d02e893f..f1f0f69ed7fa4 100644 --- a/apps/ios/Sources/Location/LocationService.swift +++ b/apps/ios/Sources/Location/LocationService.swift @@ -12,6 +12,10 @@ final class LocationService: NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() private var authContinuation: CheckedContinuation? private var locationContinuation: CheckedContinuation? + private var updatesContinuation: AsyncStream.Continuation? + private var isStreaming = false + private var significantLocationCallback: (@Sendable (CLLocation) -> Void)? + private var isMonitoringSignificantChanges = false override init() { super.init() @@ -104,6 +108,56 @@ final class LocationService: NSObject, CLLocationManagerDelegate { } } + func startLocationUpdates( + desiredAccuracy: OpenClawLocationAccuracy, + significantChangesOnly: Bool) -> AsyncStream + { + self.stopLocationUpdates() + + self.manager.desiredAccuracy = Self.accuracyValue(desiredAccuracy) + self.manager.pausesLocationUpdatesAutomatically = true + self.manager.allowsBackgroundLocationUpdates = true + + self.isStreaming = true + if significantChangesOnly { + self.manager.startMonitoringSignificantLocationChanges() + } else { + self.manager.startUpdatingLocation() + } + + return AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in + self.updatesContinuation = continuation + continuation.onTermination = { @Sendable _ in + Task { @MainActor in + self.stopLocationUpdates() + } + } + } + } + + func stopLocationUpdates() { + guard self.isStreaming else { return } + self.isStreaming = false + self.manager.stopUpdatingLocation() + self.manager.stopMonitoringSignificantLocationChanges() + self.updatesContinuation?.finish() + self.updatesContinuation = nil + } + + func startMonitoringSignificantLocationChanges(onUpdate: @escaping @Sendable (CLLocation) -> Void) { + self.significantLocationCallback = onUpdate + guard !self.isMonitoringSignificantChanges else { return } + self.isMonitoringSignificantChanges = true + self.manager.startMonitoringSignificantLocationChanges() + } + + func stopMonitoringSignificantLocationChanges() { + guard self.isMonitoringSignificantChanges else { return } + self.isMonitoringSignificantChanges = false + self.significantLocationCallback = nil + self.manager.stopMonitoringSignificantLocationChanges() + } + nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = manager.authorizationStatus Task { @MainActor in @@ -117,12 +171,22 @@ final class LocationService: NSObject, CLLocationManagerDelegate { nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let locs = locations Task { @MainActor in - guard let cont = self.locationContinuation else { return } - self.locationContinuation = nil - if let latest = locs.last { - cont.resume(returning: latest) - } else { - cont.resume(throwing: Error.unavailable) + // Resolve the one-shot continuation first (if any). + if let cont = self.locationContinuation { + self.locationContinuation = nil + if let latest = locs.last { + cont.resume(returning: latest) + } else { + cont.resume(throwing: Error.unavailable) + } + // Don't return — also forward to significant-change callback below + // so both consumers receive updates when both are active. + } + if let callback = self.significantLocationCallback, let latest = locs.last { + callback(latest) + } + if let latest = locs.last, let updates = self.updatesContinuation { + updates.yield(latest) } } } diff --git a/apps/ios/Sources/Location/SignificantLocationMonitor.swift b/apps/ios/Sources/Location/SignificantLocationMonitor.swift new file mode 100644 index 0000000000000..f12a157dc69b6 --- /dev/null +++ b/apps/ios/Sources/Location/SignificantLocationMonitor.swift @@ -0,0 +1,38 @@ +import CoreLocation +import Foundation +import OpenClawKit + +/// Monitors significant location changes and pushes `location.update` +/// events to the gateway so the severance hook can determine whether +/// the user is at their configured work location. +@MainActor +enum SignificantLocationMonitor { + static func startIfNeeded( + locationService: any LocationServicing, + locationMode: OpenClawLocationMode, + gateway: GatewayNodeSession + ) { + guard locationMode == .always else { return } + let status = locationService.authorizationStatus() + guard status == .authorizedAlways else { return } + locationService.startMonitoringSignificantLocationChanges { location in + struct Payload: Codable { + var lat: Double + var lon: Double + var accuracyMeters: Double + var source: String? + } + let payload = Payload( + lat: location.coordinate.latitude, + lon: location.coordinate.longitude, + accuracyMeters: location.horizontalAccuracy, + source: "ios-significant-location") + guard let data = try? JSONEncoder().encode(payload), + let json = String(data: data, encoding: .utf8) + else { return } + Task { @MainActor in + await gateway.sendEvent(event: "location.update", payloadJSON: json) + } + } + } +} diff --git a/apps/ios/Sources/Model/NodeAppModel+Canvas.swift b/apps/ios/Sources/Model/NodeAppModel+Canvas.swift index 372f8361d3059..e8dce2cd30cf6 100644 --- a/apps/ios/Sources/Model/NodeAppModel+Canvas.swift +++ b/apps/ios/Sources/Model/NodeAppModel+Canvas.swift @@ -61,37 +61,10 @@ extension NodeAppModel { private static func probeTCP(url: URL, timeoutSeconds: Double) async -> Bool { guard let host = url.host, !host.isEmpty else { return false } let portInt = url.port ?? ((url.scheme ?? "").lowercased() == "wss" ? 443 : 80) - guard portInt >= 1, portInt <= 65535 else { return false } - guard let nwPort = NWEndpoint.Port(rawValue: UInt16(portInt)) else { return false } - - let endpointHost = NWEndpoint.Host(host) - let connection = NWConnection(host: endpointHost, port: nwPort, using: .tcp) - return await withCheckedContinuation { cont in - let queue = DispatchQueue(label: "a2ui.preflight") - let finished = OSAllocatedUnfairLock(initialState: false) - let finish: @Sendable (Bool) -> Void = { ok in - let shouldResume = finished.withLock { flag -> Bool in - if flag { return false } - flag = true - return true - } - guard shouldResume else { return } - connection.cancel() - cont.resume(returning: ok) - } - - connection.stateUpdateHandler = { state in - switch state { - case .ready: - finish(true) - case .failed, .cancelled: - finish(false) - default: - break - } - } - connection.start(queue: queue) - queue.asyncAfter(deadline: .now() + timeoutSeconds) { finish(false) } - } + return await TCPProbe.probe( + host: host, + port: portInt, + timeoutSeconds: timeoutSeconds, + queueLabel: "a2ui.preflight") } } diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 0ca521ccc60ea..1d09251dd76e5 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -2,6 +2,7 @@ import OpenClawChatUI import OpenClawKit import OpenClawProtocol import Observation +import os import SwiftUI import UIKit import UserNotifications @@ -10,7 +11,6 @@ import UserNotifications private struct NotificationCallError: Error, Sendable { let message: String } - // Ensures notification requests return promptly even if the system prompt blocks. private final class NotificationInvokeLatch: @unchecked Sendable { private let lock = NSLock() @@ -37,10 +37,11 @@ private final class NotificationInvokeLatch: @unchecked Sendable { cont?.resume(returning: response) } } - @MainActor @Observable final class NodeAppModel { + private let deepLinkLogger = Logger(subsystem: "ai.openclaw.ios", category: "DeepLink") + private let pushWakeLogger = Logger(subsystem: "ai.openclaw.ios", category: "PushWake") enum CameraHUDKind { case photo case recording @@ -53,35 +54,24 @@ final class NodeAppModel { private let camera: any CameraServicing private let screenRecorder: any ScreenRecordingServicing var gatewayStatusText: String = "Offline" + var nodeStatusText: String = "Offline" + var operatorStatusText: String = "Offline" var gatewayServerName: String? var gatewayRemoteAddress: String? var connectedGatewayID: String? var gatewayAutoReconnectEnabled: Bool = true + // When the gateway requires pairing approval, we pause reconnect churn and show a stable UX. + // Reconnect loops (both our own and the underlying WebSocket watchdog) can otherwise generate + // multiple pending requests and cause the onboarding UI to "flip-flop". + var gatewayPairingPaused: Bool = false + var gatewayPairingRequestId: String? var seamColorHex: String? private var mainSessionBaseKey: String = "main" var selectedAgentId: String? var gatewayDefaultAgentId: String? var gatewayAgents: [AgentSummary] = [] - - var mainSessionKey: String { - let base = SessionKey.normalizeMainKey(self.mainSessionBaseKey) - let agentId = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - if agentId.isEmpty || (!defaultId.isEmpty && agentId == defaultId) { return base } - return SessionKey.makeAgentSessionKey(agentId: agentId, baseKey: base) - } - - var activeAgentName: String { - let agentId = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - let resolvedId = agentId.isEmpty ? defaultId : agentId - if resolvedId.isEmpty { return "Main" } - if let match = self.gatewayAgents.first(where: { $0.id == resolvedId }) { - let name = (match.name ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - return name.isEmpty ? match.id : name - } - return resolvedId - } + var lastShareEventText: String = "No share events yet." + var openChatRequestID: Int = 0 // Primary "node" connection: used for device capabilities and node.invoke requests. private let nodeGateway = GatewayNodeSession() @@ -104,16 +94,22 @@ final class NodeAppModel { private let calendarService: any CalendarServicing private let remindersService: any RemindersServicing private let motionService: any MotionServicing + private let watchMessagingService: any WatchMessagingServicing var lastAutoA2uiURL: String? private var pttVoiceWakeSuspended = false private var talkVoiceWakeSuspended = false private var backgroundVoiceWakeSuspended = false private var backgroundTalkSuspended = false + private var backgroundTalkKeptActive = false private var backgroundedAt: Date? private var reconnectAfterBackgroundArmed = false private var gatewayConnected = false private var operatorConnected = false + private var shareDeliveryChannel: String? + private var shareDeliveryTo: String? + private var apnsDeviceTokenHex: String? + private var apnsLastRegisteredTokenHex: String? var gatewaySession: GatewayNodeSession { self.nodeGateway } var operatorSession: GatewayNodeSession { self.operatorGateway } private(set) var activeGatewayConnectConfig: GatewayConnectConfig? @@ -135,6 +131,7 @@ final class NodeAppModel { calendarService: any CalendarServicing = CalendarService(), remindersService: any RemindersServicing = RemindersService(), motionService: any MotionServicing = MotionService(), + watchMessagingService: any WatchMessagingServicing = WatchMessagingService(), talkMode: TalkModeManager = TalkModeManager()) { self.screen = screen @@ -148,7 +145,9 @@ final class NodeAppModel { self.calendarService = calendarService self.remindersService = remindersService self.motionService = motionService + self.watchMessagingService = watchMessagingService self.talkMode = talkMode + self.apnsDeviceTokenHex = UserDefaults.standard.string(forKey: Self.apnsDeviceTokenUserDefaultsKey) GatewayDiagnostics.bootstrap() self.voiceWake.configure { [weak self] cmd in @@ -164,6 +163,7 @@ final class NodeAppModel { let enabled = UserDefaults.standard.bool(forKey: "voiceWake.enabled") self.voiceWake.setEnabled(enabled) self.talkMode.attachGateway(self.operatorGateway) + self.refreshLastShareEventFromRelay() let talkEnabled = UserDefaults.standard.bool(forKey: "talk.enabled") // Route through the coordinator so VoiceWake and Talk don't fight over the microphone. self.setTalkEnabled(talkEnabled) @@ -264,15 +264,18 @@ final class NodeAppModel { func setScenePhase(_ phase: ScenePhase) { + let keepTalkActive = UserDefaults.standard.bool(forKey: "talk.background.enabled") switch phase { case .background: self.isBackgrounded = true self.stopGatewayHealthMonitor() self.backgroundedAt = Date() self.reconnectAfterBackgroundArmed = true - // Be conservative: release the mic when the app backgrounds. + // Release voice wake mic in background. self.backgroundVoiceWakeSuspended = self.voiceWake.suspendForExternalAudioCapture() - self.backgroundTalkSuspended = self.talkMode.suspendForBackground() + let shouldKeepTalkActive = keepTalkActive && self.talkMode.isEnabled + self.backgroundTalkKeptActive = shouldKeepTalkActive + self.backgroundTalkSuspended = self.talkMode.suspendForBackground(keepActive: shouldKeepTalkActive) case .active, .inactive: self.isBackgrounded = false if self.operatorConnected { @@ -284,8 +287,12 @@ final class NodeAppModel { Task { [weak self] in guard let self else { return } let suspended = await MainActor.run { self.backgroundTalkSuspended } - await MainActor.run { self.backgroundTalkSuspended = false } - await self.talkMode.resumeAfterBackground(wasSuspended: suspended) + let keptActive = await MainActor.run { self.backgroundTalkKeptActive } + await MainActor.run { + self.backgroundTalkSuspended = false + self.backgroundTalkKeptActive = false + } + await self.talkMode.resumeAfterBackground(wasSuspended: suspended, wasKeptActive: keptActive) } } if phase == .active, self.reconnectAfterBackgroundArmed { @@ -340,6 +347,7 @@ final class NodeAppModel { } func setTalkEnabled(_ enabled: Bool) { + UserDefaults.standard.set(enabled, forKey: "talk.enabled") if enabled { // Voice wake holds the microphone continuously; talk mode needs exclusive access for STT. // When talk is enabled from the UI, prioritize talk and pause voice wake. @@ -351,6 +359,11 @@ final class NodeAppModel { self.talkVoiceWakeSuspended = false } self.talkMode.setEnabled(enabled) + Task { [weak self] in + await self?.pushTalkModeToGateway( + enabled: enabled, + phase: enabled ? "enabled" : "disabled") + } } func requestLocationPermissions(mode: OpenClawLocationMode) async -> Bool { @@ -380,6 +393,14 @@ final class NodeAppModel { } private static let defaultSeamColor = Color(red: 79 / 255.0, green: 122 / 255.0, blue: 154 / 255.0) + private static let apnsDeviceTokenUserDefaultsKey = "push.apns.deviceTokenHex" + private static var apnsEnvironment: String { +#if DEBUG + "sandbox" +#else + "production" +#endif + } private static func color(fromHex raw: String?) -> Color? { let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines) @@ -447,6 +468,16 @@ final class NodeAppModel { GatewaySettingsStore.saveGatewaySelectedAgentId(stableID: stableID, agentId: self.selectedAgentId) } self.talkMode.updateMainSessionKey(self.mainSessionKey) + if let relay = ShareGatewayRelaySettings.loadConfig() { + ShareGatewayRelaySettings.saveConfig( + ShareGatewayRelayConfig( + gatewayURLString: relay.gatewayURLString, + token: relay.token, + password: relay.password, + sessionKey: self.mainSessionKey, + deliveryChannel: self.shareDeliveryChannel, + deliveryTo: self.shareDeliveryTo)) + } } func setGlobalWakeWords(_ words: [String]) async { @@ -479,16 +510,49 @@ final class NodeAppModel { let stream = await self.operatorGateway.subscribeServerEvents(bufferingNewest: 200) for await evt in stream { if Task.isCancelled { return } - guard evt.event == "voicewake.changed" else { continue } guard let payload = evt.payload else { continue } - struct Payload: Decodable { var triggers: [String] } - guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue } - let triggers = VoiceWakePreferences.sanitizeTriggerWords(decoded.triggers) - VoiceWakePreferences.saveTriggerWords(triggers) + switch evt.event { + case "voicewake.changed": + struct Payload: Decodable { var triggers: [String] } + guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue } + let triggers = VoiceWakePreferences.sanitizeTriggerWords(decoded.triggers) + VoiceWakePreferences.saveTriggerWords(triggers) + case "talk.mode": + struct Payload: Decodable { + var enabled: Bool + var phase: String? + } + guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue } + self.applyTalkModeSync(enabled: decoded.enabled, phase: decoded.phase) + default: + continue + } } } } + private func applyTalkModeSync(enabled: Bool, phase: String?) { + _ = phase + guard self.talkMode.isEnabled != enabled else { return } + self.setTalkEnabled(enabled) + } + + private func pushTalkModeToGateway(enabled: Bool, phase: String?) async { + guard await self.isOperatorConnected() else { return } + struct TalkModePayload: Encodable { + var enabled: Bool + var phase: String? + } + let payload = TalkModePayload(enabled: enabled, phase: phase) + guard let data = try? JSONEncoder().encode(payload), + let json = String(data: data, encoding: .utf8) + else { return } + _ = try? await self.operatorGateway.request( + method: "talk.mode", + paramsJSON: json, + timeoutSeconds: 8) + } + private func startGatewayHealthMonitor() { self.gatewayHealthMonitorDisabled = false self.gatewayHealthMonitor.start( @@ -515,8 +579,11 @@ final class NodeAppModel { onFailure: { [weak self] _ in guard let self else { return } await self.operatorGateway.disconnect() + await self.nodeGateway.disconnect() await MainActor.run { self.operatorConnected = false + self.gatewayConnected = false + self.gatewayStatusText = "Reconnecting…" self.talkMode.updateGatewayConnected(false) } }) @@ -577,28 +644,41 @@ final class NodeAppModel { switch route { case let .agent(link): await self.handleAgentDeepLink(link, originalURL: url) + case .gateway: + break } } private func handleAgentDeepLink(_ link: AgentDeepLink, originalURL: URL) async { let message = link.message.trimmingCharacters(in: .whitespacesAndNewlines) guard !message.isEmpty else { return } + self.deepLinkLogger.info( + "agent deep link received messageChars=\(message.count) url=\(originalURL.absoluteString, privacy: .public)" + ) if message.count > 20000 { self.screen.errorText = "Deep link too large (message exceeds 20,000 characters)." + self.recordShareEvent("Rejected: message too large (\(message.count) chars).") return } guard await self.isGatewayConnected() else { self.screen.errorText = "Gateway not connected (cannot forward deep link)." + self.recordShareEvent("Failed: gateway not connected.") + self.deepLinkLogger.error("agent deep link rejected: gateway not connected") return } do { try await self.sendAgentRequest(link: link) self.screen.errorText = nil + self.recordShareEvent("Sent to gateway (\(message.count) chars).") + self.deepLinkLogger.info("agent deep link forwarded to gateway") + self.openChatRequestID &+= 1 } catch { self.screen.errorText = "Agent request failed: \(error.localizedDescription)" + self.recordShareEvent("Failed: \(error.localizedDescription)") + self.deepLinkLogger.error("agent deep link send failed: \(error.localizedDescription, privacy: .public)") } } @@ -1345,6 +1425,14 @@ private extension NodeAppModel { return try await self.handleDeviceInvoke(req) } + register([ + OpenClawWatchCommand.status.rawValue, + OpenClawWatchCommand.notify.rawValue, + ]) { [weak self] req in + guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } + return try await self.handleWatchInvoke(req) + } + register([OpenClawPhotosCommand.latest.rawValue]) { [weak self] req in guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } return try await self.handlePhotosInvoke(req) @@ -1395,14 +1483,67 @@ private extension NodeAppModel { return NodeCapabilityRouter(handlers: handlers) } + func handleWatchInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse { + switch req.command { + case OpenClawWatchCommand.status.rawValue: + let status = await self.watchMessagingService.status() + let payload = OpenClawWatchStatusPayload( + supported: status.supported, + paired: status.paired, + appInstalled: status.appInstalled, + reachable: status.reachable, + activationState: status.activationState) + let json = try Self.encodePayload(payload) + return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + case OpenClawWatchCommand.notify.rawValue: + let params = try Self.decodeParams(OpenClawWatchNotifyParams.self, from: req.paramsJSON) + let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines) + let body = params.body.trimmingCharacters(in: .whitespacesAndNewlines) + if title.isEmpty && body.isEmpty { + return BridgeInvokeResponse( + id: req.id, + ok: false, + error: OpenClawNodeError( + code: .invalidRequest, + message: "INVALID_REQUEST: empty watch notification")) + } + do { + let result = try await self.watchMessagingService.sendNotification( + id: req.id, + title: title, + body: body, + priority: params.priority) + let payload = OpenClawWatchNotifyPayload( + deliveredImmediately: result.deliveredImmediately, + queuedForDelivery: result.queuedForDelivery, + transport: result.transport) + let json = try Self.encodePayload(payload) + return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + } catch { + return BridgeInvokeResponse( + id: req.id, + ok: false, + error: OpenClawNodeError( + code: .unavailable, + message: error.localizedDescription)) + } + default: + return BridgeInvokeResponse( + id: req.id, + ok: false, + error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + } + } + func locationMode() -> OpenClawLocationMode { let raw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off" return OpenClawLocationMode(rawValue: raw) ?? .off } func isLocationPreciseEnabled() -> Bool { - if UserDefaults.standard.object(forKey: "location.preciseEnabled") == nil { return true } - return UserDefaults.standard.bool(forKey: "location.preciseEnabled") + // iOS settings now expose a single location mode control. + // Default location tool precision stays high unless a command explicitly requests balanced. + true } static func decodeParams(_ type: T.Type, from json: String?) throws -> T { @@ -1454,6 +1595,26 @@ private extension NodeAppModel { } extension NodeAppModel { + var mainSessionKey: String { + let base = SessionKey.normalizeMainKey(self.mainSessionBaseKey) + let agentId = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + if agentId.isEmpty || (!defaultId.isEmpty && agentId == defaultId) { return base } + return SessionKey.makeAgentSessionKey(agentId: agentId, baseKey: base) + } + + var activeAgentName: String { + let agentId = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let resolvedId = agentId.isEmpty ? defaultId : agentId + if resolvedId.isEmpty { return "Main" } + if let match = self.gatewayAgents.first(where: { $0.id == resolvedId }) { + let name = (match.name ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + return name.isEmpty ? match.id : name + } + return resolvedId + } + func connectToGateway( url: URL, gatewayStableID: String, @@ -1506,6 +1667,8 @@ extension NodeAppModel { func disconnectGateway() { self.gatewayAutoReconnectEnabled = false + self.gatewayPairingPaused = false + self.gatewayPairingRequestId = nil self.nodeGatewayTask?.cancel() self.nodeGatewayTask = nil self.operatorGatewayTask?.cancel() @@ -1528,6 +1691,7 @@ extension NodeAppModel { self.seamColorHex = nil self.mainSessionBaseKey = "main" self.talkMode.updateMainSessionKey(self.mainSessionKey) + ShareGatewayRelaySettings.clearConfig() self.showLocalCanvasOnDisconnect() } } @@ -1535,6 +1699,8 @@ extension NodeAppModel { private extension NodeAppModel { func prepareForGatewayConnect(url: URL, stableID: String) { self.gatewayAutoReconnectEnabled = true + self.gatewayPairingPaused = false + self.gatewayPairingRequestId = nil self.nodeGatewayTask?.cancel() self.operatorGatewayTask?.cancel() self.gatewayHealthMonitor.stop() @@ -1548,6 +1714,7 @@ private extension NodeAppModel { self.gatewayDefaultAgentId = nil self.gatewayAgents = [] self.selectedAgentId = GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: stableID) + self.apnsLastRegisteredTokenHex = nil } func startOperatorGatewayLoop( @@ -1564,6 +1731,14 @@ private extension NodeAppModel { guard let self else { return } var attempt = 0 while !Task.isCancelled { + if self.gatewayPairingPaused { + try? await Task.sleep(nanoseconds: 1_000_000_000) + continue + } + if !self.gatewayAutoReconnectEnabled { + try? await Task.sleep(nanoseconds: 1_000_000_000) + continue + } if await self.isOperatorConnected() { try? await Task.sleep(nanoseconds: 1_000_000_000) continue @@ -1592,6 +1767,7 @@ private extension NodeAppModel { "operator gateway connected host=\(url.host ?? "?") scheme=\(url.scheme ?? "?")") await self.refreshBrandingFromGateway() await self.refreshAgentsFromGateway() + await self.refreshShareRouteFromGateway() await self.startVoiceWakeSync() await MainActor.run { self.startGatewayHealthMonitor() } }, @@ -1639,8 +1815,17 @@ private extension NodeAppModel { var attempt = 0 var currentOptions = nodeOptions var didFallbackClientId = false + var pausedForPairingApproval = false while !Task.isCancelled { + if self.gatewayPairingPaused { + try? await Task.sleep(nanoseconds: 1_000_000_000) + continue + } + if !self.gatewayAutoReconnectEnabled { + try? await Task.sleep(nanoseconds: 1_000_000_000) + continue + } if await self.isGatewayConnected() { try? await Task.sleep(nanoseconds: 1_000_000_000) continue @@ -1669,12 +1854,28 @@ private extension NodeAppModel { self.screen.errorText = nil UserDefaults.standard.set(true, forKey: "gateway.autoconnect") } - GatewayDiagnostics.log( - "gateway connected host=\(url.host ?? "?") scheme=\(url.scheme ?? "?")") + let relayData = await MainActor.run { + ( + sessionKey: self.mainSessionKey, + deliveryChannel: self.shareDeliveryChannel, + deliveryTo: self.shareDeliveryTo + ) + } + ShareGatewayRelaySettings.saveConfig( + ShareGatewayRelayConfig( + gatewayURLString: url.absoluteString, + token: token, + password: password, + sessionKey: relayData.sessionKey, + deliveryChannel: relayData.deliveryChannel, + deliveryTo: relayData.deliveryTo)) + GatewayDiagnostics.log("gateway connected host=\(url.host ?? "?") scheme=\(url.scheme ?? "?")") if let addr = await self.nodeGateway.currentRemoteAddress() { await MainActor.run { self.gatewayRemoteAddress = addr } } await self.showA2UIOnConnectIfNeeded() + await self.onNodeGatewayConnected() + await MainActor.run { SignificantLocationMonitor.startIfNeeded(locationService: self.locationService, locationMode: self.locationMode(), gateway: self.nodeGateway) } }, onDisconnected: { [weak self] reason in guard let self else { return } @@ -1726,11 +1927,60 @@ private extension NodeAppModel { self.showLocalCanvasOnDisconnect() } GatewayDiagnostics.log("gateway connect error: \(error.localizedDescription)") + + // If auth is missing/rejected, pause reconnect churn until the user intervenes. + // Reconnect loops only spam the same failing handshake and make onboarding noisy. + let lower = error.localizedDescription.lowercased() + if lower.contains("unauthorized") || lower.contains("gateway token missing") { + await MainActor.run { + self.gatewayAutoReconnectEnabled = false + } + } + + // If pairing is required, stop reconnect churn. The user must approve the request + // on the gateway before another connect attempt will succeed, and retry loops can + // generate multiple pending requests. + if lower.contains("not_paired") || lower.contains("pairing required") { + let requestId: String? = { + // GatewayResponseError for connect decorates the message with `(requestId: ...)`. + // Keep this resilient since other layers may wrap the text. + let text = error.localizedDescription + guard let start = text.range(of: "(requestId: ")?.upperBound else { return nil } + guard let end = text[start...].firstIndex(of: ")") else { return nil } + let raw = String(text[start.. String? = { raw in + let value = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + return value.isEmpty ? nil : value + } + + do { + let data = try JSONEncoder().encode( + Params(includeGlobal: true, includeUnknown: false, limit: 80)) + guard let json = String(data: data, encoding: .utf8) else { return } + let response = try await self.operatorGateway.request( + method: "sessions.list", + paramsJSON: json, + timeoutSeconds: 10) + let decoded = try JSONDecoder().decode(SessionsListResult.self, from: response) + let currentKey = self.mainSessionKey + let sorted = decoded.sessions.sorted { ($0.updatedAt ?? 0) > ($1.updatedAt ?? 0) } + let exactMatch = sorted.first { row in + row.key == currentKey && normalize(row.lastChannel) != nil && normalize(row.lastTo) != nil + } + let selected = exactMatch + let channel = normalize(selected?.lastChannel) + let to = normalize(selected?.lastTo) + + await MainActor.run { + self.shareDeliveryChannel = channel + self.shareDeliveryTo = to + if let relay = ShareGatewayRelaySettings.loadConfig() { + ShareGatewayRelaySettings.saveConfig( + ShareGatewayRelayConfig( + gatewayURLString: relay.gatewayURLString, + token: relay.token, + password: relay.password, + sessionKey: self.mainSessionKey, + deliveryChannel: channel, + deliveryTo: to)) + } + } + } catch { + // Best-effort only. + } + } + + func runSharePipelineSelfTest() async { + self.recordShareEvent("Share self-test running…") + + let payload = SharedContentPayload( + title: "OpenClaw Share Self-Test", + url: URL(string: "https://openclaw.ai/share-self-test"), + text: "Validate iOS share->deep-link->gateway forwarding.") + guard let deepLink = ShareToAgentDeepLink.buildURL( + from: payload, + instruction: "Reply with: SHARE SELF-TEST OK") + else { + self.recordShareEvent("Self-test failed: could not build deep link.") + return + } + + await self.handleDeepLink(url: deepLink) + } + + func refreshLastShareEventFromRelay() { + if let event = ShareGatewayRelaySettings.loadLastEvent() { + self.lastShareEventText = event + } + } + + func recordShareEvent(_ text: String) { + ShareGatewayRelaySettings.saveLastEvent(text) + self.refreshLastShareEventFromRelay() + } + + func reloadTalkConfig() { + Task { [weak self] in + await self?.talkMode.reloadConfig() + } + } + + /// Back-compat hook retained for older gateway-connect flows. + func onNodeGatewayConnected() async { + await self.registerAPNsTokenIfNeeded() + } + + func handleSilentPushWake(_ userInfo: [AnyHashable: Any]) async -> Bool { + guard Self.isSilentPushPayload(userInfo) else { + self.pushWakeLogger.info("Ignored APNs payload: not silent push") + return false + } + self.pushWakeLogger.info("Silent push received; attempting reconnect if needed") + return await self.reconnectGatewaySessionsForSilentPushIfNeeded() + } + + func updateAPNsDeviceToken(_ tokenData: Data) { + let tokenHex = tokenData.map { String(format: "%02x", $0) }.joined() + let trimmed = tokenHex.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return } + self.apnsDeviceTokenHex = trimmed + UserDefaults.standard.set(trimmed, forKey: Self.apnsDeviceTokenUserDefaultsKey) + Task { [weak self] in + await self?.registerAPNsTokenIfNeeded() + } + } + + private func registerAPNsTokenIfNeeded() async { + guard self.gatewayConnected else { return } + guard let token = self.apnsDeviceTokenHex?.trimmingCharacters(in: .whitespacesAndNewlines), + !token.isEmpty + else { + return + } + if token == self.apnsLastRegisteredTokenHex { + return + } + guard let topic = Bundle.main.bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines), + !topic.isEmpty + else { + return + } + + struct PushRegistrationPayload: Codable { + var token: String + var topic: String + var environment: String + } + + let payload = PushRegistrationPayload( + token: token, + topic: topic, + environment: Self.apnsEnvironment) + do { + let json = try Self.encodePayload(payload) + await self.nodeGateway.sendEvent(event: "push.apns.register", payloadJSON: json) + self.apnsLastRegisteredTokenHex = token + } catch { + // Best-effort only. + } + } + + private static func isSilentPushPayload(_ userInfo: [AnyHashable: Any]) -> Bool { + guard let apsAny = userInfo["aps"] else { return false } + if let aps = apsAny as? [AnyHashable: Any] { + return Self.hasContentAvailable(aps["content-available"]) + } + if let aps = apsAny as? [String: Any] { + return Self.hasContentAvailable(aps["content-available"]) + } + return false + } + + private static func hasContentAvailable(_ value: Any?) -> Bool { + if let number = value as? NSNumber { + return number.intValue == 1 + } + if let text = value as? String { + return text.trimmingCharacters(in: .whitespacesAndNewlines) == "1" + } + return false + } + + private func reconnectGatewaySessionsForSilentPushIfNeeded() async -> Bool { + guard self.isBackgrounded else { + self.pushWakeLogger.info("Wake no-op: app not backgrounded") + return false + } + guard self.gatewayAutoReconnectEnabled else { + self.pushWakeLogger.info("Wake no-op: auto reconnect disabled") + return false + } + guard self.activeGatewayConnectConfig != nil else { + self.pushWakeLogger.info("Wake no-op: no active gateway config") + return false + } + + await self.operatorGateway.disconnect() + await self.nodeGateway.disconnect() + self.operatorConnected = false + self.gatewayConnected = false + self.gatewayStatusText = "Reconnecting…" + self.talkMode.updateGatewayConnected(false) + self.pushWakeLogger.info("Wake reconnect trigger applied") + return true + } +} + #if DEBUG extension NodeAppModel { func _test_handleInvoke(_ req: BridgeInvokeRequest) async -> BridgeInvokeResponse { @@ -1808,5 +2260,9 @@ extension NodeAppModel { func _test_showLocalCanvasOnDisconnect() { self.showLocalCanvasOnDisconnect() } + + func _test_applyTalkModeSync(enabled: Bool, phase: String? = nil) { + self.applyTalkModeSync(enabled: enabled, phase: phase) + } } #endif diff --git a/apps/ios/Sources/Onboarding/GatewayOnboardingView.swift b/apps/ios/Sources/Onboarding/GatewayOnboardingView.swift index 09c9e2429a694..bf6c0ba2d1874 100644 --- a/apps/ios/Sources/Onboarding/GatewayOnboardingView.swift +++ b/apps/ios/Sources/Onboarding/GatewayOnboardingView.swift @@ -257,15 +257,6 @@ private struct ManualEntryStep: View { self.manualPassword = "" } - private struct SetupPayload: Codable { - var url: String? - var host: String? - var port: Int? - var tls: Bool? - var token: String? - var password: String? - } - private func applySetupCode() { let raw = self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines) guard !raw.isEmpty else { @@ -273,7 +264,7 @@ private struct ManualEntryStep: View { return } - guard let payload = self.decodeSetupPayload(raw: raw) else { + guard let payload = GatewaySetupCode.decode(raw: raw) else { self.setupStatusText = "Setup code not recognized." return } @@ -323,34 +314,7 @@ private struct ManualEntryStep: View { } } - private func decodeSetupPayload(raw: String) -> SetupPayload? { - if let payload = decodeSetupPayloadFromJSON(raw) { - return payload - } - if let decoded = decodeBase64Payload(raw), - let payload = decodeSetupPayloadFromJSON(decoded) - { - return payload - } - return nil - } - - private func decodeSetupPayloadFromJSON(_ json: String) -> SetupPayload? { - guard let data = json.data(using: .utf8) else { return nil } - return try? JSONDecoder().decode(SetupPayload.self, from: data) - } - - private func decodeBase64Payload(_ raw: String) -> String? { - let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return nil } - let normalized = trimmed - .replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - let padding = normalized.count % 4 - let padded = padding == 0 ? normalized : normalized + String(repeating: "=", count: 4 - padding) - guard let data = Data(base64Encoded: padded) else { return nil } - return String(data: data, encoding: .utf8) - } + // (GatewaySetupCode) decode raw setup codes. } private struct ConnectionStatusBox: View { diff --git a/apps/ios/Sources/Onboarding/OnboardingStateStore.swift b/apps/ios/Sources/Onboarding/OnboardingStateStore.swift new file mode 100644 index 0000000000000..9822ac1706fc3 --- /dev/null +++ b/apps/ios/Sources/Onboarding/OnboardingStateStore.swift @@ -0,0 +1,52 @@ +import Foundation + +enum OnboardingConnectionMode: String, CaseIterable { + case homeNetwork = "home_network" + case remoteDomain = "remote_domain" + case developerLocal = "developer_local" + + var title: String { + switch self { + case .homeNetwork: + "Home Network" + case .remoteDomain: + "Remote Domain" + case .developerLocal: + "Same Machine (Dev)" + } + } +} + +enum OnboardingStateStore { + private static let completedDefaultsKey = "onboarding.completed" + private static let lastModeDefaultsKey = "onboarding.last_mode" + private static let lastSuccessTimeDefaultsKey = "onboarding.last_success_time" + + @MainActor + static func shouldPresentOnLaunch(appModel: NodeAppModel, defaults: UserDefaults = .standard) -> Bool { + if defaults.bool(forKey: Self.completedDefaultsKey) { return false } + // If we have a last-known connection config, don't force onboarding on launch. Auto-connect + // should handle reconnecting, and users can always open onboarding manually if needed. + if GatewaySettingsStore.loadLastGatewayConnection() != nil { return false } + return appModel.gatewayServerName == nil + } + + static func markCompleted(mode: OnboardingConnectionMode? = nil, defaults: UserDefaults = .standard) { + defaults.set(true, forKey: Self.completedDefaultsKey) + if let mode { + defaults.set(mode.rawValue, forKey: Self.lastModeDefaultsKey) + } + defaults.set(Int(Date().timeIntervalSince1970), forKey: Self.lastSuccessTimeDefaultsKey) + } + + static func markIncomplete(defaults: UserDefaults = .standard) { + defaults.set(false, forKey: Self.completedDefaultsKey) + } + + static func lastMode(defaults: UserDefaults = .standard) -> OnboardingConnectionMode? { + let raw = defaults.string(forKey: Self.lastModeDefaultsKey)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard !raw.isEmpty else { return nil } + return OnboardingConnectionMode(rawValue: raw) + } +} diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift new file mode 100644 index 0000000000000..c0e872b2ceb52 --- /dev/null +++ b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift @@ -0,0 +1,890 @@ +import CoreImage +import Combine +import OpenClawKit +import PhotosUI +import SwiftUI +import UIKit + +private enum OnboardingStep: Int, CaseIterable { + case welcome + case mode + case connect + case auth + case success + + var previous: Self? { + Self(rawValue: self.rawValue - 1) + } + + var next: Self? { + Self(rawValue: self.rawValue + 1) + } + + /// Progress label for the manual setup flow (mode → connect → auth → success). + var manualProgressTitle: String { + let manualSteps: [OnboardingStep] = [.mode, .connect, .auth, .success] + guard let idx = manualSteps.firstIndex(of: self) else { return "" } + return "Step \(idx + 1) of \(manualSteps.count)" + } + + var title: String { + switch self { + case .welcome: "Welcome" + case .mode: "Connection Mode" + case .connect: "Connect" + case .auth: "Authentication" + case .success: "Connected" + } + } + + var canGoBack: Bool { + self != .welcome && self != .success + } +} + +struct OnboardingWizardView: View { + @Environment(NodeAppModel.self) private var appModel: NodeAppModel + @Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController + @Environment(\.scenePhase) private var scenePhase + @AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString + @AppStorage("gateway.discovery.domain") private var discoveryDomain: String = "" + @AppStorage("onboarding.developerMode") private var developerModeEnabled: Bool = false + @State private var step: OnboardingStep = .welcome + @State private var selectedMode: OnboardingConnectionMode? + @State private var manualHost: String = "" + @State private var manualPort: Int = 18789 + @State private var manualPortText: String = "18789" + @State private var manualTLS: Bool = true + @State private var gatewayToken: String = "" + @State private var gatewayPassword: String = "" + @State private var connectMessage: String? + @State private var statusLine: String = "Scan the QR code from your gateway to connect." + @State private var connectingGatewayID: String? + @State private var issue: GatewayConnectionIssue = .none + @State private var didMarkCompleted = false + @State private var didAutoPresentQR = false + @State private var pairingRequestId: String? + @State private var discoveryRestartTask: Task? + @State private var showQRScanner: Bool = false + @State private var scannerError: String? + @State private var selectedPhoto: PhotosPickerItem? + @State private var lastPairingAutoResumeAttemptAt: Date? + private static let pairingAutoResumeTicker = Timer.publish(every: 2.0, on: .main, in: .common).autoconnect() + + let allowSkip: Bool + let onClose: () -> Void + + private var isFullScreenStep: Bool { + self.step == .welcome || self.step == .success + } + + var body: some View { + NavigationStack { + Group { + switch self.step { + case .welcome: + self.welcomeStep + case .success: + self.successStep + default: + Form { + switch self.step { + case .mode: + self.modeStep + case .connect: + self.connectStep + case .auth: + self.authStep + default: + EmptyView() + } + } + .scrollDismissesKeyboard(.interactively) + } + } + .navigationTitle(self.isFullScreenStep ? "" : self.step.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + if !self.isFullScreenStep { + ToolbarItem(placement: .principal) { + VStack(spacing: 2) { + Text(self.step.title) + .font(.headline) + Text(self.step.manualProgressTitle) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + } + ToolbarItem(placement: .topBarLeading) { + if self.step.canGoBack { + Button { + self.navigateBack() + } label: { + Label("Back", systemImage: "chevron.left") + } + } else if self.allowSkip { + Button("Close") { + self.onClose() + } + } + } + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("Done") { + UIApplication.shared.sendAction( + #selector(UIResponder.resignFirstResponder), + to: nil, from: nil, for: nil) + } + } + } + } + .gatewayTrustPromptAlert() + .alert("QR Scanner Unavailable", isPresented: Binding( + get: { self.scannerError != nil }, + set: { if !$0 { self.scannerError = nil } } + )) { + Button("OK", role: .cancel) {} + } message: { + Text(self.scannerError ?? "") + } + .sheet(isPresented: self.$showQRScanner) { + NavigationStack { + QRScannerView( + onGatewayLink: { link in + self.handleScannedLink(link) + }, + onError: { error in + self.showQRScanner = false + self.statusLine = "Scanner error: \(error)" + self.scannerError = error + }, + onDismiss: { + self.showQRScanner = false + }) + .ignoresSafeArea() + .navigationTitle("Scan QR Code") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Cancel") { self.showQRScanner = false } + } + ToolbarItem(placement: .topBarTrailing) { + PhotosPicker(selection: self.$selectedPhoto, matching: .images) { + Label("Photos", systemImage: "photo") + } + } + } + } + .onChange(of: self.selectedPhoto) { _, newValue in + guard let item = newValue else { return } + self.selectedPhoto = nil + Task { + guard let data = try? await item.loadTransferable(type: Data.self) else { + self.showQRScanner = false + self.scannerError = "Could not load the selected image." + return + } + if let message = self.detectQRCode(from: data) { + if let link = GatewayConnectDeepLink.fromSetupCode(message) { + self.handleScannedLink(link) + return + } + if let url = URL(string: message), + let route = DeepLinkParser.parse(url), + case let .gateway(link) = route + { + self.handleScannedLink(link) + return + } + } + self.showQRScanner = false + self.scannerError = "No valid QR code found in the selected image." + } + } + } + .onAppear { + self.initializeState() + } + .onDisappear { + self.discoveryRestartTask?.cancel() + self.discoveryRestartTask = nil + } + .onChange(of: self.discoveryDomain) { _, _ in + self.scheduleDiscoveryRestart() + } + .onChange(of: self.manualPortText) { _, newValue in + let digits = newValue.filter(\.isNumber) + if digits != newValue { + self.manualPortText = digits + return + } + guard let parsed = Int(digits), parsed > 0 else { + self.manualPort = 0 + return + } + self.manualPort = min(parsed, 65535) + } + .onChange(of: self.manualPort) { _, newValue in + let normalized = newValue > 0 ? String(newValue) : "" + if self.manualPortText != normalized { + self.manualPortText = normalized + } + } + .onChange(of: self.gatewayToken) { _, newValue in + self.saveGatewayCredentials(token: newValue, password: self.gatewayPassword) + } + .onChange(of: self.gatewayPassword) { _, newValue in + self.saveGatewayCredentials(token: self.gatewayToken, password: newValue) + } + .onChange(of: self.appModel.gatewayStatusText) { _, newValue in + let next = GatewayConnectionIssue.detect(from: newValue) + // Avoid "flip-flopping" the UI by clearing actionable issues when the underlying connection + // transitions through intermediate statuses (e.g. Offline/Connecting while reconnect churns). + if self.issue.needsPairing, next.needsPairing { + // Keep the requestId sticky even if the status line omits it after we pause. + let mergedRequestId = next.requestId ?? self.issue.requestId ?? self.pairingRequestId + self.issue = .pairingRequired(requestId: mergedRequestId) + } else if self.issue.needsPairing, !next.needsPairing { + // Ignore non-pairing statuses until the user explicitly retries/scans again, or we connect. + } else if self.issue.needsAuthToken, !next.needsAuthToken, !next.needsPairing { + // Same idea for auth: once we learn credentials are missing/rejected, keep that sticky until + // the user retries/scans again or we successfully connect. + } else { + self.issue = next + } + + if let requestId = next.requestId, !requestId.isEmpty { + self.pairingRequestId = requestId + } + + // If the gateway tells us auth is missing/rejected, stop reconnect churn until the user intervenes. + if next.needsAuthToken { + self.appModel.gatewayAutoReconnectEnabled = false + } + + if self.issue.needsAuthToken || self.issue.needsPairing { + self.step = .auth + } + if !newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + self.connectMessage = newValue + self.statusLine = newValue + } + } + .onChange(of: self.appModel.gatewayServerName) { _, newValue in + guard newValue != nil else { return } + self.showQRScanner = false + self.statusLine = "Connected." + if !self.didMarkCompleted, let selectedMode { + OnboardingStateStore.markCompleted(mode: selectedMode) + self.didMarkCompleted = true + } + self.onClose() + } + .onChange(of: self.scenePhase) { _, newValue in + guard newValue == .active else { return } + self.attemptAutomaticPairingResumeIfNeeded() + } + .onReceive(Self.pairingAutoResumeTicker) { _ in + self.attemptAutomaticPairingResumeIfNeeded() + } + } + + @ViewBuilder + private var welcomeStep: some View { + VStack(spacing: 0) { + Spacer() + + Image(systemName: "qrcode.viewfinder") + .font(.system(size: 64)) + .foregroundStyle(.tint) + .padding(.bottom, 20) + + Text("Welcome") + .font(.largeTitle.weight(.bold)) + .padding(.bottom, 8) + + Text("Connect to your OpenClaw gateway") + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + + Spacer() + + VStack(spacing: 12) { + Button { + self.statusLine = "Opening QR scanner…" + self.showQRScanner = true + } label: { + Label("Scan QR Code", systemImage: "qrcode") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + + Button { + self.step = .mode + } label: { + Text("Set Up Manually") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .controlSize(.large) + } + .padding(.bottom, 12) + + Text(self.statusLine) + .font(.footnote) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 24) + .padding(.horizontal, 24) + .padding(.bottom, 48) + } + } + + @ViewBuilder + private var modeStep: some View { + Section("Connection Mode") { + OnboardingModeRow( + title: OnboardingConnectionMode.homeNetwork.title, + subtitle: "LAN or Tailscale host", + selected: self.selectedMode == .homeNetwork) + { + self.selectMode(.homeNetwork) + } + + OnboardingModeRow( + title: OnboardingConnectionMode.remoteDomain.title, + subtitle: "VPS with domain", + selected: self.selectedMode == .remoteDomain) + { + self.selectMode(.remoteDomain) + } + + Toggle( + "Developer mode", + isOn: Binding( + get: { self.developerModeEnabled }, + set: { newValue in + self.developerModeEnabled = newValue + if !newValue, self.selectedMode == .developerLocal { + self.selectedMode = nil + } + })) + + if self.developerModeEnabled { + OnboardingModeRow( + title: OnboardingConnectionMode.developerLocal.title, + subtitle: "For local iOS app development", + selected: self.selectedMode == .developerLocal) + { + self.selectMode(.developerLocal) + } + } + } + + Section { + Button("Continue") { + self.step = .connect + } + .disabled(self.selectedMode == nil) + } + } + + @ViewBuilder + private var connectStep: some View { + if let selectedMode { + Section { + LabeledContent("Mode", value: selectedMode.title) + LabeledContent("Discovery", value: self.gatewayController.discoveryStatusText) + LabeledContent("Status", value: self.appModel.gatewayStatusText) + LabeledContent("Progress", value: self.statusLine) + } header: { + Text("Status") + } footer: { + if let connectMessage { + Text(connectMessage) + } + } + + switch selectedMode { + case .homeNetwork: + self.homeNetworkConnectSection + case .remoteDomain: + self.remoteDomainConnectSection + case .developerLocal: + self.developerConnectSection + } + } else { + Section { + Text("Choose a mode first.") + Button("Back to Mode Selection") { + self.step = .mode + } + } + } + } + + private var homeNetworkConnectSection: some View { + Group { + Section("Discovered Gateways") { + if self.gatewayController.gateways.isEmpty { + Text("No gateways found yet.") + .foregroundStyle(.secondary) + } else { + ForEach(self.gatewayController.gateways) { gateway in + let hasHost = self.gatewayHasResolvableHost(gateway) + + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(gateway.name) + if let host = gateway.lanHost ?? gateway.tailnetDns { + Text(host) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + Spacer() + Button { + Task { await self.connectDiscoveredGateway(gateway) } + } label: { + if self.connectingGatewayID == gateway.id { + ProgressView() + .progressViewStyle(.circular) + } else if !hasHost { + Text("Resolving…") + } else { + Text("Connect") + } + } + .disabled(self.connectingGatewayID != nil || !hasHost) + } + } + } + + Button("Restart Discovery") { + self.gatewayController.restartDiscovery() + } + .disabled(self.connectingGatewayID != nil) + } + + self.manualConnectionFieldsSection(title: "Manual Fallback") + } + } + + private var remoteDomainConnectSection: some View { + self.manualConnectionFieldsSection(title: "Domain Settings") + } + + private var developerConnectSection: some View { + Section { + TextField("Host", text: self.$manualHost) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + TextField("Port", text: self.$manualPortText) + .keyboardType(.numberPad) + Toggle("Use TLS", isOn: self.$manualTLS) + + Button { + Task { await self.connectManual() } + } label: { + if self.connectingGatewayID == "manual" { + HStack(spacing: 8) { + ProgressView() + .progressViewStyle(.circular) + Text("Connecting…") + } + } else { + Text("Connect") + } + } + .disabled(!self.canConnectManual || self.connectingGatewayID != nil) + } header: { + Text("Developer Local") + } footer: { + Text("Default host is localhost. Use your Mac LAN IP if simulator networking requires it.") + } + } + + private var authStep: some View { + Group { + Section("Authentication") { + TextField("Gateway Auth Token", text: self.$gatewayToken) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + SecureField("Gateway Password", text: self.$gatewayPassword) + + if self.issue.needsAuthToken { + Text("Gateway rejected credentials. Scan a fresh QR code or update token/password.") + .font(.footnote) + .foregroundStyle(.secondary) + } else { + Text("Auth token looks valid.") + .font(.footnote) + .foregroundStyle(.secondary) + } + } + + if self.issue.needsPairing { + Section { + Button { + self.resumeAfterPairingApproval() + } label: { + Label("Resume After Approval", systemImage: "arrow.clockwise") + } + .disabled(self.connectingGatewayID != nil) + } header: { + Text("Pairing Approval") + } footer: { + let requestLine: String = { + if let id = self.issue.requestId, !id.isEmpty { + return "Request ID: \(id)" + } + return "Request ID: check `openclaw devices list`." + }() + Text( + "Approve this device on the gateway.\n" + + "1) `openclaw devices approve` (or `openclaw devices approve `)\n" + + "2) `/pair approve` in Telegram\n" + + "\(requestLine)\n" + + "OpenClaw will also retry automatically when you return to this app.") + } + } + + Section { + Button { + self.openQRScannerFromOnboarding() + } label: { + Label("Scan QR Code Again", systemImage: "qrcode.viewfinder") + } + .disabled(self.connectingGatewayID != nil) + + Button { + Task { await self.retryLastAttempt() } + } label: { + if self.connectingGatewayID == "retry" { + ProgressView() + .progressViewStyle(.circular) + } else { + Text("Retry Connection") + } + } + .disabled(self.connectingGatewayID != nil) + } + } + } + + private var successStep: some View { + VStack(spacing: 0) { + Spacer() + + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 64)) + .foregroundStyle(.green) + .padding(.bottom, 20) + + Text("Connected") + .font(.largeTitle.weight(.bold)) + .padding(.bottom, 8) + + let server = self.appModel.gatewayServerName ?? "gateway" + Text(server) + .font(.subheadline) + .foregroundStyle(.secondary) + .padding(.bottom, 4) + + if let addr = self.appModel.gatewayRemoteAddress { + Text(addr) + .font(.subheadline) + .foregroundStyle(.secondary) + } + + Spacer() + + Button { + self.onClose() + } label: { + Text("Open OpenClaw") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .padding(.horizontal, 24) + .padding(.bottom, 48) + } + } + + @ViewBuilder + private func manualConnectionFieldsSection(title: String) -> some View { + Section(title) { + TextField("Host", text: self.$manualHost) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + TextField("Port", text: self.$manualPortText) + .keyboardType(.numberPad) + Toggle("Use TLS", isOn: self.$manualTLS) + TextField("Discovery Domain (optional)", text: self.$discoveryDomain) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + + Button { + Task { await self.connectManual() } + } label: { + if self.connectingGatewayID == "manual" { + HStack(spacing: 8) { + ProgressView() + .progressViewStyle(.circular) + Text("Connecting…") + } + } else { + Text("Connect") + } + } + .disabled(!self.canConnectManual || self.connectingGatewayID != nil) + } + } + + private func handleScannedLink(_ link: GatewayConnectDeepLink) { + self.manualHost = link.host + self.manualPort = link.port + self.manualTLS = link.tls + if let token = link.token { + self.gatewayToken = token + } + if let password = link.password { + self.gatewayPassword = password + } + self.saveGatewayCredentials(token: self.gatewayToken, password: self.gatewayPassword) + self.showQRScanner = false + self.connectMessage = "Connecting via QR code…" + self.statusLine = "QR loaded. Connecting to \(link.host):\(link.port)…" + if self.selectedMode == nil { + self.selectedMode = link.tls ? .remoteDomain : .homeNetwork + } + Task { await self.connectManual() } + } + + private func openQRScannerFromOnboarding() { + // Stop active reconnect loops before scanning new credentials. + self.appModel.disconnectGateway() + self.connectingGatewayID = nil + self.connectMessage = nil + self.issue = .none + self.pairingRequestId = nil + self.statusLine = "Opening QR scanner…" + self.showQRScanner = true + } + + private func resumeAfterPairingApproval() { + // We intentionally stop reconnect churn while unpaired to avoid generating multiple pending requests. + self.appModel.gatewayAutoReconnectEnabled = true + self.appModel.gatewayPairingPaused = false + self.appModel.gatewayPairingRequestId = nil + // Pairing state is sticky to prevent UI flip-flop during reconnect churn. + // Once the user explicitly resumes after approving, clear the sticky issue + // so new status/auth errors can surface instead of being masked as pairing. + self.issue = .none + self.connectMessage = "Retrying after approval…" + self.statusLine = "Retrying after approval…" + Task { await self.retryLastAttempt() } + } + + private func resumeAfterPairingApprovalInBackground() { + // Keep the pairing issue sticky to avoid visual flicker while we probe for approval. + self.appModel.gatewayAutoReconnectEnabled = true + self.appModel.gatewayPairingPaused = false + self.appModel.gatewayPairingRequestId = nil + Task { await self.retryLastAttempt(silent: true) } + } + + private func attemptAutomaticPairingResumeIfNeeded() { + guard self.scenePhase == .active else { return } + guard self.step == .auth else { return } + guard self.issue.needsPairing else { return } + guard self.connectingGatewayID == nil else { return } + + let now = Date() + if let last = self.lastPairingAutoResumeAttemptAt, now.timeIntervalSince(last) < 6 { + return + } + self.lastPairingAutoResumeAttemptAt = now + self.resumeAfterPairingApprovalInBackground() + } + + private func detectQRCode(from data: Data) -> String? { + guard let ciImage = CIImage(data: data) else { return nil } + let detector = CIDetector( + ofType: CIDetectorTypeQRCode, context: nil, + options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) + let features = detector?.features(in: ciImage) ?? [] + for feature in features { + if let qr = feature as? CIQRCodeFeature, let message = qr.messageString { + return message + } + } + return nil + } + + private func navigateBack() { + guard let target = self.step.previous else { return } + self.connectingGatewayID = nil + self.connectMessage = nil + self.step = target + } + private var canConnectManual: Bool { + let host = self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines) + return !host.isEmpty && self.manualPort > 0 && self.manualPort <= 65535 + } + + private func initializeState() { + if self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if let last = GatewaySettingsStore.loadLastGatewayConnection() { + switch last { + case let .manual(host, port, useTLS, _): + self.manualHost = host + self.manualPort = port + self.manualTLS = useTLS + case .discovered: + self.manualHost = "openclaw.local" + self.manualPort = 18789 + self.manualTLS = true + } + } else { + self.manualHost = "openclaw.local" + self.manualPort = 18789 + self.manualTLS = true + } + } + self.manualPortText = self.manualPort > 0 ? String(self.manualPort) : "" + if self.selectedMode == nil { + self.selectedMode = OnboardingStateStore.lastMode() + } + if self.selectedMode == .developerLocal && self.manualHost == "openclaw.local" { + self.manualHost = "localhost" + self.manualTLS = false + } + + let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmedInstanceId.isEmpty { + self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? "" + self.gatewayPassword = GatewaySettingsStore.loadGatewayPassword(instanceId: trimmedInstanceId) ?? "" + } + + let hasSavedGateway = GatewaySettingsStore.loadLastGatewayConnection() != nil + let hasToken = !self.gatewayToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + let hasPassword = !self.gatewayPassword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + if !self.didAutoPresentQR, !hasSavedGateway, !hasToken, !hasPassword { + self.didAutoPresentQR = true + self.statusLine = "No saved pairing found. Scan QR code to connect." + self.showQRScanner = true + } + } + + private func scheduleDiscoveryRestart() { + self.discoveryRestartTask?.cancel() + self.discoveryRestartTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: 350_000_000) + guard !Task.isCancelled else { return } + self.gatewayController.restartDiscovery() + } + } + + private func saveGatewayCredentials(token: String, password: String) { + let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedInstanceId.isEmpty else { return } + let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines) + GatewaySettingsStore.saveGatewayToken(trimmedToken, instanceId: trimmedInstanceId) + let trimmedPassword = password.trimmingCharacters(in: .whitespacesAndNewlines) + GatewaySettingsStore.saveGatewayPassword(trimmedPassword, instanceId: trimmedInstanceId) + } + + private func connectDiscoveredGateway(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async { + self.connectingGatewayID = gateway.id + self.issue = .none + self.connectMessage = "Connecting to \(gateway.name)…" + self.statusLine = "Connecting to \(gateway.name)…" + defer { self.connectingGatewayID = nil } + await self.gatewayController.connect(gateway) + } + + private func selectMode(_ mode: OnboardingConnectionMode) { + self.selectedMode = mode + self.applyModeDefaults(mode) + } + + private func applyModeDefaults(_ mode: OnboardingConnectionMode) { + let host = self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let hostIsDefaultLike = host.isEmpty || host == "openclaw.local" || host == "localhost" + + switch mode { + case .homeNetwork: + if hostIsDefaultLike { self.manualHost = "openclaw.local" } + self.manualTLS = true + if self.manualPort <= 0 || self.manualPort > 65535 { self.manualPort = 18789 } + case .remoteDomain: + if host == "openclaw.local" || host == "localhost" { self.manualHost = "" } + self.manualTLS = true + if self.manualPort <= 0 || self.manualPort > 65535 { self.manualPort = 18789 } + case .developerLocal: + if hostIsDefaultLike { self.manualHost = "localhost" } + self.manualTLS = false + if self.manualPort <= 0 || self.manualPort > 65535 { self.manualPort = 18789 } + } + } + + private func gatewayHasResolvableHost(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> Bool { + let lanHost = gateway.lanHost?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !lanHost.isEmpty { return true } + let tailnetDns = gateway.tailnetDns?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return !tailnetDns.isEmpty + } + + private func connectManual() async { + let host = self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines) + guard !host.isEmpty, self.manualPort > 0, self.manualPort <= 65535 else { return } + self.connectingGatewayID = "manual" + self.issue = .none + self.connectMessage = "Connecting to \(host)…" + self.statusLine = "Connecting to \(host):\(self.manualPort)…" + defer { self.connectingGatewayID = nil } + await self.gatewayController.connectManual(host: host, port: self.manualPort, useTLS: self.manualTLS) + } + + private func retryLastAttempt(silent: Bool = false) async { + self.connectingGatewayID = silent ? "retry-auto" : "retry" + // Keep current auth/pairing issue sticky while retrying to avoid Step 3 UI flip-flop. + if !silent { + self.connectMessage = "Retrying…" + self.statusLine = "Retrying last connection…" + } + defer { self.connectingGatewayID = nil } + await self.gatewayController.connectLastKnown() + } +} + +private struct OnboardingModeRow: View { + let title: String + let subtitle: String + let selected: Bool + let action: () -> Void + + var body: some View { + Button(action: self.action) { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(self.title) + .font(.body.weight(.semibold)) + Text(self.subtitle) + .font(.footnote) + .foregroundStyle(.secondary) + } + Spacer() + Image(systemName: self.selected ? "checkmark.circle.fill" : "circle") + .foregroundStyle(self.selected ? Color.accentColor : Color.secondary) + } + } + .buttonStyle(.plain) + } +} diff --git a/apps/ios/Sources/Onboarding/QRScannerView.swift b/apps/ios/Sources/Onboarding/QRScannerView.swift new file mode 100644 index 0000000000000..d326c09c42b7d --- /dev/null +++ b/apps/ios/Sources/Onboarding/QRScannerView.swift @@ -0,0 +1,96 @@ +import OpenClawKit +import SwiftUI +import VisionKit + +struct QRScannerView: UIViewControllerRepresentable { + let onGatewayLink: (GatewayConnectDeepLink) -> Void + let onError: (String) -> Void + let onDismiss: () -> Void + + func makeUIViewController(context: Context) -> UIViewController { + guard DataScannerViewController.isSupported else { + context.coordinator.reportError("QR scanning is not supported on this device.") + return UIViewController() + } + guard DataScannerViewController.isAvailable else { + context.coordinator.reportError("Camera scanning is currently unavailable.") + return UIViewController() + } + let scanner = DataScannerViewController( + recognizedDataTypes: [.barcode(symbologies: [.qr])], + isHighlightingEnabled: true) + scanner.delegate = context.coordinator + do { + try scanner.startScanning() + } catch { + context.coordinator.reportError("Could not start QR scanner.") + } + return scanner + } + + func updateUIViewController(_: UIViewController, context _: Context) {} + + static func dismantleUIViewController(_ uiViewController: UIViewController, coordinator: Coordinator) { + if let scanner = uiViewController as? DataScannerViewController { + scanner.stopScanning() + } + coordinator.parent.onDismiss() + } + + func makeCoordinator() -> Coordinator { + Coordinator(parent: self) + } + + final class Coordinator: NSObject, DataScannerViewControllerDelegate { + let parent: QRScannerView + private var handled = false + private var reportedError = false + + init(parent: QRScannerView) { + self.parent = parent + } + + func reportError(_ message: String) { + guard !self.reportedError else { return } + self.reportedError = true + Task { @MainActor in + self.parent.onError(message) + } + } + + func dataScanner(_: DataScannerViewController, didAdd items: [RecognizedItem], allItems _: [RecognizedItem]) { + guard !self.handled else { return } + for item in items { + guard case let .barcode(barcode) = item, + let payload = barcode.payloadStringValue + else { continue } + + // Try setup code format first (base64url JSON from /pair qr). + if let link = GatewayConnectDeepLink.fromSetupCode(payload) { + self.handled = true + self.parent.onGatewayLink(link) + return + } + + // Fall back to deep link URL format (openclaw://gateway?...). + if let url = URL(string: payload), + let route = DeepLinkParser.parse(url), + case let .gateway(link) = route + { + self.handled = true + self.parent.onGatewayLink(link) + return + } + } + } + + func dataScanner(_: DataScannerViewController, didRemove _: [RecognizedItem], allItems _: [RecognizedItem]) {} + + func dataScanner( + _: DataScannerViewController, + becameUnavailableWithError _: DataScannerViewController.ScanningUnavailable) + { + self.reportError("Camera is not available on this device.") + } + } +} diff --git a/apps/ios/Sources/OpenClaw.entitlements b/apps/ios/Sources/OpenClaw.entitlements new file mode 100644 index 0000000000000..a2663ce930be4 --- /dev/null +++ b/apps/ios/Sources/OpenClaw.entitlements @@ -0,0 +1,9 @@ + + + + + aps-environment + development + + + diff --git a/apps/ios/Sources/OpenClawApp.swift b/apps/ios/Sources/OpenClawApp.swift index 8ad23ae20a10a..091c1b90fdf27 100644 --- a/apps/ios/Sources/OpenClawApp.swift +++ b/apps/ios/Sources/OpenClawApp.swift @@ -1,12 +1,73 @@ import SwiftUI +import Foundation +import os +import UIKit + +final class OpenClawAppDelegate: NSObject, UIApplicationDelegate { + private let logger = Logger(subsystem: "ai.openclaw.ios", category: "Push") + private var pendingAPNsDeviceToken: Data? + weak var appModel: NodeAppModel? { + didSet { + guard let model = self.appModel, let token = self.pendingAPNsDeviceToken else { return } + self.pendingAPNsDeviceToken = nil + Task { @MainActor in + model.updateAPNsDeviceToken(token) + } + } + } + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool + { + application.registerForRemoteNotifications() + return true + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + if let appModel = self.appModel { + Task { @MainActor in + appModel.updateAPNsDeviceToken(deviceToken) + } + return + } + + self.pendingAPNsDeviceToken = deviceToken + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) { + self.logger.error("APNs registration failed: \(error.localizedDescription, privacy: .public)") + } + + func application( + _ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) + { + self.logger.info("APNs remote notification received keys=\(userInfo.keys.count, privacy: .public)") + Task { @MainActor in + guard let appModel = self.appModel else { + self.logger.info("APNs wake skipped: appModel unavailable") + completionHandler(.noData) + return + } + let handled = await appModel.handleSilentPushWake(userInfo) + self.logger.info("APNs wake handled=\(handled, privacy: .public)") + completionHandler(handled ? .newData : .noData) + } + } +} @main struct OpenClawApp: App { @State private var appModel: NodeAppModel @State private var gatewayController: GatewayConnectionController + @UIApplicationDelegateAdaptor(OpenClawAppDelegate.self) private var appDelegate @Environment(\.scenePhase) private var scenePhase init() { + Self.installUncaughtExceptionLogger() GatewaySettingsStore.bootstrapPersistence() let appModel = NodeAppModel() _appModel = State(initialValue: appModel) @@ -19,6 +80,9 @@ struct OpenClawApp: App { .environment(self.appModel) .environment(self.appModel.voiceWake) .environment(self.gatewayController) + .task { + self.appDelegate.appModel = self.appModel + } .onOpenURL { url in Task { await self.appModel.handleDeepLink(url: url) } } @@ -29,3 +93,18 @@ struct OpenClawApp: App { } } } + +extension OpenClawApp { + private static func installUncaughtExceptionLogger() { + NSLog("OpenClaw: installing uncaught exception handler") + NSSetUncaughtExceptionHandler { exception in + // Useful when the app hits NSExceptions from SwiftUI/WebKit internals; these do not + // produce a normal Swift error backtrace. + let reason = exception.reason ?? "(no reason)" + NSLog("UNCAUGHT EXCEPTION: %@ %@", exception.name.rawValue, reason) + for line in exception.callStackSymbols { + NSLog(" %@", line) + } + } + } +} diff --git a/apps/ios/Sources/Reminders/RemindersService.swift b/apps/ios/Sources/Reminders/RemindersService.swift index 36eea52217894..249f439fb1799 100644 --- a/apps/ios/Sources/Reminders/RemindersService.swift +++ b/apps/ios/Sources/Reminders/RemindersService.swift @@ -6,7 +6,7 @@ final class RemindersService: RemindersServicing { func list(params: OpenClawRemindersListParams) async throws -> OpenClawRemindersListPayload { let store = EKEventStore() let status = EKEventStore.authorizationStatus(for: .reminder) - let authorized = await Self.ensureAuthorization(store: store, status: status) + let authorized = EventKitAuthorization.allowsRead(status: status) guard authorized else { throw NSError(domain: "Reminders", code: 1, userInfo: [ NSLocalizedDescriptionKey: "REMINDERS_PERMISSION_REQUIRED: grant Reminders permission", @@ -50,7 +50,7 @@ final class RemindersService: RemindersServicing { func add(params: OpenClawRemindersAddParams) async throws -> OpenClawRemindersAddPayload { let store = EKEventStore() let status = EKEventStore.authorizationStatus(for: .reminder) - let authorized = await Self.ensureWriteAuthorization(store: store, status: status) + let authorized = EventKitAuthorization.allowsWrite(status: status) guard authorized else { throw NSError(domain: "Reminders", code: 2, userInfo: [ NSLocalizedDescriptionKey: "REMINDERS_PERMISSION_REQUIRED: grant Reminders permission", @@ -100,38 +100,6 @@ final class RemindersService: RemindersServicing { return OpenClawRemindersAddPayload(reminder: payload) } - private static func ensureAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool { - switch status { - case .authorized: - return true - case .notDetermined: - // Don’t prompt during node.invoke; prompts block the invoke and lead to timeouts. - return false - case .restricted, .denied: - return false - case .fullAccess: - return true - case .writeOnly: - return false - @unknown default: - return false - } - } - - private static func ensureWriteAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool { - switch status { - case .authorized, .fullAccess, .writeOnly: - return true - case .notDetermined: - // Don’t prompt during node.invoke; prompts block the invoke and lead to timeouts. - return false - case .restricted, .denied: - return false - @unknown default: - return false - } - } - private static func resolveList( store: EKEventStore, listId: String?, diff --git a/apps/ios/Sources/RootCanvas.swift b/apps/ios/Sources/RootCanvas.swift index 514e1b4cc47ce..70ba9cdb96fe0 100644 --- a/apps/ios/Sources/RootCanvas.swift +++ b/apps/ios/Sources/RootCanvas.swift @@ -3,34 +3,69 @@ import UIKit struct RootCanvas: View { @Environment(NodeAppModel.self) private var appModel + @Environment(GatewayConnectionController.self) private var gatewayController @Environment(VoiceWakeManager.self) private var voiceWake @Environment(\.colorScheme) private var systemColorScheme @Environment(\.scenePhase) private var scenePhase @AppStorage(VoiceWakePreferences.enabledKey) private var voiceWakeEnabled: Bool = false @AppStorage("screen.preventSleep") private var preventSleep: Bool = true @AppStorage("canvas.debugStatusEnabled") private var canvasDebugStatusEnabled: Bool = false + @AppStorage("onboarding.requestID") private var onboardingRequestID: Int = 0 @AppStorage("gateway.onboardingComplete") private var onboardingComplete: Bool = false @AppStorage("gateway.hasConnectedOnce") private var hasConnectedOnce: Bool = false @AppStorage("gateway.preferredStableID") private var preferredGatewayStableID: String = "" @AppStorage("gateway.manual.enabled") private var manualGatewayEnabled: Bool = false @AppStorage("gateway.manual.host") private var manualGatewayHost: String = "" + @AppStorage("onboarding.quickSetupDismissed") private var quickSetupDismissed: Bool = false @State private var presentedSheet: PresentedSheet? @State private var voiceWakeToastText: String? @State private var toastDismissTask: Task? + @State private var showOnboarding: Bool = false + @State private var onboardingAllowSkip: Bool = true + @State private var didEvaluateOnboarding: Bool = false @State private var didAutoOpenSettings: Bool = false private enum PresentedSheet: Identifiable { case settings case chat + case quickSetup var id: Int { switch self { case .settings: 0 case .chat: 1 + case .quickSetup: 2 } } } + enum StartupPresentationRoute: Equatable { + case none + case onboarding + case settings + } + + static func startupPresentationRoute( + gatewayConnected: Bool, + hasConnectedOnce: Bool, + onboardingComplete: Bool, + hasExistingGatewayConfig: Bool, + shouldPresentOnLaunch: Bool) -> StartupPresentationRoute + { + if gatewayConnected { + return .none + } + // On first run or explicit launch onboarding state, onboarding always wins. + if shouldPresentOnLaunch || !hasConnectedOnce || !onboardingComplete { + return .onboarding + } + // Settings auto-open is a recovery path for previously-connected installs only. + if !hasExistingGatewayConfig { + return .settings + } + return .none + } + var body: some View { ZStack { CanvasContent( @@ -57,30 +92,63 @@ struct RootCanvas: View { switch sheet { case .settings: SettingsTab() + .environment(self.appModel) + .environment(self.appModel.voiceWake) + .environment(self.gatewayController) case .chat: ChatSheet( + // Chat RPCs run on the operator session (read/write scopes). gateway: self.appModel.operatorSession, sessionKey: self.appModel.mainSessionKey, agentName: self.appModel.activeAgentName, userAccent: self.appModel.seamColor) + case .quickSetup: + GatewayQuickSetupSheet() + .environment(self.appModel) + .environment(self.gatewayController) } } + .fullScreenCover(isPresented: self.$showOnboarding) { + OnboardingWizardView( + allowSkip: self.onboardingAllowSkip, + onClose: { + self.showOnboarding = false + }) + .environment(self.appModel) + .environment(self.appModel.voiceWake) + .environment(self.gatewayController) + } .onAppear { self.updateIdleTimer() } + .onAppear { self.evaluateOnboardingPresentation(force: false) } .onAppear { self.maybeAutoOpenSettings() } .onChange(of: self.preventSleep) { _, _ in self.updateIdleTimer() } .onChange(of: self.scenePhase) { _, _ in self.updateIdleTimer() } + .onAppear { self.maybeShowQuickSetup() } + .onChange(of: self.gatewayController.gateways.count) { _, _ in self.maybeShowQuickSetup() } .onAppear { self.updateCanvasDebugStatus() } .onChange(of: self.canvasDebugStatusEnabled) { _, _ in self.updateCanvasDebugStatus() } .onChange(of: self.appModel.gatewayStatusText) { _, _ in self.updateCanvasDebugStatus() } .onChange(of: self.appModel.gatewayServerName) { _, _ in self.updateCanvasDebugStatus() } + .onChange(of: self.appModel.gatewayServerName) { _, newValue in + if newValue != nil { + self.showOnboarding = false + } + } + .onChange(of: self.onboardingRequestID) { _, _ in + self.evaluateOnboardingPresentation(force: true) + } .onChange(of: self.appModel.gatewayRemoteAddress) { _, _ in self.updateCanvasDebugStatus() } .onChange(of: self.appModel.gatewayServerName) { _, newValue in if newValue != nil { self.onboardingComplete = true self.hasConnectedOnce = true + OnboardingStateStore.markCompleted(mode: nil) } self.maybeAutoOpenSettings() } + .onChange(of: self.appModel.openChatRequestID) { _, _ in + self.presentedSheet = .chat + } .onChange(of: self.voiceWake.lastTriggeredCommand) { _, newValue in guard let newValue else { return } let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) @@ -136,11 +204,31 @@ struct RootCanvas: View { self.appModel.screen.updateDebugStatus(title: title, subtitle: subtitle) } - private func shouldAutoOpenSettings() -> Bool { - if self.appModel.gatewayServerName != nil { return false } - if !self.hasConnectedOnce { return true } - if !self.onboardingComplete { return true } - return !self.hasExistingGatewayConfig() + private func evaluateOnboardingPresentation(force: Bool) { + if force { + self.onboardingAllowSkip = true + self.showOnboarding = true + return + } + + guard !self.didEvaluateOnboarding else { return } + self.didEvaluateOnboarding = true + let route = Self.startupPresentationRoute( + gatewayConnected: self.appModel.gatewayServerName != nil, + hasConnectedOnce: self.hasConnectedOnce, + onboardingComplete: self.onboardingComplete, + hasExistingGatewayConfig: self.hasExistingGatewayConfig(), + shouldPresentOnLaunch: OnboardingStateStore.shouldPresentOnLaunch(appModel: self.appModel)) + switch route { + case .none: + break + case .onboarding: + self.onboardingAllowSkip = true + self.showOnboarding = true + case .settings: + self.didAutoOpenSettings = true + self.presentedSheet = .settings + } } private func hasExistingGatewayConfig() -> Bool { @@ -151,10 +239,26 @@ struct RootCanvas: View { private func maybeAutoOpenSettings() { guard !self.didAutoOpenSettings else { return } - guard self.shouldAutoOpenSettings() else { return } + guard !self.showOnboarding else { return } + let route = Self.startupPresentationRoute( + gatewayConnected: self.appModel.gatewayServerName != nil, + hasConnectedOnce: self.hasConnectedOnce, + onboardingComplete: self.onboardingComplete, + hasExistingGatewayConfig: self.hasExistingGatewayConfig(), + shouldPresentOnLaunch: false) + guard route == .settings else { return } self.didAutoOpenSettings = true self.presentedSheet = .settings } + + private func maybeShowQuickSetup() { + guard !self.quickSetupDismissed else { return } + guard !self.showOnboarding else { return } + guard self.presentedSheet == nil else { return } + guard self.appModel.gatewayServerName == nil else { return } + guard !self.gatewayController.gateways.isEmpty else { return } + self.presentedSheet = .quickSetup + } } private struct CanvasContent: View { diff --git a/apps/ios/Sources/RootTabs.swift b/apps/ios/Sources/RootTabs.swift index 278e56d6150d2..4733a4a30fcb8 100644 --- a/apps/ios/Sources/RootTabs.swift +++ b/apps/ios/Sources/RootTabs.swift @@ -3,6 +3,7 @@ import SwiftUI struct RootTabs: View { @Environment(NodeAppModel.self) private var appModel @Environment(VoiceWakeManager.self) private var voiceWake + @Environment(\.accessibilityReduceMotion) private var reduceMotion @AppStorage(VoiceWakePreferences.enabledKey) private var voiceWakeEnabled: Bool = false @State private var selectedTab: Int = 0 @State private var voiceWakeToastText: String? @@ -52,14 +53,14 @@ struct RootTabs: View { guard !trimmed.isEmpty else { return } self.toastDismissTask?.cancel() - withAnimation(.spring(response: 0.25, dampingFraction: 0.85)) { + withAnimation(self.reduceMotion ? .none : .spring(response: 0.25, dampingFraction: 0.85)) { self.voiceWakeToastText = trimmed } self.toastDismissTask = Task { try? await Task.sleep(nanoseconds: 2_300_000_000) await MainActor.run { - withAnimation(.easeOut(duration: 0.25)) { + withAnimation(self.reduceMotion ? .none : .easeOut(duration: 0.25)) { self.voiceWakeToastText = nil } } @@ -104,66 +105,10 @@ struct RootTabs: View { } private var statusActivity: StatusPill.Activity? { - // Keep the top pill consistent across tabs (camera + voice wake + pairing states). - if self.appModel.isBackgrounded { - return StatusPill.Activity( - title: "Foreground required", - systemImage: "exclamationmark.triangle.fill", - tint: .orange) - } - - let gatewayStatus = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines) - let gatewayLower = gatewayStatus.lowercased() - if gatewayLower.contains("repair") { - return StatusPill.Activity(title: "Repairing…", systemImage: "wrench.and.screwdriver", tint: .orange) - } - if gatewayLower.contains("approval") || gatewayLower.contains("pairing") { - return StatusPill.Activity(title: "Approval pending", systemImage: "person.crop.circle.badge.clock") - } - // Avoid duplicating the primary gateway status ("Connecting…") in the activity slot. - - if self.appModel.screenRecordActive { - return StatusPill.Activity(title: "Recording screen…", systemImage: "record.circle.fill", tint: .red) - } - - if let cameraHUDText = self.appModel.cameraHUDText, - let cameraHUDKind = self.appModel.cameraHUDKind, - !cameraHUDText.isEmpty - { - let systemImage: String - let tint: Color? - switch cameraHUDKind { - case .photo: - systemImage = "camera.fill" - tint = nil - case .recording: - systemImage = "video.fill" - tint = .red - case .success: - systemImage = "checkmark.circle.fill" - tint = .green - case .error: - systemImage = "exclamationmark.triangle.fill" - tint = .red - } - return StatusPill.Activity(title: cameraHUDText, systemImage: systemImage, tint: tint) - } - - if self.voiceWakeEnabled { - let voiceStatus = self.appModel.voiceWake.statusText - if voiceStatus.localizedCaseInsensitiveContains("microphone permission") { - return StatusPill.Activity(title: "Mic permission", systemImage: "mic.slash", tint: .orange) - } - if voiceStatus == "Paused" { - // Talk mode intentionally pauses voice wake to release the mic. Don't spam the HUD for that case. - if self.appModel.talkMode.isEnabled { - return nil - } - let suffix = self.appModel.isBackgrounded ? " (background)" : "" - return StatusPill.Activity(title: "Voice Wake paused\(suffix)", systemImage: "pause.circle.fill") - } - } - - return nil + StatusActivityBuilder.build( + appModel: self.appModel, + voiceWakeEnabled: self.voiceWakeEnabled, + cameraHUDText: self.appModel.cameraHUDText, + cameraHUDKind: self.appModel.cameraHUDKind) } } diff --git a/apps/ios/Sources/Screen/ScreenController.swift b/apps/ios/Sources/Screen/ScreenController.swift index 506b78a230815..0045232362bde 100644 --- a/apps/ios/Sources/Screen/ScreenController.swift +++ b/apps/ios/Sources/Screen/ScreenController.swift @@ -1,14 +1,12 @@ import OpenClawKit import Observation -import SwiftUI +import UIKit import WebKit @MainActor @Observable final class ScreenController { - let webView: WKWebView - private let navigationDelegate: ScreenNavigationDelegate - private let a2uiActionHandler: CanvasA2UIActionMessageHandler + private weak var activeWebView: WKWebView? var urlString: String = "" var errorText: String? @@ -24,29 +22,6 @@ final class ScreenController { private var debugStatusSubtitle: String? init() { - let config = WKWebViewConfiguration() - config.websiteDataStore = .nonPersistent() - let a2uiActionHandler = CanvasA2UIActionMessageHandler() - let userContentController = WKUserContentController() - for name in CanvasA2UIActionMessageHandler.handlerNames { - userContentController.add(a2uiActionHandler, name: name) - } - config.userContentController = userContentController - self.navigationDelegate = ScreenNavigationDelegate() - self.a2uiActionHandler = a2uiActionHandler - self.webView = WKWebView(frame: .zero, configuration: config) - // Canvas scaffold is a fully self-contained HTML page; avoid relying on transparency underlays. - self.webView.isOpaque = true - self.webView.backgroundColor = .black - self.webView.scrollView.backgroundColor = .black - self.webView.scrollView.contentInsetAdjustmentBehavior = .never - self.webView.scrollView.contentInset = .zero - self.webView.scrollView.scrollIndicatorInsets = .zero - self.webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - self.applyScrollBehavior() - self.webView.navigationDelegate = self.navigationDelegate - self.navigationDelegate.controller = self - a2uiActionHandler.controller = self self.reload() } @@ -71,24 +46,26 @@ final class ScreenController { } func reload() { - let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines) self.applyScrollBehavior() + guard let webView = self.activeWebView else { return } + + let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { guard let url = Self.canvasScaffoldURL else { return } self.errorText = nil - self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) + webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) + return + } + + guard let url = URL(string: trimmed) else { + self.errorText = "Invalid URL: \(trimmed)" return + } + self.errorText = nil + if url.isFileURL { + webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) } else { - guard let url = URL(string: trimmed) else { - self.errorText = "Invalid URL: \(trimmed)" - return - } - self.errorText = nil - if url.isFileURL { - self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) - } else { - self.webView.load(URLRequest(url: url)) - } + webView.load(URLRequest(url: url)) } } @@ -108,7 +85,8 @@ final class ScreenController { self.applyDebugStatusIfNeeded() } - fileprivate func applyDebugStatusIfNeeded() { + func applyDebugStatusIfNeeded() { + guard let webView = self.activeWebView else { return } let enabled = self.debugStatusEnabled let title = self.debugStatusTitle let subtitle = self.debugStatusSubtitle @@ -127,7 +105,7 @@ final class ScreenController { } catch (_) {} })() """ - self.webView.evaluateJavaScript(js) { _, _ in } + webView.evaluateJavaScript(js) { _, _ in } } func waitForA2UIReady(timeoutMs: Int) async -> Bool { @@ -154,8 +132,13 @@ final class ScreenController { } func eval(javaScript: String) async throws -> String { - try await withCheckedThrowingContinuation { cont in - self.webView.evaluateJavaScript(javaScript) { result, error in + guard let webView = self.activeWebView else { + throw NSError(domain: "Screen", code: 3, userInfo: [ + NSLocalizedDescriptionKey: "web view unavailable", + ]) + } + return try await withCheckedThrowingContinuation { cont in + webView.evaluateJavaScript(javaScript) { result, error in if let error { cont.resume(throwing: error) return @@ -174,8 +157,13 @@ final class ScreenController { if let maxWidth { config.snapshotWidth = NSNumber(value: Double(maxWidth)) } + guard let webView = self.activeWebView else { + throw NSError(domain: "Screen", code: 3, userInfo: [ + NSLocalizedDescriptionKey: "web view unavailable", + ]) + } let image: UIImage = try await withCheckedThrowingContinuation { cont in - self.webView.takeSnapshot(with: config) { image, error in + webView.takeSnapshot(with: config) { image, error in if let error { cont.resume(throwing: error) return @@ -206,8 +194,13 @@ final class ScreenController { if let maxWidth { config.snapshotWidth = NSNumber(value: Double(maxWidth)) } + guard let webView = self.activeWebView else { + throw NSError(domain: "Screen", code: 3, userInfo: [ + NSLocalizedDescriptionKey: "web view unavailable", + ]) + } let image: UIImage = try await withCheckedThrowingContinuation { cont in - self.webView.takeSnapshot(with: config) { image, error in + webView.takeSnapshot(with: config) { image, error in if let error { cont.resume(throwing: error) return @@ -238,6 +231,17 @@ final class ScreenController { return data.base64EncodedString() } + func attachWebView(_ webView: WKWebView) { + self.activeWebView = webView + self.reload() + self.applyDebugStatusIfNeeded() + } + + func detachWebView(_ webView: WKWebView) { + guard self.activeWebView === webView else { return } + self.activeWebView = nil + } + private static func bundledResourceURL( name: String, ext: String, @@ -277,9 +281,10 @@ final class ScreenController { } private func applyScrollBehavior() { + guard let webView = self.activeWebView else { return } let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines) let allowScroll = !trimmed.isEmpty - let scrollView = self.webView.scrollView + let scrollView = webView.scrollView // Default canvas needs raw touch events; external pages should scroll. scrollView.isScrollEnabled = allowScroll scrollView.bounces = allowScroll @@ -366,72 +371,3 @@ extension Double { return self } } - -// MARK: - Navigation Delegate - -/// Handles navigation policy to intercept openclaw:// deep links from canvas -@MainActor -private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate { - weak var controller: ScreenController? - - func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void) - { - guard let url = navigationAction.request.url else { - decisionHandler(.allow) - return - } - - // Intercept openclaw:// deep links. - if url.scheme?.lowercased() == "openclaw" { - decisionHandler(.cancel) - self.controller?.onDeepLink?(url) - return - } - - decisionHandler(.allow) - } - - func webView( - _: WKWebView, - didFailProvisionalNavigation _: WKNavigation?, - withError error: any Error) - { - self.controller?.errorText = error.localizedDescription - } - - func webView(_: WKWebView, didFinish _: WKNavigation?) { - self.controller?.errorText = nil - self.controller?.applyDebugStatusIfNeeded() - } - - func webView(_: WKWebView, didFail _: WKNavigation?, withError error: any Error) { - self.controller?.errorText = error.localizedDescription - } -} - -private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler { - static let messageName = "openclawCanvasA2UIAction" - static let handlerNames = [messageName] - - weak var controller: ScreenController? - - func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { - guard Self.handlerNames.contains(message.name) else { return } - guard let controller else { return } - - guard let url = message.webView?.url else { return } - if url.isFileURL { - guard controller.isTrustedCanvasUIURL(url) else { return } - } else { - // For security, only accept actions from local-network pages (e.g. the canvas host). - guard controller.isLocalNetworkCanvasURL(url) else { return } - } - - guard let body = ScreenController.parseA2UIActionBody(message.body) else { return } - - controller.onA2UIAction?(body) - } -} diff --git a/apps/ios/Sources/Screen/ScreenWebView.swift b/apps/ios/Sources/Screen/ScreenWebView.swift index c464521be5f9f..a30d78cbd0061 100644 --- a/apps/ios/Sources/Screen/ScreenWebView.swift +++ b/apps/ios/Sources/Screen/ScreenWebView.swift @@ -5,11 +5,189 @@ import WebKit struct ScreenWebView: UIViewRepresentable { var controller: ScreenController - func makeUIView(context: Context) -> WKWebView { - self.controller.webView + func makeCoordinator() -> ScreenWebViewCoordinator { + ScreenWebViewCoordinator(controller: self.controller) } - func updateUIView(_ webView: WKWebView, context: Context) { - // State changes are driven by ScreenController. + func makeUIView(context: Context) -> UIView { + context.coordinator.makeContainerView() + } + + func updateUIView(_: UIView, context: Context) { + context.coordinator.updateController(self.controller) + } + + static func dismantleUIView(_: UIView, coordinator: ScreenWebViewCoordinator) { + coordinator.teardown() + } +} + +@MainActor +final class ScreenWebViewCoordinator: NSObject { + private weak var controller: ScreenController? + private let navigationDelegate = ScreenNavigationDelegate() + private let a2uiActionHandler = CanvasA2UIActionMessageHandler() + private let userContentController = WKUserContentController() + + private(set) var managedWebView: WKWebView? + private weak var containerView: UIView? + + init(controller: ScreenController) { + self.controller = controller + super.init() + self.navigationDelegate.controller = controller + self.a2uiActionHandler.controller = controller + } + + func makeContainerView() -> UIView { + if let containerView { + return containerView + } + + let container = UIView(frame: .zero) + container.backgroundColor = .black + + let webView = Self.makeWebView(userContentController: self.userContentController) + webView.navigationDelegate = self.navigationDelegate + self.installA2UIHandlers() + + webView.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(webView) + NSLayoutConstraint.activate([ + webView.leadingAnchor.constraint(equalTo: container.leadingAnchor), + webView.trailingAnchor.constraint(equalTo: container.trailingAnchor), + webView.topAnchor.constraint(equalTo: container.topAnchor), + webView.bottomAnchor.constraint(equalTo: container.bottomAnchor), + ]) + + self.managedWebView = webView + self.containerView = container + self.controller?.attachWebView(webView) + return container + } + + func updateController(_ controller: ScreenController) { + let previousController = self.controller + let controllerChanged = self.controller !== controller + self.controller = controller + self.navigationDelegate.controller = controller + self.a2uiActionHandler.controller = controller + if controllerChanged, let managedWebView { + previousController?.detachWebView(managedWebView) + controller.attachWebView(managedWebView) + } + } + + func teardown() { + if let managedWebView { + self.controller?.detachWebView(managedWebView) + managedWebView.navigationDelegate = nil + } + self.removeA2UIHandlers() + self.navigationDelegate.controller = nil + self.a2uiActionHandler.controller = nil + self.managedWebView = nil + self.containerView = nil + } + + private static func makeWebView(userContentController: WKUserContentController) -> WKWebView { + let config = WKWebViewConfiguration() + config.websiteDataStore = .nonPersistent() + config.userContentController = userContentController + + let webView = WKWebView(frame: .zero, configuration: config) + // Canvas scaffold is a fully self-contained HTML page; avoid relying on transparency underlays. + webView.isOpaque = true + webView.backgroundColor = .black + + let scrollView = webView.scrollView + scrollView.backgroundColor = .black + scrollView.contentInsetAdjustmentBehavior = .never + scrollView.contentInset = .zero + scrollView.scrollIndicatorInsets = .zero + scrollView.automaticallyAdjustsScrollIndicatorInsets = false + + return webView + } + + private func installA2UIHandlers() { + for name in CanvasA2UIActionMessageHandler.handlerNames { + self.userContentController.add(self.a2uiActionHandler, name: name) + } + } + + private func removeA2UIHandlers() { + for name in CanvasA2UIActionMessageHandler.handlerNames { + self.userContentController.removeScriptMessageHandler(forName: name) + } + } +} + +// MARK: - Navigation Delegate + +/// Handles navigation policy to intercept openclaw:// deep links from canvas +@MainActor +private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate { + weak var controller: ScreenController? + + func webView( + _: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void) + { + guard let url = navigationAction.request.url else { + decisionHandler(.allow) + return + } + + // Intercept openclaw:// deep links. + if url.scheme?.lowercased() == "openclaw" { + decisionHandler(.cancel) + self.controller?.onDeepLink?(url) + return + } + + decisionHandler(.allow) + } + + func webView( + _: WKWebView, + didFailProvisionalNavigation _: WKNavigation?, + withError error: any Error) + { + self.controller?.errorText = error.localizedDescription + } + + func webView(_: WKWebView, didFinish _: WKNavigation?) { + self.controller?.errorText = nil + self.controller?.applyDebugStatusIfNeeded() + } + + func webView(_: WKWebView, didFail _: WKNavigation?, withError error: any Error) { + self.controller?.errorText = error.localizedDescription + } +} + +private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler { + static let messageName = "openclawCanvasA2UIAction" + static let handlerNames = [messageName] + + weak var controller: ScreenController? + + func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { + guard Self.handlerNames.contains(message.name) else { return } + guard let controller else { return } + + guard let url = message.webView?.url else { return } + if url.isFileURL { + guard controller.isTrustedCanvasUIURL(url) else { return } + } else { + // For security, only accept actions from local-network pages (e.g. the canvas host). + guard controller.isLocalNetworkCanvasURL(url) else { return } + } + + guard let body = ScreenController.parseA2UIActionBody(message.body) else { return } + + controller.onA2UIAction?(body) } } diff --git a/apps/ios/Sources/Services/NodeServiceProtocols.swift b/apps/ios/Sources/Services/NodeServiceProtocols.swift index 002c87ad9ca08..6f882e82a11fb 100644 --- a/apps/ios/Sources/Services/NodeServiceProtocols.swift +++ b/apps/ios/Sources/Services/NodeServiceProtocols.swift @@ -28,6 +28,12 @@ protocol LocationServicing: Sendable { desiredAccuracy: OpenClawLocationAccuracy, maxAgeMs: Int?, timeoutMs: Int?) async throws -> CLLocation + func startLocationUpdates( + desiredAccuracy: OpenClawLocationAccuracy, + significantChangesOnly: Bool) -> AsyncStream + func stopLocationUpdates() + func startMonitoringSignificantLocationChanges(onUpdate: @escaping @Sendable (CLLocation) -> Void) + func stopMonitoringSignificantLocationChanges() } protocol DeviceStatusServicing: Sendable { @@ -59,6 +65,29 @@ protocol MotionServicing: Sendable { func pedometer(params: OpenClawPedometerParams) async throws -> OpenClawPedometerPayload } +struct WatchMessagingStatus: Sendable, Equatable { + var supported: Bool + var paired: Bool + var appInstalled: Bool + var reachable: Bool + var activationState: String +} + +struct WatchNotificationSendResult: Sendable, Equatable { + var deliveredImmediately: Bool + var queuedForDelivery: Bool + var transport: String +} + +protocol WatchMessagingServicing: AnyObject, Sendable { + func status() async -> WatchMessagingStatus + func sendNotification( + id: String, + title: String, + body: String, + priority: OpenClawNotificationPriority?) async throws -> WatchNotificationSendResult +} + extension CameraController: CameraServicing {} extension ScreenRecordService: ScreenRecordingServicing {} extension LocationService: LocationServicing {} diff --git a/apps/ios/Sources/Services/WatchMessagingService.swift b/apps/ios/Sources/Services/WatchMessagingService.swift new file mode 100644 index 0000000000000..8332fb5882d7f --- /dev/null +++ b/apps/ios/Sources/Services/WatchMessagingService.swift @@ -0,0 +1,176 @@ +import Foundation +import OpenClawKit +import OSLog +@preconcurrency import WatchConnectivity + +enum WatchMessagingError: LocalizedError { + case unsupported + case notPaired + case watchAppNotInstalled + + var errorDescription: String? { + switch self { + case .unsupported: + "WATCH_UNAVAILABLE: WatchConnectivity is not supported on this device" + case .notPaired: + "WATCH_UNAVAILABLE: no paired Apple Watch" + case .watchAppNotInstalled: + "WATCH_UNAVAILABLE: OpenClaw watch companion app is not installed" + } + } +} + +final class WatchMessagingService: NSObject, WatchMessagingServicing, @unchecked Sendable { + private static let logger = Logger(subsystem: "ai.openclaw", category: "watch.messaging") + private let session: WCSession? + + override init() { + if WCSession.isSupported() { + self.session = WCSession.default + } else { + self.session = nil + } + super.init() + if let session = self.session { + session.delegate = self + session.activate() + } + } + + static func isSupportedOnDevice() -> Bool { + WCSession.isSupported() + } + + static func currentStatusSnapshot() -> WatchMessagingStatus { + guard WCSession.isSupported() else { + return WatchMessagingStatus( + supported: false, + paired: false, + appInstalled: false, + reachable: false, + activationState: "unsupported") + } + let session = WCSession.default + return status(for: session) + } + + func status() async -> WatchMessagingStatus { + await self.ensureActivated() + guard let session = self.session else { + return WatchMessagingStatus( + supported: false, + paired: false, + appInstalled: false, + reachable: false, + activationState: "unsupported") + } + return Self.status(for: session) + } + + func sendNotification( + id: String, + title: String, + body: String, + priority: OpenClawNotificationPriority?) async throws -> WatchNotificationSendResult + { + await self.ensureActivated() + guard let session = self.session else { + throw WatchMessagingError.unsupported + } + + let snapshot = Self.status(for: session) + guard snapshot.paired else { throw WatchMessagingError.notPaired } + guard snapshot.appInstalled else { throw WatchMessagingError.watchAppNotInstalled } + + let payload: [String: Any] = [ + "type": "watch.notify", + "id": id, + "title": title, + "body": body, + "priority": priority?.rawValue ?? OpenClawNotificationPriority.active.rawValue, + "sentAtMs": Int(Date().timeIntervalSince1970 * 1000), + ] + + if snapshot.reachable { + do { + try await self.sendReachableMessage(payload, with: session) + return WatchNotificationSendResult( + deliveredImmediately: true, + queuedForDelivery: false, + transport: "sendMessage") + } catch { + Self.logger.error("watch sendMessage failed: \(error.localizedDescription, privacy: .public)") + } + } + + _ = session.transferUserInfo(payload) + return WatchNotificationSendResult( + deliveredImmediately: false, + queuedForDelivery: true, + transport: "transferUserInfo") + } + + private func sendReachableMessage(_ payload: [String: Any], with session: WCSession) async throws { + try await withCheckedThrowingContinuation { continuation in + session.sendMessage(payload, replyHandler: { _ in + continuation.resume() + }, errorHandler: { error in + continuation.resume(throwing: error) + }) + } + } + + private func ensureActivated() async { + guard let session = self.session else { return } + if session.activationState == .activated { return } + session.activate() + for _ in 0..<8 { + if session.activationState == .activated { return } + try? await Task.sleep(nanoseconds: 100_000_000) + } + } + + private static func status(for session: WCSession) -> WatchMessagingStatus { + WatchMessagingStatus( + supported: true, + paired: session.isPaired, + appInstalled: session.isWatchAppInstalled, + reachable: session.isReachable, + activationState: activationStateLabel(session.activationState)) + } + + private static func activationStateLabel(_ state: WCSessionActivationState) -> String { + switch state { + case .notActivated: + "notActivated" + case .inactive: + "inactive" + case .activated: + "activated" + @unknown default: + "unknown" + } + } +} + +extension WatchMessagingService: WCSessionDelegate { + func session( + _ session: WCSession, + activationDidCompleteWith activationState: WCSessionActivationState, + error: (any Error)?) + { + if let error { + Self.logger.error("watch activation failed: \(error.localizedDescription, privacy: .public)") + return + } + Self.logger.debug("watch activation state=\(Self.activationStateLabel(activationState), privacy: .public)") + } + + func sessionDidBecomeInactive(_ session: WCSession) {} + + func sessionDidDeactivate(_ session: WCSession) { + session.activate() + } + + func sessionReachabilityDidChange(_ session: WCSession) {} +} diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift index 662a22cb04952..7825b45cb8d6f 100644 --- a/apps/ios/Sources/Settings/SettingsTab.swift +++ b/apps/ios/Sources/Settings/SettingsTab.swift @@ -6,6 +6,12 @@ import SwiftUI import UIKit struct SettingsTab: View { + private struct FeatureHelp: Identifiable { + let id = UUID() + let title: String + let message: String + } + @Environment(NodeAppModel.self) private var appModel: NodeAppModel @Environment(VoiceWakeManager.self) private var voiceWake: VoiceWakeManager @Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController @@ -15,9 +21,10 @@ struct SettingsTab: View { @AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false @AppStorage("talk.enabled") private var talkEnabled: Bool = false @AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true + @AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false + @AppStorage("talk.voiceDirectiveHint.enabled") private var talkVoiceDirectiveHintEnabled: Bool = true @AppStorage("camera.enabled") private var cameraEnabled: Bool = true @AppStorage("location.enabledMode") private var locationEnabledModeRaw: String = OpenClawLocationMode.off.rawValue - @AppStorage("location.preciseEnabled") private var locationPreciseEnabled: Bool = true @AppStorage("screen.preventSleep") private var preventSleep: Bool = true @AppStorage("gateway.preferredStableID") private var preferredGatewayStableID: String = "" @AppStorage("gateway.lastDiscoveredStableID") private var lastDiscoveredGatewayStableID: String = "" @@ -28,17 +35,27 @@ struct SettingsTab: View { @AppStorage("gateway.manual.tls") private var manualGatewayTLS: Bool = true @AppStorage("gateway.discovery.debugLogs") private var discoveryDebugLogsEnabled: Bool = false @AppStorage("canvas.debugStatusEnabled") private var canvasDebugStatusEnabled: Bool = false + + // Onboarding control (RootCanvas listens to onboarding.requestID and force-opens the wizard). + @AppStorage("onboarding.requestID") private var onboardingRequestID: Int = 0 + @AppStorage("gateway.onboardingComplete") private var onboardingComplete: Bool = false + @AppStorage("gateway.hasConnectedOnce") private var hasConnectedOnce: Bool = false + @State private var connectingGatewayID: String? - @State private var localIPAddress: String? @State private var lastLocationModeRaw: String = OpenClawLocationMode.off.rawValue @State private var gatewayToken: String = "" @State private var gatewayPassword: String = "" + @State private var defaultShareInstruction: String = "" @AppStorage("gateway.setupCode") private var setupCode: String = "" @State private var setupStatusText: String? @State private var manualGatewayPortText: String = "" @State private var gatewayExpanded: Bool = true @State private var selectedAgentPickerId: String = "" + @State private var showResetOnboardingAlert: Bool = false + @State private var activeFeatureHelp: FeatureHelp? + @State private var suppressCredentialPersist: Bool = false + private let gatewayLogger = Logger(subsystem: "ai.openclaw.ios", category: "GatewaySettings") var body: some View { @@ -103,7 +120,6 @@ struct SettingsTab: View { .foregroundStyle(.secondary) } - DisclosureGroup("Advanced") { if self.appModel.gatewayServerName == nil { LabeledContent("Discovery", value: self.gatewayController.discoveryStatusText) } @@ -148,69 +164,74 @@ struct SettingsTab: View { self.gatewayList(showing: .all) } - Toggle("Use Manual Gateway", isOn: self.$manualGatewayEnabled) + DisclosureGroup("Advanced") { + Toggle("Use Manual Gateway", isOn: self.$manualGatewayEnabled) - TextField("Host", text: self.$manualGatewayHost) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() + TextField("Host", text: self.$manualGatewayHost) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() - TextField("Port (optional)", text: self.manualPortBinding) - .keyboardType(.numberPad) + TextField("Port (optional)", text: self.manualPortBinding) + .keyboardType(.numberPad) - Toggle("Use TLS", isOn: self.$manualGatewayTLS) + Toggle("Use TLS", isOn: self.$manualGatewayTLS) - Button { - Task { await self.connectManual() } - } label: { - if self.connectingGatewayID == "manual" { - HStack(spacing: 8) { - ProgressView() - .progressViewStyle(.circular) - Text("Connecting…") + Button { + Task { await self.connectManual() } + } label: { + if self.connectingGatewayID == "manual" { + HStack(spacing: 8) { + ProgressView() + .progressViewStyle(.circular) + Text("Connecting…") + } + } else { + Text("Connect (Manual)") } - } else { - Text("Connect (Manual)") } - } - .disabled(self.connectingGatewayID != nil || self.manualGatewayHost - .trimmingCharacters(in: .whitespacesAndNewlines) - .isEmpty || !self.manualPortIsValid) + .disabled(self.connectingGatewayID != nil || self.manualGatewayHost + .trimmingCharacters(in: .whitespacesAndNewlines) + .isEmpty || !self.manualPortIsValid) - Text( - "Use this when mDNS/Bonjour discovery is blocked. " - + "Leave port empty for 443 on tailnet DNS (TLS) or 18789 otherwise.") - .font(.footnote) - .foregroundStyle(.secondary) + Text( + "Use this when mDNS/Bonjour discovery is blocked. " + + "Leave port empty for 443 on tailnet DNS (TLS) or 18789 otherwise.") + .font(.footnote) + .foregroundStyle(.secondary) + + Toggle("Discovery Debug Logs", isOn: self.$discoveryDebugLogsEnabled) + .onChange(of: self.discoveryDebugLogsEnabled) { _, newValue in + self.gatewayController.setDiscoveryDebugLoggingEnabled(newValue) + } - Toggle("Discovery Debug Logs", isOn: self.$discoveryDebugLogsEnabled) - .onChange(of: self.discoveryDebugLogsEnabled) { _, newValue in - self.gatewayController.setDiscoveryDebugLoggingEnabled(newValue) + NavigationLink("Discovery Logs") { + GatewayDiscoveryDebugLogView() } - NavigationLink("Discovery Logs") { - GatewayDiscoveryDebugLogView() - } + Toggle("Debug Canvas Status", isOn: self.$canvasDebugStatusEnabled) - Toggle("Debug Canvas Status", isOn: self.$canvasDebugStatusEnabled) + TextField("Gateway Auth Token", text: self.$gatewayToken) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() - TextField("Gateway Token", text: self.$gatewayToken) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() + SecureField("Gateway Password", text: self.$gatewayPassword) - SecureField("Gateway Password", text: self.$gatewayPassword) + Button("Reset Onboarding", role: .destructive) { + self.showResetOnboardingAlert = true + } - VStack(alignment: .leading, spacing: 6) { - Text("Debug") - .font(.footnote.weight(.semibold)) - .foregroundStyle(.secondary) - Text(self.gatewayDebugText()) - .font(.system(size: 12, weight: .regular, design: .monospaced)) - .foregroundStyle(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(10) - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + VStack(alignment: .leading, spacing: 6) { + Text("Debug") + .font(.footnote.weight(.semibold)) + .foregroundStyle(.secondary) + Text(self.gatewayDebugText()) + .font(.system(size: 12, weight: .regular, design: .monospaced)) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + } } - } } label: { HStack(spacing: 10) { Circle() @@ -227,16 +248,22 @@ struct SettingsTab: View { Section("Device") { DisclosureGroup("Features") { - Toggle("Voice Wake", isOn: self.$voiceWakeEnabled) - .onChange(of: self.voiceWakeEnabled) { _, newValue in + self.featureToggle( + "Voice Wake", + isOn: self.$voiceWakeEnabled, + help: "Enables wake-word activation to start a hands-free session.") { newValue in self.appModel.setVoiceWakeEnabled(newValue) } - Toggle("Talk Mode", isOn: self.$talkEnabled) - .onChange(of: self.talkEnabled) { _, newValue in + self.featureToggle( + "Talk Mode", + isOn: self.$talkEnabled, + help: "Enables voice conversation mode with your connected OpenClaw agent.") { newValue in self.appModel.setTalkEnabled(newValue) } - // Keep this separate so users can hide the side bubble without disabling Talk Mode. - Toggle("Show Talk Button", isOn: self.$talkButtonEnabled) + self.featureToggle( + "Background Listening", + isOn: self.$talkBackgroundEnabled, + help: "Keeps listening while the app is backgrounded. Uses more battery.") NavigationLink { VoiceWakeWordsSettingsView() @@ -246,29 +273,78 @@ struct SettingsTab: View { value: VoiceWakePreferences.displayString(for: self.voiceWake.triggerWords)) } - Toggle("Allow Camera", isOn: self.$cameraEnabled) - Text("Allows the gateway to request photos or short video clips (foreground only).") - .font(.footnote) - .foregroundStyle(.secondary) + self.featureToggle( + "Allow Camera", + isOn: self.$cameraEnabled, + help: "Allows the gateway to request photos or short video clips while OpenClaw is foregrounded.") + HStack(spacing: 8) { + Text("Location Access") + Spacer() + Button { + self.activeFeatureHelp = FeatureHelp( + title: "Location Access", + message: "Controls location permissions for OpenClaw. Off disables location tools, While Using enables foreground location, and Always enables background location.") + } label: { + Image(systemName: "info.circle") + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .accessibilityLabel("Location Access info") + } Picker("Location Access", selection: self.$locationEnabledModeRaw) { Text("Off").tag(OpenClawLocationMode.off.rawValue) Text("While Using").tag(OpenClawLocationMode.whileUsing.rawValue) Text("Always").tag(OpenClawLocationMode.always.rawValue) } + .labelsHidden() .pickerStyle(.segmented) - Toggle("Precise Location", isOn: self.$locationPreciseEnabled) - .disabled(self.locationMode == .off) + self.featureToggle( + "Prevent Sleep", + isOn: self.$preventSleep, + help: "Keeps the screen awake while OpenClaw is open.") - Text("Always requires system permission and may prompt to open Settings.") - .font(.footnote) - .foregroundStyle(.secondary) + DisclosureGroup("Advanced") { + self.featureToggle( + "Voice Directive Hint", + isOn: self.$talkVoiceDirectiveHintEnabled, + help: "Adds voice-switching instructions to Talk prompts. Disable to reduce prompt size.") + self.featureToggle( + "Show Talk Button", + isOn: self.$talkButtonEnabled, + help: "Shows the floating Talk button in the main interface.") + TextField("Default Share Instruction", text: self.$defaultShareInstruction, axis: .vertical) + .lineLimit(2 ... 6) + .textInputAutocapitalization(.sentences) + HStack(spacing: 8) { + Text("Default Share Instruction") + .font(.footnote) + .foregroundStyle(.secondary) + Spacer() + Button { + self.activeFeatureHelp = FeatureHelp( + title: "Default Share Instruction", + message: "Appends this instruction when sharing content into OpenClaw from iOS.") + } label: { + Image(systemName: "info.circle") + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .accessibilityLabel("Default Share Instruction info") + } - Toggle("Prevent Sleep", isOn: self.$preventSleep) - Text("Keeps the screen awake while OpenClaw is open.") - .font(.footnote) - .foregroundStyle(.secondary) + VStack(alignment: .leading, spacing: 8) { + Button { + Task { await self.appModel.runSharePipelineSelfTest() } + } label: { + Label("Run Share Self-Test", systemImage: "checkmark.seal") + } + Text(self.appModel.lastShareEventText) + .font(.footnote) + .foregroundStyle(.secondary) + } + } } DisclosureGroup("Device Info") { @@ -276,19 +352,11 @@ struct SettingsTab: View { Text(self.instanceId) .font(.footnote) .foregroundStyle(.secondary) - LabeledContent("IP", value: self.localIPAddress ?? "—") - .contextMenu { - if let ip = self.localIPAddress { - Button { - UIPasteboard.general.string = ip - } label: { - Label("Copy", systemImage: "doc.on.doc") - } - } - } + .lineLimit(1) + .truncationMode(.middle) + LabeledContent("Device", value: self.deviceFamily()) LabeledContent("Platform", value: self.platformString()) - LabeledContent("Version", value: self.appVersion()) - LabeledContent("Model", value: self.modelIdentifier()) + LabeledContent("OpenClaw", value: self.openClawVersionString()) } } } @@ -303,8 +371,22 @@ struct SettingsTab: View { .accessibilityLabel("Close") } } + .alert("Reset Onboarding?", isPresented: self.$showResetOnboardingAlert) { + Button("Reset", role: .destructive) { + self.resetOnboarding() + } + Button("Cancel", role: .cancel) {} + } message: { + Text( + "This will disconnect, clear saved gateway connection + credentials, and reopen the onboarding wizard.") + } + .alert(item: self.$activeFeatureHelp) { help in + Alert( + title: Text(help.title), + message: Text(help.message), + dismissButton: .default(Text("OK"))) + } .onAppear { - self.localIPAddress = Self.primaryIPv4Address() self.lastLocationModeRaw = self.locationEnabledModeRaw self.syncManualPortText() let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) @@ -312,6 +394,8 @@ struct SettingsTab: View { self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? "" self.gatewayPassword = GatewaySettingsStore.loadGatewayPassword(instanceId: trimmedInstanceId) ?? "" } + self.defaultShareInstruction = ShareToAgentSettings.loadDefaultInstruction() + self.appModel.refreshLastShareEventFromRelay() // Keep setup front-and-center when disconnected; keep things compact once connected. self.gatewayExpanded = !self.isGatewayConnected self.selectedAgentPickerId = self.appModel.selectedAgentId ?? "" @@ -331,17 +415,22 @@ struct SettingsTab: View { GatewaySettingsStore.savePreferredGatewayStableID(trimmed) } .onChange(of: self.gatewayToken) { _, newValue in + guard !self.suppressCredentialPersist else { return } let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) guard !instanceId.isEmpty else { return } GatewaySettingsStore.saveGatewayToken(trimmed, instanceId: instanceId) } .onChange(of: self.gatewayPassword) { _, newValue in + guard !self.suppressCredentialPersist else { return } let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) guard !instanceId.isEmpty else { return } GatewaySettingsStore.saveGatewayPassword(trimmed, instanceId: instanceId) } + .onChange(of: self.defaultShareInstruction) { _, newValue in + ShareToAgentSettings.saveDefaultInstruction(newValue) + } .onChange(of: self.manualGatewayPort) { _, _ in self.syncManualPortText() } @@ -421,10 +510,11 @@ struct SettingsTab: View { ForEach(rows) { gateway in HStack { VStack(alignment: .leading, spacing: 2) { - Text(gateway.name) + // Avoid localized-string formatting edge cases from Bonjour-advertised names. + Text(verbatim: gateway.name) let detailLines = self.gatewayDetailLines(gateway) ForEach(detailLines, id: \.self) { line in - Text(line) + Text(verbatim: line) .font(.footnote) .foregroundStyle(.secondary) } @@ -472,14 +562,6 @@ struct SettingsTab: View { return "iOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)" } - private var locationMode: OpenClawLocationMode { - OpenClawLocationMode(rawValue: self.locationEnabledModeRaw) ?? .off - } - - private func appVersion() -> String { - Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev" - } - private func deviceFamily() -> String { switch UIDevice.current.userInterfaceIdiom { case .pad: @@ -491,14 +573,36 @@ struct SettingsTab: View { } } - private func modelIdentifier() -> String { - var systemInfo = utsname() - uname(&systemInfo) - let machine = withUnsafeBytes(of: &systemInfo.machine) { ptr in - String(bytes: ptr.prefix { $0 != 0 }, encoding: .utf8) + private func openClawVersionString() -> String { + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev" + let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "" + let trimmedBuild = build.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmedBuild.isEmpty || trimmedBuild == version { + return version + } + return "\(version) (\(trimmedBuild))" + } + + private func featureToggle( + _ title: String, + isOn: Binding, + help: String, + onChange: ((Bool) -> Void)? = nil + ) -> some View { + HStack(spacing: 8) { + Toggle(title, isOn: isOn) + Button { + self.activeFeatureHelp = FeatureHelp(title: title, message: help) + } label: { + Image(systemName: "info.circle") + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .accessibilityLabel("\(title) info") + } + .onChange(of: isOn.wrappedValue) { _, newValue in + onChange?(newValue) } - let trimmed = machine?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - return trimmed.isEmpty ? "unknown" : trimmed } private func connect(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async { @@ -510,7 +614,10 @@ struct SettingsTab: View { GatewaySettingsStore.saveLastDiscoveredGatewayStableID(gateway.stableID) defer { self.connectingGatewayID = nil } - await self.gatewayController.connect(gateway) + let err = await self.gatewayController.connectWithDiagnostics(gateway) + if let err { + self.setupStatusText = err + } } private func connectLastKnown() async { @@ -590,15 +697,6 @@ struct SettingsTab: View { } } - private struct SetupPayload: Codable { - var url: String? - var host: String? - var port: Int? - var tls: Bool? - var token: String? - var password: String? - } - private func applySetupCodeAndConnect() async { self.setupStatusText = nil guard self.applySetupCode() else { return } @@ -626,7 +724,7 @@ struct SettingsTab: View { return false } - guard let payload = self.decodeSetupPayload(raw: raw) else { + guard let payload = GatewaySetupCode.decode(raw: raw) else { self.setupStatusText = "Setup code not recognized." return false } @@ -727,67 +825,14 @@ struct SettingsTab: View { } private static func probeTCP(host: String, port: Int, timeoutSeconds: Double) async -> Bool { - guard let nwPort = NWEndpoint.Port(rawValue: UInt16(port)) else { return false } - let endpointHost = NWEndpoint.Host(host) - let connection = NWConnection(host: endpointHost, port: nwPort, using: .tcp) - return await withCheckedContinuation { cont in - let queue = DispatchQueue(label: "gateway.preflight") - let finished = OSAllocatedUnfairLock(initialState: false) - let finish: @Sendable (Bool) -> Void = { ok in - let shouldResume = finished.withLock { flag -> Bool in - if flag { return false } - flag = true - return true - } - guard shouldResume else { return } - connection.cancel() - cont.resume(returning: ok) - } - connection.stateUpdateHandler = { state in - switch state { - case .ready: - finish(true) - case .failed, .cancelled: - finish(false) - default: - break - } - } - connection.start(queue: queue) - queue.asyncAfter(deadline: .now() + timeoutSeconds) { - finish(false) - } - } - } - - private func decodeSetupPayload(raw: String) -> SetupPayload? { - if let payload = decodeSetupPayloadFromJSON(raw) { - return payload - } - if let decoded = decodeBase64Payload(raw), - let payload = decodeSetupPayloadFromJSON(decoded) - { - return payload - } - return nil - } - - private func decodeSetupPayloadFromJSON(_ json: String) -> SetupPayload? { - guard let data = json.data(using: .utf8) else { return nil } - return try? JSONDecoder().decode(SetupPayload.self, from: data) + await TCPProbe.probe( + host: host, + port: port, + timeoutSeconds: timeoutSeconds, + queueLabel: "gateway.preflight") } - private func decodeBase64Payload(_ raw: String) -> String? { - let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return nil } - let normalized = trimmed - .replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - let padding = normalized.count % 4 - let padded = padding == 0 ? normalized : normalized + String(repeating: "=", count: 4 - padding) - guard let data = Data(base64Encoded: padded) else { return nil } - return String(data: data, encoding: .utf8) - } + // (GatewaySetupCode) decode raw setup codes. private func connectManual() async { let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines) @@ -852,44 +897,6 @@ struct SettingsTab: View { return nil } - private static func primaryIPv4Address() -> String? { - var addrList: UnsafeMutablePointer? - guard getifaddrs(&addrList) == 0, let first = addrList else { return nil } - defer { freeifaddrs(addrList) } - - var fallback: String? - var en0: String? - - for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) { - let flags = Int32(ptr.pointee.ifa_flags) - let isUp = (flags & IFF_UP) != 0 - let isLoopback = (flags & IFF_LOOPBACK) != 0 - let name = String(cString: ptr.pointee.ifa_name) - let family = ptr.pointee.ifa_addr.pointee.sa_family - if !isUp || isLoopback || family != UInt8(AF_INET) { continue } - - var addr = ptr.pointee.ifa_addr.pointee - var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) - let result = getnameinfo( - &addr, - socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), - &buffer, - socklen_t(buffer.count), - nil, - 0, - NI_NUMERICHOST) - guard result == 0 else { continue } - let len = buffer.prefix { $0 != 0 } - let bytes = len.map { UInt8(bitPattern: $0) } - guard let ip = String(bytes: bytes, encoding: .utf8) else { continue } - - if name == "en0" { en0 = ip; break } - if fallback == nil { fallback = ip } - } - - return en0 ?? fallback - } - private static func hasTailnetIPv4() -> Bool { var addrList: UnsafeMutablePointer? guard getifaddrs(&addrList) == 0, let first = addrList else { return false } @@ -949,6 +956,43 @@ struct SettingsTab: View { SettingsNetworkingHelpers.httpURLString(host: host, port: port, fallback: fallback) } + private func resetOnboarding() { + // Disconnect first so RootCanvas doesn't instantly mark onboarding complete again. + self.appModel.disconnectGateway() + self.connectingGatewayID = nil + self.setupStatusText = nil + self.setupCode = "" + self.gatewayAutoConnect = false + + self.suppressCredentialPersist = true + defer { self.suppressCredentialPersist = false } + + self.gatewayToken = "" + self.gatewayPassword = "" + + let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmedInstanceId.isEmpty { + GatewaySettingsStore.deleteGatewayCredentials(instanceId: trimmedInstanceId) + } + + // Reset onboarding state + clear saved gateway connection (the two things RootCanvas checks). + GatewaySettingsStore.clearLastGatewayConnection() + + // RootCanvas also short-circuits onboarding when these are true. + self.onboardingComplete = false + self.hasConnectedOnce = false + + // Clear manual override so it doesn't count as an existing gateway config. + self.manualGatewayEnabled = false + self.manualGatewayHost = "" + + // Force re-present even without app restart. + self.onboardingRequestID += 1 + + // The onboarding wizard is presented from RootCanvas; dismiss Settings so it can show. + self.dismiss() + } + private func gatewayDetailLines(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> [String] { var lines: [String] = [] if let lanHost = gateway.lanHost { lines.append("LAN: \(lanHost)") } diff --git a/apps/ios/Sources/Status/StatusActivityBuilder.swift b/apps/ios/Sources/Status/StatusActivityBuilder.swift new file mode 100644 index 0000000000000..381b3d2b9e8a8 --- /dev/null +++ b/apps/ios/Sources/Status/StatusActivityBuilder.swift @@ -0,0 +1,71 @@ +import SwiftUI + +enum StatusActivityBuilder { + @MainActor + static func build( + appModel: NodeAppModel, + voiceWakeEnabled: Bool, + cameraHUDText: String?, + cameraHUDKind: NodeAppModel.CameraHUDKind? + ) -> StatusPill.Activity? { + // Keep the top pill consistent across tabs (camera + voice wake + pairing states). + if appModel.isBackgrounded { + return StatusPill.Activity( + title: "Foreground required", + systemImage: "exclamationmark.triangle.fill", + tint: .orange) + } + + let gatewayStatus = appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines) + let gatewayLower = gatewayStatus.lowercased() + if gatewayLower.contains("repair") { + return StatusPill.Activity(title: "Repairing…", systemImage: "wrench.and.screwdriver", tint: .orange) + } + if gatewayLower.contains("approval") || gatewayLower.contains("pairing") { + return StatusPill.Activity(title: "Approval pending", systemImage: "person.crop.circle.badge.clock") + } + // Avoid duplicating the primary gateway status ("Connecting…") in the activity slot. + + if appModel.screenRecordActive { + return StatusPill.Activity(title: "Recording screen…", systemImage: "record.circle.fill", tint: .red) + } + + if let cameraHUDText, !cameraHUDText.isEmpty, let cameraHUDKind { + let systemImage: String + let tint: Color? + switch cameraHUDKind { + case .photo: + systemImage = "camera.fill" + tint = nil + case .recording: + systemImage = "video.fill" + tint = .red + case .success: + systemImage = "checkmark.circle.fill" + tint = .green + case .error: + systemImage = "exclamationmark.triangle.fill" + tint = .red + } + return StatusPill.Activity(title: cameraHUDText, systemImage: systemImage, tint: tint) + } + + if voiceWakeEnabled { + let voiceStatus = appModel.voiceWake.statusText + if voiceStatus.localizedCaseInsensitiveContains("microphone permission") { + return StatusPill.Activity(title: "Mic permission", systemImage: "mic.slash", tint: .orange) + } + if voiceStatus == "Paused" { + // Talk mode intentionally pauses voice wake to release the mic. Don't spam the HUD for that case. + if appModel.talkMode.isEnabled { + return nil + } + let suffix = appModel.isBackgrounded ? " (background)" : "" + return StatusPill.Activity(title: "Voice Wake paused\(suffix)", systemImage: "pause.circle.fill") + } + } + + return nil + } +} + diff --git a/apps/ios/Sources/Status/StatusPill.swift b/apps/ios/Sources/Status/StatusPill.swift index cd81c011bb1da..ea5e425c49d4c 100644 --- a/apps/ios/Sources/Status/StatusPill.swift +++ b/apps/ios/Sources/Status/StatusPill.swift @@ -2,6 +2,8 @@ import SwiftUI struct StatusPill: View { @Environment(\.scenePhase) private var scenePhase + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @Environment(\.colorSchemeContrast) private var contrast enum GatewayState: Equatable { case connected @@ -49,11 +51,11 @@ struct StatusPill: View { Circle() .fill(self.gateway.color) .frame(width: 9, height: 9) - .scaleEffect(self.gateway == .connecting ? (self.pulse ? 1.15 : 0.85) : 1.0) - .opacity(self.gateway == .connecting ? (self.pulse ? 1.0 : 0.6) : 1.0) + .scaleEffect(self.gateway == .connecting && !self.reduceMotion ? (self.pulse ? 1.15 : 0.85) : 1.0) + .opacity(self.gateway == .connecting && !self.reduceMotion ? (self.pulse ? 1.0 : 0.6) : 1.0) Text(self.gateway.title) - .font(.system(size: 13, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) } @@ -64,17 +66,17 @@ struct StatusPill: View { if let activity { HStack(spacing: 6) { Image(systemName: activity.systemImage) - .font(.system(size: 13, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundStyle(activity.tint ?? .primary) Text(activity.title) - .font(.system(size: 13, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) .lineLimit(1) } .transition(.opacity.combined(with: .move(edge: .top))) } else { Image(systemName: self.voiceWakeEnabled ? "mic.fill" : "mic.slash") - .font(.system(size: 13, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundStyle(self.voiceWakeEnabled ? .primary : .secondary) .accessibilityLabel(self.voiceWakeEnabled ? "Voice Wake enabled" : "Voice Wake disabled") .transition(.opacity.combined(with: .move(edge: .top))) @@ -87,21 +89,28 @@ struct StatusPill: View { .fill(.ultraThinMaterial) .overlay { RoundedRectangle(cornerRadius: 14, style: .continuous) - .strokeBorder(.white.opacity(self.brighten ? 0.24 : 0.18), lineWidth: 0.5) + .strokeBorder( + .white.opacity(self.contrast == .increased ? 0.5 : (self.brighten ? 0.24 : 0.18)), + lineWidth: self.contrast == .increased ? 1.0 : 0.5 + ) } .shadow(color: .black.opacity(0.25), radius: 12, y: 6) } } .buttonStyle(.plain) - .accessibilityLabel("Status") + .accessibilityLabel("Connection Status") .accessibilityValue(self.accessibilityValue) - .onAppear { self.updatePulse(for: self.gateway, scenePhase: self.scenePhase) } + .accessibilityHint("Double tap to open settings") + .onAppear { self.updatePulse(for: self.gateway, scenePhase: self.scenePhase, reduceMotion: self.reduceMotion) } .onDisappear { self.pulse = false } .onChange(of: self.gateway) { _, newValue in - self.updatePulse(for: newValue, scenePhase: self.scenePhase) + self.updatePulse(for: newValue, scenePhase: self.scenePhase, reduceMotion: self.reduceMotion) } .onChange(of: self.scenePhase) { _, newValue in - self.updatePulse(for: self.gateway, scenePhase: newValue) + self.updatePulse(for: self.gateway, scenePhase: newValue, reduceMotion: self.reduceMotion) + } + .onChange(of: self.reduceMotion) { _, newValue in + self.updatePulse(for: self.gateway, scenePhase: self.scenePhase, reduceMotion: newValue) } .animation(.easeInOut(duration: 0.18), value: self.activity?.title) } @@ -113,9 +122,9 @@ struct StatusPill: View { return "\(self.gateway.title), Voice Wake \(self.voiceWakeEnabled ? "enabled" : "disabled")" } - private func updatePulse(for gateway: GatewayState, scenePhase: ScenePhase) { - guard gateway == .connecting, scenePhase == .active else { - withAnimation(.easeOut(duration: 0.2)) { self.pulse = false } + private func updatePulse(for gateway: GatewayState, scenePhase: ScenePhase, reduceMotion: Bool) { + guard gateway == .connecting, scenePhase == .active, !reduceMotion else { + withAnimation(reduceMotion ? .none : .easeOut(duration: 0.2)) { self.pulse = false } return } diff --git a/apps/ios/Sources/Status/VoiceWakeToast.swift b/apps/ios/Sources/Status/VoiceWakeToast.swift index b7942f2036f5a..ef6fc1295a76e 100644 --- a/apps/ios/Sources/Status/VoiceWakeToast.swift +++ b/apps/ios/Sources/Status/VoiceWakeToast.swift @@ -1,17 +1,19 @@ import SwiftUI struct VoiceWakeToast: View { + @Environment(\.colorSchemeContrast) private var contrast + var command: String var brighten: Bool = false var body: some View { HStack(spacing: 10) { Image(systemName: "mic.fill") - .font(.system(size: 14, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) Text(self.command) - .font(.system(size: 14, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) .lineLimit(1) .truncationMode(.tail) @@ -23,11 +25,14 @@ struct VoiceWakeToast: View { .fill(.ultraThinMaterial) .overlay { RoundedRectangle(cornerRadius: 14, style: .continuous) - .strokeBorder(.white.opacity(self.brighten ? 0.24 : 0.18), lineWidth: 0.5) + .strokeBorder( + .white.opacity(self.contrast == .increased ? 0.5 : (self.brighten ? 0.24 : 0.18)), + lineWidth: self.contrast == .increased ? 1.0 : 0.5 + ) } .shadow(color: .black.opacity(0.25), radius: 12, y: 6) } - .accessibilityLabel("Voice Wake") - .accessibilityValue(self.command) + .accessibilityLabel("Voice Wake triggered") + .accessibilityValue("Command: \(self.command)") } } diff --git a/apps/ios/Sources/Voice/TalkModeManager.swift b/apps/ios/Sources/Voice/TalkModeManager.swift index 8351a6d5f9ad0..be90208af4788 100644 --- a/apps/ios/Sources/Voice/TalkModeManager.swift +++ b/apps/ios/Sources/Voice/TalkModeManager.swift @@ -16,6 +16,7 @@ import Speech final class TalkModeManager: NSObject { private typealias SpeechRequest = SFSpeechAudioBufferRecognitionRequest private static let defaultModelIdFallback = "eleven_v3" + private static let redactedConfigSentinel = "__OPENCLAW_REDACTED__" var isEnabled: Bool = false var isListening: Bool = false var isSpeaking: Bool = false @@ -218,8 +219,12 @@ final class TalkModeManager: NSObject { /// Suspends microphone usage without disabling Talk Mode. /// Used when the app backgrounds (or when we need to temporarily release the mic). - func suspendForBackground() -> Bool { + func suspendForBackground(keepActive: Bool = false) -> Bool { guard self.isEnabled else { return false } + if keepActive { + self.statusText = self.isListening ? "Listening" : self.statusText + return false + } let wasActive = self.isListening || self.isSpeaking || self.isPushToTalkActive self.isListening = false @@ -246,7 +251,8 @@ final class TalkModeManager: NSObject { return wasActive } - func resumeAfterBackground(wasSuspended: Bool) async { + func resumeAfterBackground(wasSuspended: Bool, wasKeptActive: Bool = false) async { + if wasKeptActive { return } guard wasSuspended else { return } guard self.isEnabled else { return } await self.start() @@ -814,29 +820,24 @@ final class TalkModeManager: NSObject { private func subscribeChatIfNeeded(sessionKey: String) async { let key = sessionKey.trimmingCharacters(in: .whitespacesAndNewlines) guard !key.isEmpty else { return } - guard let gateway else { return } guard !self.chatSubscribedSessionKeys.contains(key) else { return } - let payload = "{\"sessionKey\":\"\(key)\"}" - await gateway.sendEvent(event: "chat.subscribe", payloadJSON: payload) + // Operator clients receive chat events without node-style subscriptions. self.chatSubscribedSessionKeys.insert(key) - self.logger.info("chat.subscribe ok sessionKey=\(key, privacy: .public)") } private func unsubscribeAllChats() async { - guard let gateway else { return } - let keys = self.chatSubscribedSessionKeys self.chatSubscribedSessionKeys.removeAll() - for key in keys { - let payload = "{\"sessionKey\":\"\(key)\"}" - await gateway.sendEvent(event: "chat.unsubscribe", payloadJSON: payload) - } } private func buildPrompt(transcript: String) -> String { let interrupted = self.lastInterruptedAtSeconds self.lastInterruptedAtSeconds = nil - return TalkPromptBuilder.build(transcript: transcript, interruptedAtSeconds: interrupted) + let includeVoiceDirectiveHint = (UserDefaults.standard.object(forKey: "talk.voiceDirectiveHint.enabled") as? Bool) ?? true + return TalkPromptBuilder.build( + transcript: transcript, + interruptedAtSeconds: interrupted, + includeVoiceDirectiveHint: includeVoiceDirectiveHint) } private enum ChatCompletionState: CustomStringConvertible { @@ -1114,6 +1115,7 @@ final class TalkModeManager: NSObject { } private func shouldInterrupt(with transcript: String) -> Bool { + guard self.shouldAllowSpeechInterruptForCurrentRoute() else { return false } let trimmed = transcript.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmed.count >= 3 else { return false } if let spoken = self.lastSpokenText?.lowercased(), spoken.contains(trimmed.lowercased()) { @@ -1122,6 +1124,20 @@ final class TalkModeManager: NSObject { return true } + private func shouldAllowSpeechInterruptForCurrentRoute() -> Bool { + let route = AVAudioSession.sharedInstance().currentRoute + // Built-in speaker/receiver often feeds TTS back into STT, causing false interrupts. + // Allow barge-in for isolated outputs (headphones/Bluetooth/USB/CarPlay/AirPlay). + return !route.outputs.contains { output in + switch output.portType { + case .builtInSpeaker, .builtInReceiver: + return true + default: + return false + } + } + } + private func shouldUseIncrementalTTS() -> Bool { true } @@ -1668,6 +1684,15 @@ extension TalkModeManager { return value.allSatisfy { $0.isLetter || $0.isNumber || $0 == "-" || $0 == "_" } } + private static func normalizedTalkApiKey(_ raw: String?) -> String? { + let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + guard trimmed != Self.redactedConfigSentinel else { return nil } + // Config values may be env placeholders (for example `${ELEVENLABS_API_KEY}`). + if trimmed.hasPrefix("${"), trimmed.hasSuffix("}") { return nil } + return trimmed + } + func reloadConfig() async { guard let gateway else { return } do { @@ -1699,7 +1724,15 @@ extension TalkModeManager { } self.defaultOutputFormat = (talk?["outputFormat"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) - self.apiKey = (talk?["apiKey"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) + let rawConfigApiKey = (talk?["apiKey"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) + let configApiKey = Self.normalizedTalkApiKey(rawConfigApiKey) + let localApiKey = Self.normalizedTalkApiKey(GatewaySettingsStore.loadTalkElevenLabsApiKey()) + if rawConfigApiKey == Self.redactedConfigSentinel { + self.apiKey = (localApiKey?.isEmpty == false) ? localApiKey : nil + GatewayDiagnostics.log("talk config apiKey redacted; using local override if present") + } else { + self.apiKey = (localApiKey?.isEmpty == false) ? localApiKey : configApiKey + } if let interrupt = talk?["interruptOnSpeech"] as? Bool { self.interruptOnSpeech = interrupt } diff --git a/apps/ios/Tests/DeepLinkParserTests.swift b/apps/ios/Tests/DeepLinkParserTests.swift index 9a3d861873870..ea8b2a81203eb 100644 --- a/apps/ios/Tests/DeepLinkParserTests.swift +++ b/apps/ios/Tests/DeepLinkParserTests.swift @@ -76,4 +76,52 @@ import Testing timeoutSeconds: nil, key: nil))) } + + @Test func parseGatewayLinkParsesCommonFields() { + let url = URL( + string: "openclaw://gateway?host=openclaw.local&port=18789&tls=1&token=abc&password=def")! + #expect( + DeepLinkParser.parse(url) == .gateway( + .init(host: "openclaw.local", port: 18789, tls: true, token: "abc", password: "def"))) + } + + @Test func parseGatewaySetupCodeParsesBase64UrlPayload() { + let payload = #"{"url":"wss://gateway.example.com:443","token":"tok","password":"pw"}"# + let encoded = Data(payload.utf8) + .base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + + let link = GatewayConnectDeepLink.fromSetupCode(encoded) + + #expect(link == .init( + host: "gateway.example.com", + port: 443, + tls: true, + token: "tok", + password: "pw")) + } + + @Test func parseGatewaySetupCodeRejectsInvalidInput() { + #expect(GatewayConnectDeepLink.fromSetupCode("not-a-valid-setup-code") == nil) + } + + @Test func parseGatewaySetupCodeDefaultsTo443ForWssWithoutPort() { + let payload = #"{"url":"wss://gateway.example.com","token":"tok"}"# + let encoded = Data(payload.utf8) + .base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + + let link = GatewayConnectDeepLink.fromSetupCode(encoded) + + #expect(link == .init( + host: "gateway.example.com", + port: 443, + tls: true, + token: "tok", + password: nil)) + } } diff --git a/apps/ios/Tests/GatewayConnectionControllerTests.swift b/apps/ios/Tests/GatewayConnectionControllerTests.swift index 0d3bdbba0ee57..27e7aed7aea5d 100644 --- a/apps/ios/Tests/GatewayConnectionControllerTests.swift +++ b/apps/ios/Tests/GatewayConnectionControllerTests.swift @@ -76,4 +76,47 @@ private func withUserDefaults(_ updates: [String: Any?], _ body: () throws -> #expect(commands.contains(OpenClawLocationCommand.get.rawValue)) } } + @Test @MainActor func currentCommandsExcludeDangerousSystemExecCommands() { + withUserDefaults([ + "node.instanceId": "ios-test", + "camera.enabled": true, + "location.enabledMode": OpenClawLocationMode.whileUsing.rawValue, + ]) { + let appModel = NodeAppModel() + let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false) + let commands = Set(controller._test_currentCommands()) + + // iOS should expose notify, but not host shell/exec-approval commands. + #expect(commands.contains(OpenClawSystemCommand.notify.rawValue)) + #expect(!commands.contains(OpenClawSystemCommand.run.rawValue)) + #expect(!commands.contains(OpenClawSystemCommand.which.rawValue)) + #expect(!commands.contains(OpenClawSystemCommand.execApprovalsGet.rawValue)) + #expect(!commands.contains(OpenClawSystemCommand.execApprovalsSet.rawValue)) + } + } + + @Test @MainActor func loadLastConnectionReadsSavedValues() { + withUserDefaults([:]) { + GatewaySettingsStore.saveLastGatewayConnectionManual( + host: "gateway.example.com", + port: 443, + useTLS: true, + stableID: "manual|gateway.example.com|443") + let loaded = GatewaySettingsStore.loadLastGatewayConnection() + #expect(loaded == .manual(host: "gateway.example.com", port: 443, useTLS: true, stableID: "manual|gateway.example.com|443")) + } + } + + @Test @MainActor func loadLastConnectionReturnsNilForInvalidData() { + withUserDefaults([ + "gateway.last.kind": "manual", + "gateway.last.host": "", + "gateway.last.port": 0, + "gateway.last.tls": false, + "gateway.last.stableID": "manual|invalid|0", + ]) { + let loaded = GatewaySettingsStore.loadLastGatewayConnection() + #expect(loaded == nil) + } + } } diff --git a/apps/ios/Tests/GatewayConnectionIssueTests.swift b/apps/ios/Tests/GatewayConnectionIssueTests.swift new file mode 100644 index 0000000000000..8eb63f268baea --- /dev/null +++ b/apps/ios/Tests/GatewayConnectionIssueTests.swift @@ -0,0 +1,33 @@ +import Testing +@testable import OpenClaw + +@Suite(.serialized) struct GatewayConnectionIssueTests { + @Test func detectsTokenMissing() { + let issue = GatewayConnectionIssue.detect(from: "unauthorized: gateway token missing") + #expect(issue == .tokenMissing) + #expect(issue.needsAuthToken) + } + + @Test func detectsUnauthorized() { + let issue = GatewayConnectionIssue.detect(from: "Gateway error: unauthorized role") + #expect(issue == .unauthorized) + #expect(issue.needsAuthToken) + } + + @Test func detectsPairingWithRequestId() { + let issue = GatewayConnectionIssue.detect(from: "pairing required (requestId: abc123)") + #expect(issue == .pairingRequired(requestId: "abc123")) + #expect(issue.needsPairing) + #expect(issue.requestId == "abc123") + } + + @Test func detectsNetworkError() { + let issue = GatewayConnectionIssue.detect(from: "Gateway error: Connection refused") + #expect(issue == .network) + } + + @Test func returnsNoneForBenignStatus() { + let issue = GatewayConnectionIssue.detect(from: "Connected") + #expect(issue == .none) + } +} diff --git a/apps/ios/Tests/Info.plist b/apps/ios/Tests/Info.plist index 3c51da578a501..59d3478717363 100644 --- a/apps/ios/Tests/Info.plist +++ b/apps/ios/Tests/Info.plist @@ -15,10 +15,10 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - BNDL - CFBundleShortVersionString - 2026.2.13 - CFBundleVersion - 20260213 - - + BNDL + CFBundleShortVersionString + 2026.2.19 + CFBundleVersion + 20260219 + + diff --git a/apps/ios/Tests/NodeAppModelInvokeTests.swift b/apps/ios/Tests/NodeAppModelInvokeTests.swift index 3041439399699..f5f40fc8b7cf8 100644 --- a/apps/ios/Tests/NodeAppModelInvokeTests.swift +++ b/apps/ios/Tests/NodeAppModelInvokeTests.swift @@ -29,6 +29,39 @@ private func withUserDefaults(_ updates: [String: Any?], _ body: () throws -> return try body() } +@MainActor +private final class MockWatchMessagingService: WatchMessagingServicing, @unchecked Sendable { + var currentStatus = WatchMessagingStatus( + supported: true, + paired: true, + appInstalled: true, + reachable: true, + activationState: "activated") + var nextSendResult = WatchNotificationSendResult( + deliveredImmediately: true, + queuedForDelivery: false, + transport: "sendMessage") + var sendError: Error? + var lastSent: (id: String, title: String, body: String, priority: OpenClawNotificationPriority?)? + + func status() async -> WatchMessagingStatus { + self.currentStatus + } + + func sendNotification( + id: String, + title: String, + body: String, + priority: OpenClawNotificationPriority?) async throws -> WatchNotificationSendResult + { + self.lastSent = (id: id, title: title, body: body, priority: priority) + if let sendError = self.sendError { + throw sendError + } + return self.nextSendResult + } +} + @Suite(.serialized) struct NodeAppModelInvokeTests { @Test @MainActor func decodeParamsFailsWithoutJSON() { #expect(throws: Error.self) { @@ -156,6 +189,96 @@ private func withUserDefaults(_ updates: [String: Any?], _ body: () throws -> #expect(res.error?.code == .invalidRequest) } + @Test @MainActor func handleInvokeWatchStatusReturnsServiceSnapshot() async throws { + let watchService = MockWatchMessagingService() + watchService.currentStatus = WatchMessagingStatus( + supported: true, + paired: true, + appInstalled: true, + reachable: false, + activationState: "inactive") + let appModel = NodeAppModel(watchMessagingService: watchService) + let req = BridgeInvokeRequest(id: "watch-status", command: OpenClawWatchCommand.status.rawValue) + + let res = await appModel._test_handleInvoke(req) + #expect(res.ok == true) + + let payloadData = try #require(res.payloadJSON?.data(using: .utf8)) + let payload = try JSONDecoder().decode(OpenClawWatchStatusPayload.self, from: payloadData) + #expect(payload.supported == true) + #expect(payload.reachable == false) + #expect(payload.activationState == "inactive") + } + + @Test @MainActor func handleInvokeWatchNotifyRoutesToWatchService() async throws { + let watchService = MockWatchMessagingService() + watchService.nextSendResult = WatchNotificationSendResult( + deliveredImmediately: false, + queuedForDelivery: true, + transport: "transferUserInfo") + let appModel = NodeAppModel(watchMessagingService: watchService) + let params = OpenClawWatchNotifyParams( + title: "OpenClaw", + body: "Meeting with Peter is at 4pm", + priority: .timeSensitive) + let paramsData = try JSONEncoder().encode(params) + let paramsJSON = String(decoding: paramsData, as: UTF8.self) + let req = BridgeInvokeRequest( + id: "watch-notify", + command: OpenClawWatchCommand.notify.rawValue, + paramsJSON: paramsJSON) + + let res = await appModel._test_handleInvoke(req) + #expect(res.ok == true) + #expect(watchService.lastSent?.title == "OpenClaw") + #expect(watchService.lastSent?.body == "Meeting with Peter is at 4pm") + #expect(watchService.lastSent?.priority == .timeSensitive) + + let payloadData = try #require(res.payloadJSON?.data(using: .utf8)) + let payload = try JSONDecoder().decode(OpenClawWatchNotifyPayload.self, from: payloadData) + #expect(payload.deliveredImmediately == false) + #expect(payload.queuedForDelivery == true) + #expect(payload.transport == "transferUserInfo") + } + + @Test @MainActor func handleInvokeWatchNotifyRejectsEmptyMessage() async throws { + let watchService = MockWatchMessagingService() + let appModel = NodeAppModel(watchMessagingService: watchService) + let params = OpenClawWatchNotifyParams(title: " ", body: "\n") + let paramsData = try JSONEncoder().encode(params) + let paramsJSON = String(decoding: paramsData, as: UTF8.self) + let req = BridgeInvokeRequest( + id: "watch-notify-empty", + command: OpenClawWatchCommand.notify.rawValue, + paramsJSON: paramsJSON) + + let res = await appModel._test_handleInvoke(req) + #expect(res.ok == false) + #expect(res.error?.code == .invalidRequest) + #expect(watchService.lastSent == nil) + } + + @Test @MainActor func handleInvokeWatchNotifyReturnsUnavailableOnDeliveryFailure() async throws { + let watchService = MockWatchMessagingService() + watchService.sendError = NSError( + domain: "watch", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "WATCH_UNAVAILABLE: no paired Apple Watch"]) + let appModel = NodeAppModel(watchMessagingService: watchService) + let params = OpenClawWatchNotifyParams(title: "OpenClaw", body: "Delivery check") + let paramsData = try JSONEncoder().encode(params) + let paramsJSON = String(decoding: paramsData, as: UTF8.self) + let req = BridgeInvokeRequest( + id: "watch-notify-fail", + command: OpenClawWatchCommand.notify.rawValue, + paramsJSON: paramsJSON) + + let res = await appModel._test_handleInvoke(req) + #expect(res.ok == false) + #expect(res.error?.code == .unavailable) + #expect(res.error?.message.contains("WATCH_UNAVAILABLE") == true) + } + @Test @MainActor func handleDeepLinkSetsErrorWhenNotConnected() async { let appModel = NodeAppModel() let url = URL(string: "openclaw://agent?message=hello")! diff --git a/apps/ios/Tests/OnboardingStateStoreTests.swift b/apps/ios/Tests/OnboardingStateStoreTests.swift new file mode 100644 index 0000000000000..30c014647b684 --- /dev/null +++ b/apps/ios/Tests/OnboardingStateStoreTests.swift @@ -0,0 +1,57 @@ +import Foundation +import Testing +@testable import OpenClaw + +@Suite(.serialized) struct OnboardingStateStoreTests { + @Test @MainActor func shouldPresentWhenFreshAndDisconnected() { + let testDefaults = self.makeDefaults() + let defaults = testDefaults.defaults + defer { self.reset(testDefaults) } + + let appModel = NodeAppModel() + appModel.gatewayServerName = nil + #expect(OnboardingStateStore.shouldPresentOnLaunch(appModel: appModel, defaults: defaults)) + } + + @Test @MainActor func doesNotPresentWhenConnected() { + let testDefaults = self.makeDefaults() + let defaults = testDefaults.defaults + defer { self.reset(testDefaults) } + + let appModel = NodeAppModel() + appModel.gatewayServerName = "gateway" + #expect(!OnboardingStateStore.shouldPresentOnLaunch(appModel: appModel, defaults: defaults)) + } + + @Test @MainActor func markCompletedPersistsMode() { + let testDefaults = self.makeDefaults() + let defaults = testDefaults.defaults + defer { self.reset(testDefaults) } + + let appModel = NodeAppModel() + appModel.gatewayServerName = nil + + OnboardingStateStore.markCompleted(mode: .remoteDomain, defaults: defaults) + #expect(OnboardingStateStore.lastMode(defaults: defaults) == .remoteDomain) + #expect(!OnboardingStateStore.shouldPresentOnLaunch(appModel: appModel, defaults: defaults)) + + OnboardingStateStore.markIncomplete(defaults: defaults) + #expect(OnboardingStateStore.shouldPresentOnLaunch(appModel: appModel, defaults: defaults)) + } + + private struct TestDefaults { + var suiteName: String + var defaults: UserDefaults + } + + private func makeDefaults() -> TestDefaults { + let suiteName = "OnboardingStateStoreTests.\(UUID().uuidString)" + return TestDefaults( + suiteName: suiteName, + defaults: UserDefaults(suiteName: suiteName) ?? .standard) + } + + private func reset(_ defaults: TestDefaults) { + defaults.defaults.removePersistentDomain(forName: defaults.suiteName) + } +} diff --git a/apps/ios/Tests/ScreenControllerTests.swift b/apps/ios/Tests/ScreenControllerTests.swift index 32c36acacb7b1..d0e47c84fb366 100644 --- a/apps/ios/Tests/ScreenControllerTests.swift +++ b/apps/ios/Tests/ScreenControllerTests.swift @@ -2,25 +2,38 @@ import Testing import WebKit @testable import OpenClaw +@MainActor +private func mountScreen(_ screen: ScreenController) throws -> (ScreenWebViewCoordinator, WKWebView) { + let coordinator = ScreenWebViewCoordinator(controller: screen) + _ = coordinator.makeContainerView() + let webView = try #require(coordinator.managedWebView) + return (coordinator, webView) +} + @Suite struct ScreenControllerTests { - @Test @MainActor func canvasModeConfiguresWebViewForTouch() { + @Test @MainActor func canvasModeConfiguresWebViewForTouch() throws { let screen = ScreenController() + let (coordinator, webView) = try mountScreen(screen) + defer { coordinator.teardown() } - #expect(screen.webView.isOpaque == true) - #expect(screen.webView.backgroundColor == .black) + #expect(webView.isOpaque == true) + #expect(webView.backgroundColor == .black) - let scrollView = screen.webView.scrollView + let scrollView = webView.scrollView #expect(scrollView.backgroundColor == .black) #expect(scrollView.contentInsetAdjustmentBehavior == .never) #expect(scrollView.isScrollEnabled == false) #expect(scrollView.bounces == false) } - @Test @MainActor func navigateEnablesScrollForWebPages() { + @Test @MainActor func navigateEnablesScrollForWebPages() throws { let screen = ScreenController() + let (coordinator, webView) = try mountScreen(screen) + defer { coordinator.teardown() } + screen.navigate(to: "https://example.com") - let scrollView = screen.webView.scrollView + let scrollView = webView.scrollView #expect(scrollView.isScrollEnabled == true) #expect(scrollView.bounces == true) } @@ -34,6 +47,9 @@ import WebKit @Test @MainActor func evalExecutesJavaScript() async throws { let screen = ScreenController() + let (coordinator, _) = try mountScreen(screen) + defer { coordinator.teardown() } + let deadline = ContinuousClock().now.advanced(by: .seconds(3)) while true { diff --git a/apps/ios/Tests/ShareToAgentDeepLinkTests.swift b/apps/ios/Tests/ShareToAgentDeepLinkTests.swift new file mode 100644 index 0000000000000..4ea178ecfa291 --- /dev/null +++ b/apps/ios/Tests/ShareToAgentDeepLinkTests.swift @@ -0,0 +1,51 @@ +import OpenClawKit +import Foundation +import Testing + +@Suite struct ShareToAgentDeepLinkTests { + @Test func buildMessageIncludesSharedFields() { + let payload = SharedContentPayload( + title: "Article", + url: URL(string: "https://example.com/post")!, + text: "Read this") + + let message = ShareToAgentDeepLink.buildMessage( + from: payload, + instruction: "Summarize and give next steps.") + #expect(message.contains("Shared from iOS.")) + #expect(message.contains("Title: Article")) + #expect(message.contains("URL: https://example.com/post")) + #expect(message.contains("Text:\nRead this")) + #expect(message.contains("Summarize and give next steps.")) + } + + @Test func buildURLEncodesAgentRoute() { + let payload = SharedContentPayload( + title: "", + url: URL(string: "https://example.com")!, + text: nil) + + let url = ShareToAgentDeepLink.buildURL(from: payload) + let parsed = url.flatMap { DeepLinkParser.parse($0) } + guard case let .agent(agent)? = parsed else { + Issue.record("Expected openclaw://agent deep link") + return + } + + #expect(agent.thinking == "low") + #expect(agent.message.contains("https://example.com")) + } + + @Test func buildURLReturnsNilWhenPayloadEmpty() { + let payload = SharedContentPayload(title: nil, url: nil, text: nil) + #expect(ShareToAgentDeepLink.buildURL(from: payload) == nil) + } + + @Test func shareInstructionSettingsRoundTrip() { + let value = "Focus on booking constraints and alternatives." + ShareToAgentSettings.saveDefaultInstruction(value) + defer { ShareToAgentSettings.saveDefaultInstruction(nil) } + + #expect(ShareToAgentSettings.loadDefaultInstruction() == value) + } +} diff --git a/apps/ios/WatchApp/Info.plist b/apps/ios/WatchApp/Info.plist new file mode 100644 index 0000000000000..1ad5574ff829d --- /dev/null +++ b/apps/ios/WatchApp/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + OpenClaw + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 2026.2.19 + CFBundleVersion + 20260219 + WKCompanionAppBundleIdentifier + $(OPENCLAW_APP_BUNDLE_ID) + WKWatchKitApp + + + diff --git a/apps/ios/WatchExtension/Info.plist b/apps/ios/WatchExtension/Info.plist new file mode 100644 index 0000000000000..f1395e24b0383 --- /dev/null +++ b/apps/ios/WatchExtension/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + OpenClaw + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundleShortVersionString + 2026.2.19 + CFBundleVersion + 20260219 + NSExtension + + NSExtensionAttributes + + WKAppBundleIdentifier + $(OPENCLAW_WATCH_APP_BUNDLE_ID) + + NSExtensionPointIdentifier + com.apple.watchkit + + + diff --git a/apps/ios/WatchExtension/Sources/OpenClawWatchApp.swift b/apps/ios/WatchExtension/Sources/OpenClawWatchApp.swift new file mode 100644 index 0000000000000..6084f5744422b --- /dev/null +++ b/apps/ios/WatchExtension/Sources/OpenClawWatchApp.swift @@ -0,0 +1,20 @@ +import SwiftUI + +@main +struct OpenClawWatchApp: App { + @State private var inboxStore = WatchInboxStore() + @State private var receiver: WatchConnectivityReceiver? + + var body: some Scene { + WindowGroup { + WatchInboxView(store: self.inboxStore) + .task { + if self.receiver == nil { + let receiver = WatchConnectivityReceiver(store: self.inboxStore) + receiver.activate() + self.receiver = receiver + } + } + } + } +} diff --git a/apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift b/apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift new file mode 100644 index 0000000000000..fd0d84cc55c80 --- /dev/null +++ b/apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift @@ -0,0 +1,92 @@ +import Foundation +import WatchConnectivity + +final class WatchConnectivityReceiver: NSObject, @unchecked Sendable { + private let store: WatchInboxStore + private let session: WCSession? + + init(store: WatchInboxStore) { + self.store = store + if WCSession.isSupported() { + self.session = WCSession.default + } else { + self.session = nil + } + super.init() + } + + func activate() { + guard let session = self.session else { return } + session.delegate = self + session.activate() + } + + private static func parseNotificationPayload(_ payload: [String: Any]) -> WatchNotifyMessage? { + guard let type = payload["type"] as? String, type == "watch.notify" else { + return nil + } + + let title = (payload["title"] as? String)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let body = (payload["body"] as? String)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + + guard title.isEmpty == false || body.isEmpty == false else { + return nil + } + + let id = (payload["id"] as? String)? + .trimmingCharacters(in: .whitespacesAndNewlines) + let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue + + return WatchNotifyMessage( + id: id, + title: title, + body: body, + sentAtMs: sentAtMs) + } +} + +extension WatchConnectivityReceiver: WCSessionDelegate { + func session( + _: WCSession, + activationDidCompleteWith _: WCSessionActivationState, + error _: (any Error)?) + {} + + func session(_: WCSession, didReceiveMessage message: [String: Any]) { + guard let incoming = Self.parseNotificationPayload(message) else { return } + Task { @MainActor in + self.store.consume(message: incoming, transport: "sendMessage") + } + } + + func session( + _: WCSession, + didReceiveMessage message: [String: Any], + replyHandler: @escaping ([String: Any]) -> Void) + { + guard let incoming = Self.parseNotificationPayload(message) else { + replyHandler(["ok": false]) + return + } + replyHandler(["ok": true]) + Task { @MainActor in + self.store.consume(message: incoming, transport: "sendMessage") + } + } + + func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any]) { + guard let incoming = Self.parseNotificationPayload(userInfo) else { return } + Task { @MainActor in + self.store.consume(message: incoming, transport: "transferUserInfo") + } + } + + func session(_: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) { + guard let incoming = Self.parseNotificationPayload(applicationContext) else { return } + Task { @MainActor in + self.store.consume(message: incoming, transport: "applicationContext") + } + } +} diff --git a/apps/ios/WatchExtension/Sources/WatchInboxStore.swift b/apps/ios/WatchExtension/Sources/WatchInboxStore.swift new file mode 100644 index 0000000000000..0a715f16b63e0 --- /dev/null +++ b/apps/ios/WatchExtension/Sources/WatchInboxStore.swift @@ -0,0 +1,124 @@ +import Foundation +import Observation +import UserNotifications +import WatchKit + +struct WatchNotifyMessage: Sendable { + var id: String? + var title: String + var body: String + var sentAtMs: Int? +} + +@MainActor @Observable final class WatchInboxStore { + private struct PersistedState: Codable { + var title: String + var body: String + var transport: String + var updatedAt: Date + var lastDeliveryKey: String? + } + + private static let persistedStateKey = "watch.inbox.state.v1" + private let defaults: UserDefaults + + var title = "OpenClaw" + var body = "Waiting for messages from your iPhone." + var transport = "none" + var updatedAt: Date? + private var lastDeliveryKey: String? + + init(defaults: UserDefaults = .standard) { + self.defaults = defaults + self.restorePersistedState() + Task { + await self.ensureNotificationAuthorization() + } + } + + func consume(message: WatchNotifyMessage, transport: String) { + let messageID = message.id? + .trimmingCharacters(in: .whitespacesAndNewlines) + let deliveryKey = self.deliveryKey( + messageID: messageID, + title: message.title, + body: message.body, + sentAtMs: message.sentAtMs) + guard deliveryKey != self.lastDeliveryKey else { return } + + let normalizedTitle = message.title.isEmpty ? "OpenClaw" : message.title + self.title = normalizedTitle + self.body = message.body + self.transport = transport + self.updatedAt = Date() + self.lastDeliveryKey = deliveryKey + self.persistState() + + Task { + await self.postLocalNotification( + identifier: deliveryKey, + title: normalizedTitle, + body: message.body) + } + } + + private func restorePersistedState() { + guard let data = self.defaults.data(forKey: Self.persistedStateKey), + let state = try? JSONDecoder().decode(PersistedState.self, from: data) + else { + return + } + + self.title = state.title + self.body = state.body + self.transport = state.transport + self.updatedAt = state.updatedAt + self.lastDeliveryKey = state.lastDeliveryKey + } + + private func persistState() { + guard let updatedAt = self.updatedAt else { return } + let state = PersistedState( + title: self.title, + body: self.body, + transport: self.transport, + updatedAt: updatedAt, + lastDeliveryKey: self.lastDeliveryKey) + guard let data = try? JSONEncoder().encode(state) else { return } + self.defaults.set(data, forKey: Self.persistedStateKey) + } + + private func deliveryKey(messageID: String?, title: String, body: String, sentAtMs: Int?) -> String { + if let messageID, messageID.isEmpty == false { + return "id:\(messageID)" + } + return "content:\(title)|\(body)|\(sentAtMs ?? 0)" + } + + private func ensureNotificationAuthorization() async { + let center = UNUserNotificationCenter.current() + let settings = await center.notificationSettings() + switch settings.authorizationStatus { + case .notDetermined: + _ = try? await center.requestAuthorization(options: [.alert, .sound]) + default: + break + } + } + + private func postLocalNotification(identifier: String, title: String, body: String) async { + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = .default + content.threadIdentifier = "openclaw-watch" + + let request = UNNotificationRequest( + identifier: identifier, + content: content, + trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.2, repeats: false)) + + _ = try? await UNUserNotificationCenter.current().add(request) + WKInterfaceDevice.current().play(.notification) + } +} diff --git a/apps/ios/WatchExtension/Sources/WatchInboxView.swift b/apps/ios/WatchExtension/Sources/WatchInboxView.swift new file mode 100644 index 0000000000000..c5ea9a9f534d9 --- /dev/null +++ b/apps/ios/WatchExtension/Sources/WatchInboxView.swift @@ -0,0 +1,27 @@ +import SwiftUI + +struct WatchInboxView: View { + @Bindable var store: WatchInboxStore + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 8) { + Text(store.title) + .font(.headline) + .lineLimit(2) + + Text(store.body) + .font(.body) + .fixedSize(horizontal: false, vertical: true) + + if let updatedAt = store.updatedAt { + Text("Updated \(updatedAt.formatted(date: .omitted, time: .shortened))") + .font(.footnote) + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + } +} diff --git a/apps/ios/fastlane/Fastfile b/apps/ios/fastlane/Fastfile index b777c25c7a516..f1dbf6df18c6c 100644 --- a/apps/ios/fastlane/Fastfile +++ b/apps/ios/fastlane/Fastfile @@ -66,7 +66,8 @@ platform :ios do if team_id.nil? || team_id.strip.empty? helper_path = File.expand_path("../../scripts/ios-team-id.sh", __dir__) if File.exist?(helper_path) - team_id = sh("bash #{helper_path.shellescape}").strip + # Keep CI/local compatibility where teams are present in keychain but not Xcode account metadata. + team_id = sh("IOS_ALLOW_KEYCHAIN_TEAM_FALLBACK=1 bash #{helper_path.shellescape}").strip end end UI.user_error!("Missing IOS_DEVELOPMENT_TEAM (Apple Team ID). Add it to fastlane/.env or export it in your shell.") if team_id.nil? || team_id.strip.empty? diff --git a/apps/ios/fastlane/SETUP.md b/apps/ios/fastlane/SETUP.md index 832f1ebc15bd7..930258fcc79fb 100644 --- a/apps/ios/fastlane/SETUP.md +++ b/apps/ios/fastlane/SETUP.md @@ -22,7 +22,7 @@ ASC_KEY_PATH=/absolute/path/to/AuthKey_XXXXXXXXXX.p8 IOS_DEVELOPMENT_TEAM=YOUR_TEAM_ID ``` -Tip: run `scripts/ios-team-id.sh` from the repo root to print a Team ID to paste into `.env`. Fastlane falls back to this helper if `IOS_DEVELOPMENT_TEAM` is missing. +Tip: run `scripts/ios-team-id.sh` from the repo root to print a Team ID to paste into `.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. Run: diff --git a/apps/ios/project.yml b/apps/ios/project.yml index c4342f8f22bcf..6c3713e65de0c 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -29,9 +29,15 @@ targets: OpenClaw: type: application platform: iOS + configFiles: + Debug: Signing.xcconfig + Release: Signing.xcconfig sources: - path: Sources dependencies: + - target: OpenClawShareExtension + embed: true + - target: OpenClawWatchApp - package: OpenClawKit - package: OpenClawKit product: OpenClawChatUI @@ -69,10 +75,11 @@ targets: settings: base: CODE_SIGN_IDENTITY: "Apple Development" - CODE_SIGN_STYLE: Manual - DEVELOPMENT_TEAM: Y5PE65HELJ - PRODUCT_BUNDLE_IDENTIFIER: ai.openclaw.ios - PROVISIONING_PROFILE_SPECIFIER: "ai.openclaw.ios Development" + CODE_SIGN_ENTITLEMENTS: Sources/OpenClaw.entitlements + CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)" + DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)" + PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_APP_BUNDLE_ID)" + PROVISIONING_PROFILE_SPECIFIER: "$(OPENCLAW_APP_PROFILE)" SWIFT_VERSION: "6.0" SWIFT_STRICT_CONCURRENCY: complete ENABLE_APPINTENTS_METADATA: NO @@ -81,13 +88,18 @@ targets: properties: CFBundleDisplayName: OpenClaw CFBundleIconName: AppIcon - CFBundleShortVersionString: "2026.2.13" - CFBundleVersion: "20260213" + CFBundleURLTypes: + - CFBundleURLName: ai.openclaw.ios + CFBundleURLSchemes: + - openclaw + CFBundleShortVersionString: "2026.2.19" + CFBundleVersion: "20260219" UILaunchScreen: {} UIApplicationSceneManifest: UIApplicationSupportsMultipleScenes: false UIBackgroundModes: - audio + - remote-notification NSLocalNetworkUsageDescription: OpenClaw discovers and connects to your OpenClaw gateway on the local network. NSAppTransportSecurity: NSAllowsArbitraryLoadsInWebContent: true @@ -109,6 +121,90 @@ targets: - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight + OpenClawShareExtension: + type: app-extension + platform: iOS + configFiles: + Debug: Signing.xcconfig + Release: Signing.xcconfig + sources: + - path: ShareExtension + dependencies: + - package: OpenClawKit + settings: + base: + CODE_SIGN_IDENTITY: "Apple Development" + CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)" + DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)" + PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_SHARE_BUNDLE_ID)" + PROVISIONING_PROFILE_SPECIFIER: "$(OPENCLAW_SHARE_PROFILE)" + SWIFT_VERSION: "6.0" + SWIFT_STRICT_CONCURRENCY: complete + info: + path: ShareExtension/Info.plist + properties: + CFBundleDisplayName: OpenClaw Share + CFBundleShortVersionString: "2026.2.19" + CFBundleVersion: "20260219" + NSExtension: + NSExtensionPointIdentifier: com.apple.share-services + NSExtensionPrincipalClass: "$(PRODUCT_MODULE_NAME).ShareViewController" + NSExtensionAttributes: + NSExtensionActivationRule: + NSExtensionActivationSupportsText: true + NSExtensionActivationSupportsWebURLWithMaxCount: 1 + NSExtensionActivationSupportsImageWithMaxCount: 10 + NSExtensionActivationSupportsMovieWithMaxCount: 1 + + OpenClawWatchApp: + type: application.watchapp2 + platform: watchOS + deploymentTarget: "11.0" + sources: + - path: WatchApp + dependencies: + - target: OpenClawWatchExtension + configFiles: + Debug: Config/Signing.xcconfig + Release: Config/Signing.xcconfig + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_WATCH_APP_BUNDLE_ID)" + info: + path: WatchApp/Info.plist + properties: + CFBundleDisplayName: OpenClaw + CFBundleShortVersionString: "2026.2.19" + CFBundleVersion: "20260219" + WKCompanionAppBundleIdentifier: "$(OPENCLAW_APP_BUNDLE_ID)" + WKWatchKitApp: true + + OpenClawWatchExtension: + type: watchkit2-extension + platform: watchOS + deploymentTarget: "11.0" + sources: + - path: WatchExtension/Sources + dependencies: + - sdk: WatchConnectivity.framework + - sdk: UserNotifications.framework + configFiles: + Debug: Config/Signing.xcconfig + Release: Config/Signing.xcconfig + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_WATCH_EXTENSION_BUNDLE_ID)" + info: + path: WatchExtension/Info.plist + properties: + CFBundleDisplayName: OpenClaw + CFBundleShortVersionString: "2026.2.19" + CFBundleVersion: "20260219" + NSExtension: + NSExtensionAttributes: + WKAppBundleIdentifier: "$(OPENCLAW_WATCH_APP_BUNDLE_ID)" + NSExtensionPointIdentifier: com.apple.watchkit + OpenClawTests: type: bundle.unit-test platform: iOS @@ -130,5 +226,5 @@ targets: path: Tests/Info.plist properties: CFBundleDisplayName: OpenClawTests - CFBundleShortVersionString: "2026.2.13" - CFBundleVersion: "20260213" + CFBundleShortVersionString: "2026.2.19" + CFBundleVersion: "20260219" diff --git a/apps/macos/Sources/OpenClaw/AboutSettings.swift b/apps/macos/Sources/OpenClaw/AboutSettings.swift index ede898ebac2e3..b61cfee89a57b 100644 --- a/apps/macos/Sources/OpenClaw/AboutSettings.swift +++ b/apps/macos/Sources/OpenClaw/AboutSettings.swift @@ -110,8 +110,8 @@ struct AboutSettings: View { private var buildTimestamp: String? { guard let raw = - (Bundle.main.object(forInfoDictionaryKey: "OpenClawBuildTimestamp") as? String) ?? - (Bundle.main.object(forInfoDictionaryKey: "OpenClawBuildTimestamp") as? String) + (Bundle.main.object(forInfoDictionaryKey: "OpenClawBuildTimestamp") as? String) ?? + (Bundle.main.object(forInfoDictionaryKey: "OpenClawBuildTimestamp") as? String) else { return nil } let parser = ISO8601DateFormatter() parser.formatOptions = [.withInternetDateTime] diff --git a/apps/macos/Sources/OpenClaw/AgeFormatting.swift b/apps/macos/Sources/OpenClaw/AgeFormatting.swift index f992c2d95e3c5..5bb46bf459db5 100644 --- a/apps/macos/Sources/OpenClaw/AgeFormatting.swift +++ b/apps/macos/Sources/OpenClaw/AgeFormatting.swift @@ -1,6 +1,6 @@ import Foundation -// Human-friendly age string (e.g., "2m ago"). +/// Human-friendly age string (e.g., "2m ago"). func age(from date: Date, now: Date = .init()) -> String { let seconds = max(0, Int(now.timeIntervalSince(date))) let minutes = seconds / 60 diff --git a/apps/macos/Sources/OpenClaw/AgentWorkspace.swift b/apps/macos/Sources/OpenClaw/AgentWorkspace.swift index 603f837f45e5e..57164ebb892de 100644 --- a/apps/macos/Sources/OpenClaw/AgentWorkspace.swift +++ b/apps/macos/Sources/OpenClaw/AgentWorkspace.swift @@ -19,7 +19,7 @@ enum AgentWorkspace { ] enum BootstrapSafety: Equatable { case safe - case unsafe(reason: String) + case unsafe (reason: String) } static func displayPath(for url: URL) -> String { @@ -72,7 +72,7 @@ enum AgentWorkspace { return .safe } if !isDir.boolValue { - return .unsafe(reason: "Workspace path points to a file.") + return .unsafe (reason: "Workspace path points to a file.") } let agentsURL = self.agentsURL(workspaceURL: workspaceURL) if fm.fileExists(atPath: agentsURL.path) { @@ -82,9 +82,9 @@ enum AgentWorkspace { let entries = try self.workspaceEntries(workspaceURL: workspaceURL) return entries.isEmpty ? .safe - : .unsafe(reason: "Folder isn't empty. Choose a new folder or add AGENTS.md first.") + : .unsafe (reason: "Folder isn't empty. Choose a new folder or add AGENTS.md first.") } catch { - return .unsafe(reason: "Couldn't inspect the workspace folder.") + return .unsafe (reason: "Couldn't inspect the workspace folder.") } } diff --git a/apps/macos/Sources/OpenClaw/AnthropicOAuth.swift b/apps/macos/Sources/OpenClaw/AnthropicOAuth.swift index 408b881ba8fc6..f594cc04c3114 100644 --- a/apps/macos/Sources/OpenClaw/AnthropicOAuth.swift +++ b/apps/macos/Sources/OpenClaw/AnthropicOAuth.swift @@ -234,9 +234,8 @@ enum OpenClawOAuthStore { return URL(fileURLWithPath: expanded, isDirectory: true) } let home = FileManager().homeDirectoryForCurrentUser - let preferred = home.appendingPathComponent(".openclaw", isDirectory: true) + return home.appendingPathComponent(".openclaw", isDirectory: true) .appendingPathComponent("credentials", isDirectory: true) - return preferred } static func oauthURL() -> URL { diff --git a/apps/macos/Sources/OpenClaw/AnyCodable+Helpers.swift b/apps/macos/Sources/OpenClaw/AnyCodable+Helpers.swift index acc54a0a14eb7..3cb8f54e39660 100644 --- a/apps/macos/Sources/OpenClaw/AnyCodable+Helpers.swift +++ b/apps/macos/Sources/OpenClaw/AnyCodable+Helpers.swift @@ -1,44 +1,40 @@ -import OpenClawKit -import OpenClawProtocol import Foundation +import OpenClawKit // Prefer the OpenClawKit wrapper to keep gateway request payloads consistent. typealias AnyCodable = OpenClawKit.AnyCodable typealias InstanceIdentity = OpenClawKit.InstanceIdentity extension AnyCodable { - var stringValue: String? { self.value as? String } - var boolValue: Bool? { self.value as? Bool } - var intValue: Int? { self.value as? Int } - var doubleValue: Double? { self.value as? Double } - var dictionaryValue: [String: AnyCodable]? { self.value as? [String: AnyCodable] } - var arrayValue: [AnyCodable]? { self.value as? [AnyCodable] } + var stringValue: String? { + self.value as? String + } - var foundationValue: Any { - switch self.value { - case let dict as [String: AnyCodable]: - dict.mapValues { $0.foundationValue } - case let array as [AnyCodable]: - array.map(\.foundationValue) - default: - self.value - } + var boolValue: Bool? { + self.value as? Bool + } + + var intValue: Int? { + self.value as? Int } -} -extension OpenClawProtocol.AnyCodable { - var stringValue: String? { self.value as? String } - var boolValue: Bool? { self.value as? Bool } - var intValue: Int? { self.value as? Int } - var doubleValue: Double? { self.value as? Double } - var dictionaryValue: [String: OpenClawProtocol.AnyCodable]? { self.value as? [String: OpenClawProtocol.AnyCodable] } - var arrayValue: [OpenClawProtocol.AnyCodable]? { self.value as? [OpenClawProtocol.AnyCodable] } + var doubleValue: Double? { + self.value as? Double + } + + var dictionaryValue: [String: AnyCodable]? { + self.value as? [String: AnyCodable] + } + + var arrayValue: [AnyCodable]? { + self.value as? [AnyCodable] + } var foundationValue: Any { switch self.value { - case let dict as [String: OpenClawProtocol.AnyCodable]: + case let dict as [String: AnyCodable]: dict.mapValues { $0.foundationValue } - case let array as [OpenClawProtocol.AnyCodable]: + case let array as [AnyCodable]: array.map(\.foundationValue) default: self.value diff --git a/apps/macos/Sources/OpenClaw/AppState.swift b/apps/macos/Sources/OpenClaw/AppState.swift index ce2a251cfc961..d960d3c038a75 100644 --- a/apps/macos/Sources/OpenClaw/AppState.swift +++ b/apps/macos/Sources/OpenClaw/AppState.swift @@ -422,11 +422,10 @@ final class AppState { let trimmedUser = parsed.user?.trimmingCharacters(in: .whitespacesAndNewlines) let user = (trimmedUser?.isEmpty ?? true) ? nil : trimmedUser let port = parsed.port - let assembled: String - if let user { - assembled = port == 22 ? "\(user)@\(host)" : "\(user)@\(host):\(port)" + let assembled: String = if let user { + port == 22 ? "\(user)@\(host)" : "\(user)@\(host):\(port)" } else { - assembled = port == 22 ? host : "\(host):\(port)" + port == 22 ? host : "\(host):\(port)" } if assembled != self.remoteTarget { self.remoteTarget = assembled @@ -698,7 +697,9 @@ extension AppState { @MainActor enum AppStateStore { static let shared = AppState() - static var isPausedFlag: Bool { UserDefaults.standard.bool(forKey: pauseDefaultsKey) } + static var isPausedFlag: Bool { + UserDefaults.standard.bool(forKey: pauseDefaultsKey) + } static func updateLaunchAtLogin(enabled: Bool) { Task.detached(priority: .utility) { diff --git a/apps/macos/Sources/OpenClaw/CameraCaptureService.swift b/apps/macos/Sources/OpenClaw/CameraCaptureService.swift index 8653b05dcbb2a..4e3749d6a68da 100644 --- a/apps/macos/Sources/OpenClaw/CameraCaptureService.swift +++ b/apps/macos/Sources/OpenClaw/CameraCaptureService.swift @@ -1,8 +1,8 @@ import AVFoundation -import OpenClawIPC -import OpenClawKit import CoreGraphics import Foundation +import OpenClawIPC +import OpenClawKit import OSLog actor CameraCaptureService { @@ -106,14 +106,16 @@ actor CameraCaptureService { } withExtendedLifetime(delegate) {} - let maxPayloadBytes = 5 * 1024 * 1024 - // Base64 inflates payloads by ~4/3; cap encoded bytes so the payload stays under 5MB (API limit). - let maxEncodedBytes = (maxPayloadBytes / 4) * 3 - let res = try JPEGTranscoder.transcodeToJPEG( - imageData: rawData, - maxWidthPx: maxWidth, - quality: quality, - maxBytes: maxEncodedBytes) + let res: (data: Data, widthPx: Int, heightPx: Int) + do { + res = try PhotoCapture.transcodeJPEGForGateway( + rawData: rawData, + maxWidthPx: maxWidth, + quality: quality) + } catch { + throw CameraError.captureFailed(error.localizedDescription) + } + return (data: res.data, size: CGSize(width: res.widthPx, height: res.heightPx)) } diff --git a/apps/macos/Sources/OpenClaw/CanvasA2UIActionMessageHandler.swift b/apps/macos/Sources/OpenClaw/CanvasA2UIActionMessageHandler.swift index 2faca73c18f7a..40f443c5c8b8f 100644 --- a/apps/macos/Sources/OpenClaw/CanvasA2UIActionMessageHandler.swift +++ b/apps/macos/Sources/OpenClaw/CanvasA2UIActionMessageHandler.swift @@ -1,7 +1,7 @@ import AppKit +import Foundation import OpenClawIPC import OpenClawKit -import Foundation import WebKit final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler { diff --git a/apps/macos/Sources/OpenClaw/CanvasChromeContainerView.swift b/apps/macos/Sources/OpenClaw/CanvasChromeContainerView.swift index 89c19ef138564..b4158167dcf8e 100644 --- a/apps/macos/Sources/OpenClaw/CanvasChromeContainerView.swift +++ b/apps/macos/Sources/OpenClaw/CanvasChromeContainerView.swift @@ -39,7 +39,9 @@ final class HoverChromeContainerView: NSView { } @available(*, unavailable) - required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported") + } override func updateTrackingAreas() { super.updateTrackingAreas() @@ -60,14 +62,18 @@ final class HoverChromeContainerView: NSView { self.window?.performDrag(with: event) } - override func acceptsFirstMouse(for _: NSEvent?) -> Bool { true } + override func acceptsFirstMouse(for _: NSEvent?) -> Bool { + true + } } private final class CanvasResizeHandleView: NSView { private var startPoint: NSPoint = .zero private var startFrame: NSRect = .zero - override func acceptsFirstMouse(for _: NSEvent?) -> Bool { true } + override func acceptsFirstMouse(for _: NSEvent?) -> Bool { + true + } override func mouseDown(with event: NSEvent) { guard let window else { return } @@ -102,7 +108,9 @@ final class HoverChromeContainerView: NSView { private let resizeHandle = CanvasResizeHandleView(frame: .zero) private final class PassthroughVisualEffectView: NSVisualEffectView { - override func hitTest(_: NSPoint) -> NSView? { nil } + override func hitTest(_: NSPoint) -> NSView? { + nil + } } private let closeBackground: NSVisualEffectView = { @@ -190,7 +198,9 @@ final class HoverChromeContainerView: NSView { } @available(*, unavailable) - required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported") + } override func hitTest(_ point: NSPoint) -> NSView? { // When the chrome is hidden, do not intercept any mouse events (let the WKWebView receive them). diff --git a/apps/macos/Sources/OpenClaw/CanvasFileWatcher.swift b/apps/macos/Sources/OpenClaw/CanvasFileWatcher.swift index 3cf800fd10849..3ed0d67ffbcbc 100644 --- a/apps/macos/Sources/OpenClaw/CanvasFileWatcher.swift +++ b/apps/macos/Sources/OpenClaw/CanvasFileWatcher.swift @@ -1,17 +1,13 @@ -import CoreServices import Foundation final class CanvasFileWatcher: @unchecked Sendable { - private let url: URL - private let queue: DispatchQueue - private var stream: FSEventStreamRef? - private var pending = false - private let onChange: () -> Void + private let watcher: CoalescingFSEventsWatcher init(url: URL, onChange: @escaping () -> Void) { - self.url = url - self.queue = DispatchQueue(label: "ai.openclaw.canvaswatcher") - self.onChange = onChange + self.watcher = CoalescingFSEventsWatcher( + paths: [url.path], + queueLabel: "ai.openclaw.canvaswatcher", + onChange: onChange) } deinit { @@ -19,76 +15,10 @@ final class CanvasFileWatcher: @unchecked Sendable { } func start() { - guard self.stream == nil else { return } - - let retainedSelf = Unmanaged.passRetained(self) - var context = FSEventStreamContext( - version: 0, - info: retainedSelf.toOpaque(), - retain: nil, - release: { pointer in - guard let pointer else { return } - Unmanaged.fromOpaque(pointer).release() - }, - copyDescription: nil) - - let paths = [self.url.path] as CFArray - let flags = FSEventStreamCreateFlags( - kFSEventStreamCreateFlagFileEvents | - kFSEventStreamCreateFlagUseCFTypes | - kFSEventStreamCreateFlagNoDefer) - - guard let stream = FSEventStreamCreate( - kCFAllocatorDefault, - Self.callback, - &context, - paths, - FSEventStreamEventId(kFSEventStreamEventIdSinceNow), - 0.05, - flags) - else { - retainedSelf.release() - return - } - - self.stream = stream - FSEventStreamSetDispatchQueue(stream, self.queue) - if FSEventStreamStart(stream) == false { - self.stream = nil - FSEventStreamSetDispatchQueue(stream, nil) - FSEventStreamInvalidate(stream) - FSEventStreamRelease(stream) - } + self.watcher.start() } func stop() { - guard let stream = self.stream else { return } - self.stream = nil - FSEventStreamStop(stream) - FSEventStreamSetDispatchQueue(stream, nil) - FSEventStreamInvalidate(stream) - FSEventStreamRelease(stream) - } -} - -extension CanvasFileWatcher { - private static let callback: FSEventStreamCallback = { _, info, numEvents, _, eventFlags, _ in - guard let info else { return } - let watcher = Unmanaged.fromOpaque(info).takeUnretainedValue() - watcher.handleEvents(numEvents: numEvents, eventFlags: eventFlags) - } - - private func handleEvents(numEvents: Int, eventFlags: UnsafePointer?) { - guard numEvents > 0 else { return } - guard eventFlags != nil else { return } - - // Coalesce rapid changes (common during builds/atomic saves). - if self.pending { return } - self.pending = true - self.queue.asyncAfter(deadline: .now() + 0.12) { [weak self] in - guard let self else { return } - self.pending = false - self.onChange() - } + self.watcher.stop() } } diff --git a/apps/macos/Sources/OpenClaw/CanvasManager.swift b/apps/macos/Sources/OpenClaw/CanvasManager.swift index 0055ffcfe210e..843f78842bdf6 100644 --- a/apps/macos/Sources/OpenClaw/CanvasManager.swift +++ b/apps/macos/Sources/OpenClaw/CanvasManager.swift @@ -1,7 +1,7 @@ import AppKit +import Foundation import OpenClawIPC import OpenClawKit -import Foundation import OSLog @MainActor diff --git a/apps/macos/Sources/OpenClaw/CanvasSchemeHandler.swift b/apps/macos/Sources/OpenClaw/CanvasSchemeHandler.swift index 3241c08e0d271..6905af500146b 100644 --- a/apps/macos/Sources/OpenClaw/CanvasSchemeHandler.swift +++ b/apps/macos/Sources/OpenClaw/CanvasSchemeHandler.swift @@ -1,5 +1,5 @@ -import OpenClawKit import Foundation +import OpenClawKit import OSLog import WebKit diff --git a/apps/macos/Sources/OpenClaw/CanvasWindow.swift b/apps/macos/Sources/OpenClaw/CanvasWindow.swift index 0cb3b7c0769af..a87f325617038 100644 --- a/apps/macos/Sources/OpenClaw/CanvasWindow.swift +++ b/apps/macos/Sources/OpenClaw/CanvasWindow.swift @@ -11,8 +11,13 @@ enum CanvasLayout { } final class CanvasPanel: NSPanel { - override var canBecomeKey: Bool { true } - override var canBecomeMain: Bool { true } + override var canBecomeKey: Bool { + true + } + + override var canBecomeMain: Bool { + true + } } enum CanvasPresentation { diff --git a/apps/macos/Sources/OpenClaw/CanvasWindowController+Navigation.swift b/apps/macos/Sources/OpenClaw/CanvasWindowController+Navigation.swift index 7139b6834d409..16e0b01d294c1 100644 --- a/apps/macos/Sources/OpenClaw/CanvasWindowController+Navigation.swift +++ b/apps/macos/Sources/OpenClaw/CanvasWindowController+Navigation.swift @@ -19,7 +19,8 @@ extension CanvasWindowController { // Deep links: allow local Canvas content to invoke the agent without bouncing through NSWorkspace. if scheme == "openclaw" { if let currentScheme = self.webView.url?.scheme, - CanvasScheme.allSchemes.contains(currentScheme) { + CanvasScheme.allSchemes.contains(currentScheme) + { Task { await DeepLinkHandler.shared.handle(url: url) } } else { canvasWindowLogger diff --git a/apps/macos/Sources/OpenClaw/CanvasWindowController.swift b/apps/macos/Sources/OpenClaw/CanvasWindowController.swift index ee15a6abb671b..d30f54186aee6 100644 --- a/apps/macos/Sources/OpenClaw/CanvasWindowController.swift +++ b/apps/macos/Sources/OpenClaw/CanvasWindowController.swift @@ -1,7 +1,7 @@ import AppKit +import Foundation import OpenClawIPC import OpenClawKit -import Foundation import WebKit @MainActor @@ -183,7 +183,9 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS } @available(*, unavailable) - required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported") + } @MainActor deinit { for name in CanvasA2UIActionMessageHandler.allMessageNames { diff --git a/apps/macos/Sources/OpenClaw/ChannelsSettings+ChannelSections.swift b/apps/macos/Sources/OpenClaw/ChannelsSettings+ChannelSections.swift index ea82aac013d32..2bef47f2dea88 100644 --- a/apps/macos/Sources/OpenClaw/ChannelsSettings+ChannelSections.swift +++ b/apps/macos/Sources/OpenClaw/ChannelsSettings+ChannelSections.swift @@ -10,7 +10,6 @@ extension ChannelsSettings { } } - @ViewBuilder func channelHeaderActions(_ channel: ChannelItem) -> some View { HStack(spacing: 8) { if channel.id == "whatsapp" { @@ -88,7 +87,6 @@ extension ChannelsSettings { } } - @ViewBuilder func genericChannelSection(_ channel: ChannelItem) -> some View { VStack(alignment: .leading, spacing: 16) { self.configEditorSection(channelId: channel.id) diff --git a/apps/macos/Sources/OpenClaw/ChannelsStore+Config.swift b/apps/macos/Sources/OpenClaw/ChannelsStore+Config.swift index c56cb32078548..703c7efed63e3 100644 --- a/apps/macos/Sources/OpenClaw/ChannelsStore+Config.swift +++ b/apps/macos/Sources/OpenClaw/ChannelsStore+Config.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Foundation +import OpenClawProtocol extension ChannelsStore { func loadConfigSchema() async { diff --git a/apps/macos/Sources/OpenClaw/ChannelsStore+Lifecycle.swift b/apps/macos/Sources/OpenClaw/ChannelsStore+Lifecycle.swift index 0610fe46438f3..fd516480f965d 100644 --- a/apps/macos/Sources/OpenClaw/ChannelsStore+Lifecycle.swift +++ b/apps/macos/Sources/OpenClaw/ChannelsStore+Lifecycle.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Foundation +import OpenClawProtocol extension ChannelsStore { func start() { diff --git a/apps/macos/Sources/OpenClaw/ChannelsStore.swift b/apps/macos/Sources/OpenClaw/ChannelsStore.swift index 724862efd72dc..09b9b75a532ff 100644 --- a/apps/macos/Sources/OpenClaw/ChannelsStore.swift +++ b/apps/macos/Sources/OpenClaw/ChannelsStore.swift @@ -1,6 +1,6 @@ -import OpenClawProtocol import Foundation import Observation +import OpenClawProtocol struct ChannelsStatusSnapshot: Codable { struct WhatsAppSelf: Codable { diff --git a/apps/macos/Sources/OpenClaw/CoalescingFSEventsWatcher.swift b/apps/macos/Sources/OpenClaw/CoalescingFSEventsWatcher.swift new file mode 100644 index 0000000000000..f9e38d81170fd --- /dev/null +++ b/apps/macos/Sources/OpenClaw/CoalescingFSEventsWatcher.swift @@ -0,0 +1,110 @@ +import CoreServices +import Foundation + +final class CoalescingFSEventsWatcher: @unchecked Sendable { + private let queue: DispatchQueue + private var stream: FSEventStreamRef? + private var pending = false + + private let paths: [String] + private let shouldNotify: (Int, UnsafeMutableRawPointer?) -> Bool + private let onChange: () -> Void + private let coalesceDelay: TimeInterval + + init( + paths: [String], + queueLabel: String, + coalesceDelay: TimeInterval = 0.12, + shouldNotify: @escaping (Int, UnsafeMutableRawPointer?) -> Bool = { _, _ in true }, + onChange: @escaping () -> Void) + { + self.paths = paths + self.queue = DispatchQueue(label: queueLabel) + self.coalesceDelay = coalesceDelay + self.shouldNotify = shouldNotify + self.onChange = onChange + } + + deinit { + self.stop() + } + + func start() { + guard self.stream == nil else { return } + + let retainedSelf = Unmanaged.passRetained(self) + var context = FSEventStreamContext( + version: 0, + info: retainedSelf.toOpaque(), + retain: nil, + release: { pointer in + guard let pointer else { return } + Unmanaged.fromOpaque(pointer).release() + }, + copyDescription: nil) + + let paths = self.paths as CFArray + let flags = FSEventStreamCreateFlags( + kFSEventStreamCreateFlagFileEvents | + kFSEventStreamCreateFlagUseCFTypes | + kFSEventStreamCreateFlagNoDefer) + + guard let stream = FSEventStreamCreate( + kCFAllocatorDefault, + Self.callback, + &context, + paths, + FSEventStreamEventId(kFSEventStreamEventIdSinceNow), + 0.05, + flags) + else { + retainedSelf.release() + return + } + + self.stream = stream + FSEventStreamSetDispatchQueue(stream, self.queue) + if FSEventStreamStart(stream) == false { + self.stream = nil + FSEventStreamSetDispatchQueue(stream, nil) + FSEventStreamInvalidate(stream) + FSEventStreamRelease(stream) + } + } + + func stop() { + guard let stream = self.stream else { return } + self.stream = nil + FSEventStreamStop(stream) + FSEventStreamSetDispatchQueue(stream, nil) + FSEventStreamInvalidate(stream) + FSEventStreamRelease(stream) + } +} + +extension CoalescingFSEventsWatcher { + private static let callback: FSEventStreamCallback = { _, info, numEvents, eventPaths, eventFlags, _ in + guard let info else { return } + let watcher = Unmanaged.fromOpaque(info).takeUnretainedValue() + watcher.handleEvents(numEvents: numEvents, eventPaths: eventPaths, eventFlags: eventFlags) + } + + private func handleEvents( + numEvents: Int, + eventPaths: UnsafeMutableRawPointer?, + eventFlags: UnsafePointer?) + { + guard numEvents > 0 else { return } + guard eventFlags != nil else { return } + guard self.shouldNotify(numEvents, eventPaths) else { return } + + // Coalesce rapid changes (common during builds/atomic saves). + if self.pending { return } + self.pending = true + self.queue.asyncAfter(deadline: .now() + self.coalesceDelay) { [weak self] in + guard let self else { return } + self.pending = false + self.onChange() + } + } +} diff --git a/apps/macos/Sources/OpenClaw/ConfigFileWatcher.swift b/apps/macos/Sources/OpenClaw/ConfigFileWatcher.swift index 23689f1fb9d90..4434443497e73 100644 --- a/apps/macos/Sources/OpenClaw/ConfigFileWatcher.swift +++ b/apps/macos/Sources/OpenClaw/ConfigFileWatcher.swift @@ -1,23 +1,34 @@ -import CoreServices import Foundation final class ConfigFileWatcher: @unchecked Sendable { private let url: URL - private let queue: DispatchQueue - private var stream: FSEventStreamRef? - private var pending = false - private let onChange: () -> Void private let watchedDir: URL private let targetPath: String private let targetName: String + private let watcher: CoalescingFSEventsWatcher init(url: URL, onChange: @escaping () -> Void) { self.url = url - self.queue = DispatchQueue(label: "ai.openclaw.configwatcher") - self.onChange = onChange self.watchedDir = url.deletingLastPathComponent() self.targetPath = url.path self.targetName = url.lastPathComponent + let watchedDirPath = self.watchedDir.path + let targetPath = self.targetPath + let targetName = self.targetName + self.watcher = CoalescingFSEventsWatcher( + paths: [watchedDirPath], + queueLabel: "ai.openclaw.configwatcher", + shouldNotify: { _, eventPaths in + guard let eventPaths else { return true } + let paths = unsafeBitCast(eventPaths, to: NSArray.self) + for case let path as String in paths { + if path == targetPath { return true } + if path.hasSuffix("/\(targetName)") { return true } + if path == watchedDirPath { return true } + } + return false + }, + onChange: onChange) } deinit { @@ -25,94 +36,10 @@ final class ConfigFileWatcher: @unchecked Sendable { } func start() { - guard self.stream == nil else { return } - - let retainedSelf = Unmanaged.passRetained(self) - var context = FSEventStreamContext( - version: 0, - info: retainedSelf.toOpaque(), - retain: nil, - release: { pointer in - guard let pointer else { return } - Unmanaged.fromOpaque(pointer).release() - }, - copyDescription: nil) - - let paths = [self.watchedDir.path] as CFArray - let flags = FSEventStreamCreateFlags( - kFSEventStreamCreateFlagFileEvents | - kFSEventStreamCreateFlagUseCFTypes | - kFSEventStreamCreateFlagNoDefer) - - guard let stream = FSEventStreamCreate( - kCFAllocatorDefault, - Self.callback, - &context, - paths, - FSEventStreamEventId(kFSEventStreamEventIdSinceNow), - 0.05, - flags) - else { - retainedSelf.release() - return - } - - self.stream = stream - FSEventStreamSetDispatchQueue(stream, self.queue) - if FSEventStreamStart(stream) == false { - self.stream = nil - FSEventStreamSetDispatchQueue(stream, nil) - FSEventStreamInvalidate(stream) - FSEventStreamRelease(stream) - } + self.watcher.start() } func stop() { - guard let stream = self.stream else { return } - self.stream = nil - FSEventStreamStop(stream) - FSEventStreamSetDispatchQueue(stream, nil) - FSEventStreamInvalidate(stream) - FSEventStreamRelease(stream) - } -} - -extension ConfigFileWatcher { - private static let callback: FSEventStreamCallback = { _, info, numEvents, eventPaths, eventFlags, _ in - guard let info else { return } - let watcher = Unmanaged.fromOpaque(info).takeUnretainedValue() - watcher.handleEvents( - numEvents: numEvents, - eventPaths: eventPaths, - eventFlags: eventFlags) - } - - private func handleEvents( - numEvents: Int, - eventPaths: UnsafeMutableRawPointer?, - eventFlags: UnsafePointer?) - { - guard numEvents > 0 else { return } - guard eventFlags != nil else { return } - guard self.matchesTarget(eventPaths: eventPaths) else { return } - - if self.pending { return } - self.pending = true - self.queue.asyncAfter(deadline: .now() + 0.12) { [weak self] in - guard let self else { return } - self.pending = false - self.onChange() - } - } - - private func matchesTarget(eventPaths: UnsafeMutableRawPointer?) -> Bool { - guard let eventPaths else { return true } - let paths = unsafeBitCast(eventPaths, to: NSArray.self) - for case let path as String in paths { - if path == self.targetPath { return true } - if path.hasSuffix("/\(self.targetName)") { return true } - if path == self.watchedDir.path { return true } - } - return false + self.watcher.stop() } } diff --git a/apps/macos/Sources/OpenClaw/ConfigSchemaSupport.swift b/apps/macos/Sources/OpenClaw/ConfigSchemaSupport.swift index 4a7d4e0a48af1..406d908d0b72e 100644 --- a/apps/macos/Sources/OpenClaw/ConfigSchemaSupport.swift +++ b/apps/macos/Sources/OpenClaw/ConfigSchemaSupport.swift @@ -39,11 +39,26 @@ struct ConfigSchemaNode { self.raw = dict } - var title: String? { self.raw["title"] as? String } - var description: String? { self.raw["description"] as? String } - var enumValues: [Any]? { self.raw["enum"] as? [Any] } - var constValue: Any? { self.raw["const"] } - var explicitDefault: Any? { self.raw["default"] } + var title: String? { + self.raw["title"] as? String + } + + var description: String? { + self.raw["description"] as? String + } + + var enumValues: [Any]? { + self.raw["enum"] as? [Any] + } + + var constValue: Any? { + self.raw["const"] + } + + var explicitDefault: Any? { + self.raw["default"] + } + var requiredKeys: Set { Set((self.raw["required"] as? [String]) ?? []) } diff --git a/apps/macos/Sources/OpenClaw/ConfigSettings.swift b/apps/macos/Sources/OpenClaw/ConfigSettings.swift index f64a6bce94ebd..096ae3f714978 100644 --- a/apps/macos/Sources/OpenClaw/ConfigSettings.swift +++ b/apps/macos/Sources/OpenClaw/ConfigSettings.swift @@ -45,7 +45,9 @@ extension ConfigSettings { let help: String? let node: ConfigSchemaNode - var id: String { self.key } + var id: String { + self.key + } } private struct ConfigSubsection: Identifiable { @@ -55,7 +57,9 @@ extension ConfigSettings { let node: ConfigSchemaNode let path: ConfigPath - var id: String { self.key } + var id: String { + self.key + } } private var sections: [ConfigSection] { diff --git a/apps/macos/Sources/OpenClaw/ConfigStore.swift b/apps/macos/Sources/OpenClaw/ConfigStore.swift index 4e9437ff86eb4..8fd779c645674 100644 --- a/apps/macos/Sources/OpenClaw/ConfigStore.swift +++ b/apps/macos/Sources/OpenClaw/ConfigStore.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Foundation +import OpenClawProtocol enum ConfigStore { struct Overrides: Sendable { diff --git a/apps/macos/Sources/OpenClaw/ContextMenuCardView.swift b/apps/macos/Sources/OpenClaw/ContextMenuCardView.swift index 41005e8260e41..f9a11b9e51298 100644 --- a/apps/macos/Sources/OpenClaw/ContextMenuCardView.swift +++ b/apps/macos/Sources/OpenClaw/ContextMenuCardView.swift @@ -70,7 +70,6 @@ struct ContextMenuCardView: View { return "\(count) sessions · 24h" } - @ViewBuilder private func sessionRow(_ row: SessionRow) -> some View { VStack(alignment: .leading, spacing: 5) { ContextUsageBar( diff --git a/apps/macos/Sources/OpenClaw/ControlChannel.swift b/apps/macos/Sources/OpenClaw/ControlChannel.swift index 9436b22ecb848..16b4d6d3ad456 100644 --- a/apps/macos/Sources/OpenClaw/ControlChannel.swift +++ b/apps/macos/Sources/OpenClaw/ControlChannel.swift @@ -1,7 +1,7 @@ -import OpenClawKit -import OpenClawProtocol import Foundation import Observation +import OpenClawKit +import OpenClawProtocol import SwiftUI struct ControlHeartbeatEvent: Codable { @@ -15,7 +15,10 @@ struct ControlHeartbeatEvent: Codable { } struct ControlAgentEvent: Codable, Sendable, Identifiable { - var id: String { "\(self.runId)-\(self.seq)" } + var id: String { + "\(self.runId)-\(self.seq)" + } + let runId: String let seq: Int let stream: String diff --git a/apps/macos/Sources/OpenClaw/CronJobEditor+Helpers.swift b/apps/macos/Sources/OpenClaw/CronJobEditor+Helpers.swift index 544c9a7c6c8cc..6b3fc85a7c0e3 100644 --- a/apps/macos/Sources/OpenClaw/CronJobEditor+Helpers.swift +++ b/apps/macos/Sources/OpenClaw/CronJobEditor+Helpers.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Foundation +import OpenClawProtocol import SwiftUI extension CronJobEditor { diff --git a/apps/macos/Sources/OpenClaw/CronJobEditor.swift b/apps/macos/Sources/OpenClaw/CronJobEditor.swift index 517d32df44502..a7d88a4f2fb3b 100644 --- a/apps/macos/Sources/OpenClaw/CronJobEditor.swift +++ b/apps/macos/Sources/OpenClaw/CronJobEditor.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Observation +import OpenClawProtocol import SwiftUI struct CronJobEditor: View { @@ -32,18 +32,24 @@ struct CronJobEditor: View { @State var wakeMode: CronWakeMode = .now @State var deleteAfterRun: Bool = false - enum ScheduleKind: String, CaseIterable, Identifiable { case at, every, cron; var id: String { rawValue } } + enum ScheduleKind: String, CaseIterable, Identifiable { case at, every, cron; var id: String { + rawValue + } } @State var scheduleKind: ScheduleKind = .every @State var atDate: Date = .init().addingTimeInterval(60 * 5) @State var everyText: String = "1h" @State var cronExpr: String = "0 9 * * 3" @State var cronTz: String = "" - enum PayloadKind: String, CaseIterable, Identifiable { case systemEvent, agentTurn; var id: String { rawValue } } + enum PayloadKind: String, CaseIterable, Identifiable { case systemEvent, agentTurn; var id: String { + rawValue + } } @State var payloadKind: PayloadKind = .systemEvent @State var systemEventText: String = "" @State var agentMessage: String = "" - enum DeliveryChoice: String, CaseIterable, Identifiable { case announce, none; var id: String { rawValue } } + enum DeliveryChoice: String, CaseIterable, Identifiable { case announce, none; var id: String { + rawValue + } } @State var deliveryMode: DeliveryChoice = .announce @State var channel: String = "last" @State var to: String = "" @@ -244,7 +250,6 @@ struct CronJobEditor: View { } } } - } .frame(maxWidth: .infinity, alignment: .leading) .padding(.vertical, 2) diff --git a/apps/macos/Sources/OpenClaw/CronJobsStore.swift b/apps/macos/Sources/OpenClaw/CronJobsStore.swift index cb84a2b41fd05..21c70ded58479 100644 --- a/apps/macos/Sources/OpenClaw/CronJobsStore.swift +++ b/apps/macos/Sources/OpenClaw/CronJobsStore.swift @@ -1,7 +1,7 @@ -import OpenClawKit -import OpenClawProtocol import Foundation import Observation +import OpenClawKit +import OpenClawProtocol import OSLog @MainActor diff --git a/apps/macos/Sources/OpenClaw/CronModels.swift b/apps/macos/Sources/OpenClaw/CronModels.swift index 4c977c9c12871..cbfbc061d6ae4 100644 --- a/apps/macos/Sources/OpenClaw/CronModels.swift +++ b/apps/macos/Sources/OpenClaw/CronModels.swift @@ -4,21 +4,28 @@ enum CronSessionTarget: String, CaseIterable, Identifiable, Codable { case main case isolated - var id: String { self.rawValue } + var id: String { + self.rawValue + } } enum CronWakeMode: String, CaseIterable, Identifiable, Codable { case now case nextHeartbeat = "next-heartbeat" - var id: String { self.rawValue } + var id: String { + self.rawValue + } } enum CronDeliveryMode: String, CaseIterable, Identifiable, Codable { case none case announce + case webhook - var id: String { self.rawValue } + var id: String { + self.rawValue + } } struct CronDelivery: Codable, Equatable { @@ -98,11 +105,11 @@ enum CronSchedule: Codable, Equatable { let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { return nil } if let date = makeIsoFormatter(withFractional: true).date(from: trimmed) { return date } - return makeIsoFormatter(withFractional: false).date(from: trimmed) + return self.makeIsoFormatter(withFractional: false).date(from: trimmed) } static func formatIsoDate(_ date: Date) -> String { - makeIsoFormatter(withFractional: false).string(from: date) + self.makeIsoFormatter(withFractional: false).string(from: date) } private static func makeIsoFormatter(withFractional: Bool) -> ISO8601DateFormatter { @@ -231,7 +238,9 @@ struct CronEvent: Codable, Sendable { } struct CronRunLogEntry: Codable, Identifiable, Sendable { - var id: String { "\(self.jobId)-\(self.ts)" } + var id: String { + "\(self.jobId)-\(self.ts)" + } let ts: Int let jobId: String @@ -243,7 +252,10 @@ struct CronRunLogEntry: Codable, Identifiable, Sendable { let durationMs: Int? let nextRunAtMs: Int? - var date: Date { Date(timeIntervalSince1970: TimeInterval(self.ts) / 1000) } + var date: Date { + Date(timeIntervalSince1970: TimeInterval(self.ts) / 1000) + } + var runDate: Date? { guard let runAtMs else { return nil } return Date(timeIntervalSince1970: TimeInterval(runAtMs) / 1000) diff --git a/apps/macos/Sources/OpenClaw/CronSettings+Actions.swift b/apps/macos/Sources/OpenClaw/CronSettings+Actions.swift index d5fe92ae01007..3fffaf90fd5c4 100644 --- a/apps/macos/Sources/OpenClaw/CronSettings+Actions.swift +++ b/apps/macos/Sources/OpenClaw/CronSettings+Actions.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Foundation +import OpenClawProtocol extension CronSettings { func save(payload: [String: AnyCodable]) async { diff --git a/apps/macos/Sources/OpenClaw/DeepLinks.swift b/apps/macos/Sources/OpenClaw/DeepLinks.swift index bb1fd73b66085..d11d4d524c360 100644 --- a/apps/macos/Sources/OpenClaw/DeepLinks.swift +++ b/apps/macos/Sources/OpenClaw/DeepLinks.swift @@ -1,13 +1,13 @@ import AppKit -import OpenClawKit import Foundation +import OpenClawKit import OSLog import Security private let deepLinkLogger = Logger(subsystem: "ai.openclaw", category: "DeepLink") enum DeepLinkAgentPolicy { - static let maxMessageChars = 20_000 + static let maxMessageChars = 20000 static let maxUnkeyedConfirmChars = 240 enum ValidationError: Error, Equatable, LocalizedError { @@ -16,7 +16,7 @@ enum DeepLinkAgentPolicy { var errorDescription: String? { switch self { case let .messageTooLongForConfirmation(max, actual): - return "Message is too long to confirm safely (\(actual) chars; max \(max) without key)." + "Message is too long to confirm safely (\(actual) chars; max \(max) without key)." } } } @@ -49,9 +49,9 @@ final class DeepLinkHandler { private var lastPromptAt: Date = .distantPast - // Ephemeral, in-memory key used for unattended deep links originating from the in-app Canvas. - // This avoids blocking Canvas init on UserDefaults and doesn't weaken the external deep-link prompt: - // outside callers can't know this randomly generated key. + /// Ephemeral, in-memory key used for unattended deep links originating from the in-app Canvas. + /// This avoids blocking Canvas init on UserDefaults and doesn't weaken the external deep-link prompt: + /// outside callers can't know this randomly generated key. private nonisolated static let canvasUnattendedKey: String = DeepLinkHandler.generateRandomKey() func handle(url: URL) async { @@ -67,6 +67,8 @@ final class DeepLinkHandler { switch route { case let .agent(link): await self.handleAgent(link: link, originalURL: url) + case .gateway: + break } } diff --git a/apps/macos/Sources/OpenClaw/DevicePairingApprovalPrompter.swift b/apps/macos/Sources/OpenClaw/DevicePairingApprovalPrompter.swift index 73ae0188a39f3..f85e8d1a5df3f 100644 --- a/apps/macos/Sources/OpenClaw/DevicePairingApprovalPrompter.swift +++ b/apps/macos/Sources/OpenClaw/DevicePairingApprovalPrompter.swift @@ -1,8 +1,8 @@ import AppKit -import OpenClawKit -import OpenClawProtocol import Foundation import Observation +import OpenClawKit +import OpenClawProtocol import OSLog @MainActor @@ -22,11 +22,6 @@ final class DevicePairingApprovalPrompter { private var alertHostWindow: NSWindow? private var resolvedByRequestId: Set = [] - private final class AlertHostWindow: NSWindow { - override var canBecomeKey: Bool { true } - override var canBecomeMain: Bool { true } - } - private struct PairingList: Codable { let pending: [PendingRequest] let paired: [PairedDevice]? @@ -55,7 +50,9 @@ final class DevicePairingApprovalPrompter { let isRepair: Bool? let ts: Double - var id: String { self.requestId } + var id: String { + self.requestId + } } private struct PairingResolvedEvent: Codable { @@ -231,35 +228,11 @@ final class DevicePairingApprovalPrompter { } private func endActiveAlert() { - guard let alert = self.activeAlert else { return } - if let parent = alert.window.sheetParent { - parent.endSheet(alert.window, returnCode: .abort) - } - self.activeAlert = nil - self.activeRequestId = nil + PairingAlertSupport.endActiveAlert(activeAlert: &self.activeAlert, activeRequestId: &self.activeRequestId) } private func requireAlertHostWindow() -> NSWindow { - if let alertHostWindow { - return alertHostWindow - } - - let window = AlertHostWindow( - contentRect: NSRect(x: 0, y: 0, width: 520, height: 1), - styleMask: [.borderless], - backing: .buffered, - defer: false) - window.title = "" - window.isReleasedWhenClosed = false - window.level = .floating - window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - window.isOpaque = false - window.hasShadow = false - window.backgroundColor = .clear - window.ignoresMouseEvents = true - - self.alertHostWindow = window - return window + PairingAlertSupport.requireAlertHostWindow(alertHostWindow: &self.alertHostWindow) } private func handle(push: GatewayPush) { diff --git a/apps/macos/Sources/OpenClaw/ExecApprovals.swift b/apps/macos/Sources/OpenClaw/ExecApprovals.swift index 21ab5b1749f53..f6bc839250385 100644 --- a/apps/macos/Sources/OpenClaw/ExecApprovals.swift +++ b/apps/macos/Sources/OpenClaw/ExecApprovals.swift @@ -8,7 +8,9 @@ enum ExecSecurity: String, CaseIterable, Codable, Identifiable { case allowlist case full - var id: String { self.rawValue } + var id: String { + self.rawValue + } var title: String { switch self { @@ -24,7 +26,9 @@ enum ExecApprovalQuickMode: String, CaseIterable, Identifiable { case ask case allow - var id: String { self.rawValue } + var id: String { + self.rawValue + } var title: String { switch self { @@ -67,7 +71,9 @@ enum ExecAsk: String, CaseIterable, Codable, Identifiable { case onMiss = "on-miss" case always - var id: String { self.rawValue } + var id: String { + self.rawValue + } var title: String { switch self { diff --git a/apps/macos/Sources/OpenClaw/ExecApprovalsGatewayPrompter.swift b/apps/macos/Sources/OpenClaw/ExecApprovalsGatewayPrompter.swift index add04c73087ba..670fa891c5b1e 100644 --- a/apps/macos/Sources/OpenClaw/ExecApprovalsGatewayPrompter.swift +++ b/apps/macos/Sources/OpenClaw/ExecApprovalsGatewayPrompter.swift @@ -1,7 +1,7 @@ -import OpenClawKit -import OpenClawProtocol import CoreGraphics import Foundation +import OpenClawKit +import OpenClawProtocol import OSLog @MainActor diff --git a/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift b/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift index c87dd1e5884f3..e1432aaea1c2c 100644 --- a/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift +++ b/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift @@ -1,8 +1,8 @@ import AppKit -import OpenClawKit import CryptoKit import Darwin import Foundation +import OpenClawKit import OSLog struct ExecApprovalPromptRequest: Codable, Sendable { @@ -76,7 +76,9 @@ private struct ExecHostResponse: Codable { enum ExecApprovalsSocketClient { private struct TimeoutError: LocalizedError { var message: String - var errorDescription: String? { self.message } + var errorDescription: String? { + self.message + } } static func requestDecision( diff --git a/apps/macos/Sources/OpenClaw/GatewayConnection.swift b/apps/macos/Sources/OpenClaw/GatewayConnection.swift index 4cf4d18b15111..0d7d582dd3357 100644 --- a/apps/macos/Sources/OpenClaw/GatewayConnection.swift +++ b/apps/macos/Sources/OpenClaw/GatewayConnection.swift @@ -1,7 +1,7 @@ +import Foundation import OpenClawChatUI import OpenClawKit import OpenClawProtocol -import Foundation import OSLog private let gatewayConnectionLogger = Logger(subsystem: "ai.openclaw", category: "gateway.connection") @@ -24,9 +24,13 @@ enum GatewayAgentChannel: String, Codable, CaseIterable, Sendable { self = GatewayAgentChannel(rawValue: normalized) ?? .last } - var isDeliverable: Bool { self != .webchat } + var isDeliverable: Bool { + self != .webchat + } - func shouldDeliver(_ deliver: Bool) -> Bool { deliver && self.isDeliverable } + func shouldDeliver(_ deliver: Bool) -> Bool { + deliver && self.isDeliverable + } } struct GatewayAgentInvocation: Sendable { diff --git a/apps/macos/Sources/OpenClaw/GatewayDiscoveryHelpers.swift b/apps/macos/Sources/OpenClaw/GatewayDiscoveryHelpers.swift index a533b92ebb9dc..281dcb9e8bd4c 100644 --- a/apps/macos/Sources/OpenClaw/GatewayDiscoveryHelpers.swift +++ b/apps/macos/Sources/OpenClaw/GatewayDiscoveryHelpers.swift @@ -1,5 +1,5 @@ -import OpenClawDiscovery import Foundation +import OpenClawDiscovery enum GatewayDiscoveryHelpers { static func sshTarget(for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String? { diff --git a/apps/macos/Sources/OpenClaw/GatewayEnvironment.swift b/apps/macos/Sources/OpenClaw/GatewayEnvironment.swift index 1e10394c2d277..059eb4da6e0cc 100644 --- a/apps/macos/Sources/OpenClaw/GatewayEnvironment.swift +++ b/apps/macos/Sources/OpenClaw/GatewayEnvironment.swift @@ -1,14 +1,16 @@ -import OpenClawIPC import Foundation +import OpenClawIPC import OSLog -// Lightweight SemVer helper (major.minor.patch only) for gateway compatibility checks. +/// Lightweight SemVer helper (major.minor.patch only) for gateway compatibility checks. struct Semver: Comparable, CustomStringConvertible, Sendable { let major: Int let minor: Int let patch: Int - var description: String { "\(self.major).\(self.minor).\(self.patch)" } + var description: String { + "\(self.major).\(self.minor).\(self.patch)" + } static func < (lhs: Semver, rhs: Semver) -> Bool { if lhs.major != rhs.major { return lhs.major < rhs.major } @@ -93,7 +95,7 @@ enum GatewayEnvironment { return (trimmed?.isEmpty == false) ? trimmed : nil } - // Exposed for tests so we can inject fake version checks without rewriting bundle metadata. + /// Exposed for tests so we can inject fake version checks without rewriting bundle metadata. static func expectedGatewayVersion(from versionString: String?) -> Semver? { Semver.parse(versionString) } diff --git a/apps/macos/Sources/OpenClaw/GeneralSettings.swift b/apps/macos/Sources/OpenClaw/GeneralSettings.swift index 40a105d1cbbc2..d55f7c1b01583 100644 --- a/apps/macos/Sources/OpenClaw/GeneralSettings.swift +++ b/apps/macos/Sources/OpenClaw/GeneralSettings.swift @@ -1,8 +1,8 @@ import AppKit +import Observation import OpenClawDiscovery import OpenClawIPC import OpenClawKit -import Observation import SwiftUI struct GeneralSettings: View { @@ -16,8 +16,13 @@ struct GeneralSettings: View { @State private var remoteStatus: RemoteStatus = .idle @State private var showRemoteAdvanced = false private let isPreview = ProcessInfo.processInfo.isPreview - private var isNixMode: Bool { ProcessInfo.processInfo.isNixMode } - private var remoteLabelWidth: CGFloat { 88 } + private var isNixMode: Bool { + ProcessInfo.processInfo.isNixMode + } + + private var remoteLabelWidth: CGFloat { + 88 + } var body: some View { ScrollView(.vertical) { diff --git a/apps/macos/Sources/OpenClaw/HealthStore.swift b/apps/macos/Sources/OpenClaw/HealthStore.swift index 4fb08f0c3da79..22c1409fca77d 100644 --- a/apps/macos/Sources/OpenClaw/HealthStore.swift +++ b/apps/macos/Sources/OpenClaw/HealthStore.swift @@ -89,8 +89,8 @@ final class HealthStore { } } - // Test-only escape hatch: the HealthStore is a process-wide singleton but - // state derivation is pure from `snapshot` + `lastError`. + /// Test-only escape hatch: the HealthStore is a process-wide singleton but + /// state derivation is pure from `snapshot` + `lastError`. func __setSnapshotForTest(_ snapshot: HealthSnapshot?, lastError: String? = nil) { self.snapshot = snapshot self.lastError = lastError diff --git a/apps/macos/Sources/OpenClaw/IconState.swift b/apps/macos/Sources/OpenClaw/IconState.swift index ec27385835428..c2eab0e501046 100644 --- a/apps/macos/Sources/OpenClaw/IconState.swift +++ b/apps/macos/Sources/OpenClaw/IconState.swift @@ -72,7 +72,9 @@ enum IconOverrideSelection: String, CaseIterable, Identifiable { case mainBash, mainRead, mainWrite, mainEdit, mainOther case otherBash, otherRead, otherWrite, otherEdit, otherOther - var id: String { self.rawValue } + var id: String { + self.rawValue + } var label: String { switch self { diff --git a/apps/macos/Sources/OpenClaw/InstancesStore.swift b/apps/macos/Sources/OpenClaw/InstancesStore.swift index 1f9dce6cb9a2e..566340337db69 100644 --- a/apps/macos/Sources/OpenClaw/InstancesStore.swift +++ b/apps/macos/Sources/OpenClaw/InstancesStore.swift @@ -1,8 +1,8 @@ -import OpenClawKit -import OpenClawProtocol import Cocoa import Foundation import Observation +import OpenClawKit +import OpenClawProtocol import OSLog struct InstanceInfo: Identifiable, Codable { @@ -158,7 +158,7 @@ final class InstancesStore { private func localFallbackInstance(reason: String) -> InstanceInfo { let host = Host.current().localizedName ?? "this-mac" - let ip = Self.primaryIPv4Address() + let ip = SystemPresenceInfo.primaryIPv4Address() let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String let osVersion = ProcessInfo.processInfo.operatingSystemVersion let platform = "macos \(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" @@ -172,58 +172,13 @@ final class InstancesStore { platform: platform, deviceFamily: "Mac", modelIdentifier: InstanceIdentity.modelIdentifier, - lastInputSeconds: Self.lastInputSeconds(), + lastInputSeconds: SystemPresenceInfo.lastInputSeconds(), mode: "local", reason: reason, text: text, ts: ts) } - private static func lastInputSeconds() -> Int? { - let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null - let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent) - if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil } - return Int(seconds.rounded()) - } - - private static func primaryIPv4Address() -> String? { - var addrList: UnsafeMutablePointer? - guard getifaddrs(&addrList) == 0, let first = addrList else { return nil } - defer { freeifaddrs(addrList) } - - var fallback: String? - var en0: String? - - for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) { - let flags = Int32(ptr.pointee.ifa_flags) - let isUp = (flags & IFF_UP) != 0 - let isLoopback = (flags & IFF_LOOPBACK) != 0 - let name = String(cString: ptr.pointee.ifa_name) - let family = ptr.pointee.ifa_addr.pointee.sa_family - if !isUp || isLoopback || family != UInt8(AF_INET) { continue } - - var addr = ptr.pointee.ifa_addr.pointee - var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) - let result = getnameinfo( - &addr, - socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), - &buffer, - socklen_t(buffer.count), - nil, - 0, - NI_NUMERICHOST) - guard result == 0 else { continue } - let len = buffer.prefix { $0 != 0 } - let bytes = len.map { UInt8(bitPattern: $0) } - guard let ip = String(bytes: bytes, encoding: .utf8) else { continue } - - if name == "en0" { en0 = ip; break } - if fallback == nil { fallback = ip } - } - - return en0 ?? fallback - } - // MARK: - Helpers /// Keep the last raw payload for logging. diff --git a/apps/macos/Sources/OpenClaw/LogLocator.swift b/apps/macos/Sources/OpenClaw/LogLocator.swift index 927b7892a2800..b504ab02acecb 100644 --- a/apps/macos/Sources/OpenClaw/LogLocator.swift +++ b/apps/macos/Sources/OpenClaw/LogLocator.swift @@ -7,8 +7,7 @@ enum LogLocator { { return URL(fileURLWithPath: override) } - let preferred = URL(fileURLWithPath: "/tmp/openclaw") - return preferred + return URL(fileURLWithPath: "/tmp/openclaw") } private static var stdoutLog: URL { diff --git a/apps/macos/Sources/OpenClaw/Logging/OpenClawLogging.swift b/apps/macos/Sources/OpenClaw/Logging/OpenClawLogging.swift index bd46a8e6ff095..7692887e6c7ec 100644 --- a/apps/macos/Sources/OpenClaw/Logging/OpenClawLogging.swift +++ b/apps/macos/Sources/OpenClaw/Logging/OpenClawLogging.swift @@ -37,7 +37,9 @@ enum AppLogLevel: String, CaseIterable, Identifiable { static let `default`: AppLogLevel = .info - var id: String { self.rawValue } + var id: String { + self.rawValue + } var title: String { switch self { diff --git a/apps/macos/Sources/OpenClaw/MenuBar.swift b/apps/macos/Sources/OpenClaw/MenuBar.swift index 406d4e063dcbf..00e2a9be0a635 100644 --- a/apps/macos/Sources/OpenClaw/MenuBar.swift +++ b/apps/macos/Sources/OpenClaw/MenuBar.swift @@ -345,7 +345,7 @@ protocol UpdaterProviding: AnyObject { func checkForUpdates(_ sender: Any?) } -// No-op updater used for debug/dev runs to suppress Sparkle dialogs. +/// No-op updater used for debug/dev runs to suppress Sparkle dialogs. final class DisabledUpdaterController: UpdaterProviding { var automaticallyChecksForUpdates: Bool = false var automaticallyDownloadsUpdates: Bool = false @@ -394,7 +394,9 @@ final class SparkleUpdaterController: NSObject, UpdaterProviding { set { self.controller.updater.automaticallyDownloadsUpdates = newValue } } - var isAvailable: Bool { true } + var isAvailable: Bool { + true + } func checkForUpdates(_ sender: Any?) { self.controller.checkForUpdates(sender) diff --git a/apps/macos/Sources/OpenClaw/MenuContentView.swift b/apps/macos/Sources/OpenClaw/MenuContentView.swift index fd1b437cf7cb7..3416d23f81211 100644 --- a/apps/macos/Sources/OpenClaw/MenuContentView.swift +++ b/apps/macos/Sources/OpenClaw/MenuContentView.swift @@ -400,7 +400,6 @@ struct MenuContent: View { } } - @ViewBuilder private func statusLine(label: String, color: Color) -> some View { HStack(spacing: 6) { Circle() @@ -590,6 +589,8 @@ struct MenuContent: View { private struct AudioInputDevice: Identifiable, Equatable { let uid: String let name: String - var id: String { self.uid } + var id: String { + self.uid + } } } diff --git a/apps/macos/Sources/OpenClaw/MenuHighlightedHostView.swift b/apps/macos/Sources/OpenClaw/MenuHighlightedHostView.swift index f1e85cba1528f..7107946989ecb 100644 --- a/apps/macos/Sources/OpenClaw/MenuHighlightedHostView.swift +++ b/apps/macos/Sources/OpenClaw/MenuHighlightedHostView.swift @@ -22,7 +22,9 @@ final class HighlightedMenuItemHostView: NSView { } @available(*, unavailable) - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override var intrinsicContentSize: NSSize { let size = self.hosting.fittingSize diff --git a/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift b/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift index 9b6bb09934135..37fd6ca25052b 100644 --- a/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift +++ b/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift @@ -159,7 +159,9 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { extension MenuSessionsInjector { // MARK: - Injection - private var mainSessionKey: String { WorkActivityStore.shared.mainSessionKey } + private var mainSessionKey: String { + WorkActivityStore.shared.mainSessionKey + } private func inject(into menu: NSMenu) { self.cancelPreviewTasks() @@ -1175,8 +1177,7 @@ extension MenuSessionsInjector { private func makeHostedView(rootView: AnyView, width: CGFloat, highlighted: Bool) -> NSView { if highlighted { - let container = HighlightedMenuItemHostView(rootView: rootView, width: width) - return container + return HighlightedMenuItemHostView(rootView: rootView, width: width) } let hosting = NSHostingView(rootView: rootView) diff --git a/apps/macos/Sources/OpenClaw/MicLevelMonitor.swift b/apps/macos/Sources/OpenClaw/MicLevelMonitor.swift index af72740a676f5..e35057d28cfab 100644 --- a/apps/macos/Sources/OpenClaw/MicLevelMonitor.swift +++ b/apps/macos/Sources/OpenClaw/MicLevelMonitor.swift @@ -64,8 +64,7 @@ actor MicLevelMonitor { } let rms = sqrt(sum / Float(frameCount) + 1e-12) let db = 20 * log10(Double(rms)) - let normalized = max(0, min(1, (db + 50) / 50)) - return normalized + return max(0, min(1, (db + 50) / 50)) } } diff --git a/apps/macos/Sources/OpenClaw/ModelCatalogLoader.swift b/apps/macos/Sources/OpenClaw/ModelCatalogLoader.swift index ff966e1eabcea..b320c84d2327e 100644 --- a/apps/macos/Sources/OpenClaw/ModelCatalogLoader.swift +++ b/apps/macos/Sources/OpenClaw/ModelCatalogLoader.swift @@ -2,7 +2,10 @@ import Foundation import JavaScriptCore enum ModelCatalogLoader { - static var defaultPath: String { self.resolveDefaultPath() } + static var defaultPath: String { + self.resolveDefaultPath() + } + private static let logger = Logger(subsystem: "ai.openclaw", category: "models") private nonisolated static let appSupportDir: URL = { let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first! diff --git a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeLocationService.swift b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeLocationService.swift index db404aa6e171f..bd4df512ca499 100644 --- a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeLocationService.swift +++ b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeLocationService.swift @@ -1,6 +1,6 @@ -import OpenClawKit import CoreLocation import Foundation +import OpenClawKit @MainActor final class MacNodeLocationService: NSObject, CLLocationManagerDelegate { diff --git a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeModeCoordinator.swift b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeModeCoordinator.swift index eed0755f9b75c..af46788c9ccd7 100644 --- a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeModeCoordinator.swift +++ b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeModeCoordinator.swift @@ -1,5 +1,5 @@ -import OpenClawKit import Foundation +import OpenClawKit import OSLog @MainActor diff --git a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntime.swift b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntime.swift index 0b88f159098ed..60bd95f2894be 100644 --- a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntime.swift +++ b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntime.swift @@ -1,7 +1,7 @@ import AppKit +import Foundation import OpenClawIPC import OpenClawKit -import Foundation actor MacNodeRuntime { private let cameraCapture = CameraCaptureService() diff --git a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntimeMainActorServices.swift b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntimeMainActorServices.swift index 982ec8bf90f9e..733410b186015 100644 --- a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntimeMainActorServices.swift +++ b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntimeMainActorServices.swift @@ -1,6 +1,6 @@ -import OpenClawKit import CoreLocation import Foundation +import OpenClawKit @MainActor protocol MacNodeRuntimeMainActorServices: Sendable { diff --git a/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift b/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift index 9853294662432..ee994b38f6505 100644 --- a/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift +++ b/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift @@ -1,10 +1,10 @@ import AppKit +import Foundation +import Observation import OpenClawDiscovery import OpenClawIPC import OpenClawKit import OpenClawProtocol -import Foundation -import Observation import OSLog import UserNotifications @@ -38,11 +38,6 @@ final class NodePairingApprovalPrompter { private var remoteResolutionsByRequestId: [String: PairingResolution] = [:] private var autoApproveAttempts: Set = [] - private final class AlertHostWindow: NSWindow { - override var canBecomeKey: Bool { true } - override var canBecomeMain: Bool { true } - } - private struct PairingList: Codable { let pending: [PendingRequest] let paired: [PairedNode]? @@ -68,7 +63,9 @@ final class NodePairingApprovalPrompter { let silent: Bool? let ts: Double - var id: String { self.requestId } + var id: String { + self.requestId + } } private struct PairingResolvedEvent: Codable { @@ -235,35 +232,11 @@ final class NodePairingApprovalPrompter { } private func endActiveAlert() { - guard let alert = self.activeAlert else { return } - if let parent = alert.window.sheetParent { - parent.endSheet(alert.window, returnCode: .abort) - } - self.activeAlert = nil - self.activeRequestId = nil + PairingAlertSupport.endActiveAlert(activeAlert: &self.activeAlert, activeRequestId: &self.activeRequestId) } private func requireAlertHostWindow() -> NSWindow { - if let alertHostWindow { - return alertHostWindow - } - - let window = AlertHostWindow( - contentRect: NSRect(x: 0, y: 0, width: 520, height: 1), - styleMask: [.borderless], - backing: .buffered, - defer: false) - window.title = "" - window.isReleasedWhenClosed = false - window.level = .floating - window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - window.isOpaque = false - window.hasShadow = false - window.backgroundColor = .clear - window.ignoresMouseEvents = true - - self.alertHostWindow = window - return window + PairingAlertSupport.requireAlertHostWindow(alertHostWindow: &self.alertHostWindow) } private func handle(push: GatewayPush) { diff --git a/apps/macos/Sources/OpenClaw/NodesStore.swift b/apps/macos/Sources/OpenClaw/NodesStore.swift index 6ea5fbe90876d..5cc94858645bc 100644 --- a/apps/macos/Sources/OpenClaw/NodesStore.swift +++ b/apps/macos/Sources/OpenClaw/NodesStore.swift @@ -18,9 +18,17 @@ struct NodeInfo: Identifiable, Codable { let paired: Bool? let connected: Bool? - var id: String { self.nodeId } - var isConnected: Bool { self.connected ?? false } - var isPaired: Bool { self.paired ?? false } + var id: String { + self.nodeId + } + + var isConnected: Bool { + self.connected ?? false + } + + var isPaired: Bool { + self.paired ?? false + } } private struct NodeListResponse: Codable { diff --git a/apps/macos/Sources/OpenClaw/NotificationManager.swift b/apps/macos/Sources/OpenClaw/NotificationManager.swift index f522e6317643a..b8e6fcddc8cec 100644 --- a/apps/macos/Sources/OpenClaw/NotificationManager.swift +++ b/apps/macos/Sources/OpenClaw/NotificationManager.swift @@ -1,5 +1,5 @@ -import OpenClawIPC import Foundation +import OpenClawIPC import Security import UserNotifications diff --git a/apps/macos/Sources/OpenClaw/NotifyOverlay.swift b/apps/macos/Sources/OpenClaw/NotifyOverlay.swift index 1191c7e22227a..31157b0d831b5 100644 --- a/apps/macos/Sources/OpenClaw/NotifyOverlay.swift +++ b/apps/macos/Sources/OpenClaw/NotifyOverlay.swift @@ -10,7 +10,9 @@ final class NotifyOverlayController { static let shared = NotifyOverlayController() private(set) var model = Model() - var isVisible: Bool { self.model.isVisible } + var isVisible: Bool { + self.model.isVisible + } struct Model { var title: String = "" diff --git a/apps/macos/Sources/OpenClaw/Onboarding.swift b/apps/macos/Sources/OpenClaw/Onboarding.swift index def8af4b2197d..b8a6377b419e6 100644 --- a/apps/macos/Sources/OpenClaw/Onboarding.swift +++ b/apps/macos/Sources/OpenClaw/Onboarding.swift @@ -1,9 +1,9 @@ import AppKit +import Combine +import Observation import OpenClawChatUI import OpenClawDiscovery import OpenClawIPC -import Combine -import Observation import SwiftUI enum UIStrings { @@ -142,18 +142,30 @@ struct OnboardingView: View { Self.pageOrder(for: self.state.connectionMode, showOnboardingChat: self.showOnboardingChat) } - var pageCount: Int { self.pageOrder.count } + var pageCount: Int { + self.pageOrder.count + } + var activePageIndex: Int { self.activePageIndex(for: self.currentPage) } - var buttonTitle: String { self.currentPage == self.pageCount - 1 ? "Finish" : "Next" } - var wizardPageOrderIndex: Int? { self.pageOrder.firstIndex(of: self.wizardPageIndex) } + var buttonTitle: String { + self.currentPage == self.pageCount - 1 ? "Finish" : "Next" + } + + var wizardPageOrderIndex: Int? { + self.pageOrder.firstIndex(of: self.wizardPageIndex) + } + var isWizardBlocking: Bool { self.activePageIndex == self.wizardPageIndex && !self.onboardingWizard.isComplete } - var canAdvance: Bool { !self.isWizardBlocking } + var canAdvance: Bool { + !self.isWizardBlocking + } + var devLinkCommand: String { let version = GatewayEnvironment.expectedGatewayVersionString() ?? "latest" return "npm install -g openclaw@\(version)" diff --git a/apps/macos/Sources/OpenClaw/OnboardingView+Actions.swift b/apps/macos/Sources/OpenClaw/OnboardingView+Actions.swift index 47cce949db63f..ba43424aa9a76 100644 --- a/apps/macos/Sources/OpenClaw/OnboardingView+Actions.swift +++ b/apps/macos/Sources/OpenClaw/OnboardingView+Actions.swift @@ -1,7 +1,7 @@ import AppKit +import Foundation import OpenClawDiscovery import OpenClawIPC -import Foundation import SwiftUI extension OnboardingView { diff --git a/apps/macos/Sources/OpenClaw/OnboardingView+Monitoring.swift b/apps/macos/Sources/OpenClaw/OnboardingView+Monitoring.swift index 64ddc332e4ac0..dfbdf91d44d8f 100644 --- a/apps/macos/Sources/OpenClaw/OnboardingView+Monitoring.swift +++ b/apps/macos/Sources/OpenClaw/OnboardingView+Monitoring.swift @@ -1,5 +1,5 @@ -import OpenClawIPC import Foundation +import OpenClawIPC extension OnboardingView { @MainActor diff --git a/apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift b/apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift index 309c4aa026e69..5760bfff8c20d 100644 --- a/apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift +++ b/apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift @@ -206,7 +206,9 @@ extension OnboardingView { .textFieldStyle(.roundedBorder) .frame(width: fieldWidth) } - if let message = CommandResolver.sshTargetValidationMessage(self.state.remoteTarget) { + if let message = CommandResolver + .sshTargetValidationMessage(self.state.remoteTarget) + { GridRow { Text("") .frame(width: labelWidth, alignment: .leading) diff --git a/apps/macos/Sources/OpenClaw/OnboardingView+Wizard.swift b/apps/macos/Sources/OpenClaw/OnboardingView+Wizard.swift index 51424fdb78c85..0c77f1e327dd7 100644 --- a/apps/macos/Sources/OpenClaw/OnboardingView+Wizard.swift +++ b/apps/macos/Sources/OpenClaw/OnboardingView+Wizard.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Observation +import OpenClawProtocol import SwiftUI extension OnboardingView { diff --git a/apps/macos/Sources/OpenClaw/OnboardingView+Workspace.swift b/apps/macos/Sources/OpenClaw/OnboardingView+Workspace.swift index 0b413433666b7..1895b2af94f7a 100644 --- a/apps/macos/Sources/OpenClaw/OnboardingView+Workspace.swift +++ b/apps/macos/Sources/OpenClaw/OnboardingView+Workspace.swift @@ -23,7 +23,7 @@ extension OnboardingView { } catch { self.workspaceStatus = "Failed to create workspace: \(error.localizedDescription)" } - case let .unsafe(reason): + case let .unsafe (reason): self.workspaceStatus = "Workspace not touched: \(reason)" } self.refreshBootstrapStatus() @@ -54,7 +54,7 @@ extension OnboardingView { do { let url = AgentWorkspace.resolveWorkspaceURL(from: self.workspacePath) - if case let .unsafe(reason) = AgentWorkspace.bootstrapSafety(for: url) { + if case let .unsafe (reason) = AgentWorkspace.bootstrapSafety(for: url) { self.workspaceStatus = "Workspace not created: \(reason)" return } diff --git a/apps/macos/Sources/OpenClaw/OnboardingWizard.swift b/apps/macos/Sources/OpenClaw/OnboardingWizard.swift index 412826650a66f..75b9522a4d100 100644 --- a/apps/macos/Sources/OpenClaw/OnboardingWizard.swift +++ b/apps/macos/Sources/OpenClaw/OnboardingWizard.swift @@ -1,7 +1,7 @@ -import OpenClawKit -import OpenClawProtocol import Foundation import Observation +import OpenClawKit +import OpenClawProtocol import OSLog import SwiftUI @@ -41,8 +41,13 @@ final class OnboardingWizardModel { private var restartAttempts = 0 private let maxRestartAttempts = 1 - var isComplete: Bool { self.status == "done" } - var isRunning: Bool { self.status == "running" } + var isComplete: Bool { + self.status == "done" + } + + var isRunning: Bool { + self.status == "running" + } func reset() { self.sessionId = nil @@ -408,5 +413,7 @@ private struct WizardOptionItem: Identifiable { let index: Int let option: WizardOption - var id: Int { self.index } + var id: Int { + self.index + } } diff --git a/apps/macos/Sources/OpenClaw/OpenClawConfigFile.swift b/apps/macos/Sources/OpenClaw/OpenClawConfigFile.swift index fc66030e3f52d..f49f2b7e0d4fd 100644 --- a/apps/macos/Sources/OpenClaw/OpenClawConfigFile.swift +++ b/apps/macos/Sources/OpenClaw/OpenClawConfigFile.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Foundation +import OpenClawProtocol enum OpenClawConfigFile { private static let logger = Logger(subsystem: "ai.openclaw", category: "config") diff --git a/apps/macos/Sources/OpenClaw/OpenClawPaths.swift b/apps/macos/Sources/OpenClaw/OpenClawPaths.swift index 632c07c802bdf..206031f9aa19b 100644 --- a/apps/macos/Sources/OpenClaw/OpenClawPaths.swift +++ b/apps/macos/Sources/OpenClaw/OpenClawPaths.swift @@ -24,8 +24,7 @@ enum OpenClawPaths { } } let home = FileManager().homeDirectoryForCurrentUser - let preferred = home.appendingPathComponent(".openclaw", isDirectory: true) - return preferred + return home.appendingPathComponent(".openclaw", isDirectory: true) } private static func resolveConfigCandidate(in dir: URL) -> URL? { diff --git a/apps/macos/Sources/OpenClaw/PairingAlertSupport.swift b/apps/macos/Sources/OpenClaw/PairingAlertSupport.swift new file mode 100644 index 0000000000000..e8e4428bf3fd6 --- /dev/null +++ b/apps/macos/Sources/OpenClaw/PairingAlertSupport.swift @@ -0,0 +1,46 @@ +import AppKit + +final class PairingAlertHostWindow: NSWindow { + override var canBecomeKey: Bool { + true + } + + override var canBecomeMain: Bool { + true + } +} + +@MainActor +enum PairingAlertSupport { + static func endActiveAlert(activeAlert: inout NSAlert?, activeRequestId: inout String?) { + guard let alert = activeAlert else { return } + if let parent = alert.window.sheetParent { + parent.endSheet(alert.window, returnCode: .abort) + } + activeAlert = nil + activeRequestId = nil + } + + static func requireAlertHostWindow(alertHostWindow: inout NSWindow?) -> NSWindow { + if let alertHostWindow { + return alertHostWindow + } + + let window = PairingAlertHostWindow( + contentRect: NSRect(x: 0, y: 0, width: 520, height: 1), + styleMask: [.borderless], + backing: .buffered, + defer: false) + window.title = "" + window.isReleasedWhenClosed = false + window.level = .floating + window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] + window.isOpaque = false + window.hasShadow = false + window.backgroundColor = .clear + window.ignoresMouseEvents = true + + alertHostWindow = window + return window + } +} diff --git a/apps/macos/Sources/OpenClaw/PermissionManager.swift b/apps/macos/Sources/OpenClaw/PermissionManager.swift index 3cf1cba3f6ec8..b5bcd167a4641 100644 --- a/apps/macos/Sources/OpenClaw/PermissionManager.swift +++ b/apps/macos/Sources/OpenClaw/PermissionManager.swift @@ -1,11 +1,11 @@ import AppKit import ApplicationServices import AVFoundation -import OpenClawIPC import CoreGraphics import CoreLocation import Foundation import Observation +import OpenClawIPC import Speech import UserNotifications @@ -336,7 +336,7 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate { cont.resume(returning: status) } - // nonisolated for Swift 6 strict concurrency compatibility + /// nonisolated for Swift 6 strict concurrency compatibility nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = manager.authorizationStatus Task { @MainActor in @@ -344,7 +344,7 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate { } } - // Legacy callback (still used on some macOS versions / configurations). + /// Legacy callback (still used on some macOS versions / configurations). nonisolated func locationManager( _ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) diff --git a/apps/macos/Sources/OpenClaw/PermissionsSettings.swift b/apps/macos/Sources/OpenClaw/PermissionsSettings.swift index a8f6accf8af7b..de15e5ebb63d1 100644 --- a/apps/macos/Sources/OpenClaw/PermissionsSettings.swift +++ b/apps/macos/Sources/OpenClaw/PermissionsSettings.swift @@ -1,6 +1,6 @@ +import CoreLocation import OpenClawIPC import OpenClawKit -import CoreLocation import SwiftUI struct PermissionsSettings: View { @@ -164,7 +164,9 @@ struct PermissionRow: View { .padding(.vertical, self.compact ? 4 : 6) } - private var iconSize: CGFloat { self.compact ? 28 : 32 } + private var iconSize: CGFloat { + self.compact ? 28 : 32 + } private var title: String { switch self.capability { diff --git a/apps/macos/Sources/OpenClaw/PortGuardian.swift b/apps/macos/Sources/OpenClaw/PortGuardian.swift index 98225f30e1e56..7ab7e8def3f77 100644 --- a/apps/macos/Sources/OpenClaw/PortGuardian.swift +++ b/apps/macos/Sources/OpenClaw/PortGuardian.swift @@ -103,7 +103,9 @@ actor PortGuardian { let status: Status let listeners: [ReportListener] - var id: Int { self.port } + var id: Int { + self.port + } var offenders: [ReportListener] { if case let .interference(_, offenders) = self.status { return offenders } @@ -141,7 +143,9 @@ actor PortGuardian { let user: String? let expected: Bool - var id: Int32 { self.pid } + var id: Int32 { + self.pid + } } func diagnose(mode: AppState.ConnectionMode) async -> [PortReport] { diff --git a/apps/macos/Sources/OpenClaw/PresenceReporter.swift b/apps/macos/Sources/OpenClaw/PresenceReporter.swift index 16d70b8a92c0c..2e7a1d4c472c4 100644 --- a/apps/macos/Sources/OpenClaw/PresenceReporter.swift +++ b/apps/macos/Sources/OpenClaw/PresenceReporter.swift @@ -1,5 +1,4 @@ import Cocoa -import Darwin import Foundation import OSLog @@ -33,10 +32,10 @@ final class PresenceReporter { private func push(reason: String) async { let mode = await MainActor.run { AppStateStore.shared.connectionMode.rawValue } let host = InstanceIdentity.displayName - let ip = Self.primaryIPv4Address() ?? "ip-unknown" + let ip = SystemPresenceInfo.primaryIPv4Address() ?? "ip-unknown" let version = Self.appVersionString() let platform = Self.platformString() - let lastInput = Self.lastInputSeconds() + let lastInput = SystemPresenceInfo.lastInputSeconds() let text = Self.composePresenceSummary(mode: mode, reason: reason) var params: [String: AnyHashable] = [ "instanceId": AnyHashable(self.instanceId), @@ -64,9 +63,9 @@ final class PresenceReporter { private static func composePresenceSummary(mode: String, reason: String) -> String { let host = InstanceIdentity.displayName - let ip = Self.primaryIPv4Address() ?? "ip-unknown" + let ip = SystemPresenceInfo.primaryIPv4Address() ?? "ip-unknown" let version = Self.appVersionString() - let lastInput = Self.lastInputSeconds() + let lastInput = SystemPresenceInfo.lastInputSeconds() let lastLabel = lastInput.map { "last input \($0)s ago" } ?? "last input unknown" return "Node: \(host) (\(ip)) · app \(version) · \(lastLabel) · mode \(mode) · reason \(reason)" } @@ -87,50 +86,7 @@ final class PresenceReporter { return "macos \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)" } - private static func lastInputSeconds() -> Int? { - let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null - let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent) - if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil } - return Int(seconds.rounded()) - } - - private static func primaryIPv4Address() -> String? { - var addrList: UnsafeMutablePointer? - guard getifaddrs(&addrList) == 0, let first = addrList else { return nil } - defer { freeifaddrs(addrList) } - - var fallback: String? - var en0: String? - - for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) { - let flags = Int32(ptr.pointee.ifa_flags) - let isUp = (flags & IFF_UP) != 0 - let isLoopback = (flags & IFF_LOOPBACK) != 0 - let name = String(cString: ptr.pointee.ifa_name) - let family = ptr.pointee.ifa_addr.pointee.sa_family - if !isUp || isLoopback || family != UInt8(AF_INET) { continue } - - var addr = ptr.pointee.ifa_addr.pointee - var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) - let result = getnameinfo( - &addr, - socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), - &buffer, - socklen_t(buffer.count), - nil, - 0, - NI_NUMERICHOST) - guard result == 0 else { continue } - let len = buffer.prefix { $0 != 0 } - let bytes = len.map { UInt8(bitPattern: $0) } - guard let ip = String(bytes: bytes, encoding: .utf8) else { continue } - - if name == "en0" { en0 = ip; break } - if fallback == nil { fallback = ip } - } - - return en0 ?? fallback - } + // (SystemPresenceInfo) last input + primary IPv4. } #if DEBUG @@ -148,11 +104,11 @@ extension PresenceReporter { } static func _testLastInputSeconds() -> Int? { - self.lastInputSeconds() + SystemPresenceInfo.lastInputSeconds() } static func _testPrimaryIPv4Address() -> String? { - self.primaryIPv4Address() + SystemPresenceInfo.primaryIPv4Address() } } #endif diff --git a/apps/macos/Sources/OpenClaw/ProcessInfo+OpenClaw.swift b/apps/macos/Sources/OpenClaw/ProcessInfo+OpenClaw.swift index d05e593388ea0..a219f49533664 100644 --- a/apps/macos/Sources/OpenClaw/ProcessInfo+OpenClaw.swift +++ b/apps/macos/Sources/OpenClaw/ProcessInfo+OpenClaw.swift @@ -12,8 +12,8 @@ extension ProcessInfo { environment: [String: String], standard: UserDefaults, stableSuite: UserDefaults?, - isAppBundle: Bool - ) -> Bool { + isAppBundle: Bool) -> Bool + { if environment["OPENCLAW_NIX_MODE"] == "1" { return true } if standard.bool(forKey: "openclaw.nixMode") { return true } diff --git a/apps/macos/Sources/OpenClaw/Resources/Info.plist b/apps/macos/Sources/OpenClaw/Resources/Info.plist index 51081d43df58d..580a1ef00635c 100644 --- a/apps/macos/Sources/OpenClaw/Resources/Info.plist +++ b/apps/macos/Sources/OpenClaw/Resources/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2026.2.13 + 2026.2.19 CFBundleVersion - 202602130 + 202602190 CFBundleIconFile OpenClaw CFBundleURLTypes diff --git a/apps/macos/Sources/OpenClaw/RuntimeLocator.swift b/apps/macos/Sources/OpenClaw/RuntimeLocator.swift index 8ec23a067be98..3112f57879b1c 100644 --- a/apps/macos/Sources/OpenClaw/RuntimeLocator.swift +++ b/apps/macos/Sources/OpenClaw/RuntimeLocator.swift @@ -10,7 +10,9 @@ struct RuntimeVersion: Comparable, CustomStringConvertible { let minor: Int let patch: Int - var description: String { "\(self.major).\(self.minor).\(self.patch)" } + var description: String { + "\(self.major).\(self.minor).\(self.patch)" + } static func < (lhs: RuntimeVersion, rhs: RuntimeVersion) -> Bool { if lhs.major != rhs.major { return lhs.major < rhs.major } @@ -163,5 +165,7 @@ enum RuntimeLocator { } extension RuntimeKind { - fileprivate var binaryName: String { "node" } + fileprivate var binaryName: String { + "node" + } } diff --git a/apps/macos/Sources/OpenClaw/SessionData.swift b/apps/macos/Sources/OpenClaw/SessionData.swift index defd4fe8aa113..8234cbdef854a 100644 --- a/apps/macos/Sources/OpenClaw/SessionData.swift +++ b/apps/macos/Sources/OpenClaw/SessionData.swift @@ -84,8 +84,13 @@ struct SessionRow: Identifiable { let tokens: SessionTokenStats let model: String? - var ageText: String { relativeAge(from: self.updatedAt) } - var label: String { self.displayName ?? self.key } + var ageText: String { + relativeAge(from: self.updatedAt) + } + + var label: String { + self.displayName ?? self.key + } var flagLabels: [String] { var flags: [String] = [] diff --git a/apps/macos/Sources/OpenClaw/SessionMenuLabelView.swift b/apps/macos/Sources/OpenClaw/SessionMenuLabelView.swift index 1cbeedd392d6d..51646e0a36a33 100644 --- a/apps/macos/Sources/OpenClaw/SessionMenuLabelView.swift +++ b/apps/macos/Sources/OpenClaw/SessionMenuLabelView.swift @@ -1,14 +1,7 @@ import SwiftUI -private struct MenuItemHighlightedKey: EnvironmentKey { - static let defaultValue = false -} - extension EnvironmentValues { - var menuItemHighlighted: Bool { - get { self[MenuItemHighlightedKey.self] } - set { self[MenuItemHighlightedKey.self] = newValue } - } + @Entry var menuItemHighlighted: Bool = false } struct SessionMenuLabelView: View { diff --git a/apps/macos/Sources/OpenClaw/SessionMenuPreviewView.swift b/apps/macos/Sources/OpenClaw/SessionMenuPreviewView.swift index dc129df9f41e8..8840bce5569ac 100644 --- a/apps/macos/Sources/OpenClaw/SessionMenuPreviewView.swift +++ b/apps/macos/Sources/OpenClaw/SessionMenuPreviewView.swift @@ -183,7 +183,6 @@ struct SessionMenuPreviewView: View { .frame(width: max(1, self.width), alignment: .leading) } - @ViewBuilder private func previewRow(_ item: SessionPreviewItem) -> some View { HStack(alignment: .top, spacing: 4) { Text(item.role.label) @@ -212,7 +211,6 @@ struct SessionMenuPreviewView: View { } } - @ViewBuilder private func placeholder(_ text: String) -> some View { Text(text) .font(.caption) @@ -227,7 +225,9 @@ enum SessionMenuPreviewLoader { private static let previewMaxChars = 240 private struct PreviewTimeoutError: LocalizedError { - var errorDescription: String? { "preview timeout" } + var errorDescription: String? { + "preview timeout" + } } static func prewarm(sessionKeys: [String], maxItems: Int) async { diff --git a/apps/macos/Sources/OpenClaw/SessionsSettings.swift b/apps/macos/Sources/OpenClaw/SessionsSettings.swift index 4a2a0e81e0297..826f1128f54d0 100644 --- a/apps/macos/Sources/OpenClaw/SessionsSettings.swift +++ b/apps/macos/Sources/OpenClaw/SessionsSettings.swift @@ -85,7 +85,6 @@ struct SessionsSettings: View { } } - @ViewBuilder private func sessionRow(_ row: SessionRow) -> some View { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .firstTextBaseline, spacing: 8) { diff --git a/apps/macos/Sources/OpenClaw/ShellExecutor.swift b/apps/macos/Sources/OpenClaw/ShellExecutor.swift index 9633f0f8da0a6..ec757441a15e1 100644 --- a/apps/macos/Sources/OpenClaw/ShellExecutor.swift +++ b/apps/macos/Sources/OpenClaw/ShellExecutor.swift @@ -1,5 +1,5 @@ -import OpenClawIPC import Foundation +import OpenClawIPC enum ShellExecutor { struct ShellResult { @@ -69,7 +69,7 @@ enum ShellExecutor { if let timeout, timeout > 0 { let nanos = UInt64(timeout * 1_000_000_000) - let result = await withTaskGroup(of: ShellResult.self) { group in + return await withTaskGroup(of: ShellResult.self) { group in group.addTask { await waitTask.value } group.addTask { try? await Task.sleep(nanoseconds: nanos) @@ -87,7 +87,6 @@ enum ShellExecutor { group.cancelAll() return first } - return result } return await waitTask.value diff --git a/apps/macos/Sources/OpenClaw/SkillsModels.swift b/apps/macos/Sources/OpenClaw/SkillsModels.swift index 1fb40d99f1597..d143484c40f67 100644 --- a/apps/macos/Sources/OpenClaw/SkillsModels.swift +++ b/apps/macos/Sources/OpenClaw/SkillsModels.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Foundation +import OpenClawProtocol struct SkillsStatusReport: Codable { let workspaceDir: String @@ -25,7 +25,9 @@ struct SkillStatus: Codable, Identifiable { let configChecks: [SkillStatusConfigCheck] let install: [SkillInstallOption] - var id: String { self.name } + var id: String { + self.name + } } struct SkillRequirements: Codable { @@ -45,7 +47,9 @@ struct SkillStatusConfigCheck: Codable, Identifiable { let value: AnyCodable? let satisfied: Bool - var id: String { self.path } + var id: String { + self.path + } } struct SkillInstallOption: Codable, Identifiable { diff --git a/apps/macos/Sources/OpenClaw/SkillsSettings.swift b/apps/macos/Sources/OpenClaw/SkillsSettings.swift index 83aaa66c55db4..02db8495112d4 100644 --- a/apps/macos/Sources/OpenClaw/SkillsSettings.swift +++ b/apps/macos/Sources/OpenClaw/SkillsSettings.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Observation +import OpenClawProtocol import SwiftUI struct SkillsSettings: View { @@ -142,7 +142,9 @@ private enum SkillsFilter: String, CaseIterable, Identifiable { case needsSetup case disabled - var id: String { self.rawValue } + var id: String { + self.rawValue + } var title: String { switch self { @@ -171,24 +173,16 @@ private struct SkillRow: View { let onInstall: (SkillInstallOption, InstallTarget) -> Void let onSetEnv: (String, Bool) -> Void - private var missingBins: [String] { self.skill.missing.bins } - private var missingEnv: [String] { self.skill.missing.env } - private var missingConfig: [String] { self.skill.missing.config } - - init( - skill: SkillStatus, - isBusy: Bool, - connectionMode: AppState.ConnectionMode, - onToggleEnabled: @escaping (Bool) -> Void, - onInstall: @escaping (SkillInstallOption, InstallTarget) -> Void, - onSetEnv: @escaping (String, Bool) -> Void) - { - self.skill = skill - self.isBusy = isBusy - self.connectionMode = connectionMode - self.onToggleEnabled = onToggleEnabled - self.onInstall = onInstall - self.onSetEnv = onSetEnv + private var missingBins: [String] { + self.skill.missing.bins + } + + private var missingEnv: [String] { + self.skill.missing.env + } + + private var missingConfig: [String] { + self.skill.missing.config } var body: some View { @@ -274,7 +268,6 @@ private struct SkillRow: View { set: { self.onToggleEnabled($0) }) } - @ViewBuilder private var missingSummary: some View { VStack(alignment: .leading, spacing: 4) { if self.shouldShowMissingBins { @@ -295,7 +288,6 @@ private struct SkillRow: View { } } - @ViewBuilder private var configChecksView: some View { VStack(alignment: .leading, spacing: 4) { ForEach(self.skill.configChecks) { check in @@ -326,7 +318,6 @@ private struct SkillRow: View { } } - @ViewBuilder private var trailingActions: some View { VStack(alignment: .trailing, spacing: 8) { if !self.installOptions.isEmpty { @@ -438,7 +429,9 @@ private struct EnvEditorState: Identifiable { let envKey: String let isPrimary: Bool - var id: String { "\(self.skillKey)::\(self.envKey)" } + var id: String { + "\(self.skillKey)::\(self.envKey)" + } } private struct EnvEditorView: View { diff --git a/apps/macos/Sources/OpenClaw/SoundEffects.swift b/apps/macos/Sources/OpenClaw/SoundEffects.swift index b321238295df9..37df8455f8f09 100644 --- a/apps/macos/Sources/OpenClaw/SoundEffects.swift +++ b/apps/macos/Sources/OpenClaw/SoundEffects.swift @@ -10,7 +10,9 @@ enum SoundEffectCatalog { return ["Glass"] + sorted } - static func displayName(for raw: String) -> String { raw } + static func displayName(for raw: String) -> String { + raw + } static func url(for name: String) -> URL? { self.discoveredSoundMap[name] diff --git a/apps/macos/Sources/OpenClaw/SystemPresenceInfo.swift b/apps/macos/Sources/OpenClaw/SystemPresenceInfo.swift new file mode 100644 index 0000000000000..843ed371fb55d --- /dev/null +++ b/apps/macos/Sources/OpenClaw/SystemPresenceInfo.swift @@ -0,0 +1,16 @@ +import CoreGraphics +import Foundation +import OpenClawKit + +enum SystemPresenceInfo { + static func lastInputSeconds() -> Int? { + let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null + let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent) + if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil } + return Int(seconds.rounded()) + } + + static func primaryIPv4Address() -> String? { + NetworkInterfaces.primaryIPv4Address() + } +} diff --git a/apps/macos/Sources/OpenClaw/SystemRunSettingsView.swift b/apps/macos/Sources/OpenClaw/SystemRunSettingsView.swift index eef826c3f0c71..b9bd6bd0c8cc5 100644 --- a/apps/macos/Sources/OpenClaw/SystemRunSettingsView.swift +++ b/apps/macos/Sources/OpenClaw/SystemRunSettingsView.swift @@ -150,7 +150,9 @@ private enum ExecApprovalsSettingsTab: String, CaseIterable, Identifiable { case policy case allowlist - var id: String { self.rawValue } + var id: String { + self.rawValue + } var title: String { switch self { diff --git a/apps/macos/Sources/OpenClaw/TailscaleIntegrationSection.swift b/apps/macos/Sources/OpenClaw/TailscaleIntegrationSection.swift index c1a3a3489a69d..c9354d38bc225 100644 --- a/apps/macos/Sources/OpenClaw/TailscaleIntegrationSection.swift +++ b/apps/macos/Sources/OpenClaw/TailscaleIntegrationSection.swift @@ -5,7 +5,9 @@ private enum GatewayTailscaleMode: String, CaseIterable, Identifiable { case serve case funnel - var id: String { self.rawValue } + var id: String { + self.rawValue + } var label: String { switch self { diff --git a/apps/macos/Sources/OpenClaw/TailscaleService.swift b/apps/macos/Sources/OpenClaw/TailscaleService.swift index b7f716a427047..2cefa69d59d40 100644 --- a/apps/macos/Sources/OpenClaw/TailscaleService.swift +++ b/apps/macos/Sources/OpenClaw/TailscaleService.swift @@ -1,10 +1,8 @@ import AppKit import Foundation import Observation +import OpenClawDiscovery import os -#if canImport(Darwin) -import Darwin -#endif /// Manages Tailscale integration and status checking. @Observable @@ -140,7 +138,7 @@ final class TailscaleService { self.logger.info("Tailscale API not responding; app likely not running") } - if self.tailscaleIP == nil, let fallback = Self.detectTailnetIPv4() { + if self.tailscaleIP == nil, let fallback = TailscaleNetwork.detectTailnetIPv4() { self.tailscaleIP = fallback if !self.isRunning { self.isRunning = true @@ -178,49 +176,7 @@ final class TailscaleService { } } - private nonisolated static func isTailnetIPv4(_ address: String) -> Bool { - let parts = address.split(separator: ".") - guard parts.count == 4 else { return false } - let octets = parts.compactMap { Int($0) } - guard octets.count == 4 else { return false } - let a = octets[0] - let b = octets[1] - return a == 100 && b >= 64 && b <= 127 - } - - private nonisolated static func detectTailnetIPv4() -> String? { - var addrList: UnsafeMutablePointer? - guard getifaddrs(&addrList) == 0, let first = addrList else { return nil } - defer { freeifaddrs(addrList) } - - for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) { - let flags = Int32(ptr.pointee.ifa_flags) - let isUp = (flags & IFF_UP) != 0 - let isLoopback = (flags & IFF_LOOPBACK) != 0 - let family = ptr.pointee.ifa_addr.pointee.sa_family - if !isUp || isLoopback || family != UInt8(AF_INET) { continue } - - var addr = ptr.pointee.ifa_addr.pointee - var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) - let result = getnameinfo( - &addr, - socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), - &buffer, - socklen_t(buffer.count), - nil, - 0, - NI_NUMERICHOST) - guard result == 0 else { continue } - let len = buffer.prefix { $0 != 0 } - let bytes = len.map { UInt8(bitPattern: $0) } - guard let ip = String(bytes: bytes, encoding: .utf8) else { continue } - if Self.isTailnetIPv4(ip) { return ip } - } - - return nil - } - nonisolated static func fallbackTailnetIPv4() -> String? { - self.detectTailnetIPv4() + TailscaleNetwork.detectTailnetIPv4() } } diff --git a/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift b/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift index 9ef7b010fa80f..47b041a5873e6 100644 --- a/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift +++ b/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift @@ -1,7 +1,7 @@ import AVFoundation +import Foundation import OpenClawChatUI import OpenClawKit -import Foundation import OSLog import Speech diff --git a/apps/macos/Sources/OpenClaw/TalkOverlayView.swift b/apps/macos/Sources/OpenClaw/TalkOverlayView.swift index a24ba17437481..80599d55ec338 100644 --- a/apps/macos/Sources/OpenClaw/TalkOverlayView.swift +++ b/apps/macos/Sources/OpenClaw/TalkOverlayView.swift @@ -99,8 +99,13 @@ private final class OrbInteractionNSView: NSView { private var didDrag = false private var suppressSingleClick = false - override var acceptsFirstResponder: Bool { true } - override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true } + override var acceptsFirstResponder: Bool { + true + } + + override func acceptsFirstMouse(for event: NSEvent?) -> Bool { + true + } override func mouseDown(with event: NSEvent) { self.mouseDownEvent = event diff --git a/apps/macos/Sources/OpenClaw/UsageData.swift b/apps/macos/Sources/OpenClaw/UsageData.swift index 7800054c66c73..3886c966edb1c 100644 --- a/apps/macos/Sources/OpenClaw/UsageData.swift +++ b/apps/macos/Sources/OpenClaw/UsageData.swift @@ -41,8 +41,7 @@ struct UsageRow: Identifiable { var remainingPercent: Int? { guard let usedPercent, usedPercent.isFinite else { return nil } - let remaining = max(0, min(100, Int(round(100 - usedPercent)))) - return remaining + return max(0, min(100, Int(round(100 - usedPercent)))) } func detailText(now: Date = .init()) -> String { diff --git a/apps/macos/Sources/OpenClaw/VoicePushToTalk.swift b/apps/macos/Sources/OpenClaw/VoicePushToTalk.swift index 819bafd127149..e535ebd6616f9 100644 --- a/apps/macos/Sources/OpenClaw/VoicePushToTalk.swift +++ b/apps/macos/Sources/OpenClaw/VoicePushToTalk.swift @@ -122,7 +122,7 @@ actor VoicePushToTalk { private var recognitionTask: SFSpeechRecognitionTask? private var tapInstalled = false - // Session token used to drop stale callbacks when a new capture starts. + /// Session token used to drop stale callbacks when a new capture starts. private var sessionID = UUID() private var committed: String = "" diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeChime.swift b/apps/macos/Sources/OpenClaw/VoiceWakeChime.swift index c41ecf4fd4358..8a25838997669 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeChime.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeChime.swift @@ -28,7 +28,9 @@ enum VoiceWakeChime: Codable, Equatable, Sendable { enum VoiceWakeChimeCatalog { /// Options shown in the picker. - static var systemOptions: [String] { SoundEffectCatalog.systemOptions } + static var systemOptions: [String] { + SoundEffectCatalog.systemOptions + } static func displayName(for raw: String) -> String { SoundEffectCatalog.displayName(for: raw) diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeGlobalSettingsSync.swift b/apps/macos/Sources/OpenClaw/VoiceWakeGlobalSettingsSync.swift index fd888c8aa4fdc..af4fae356ee12 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeGlobalSettingsSync.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeGlobalSettingsSync.swift @@ -1,5 +1,5 @@ -import OpenClawKit import Foundation +import OpenClawKit import OSLog @MainActor diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeOverlay.swift b/apps/macos/Sources/OpenClaw/VoiceWakeOverlay.swift index 7e5ffe76c1068..04bbfd69db021 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeOverlay.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeOverlay.swift @@ -18,7 +18,9 @@ final class VoiceWakeOverlayController { enum Source: String { case wakeWord, pushToTalk } var model = Model() - var isVisible: Bool { self.model.isVisible } + var isVisible: Bool { + self.model.isVisible + } struct Model { var text: String = "" diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeOverlayTextViews.swift b/apps/macos/Sources/OpenClaw/VoiceWakeOverlayTextViews.swift index 151db8c9324d5..8e88c86d45d17 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeOverlayTextViews.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeOverlayTextViews.swift @@ -11,7 +11,9 @@ struct TranscriptTextView: NSViewRepresentable { var onEndEditing: () -> Void var onSend: () -> Void - func makeCoordinator() -> Coordinator { Coordinator(self) } + func makeCoordinator() -> Coordinator { + Coordinator(self) + } func makeNSView(context: Context) -> NSScrollView { let textView = TranscriptNSTextView() @@ -77,7 +79,9 @@ struct TranscriptTextView: NSViewRepresentable { var parent: TranscriptTextView var isProgrammaticUpdate = false - init(_ parent: TranscriptTextView) { self.parent = parent } + init(_ parent: TranscriptTextView) { + self.parent = parent + } func textDidBeginEditing(_ notification: Notification) { self.parent.onBeginEditing() @@ -147,7 +151,9 @@ private final class ClickCatcher: NSView { } @available(*, unavailable) - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func mouseDown(with event: NSEvent) { super.mouseDown(with: event) diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeOverlayView.swift b/apps/macos/Sources/OpenClaw/VoiceWakeOverlayView.swift index 48055c10a6c37..516da776ace16 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeOverlayView.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeOverlayView.swift @@ -131,7 +131,9 @@ private struct OverlayBackground: View { } extension OverlayBackground: @MainActor Equatable { - static func == (lhs: Self, rhs: Self) -> Bool { true } + static func == (lhs: Self, rhs: Self) -> Bool { + true + } } struct CloseHoverButton: View { diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift b/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift index 7ef86c28507e1..61f913b9da889 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift @@ -48,10 +48,10 @@ actor VoiceWakeRuntime { private var isStarting: Bool = false private var triggerOnlyTask: Task? - // Tunables - // Silence threshold once we've captured user speech (post-trigger). + /// Tunables + /// Silence threshold once we've captured user speech (post-trigger). private let silenceWindow: TimeInterval = 2.0 - // Silence threshold when we only heard the trigger but no post-trigger speech yet. + /// Silence threshold when we only heard the trigger but no post-trigger speech yet. private let triggerOnlySilenceWindow: TimeInterval = 5.0 // Maximum capture duration from trigger until we force-send, to avoid runaway sessions. private let captureHardStop: TimeInterval = 120.0 diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift b/apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift index ca4f4a203553e..d4413618e11cb 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift @@ -29,7 +29,9 @@ struct VoiceWakeSettings: View { private struct AudioInputDevice: Identifiable, Equatable { let uid: String let name: String - var id: String { self.uid } + var id: String { + self.uid + } } private struct TriggerEntry: Identifiable { diff --git a/apps/macos/Sources/OpenClaw/WebChatManager.swift b/apps/macos/Sources/OpenClaw/WebChatManager.swift index 2f77692de820d..61d1b4d39b7b6 100644 --- a/apps/macos/Sources/OpenClaw/WebChatManager.swift +++ b/apps/macos/Sources/OpenClaw/WebChatManager.swift @@ -3,8 +3,13 @@ import Foundation /// A borderless panel that can still accept key focus (needed for typing). final class WebChatPanel: NSPanel { - override var canBecomeKey: Bool { true } - override var canBecomeMain: Bool { true } + override var canBecomeKey: Bool { + true + } + + override var canBecomeMain: Bool { + true + } } enum WebChatPresentation { diff --git a/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift b/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift index d6b4417f06af3..5b866304b090f 100644 --- a/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift +++ b/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift @@ -1,8 +1,8 @@ import AppKit +import Foundation import OpenClawChatUI import OpenClawKit import OpenClawProtocol -import Foundation import OSLog import QuartzCore import SwiftUI diff --git a/apps/macos/Sources/OpenClaw/WorkActivityStore.swift b/apps/macos/Sources/OpenClaw/WorkActivityStore.swift index b6fd97477fc24..77d6296303002 100644 --- a/apps/macos/Sources/OpenClaw/WorkActivityStore.swift +++ b/apps/macos/Sources/OpenClaw/WorkActivityStore.swift @@ -1,7 +1,7 @@ -import OpenClawKit -import OpenClawProtocol import Foundation import Observation +import OpenClawKit +import OpenClawProtocol import SwiftUI @MainActor @@ -31,7 +31,9 @@ final class WorkActivityStore { private var mainSessionKeyStorage = "main" private let toolResultGrace: TimeInterval = 2.0 - var mainSessionKey: String { self.mainSessionKeyStorage } + var mainSessionKey: String { + self.mainSessionKeyStorage + } func handleJob(sessionKey: String, state: String) { let isStart = state.lowercased() == "started" || state.lowercased() == "streaming" diff --git a/apps/macos/Sources/OpenClawDiscovery/GatewayDiscoveryModel.swift b/apps/macos/Sources/OpenClawDiscovery/GatewayDiscoveryModel.swift index 27548d90657f0..abd18efaa9a41 100644 --- a/apps/macos/Sources/OpenClawDiscovery/GatewayDiscoveryModel.swift +++ b/apps/macos/Sources/OpenClawDiscovery/GatewayDiscoveryModel.swift @@ -1,7 +1,7 @@ -import OpenClawKit import Foundation import Network import Observation +import OpenClawKit import OSLog @MainActor @@ -18,7 +18,10 @@ public final class GatewayDiscoveryModel { } public struct DiscoveredGateway: Identifiable, Equatable, Sendable { - public var id: String { self.stableID } + public var id: String { + self.stableID + } + public var displayName: String // Resolved service endpoint (SRV + A/AAAA). Used for routing; do not trust TXT for routing. public var serviceHost: String? @@ -326,43 +329,9 @@ public final class GatewayDiscoveryModel { } private func updateStatusText() { - let states = Array(self.statesByDomain.values) - if states.isEmpty { - self.statusText = self.browsers.isEmpty ? "Idle" : "Setup" - return - } - - if let failed = states.first(where: { state in - if case .failed = state { return true } - return false - }) { - if case let .failed(err) = failed { - self.statusText = "Failed: \(err)" - return - } - } - - if let waiting = states.first(where: { state in - if case .waiting = state { return true } - return false - }) { - if case let .waiting(err) = waiting { - self.statusText = "Waiting: \(err)" - return - } - } - - if states.contains(where: { if case .ready = $0 { true } else { false } }) { - self.statusText = "Searching…" - return - } - - if states.contains(where: { if case .setup = $0 { true } else { false } }) { - self.statusText = "Setup" - return - } - - self.statusText = "Searching…" + self.statusText = GatewayDiscoveryStatusText.make( + states: Array(self.statesByDomain.values), + hasBrowsers: !self.browsers.isEmpty) } private static func txtDictionary(from result: NWBrowser.Result) -> [String: String] { diff --git a/apps/macos/Sources/OpenClawDiscovery/TailscaleNetwork.swift b/apps/macos/Sources/OpenClawDiscovery/TailscaleNetwork.swift new file mode 100644 index 0000000000000..ef78e6f400ff1 --- /dev/null +++ b/apps/macos/Sources/OpenClawDiscovery/TailscaleNetwork.swift @@ -0,0 +1,46 @@ +import Darwin +import Foundation + +public enum TailscaleNetwork { + public static func isTailnetIPv4(_ address: String) -> Bool { + let parts = address.split(separator: ".") + guard parts.count == 4 else { return false } + let octets = parts.compactMap { Int($0) } + guard octets.count == 4 else { return false } + let a = octets[0] + let b = octets[1] + return a == 100 && b >= 64 && b <= 127 + } + + public static func detectTailnetIPv4() -> String? { + var addrList: UnsafeMutablePointer? + guard getifaddrs(&addrList) == 0, let first = addrList else { return nil } + defer { freeifaddrs(addrList) } + + for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) { + let flags = Int32(ptr.pointee.ifa_flags) + let isUp = (flags & IFF_UP) != 0 + let isLoopback = (flags & IFF_LOOPBACK) != 0 + let family = ptr.pointee.ifa_addr.pointee.sa_family + if !isUp || isLoopback || family != UInt8(AF_INET) { continue } + + var addr = ptr.pointee.ifa_addr.pointee + var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + let result = getnameinfo( + &addr, + socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), + &buffer, + socklen_t(buffer.count), + nil, + 0, + NI_NUMERICHOST) + guard result == 0 else { continue } + let len = buffer.prefix { $0 != 0 } + let bytes = len.map { UInt8(bitPattern: $0) } + guard let ip = String(bytes: bytes, encoding: .utf8) else { continue } + if self.isTailnetIPv4(ip) { return ip } + } + + return nil + } +} diff --git a/apps/macos/Sources/OpenClawDiscovery/WideAreaGatewayDiscovery.swift b/apps/macos/Sources/OpenClawDiscovery/WideAreaGatewayDiscovery.swift index bacff45d604cb..fea0aca91c15f 100644 --- a/apps/macos/Sources/OpenClawDiscovery/WideAreaGatewayDiscovery.swift +++ b/apps/macos/Sources/OpenClawDiscovery/WideAreaGatewayDiscovery.swift @@ -1,5 +1,5 @@ -import OpenClawKit import Foundation +import OpenClawKit struct WideAreaGatewayBeacon: Sendable, Equatable { var instanceName: String @@ -117,13 +117,12 @@ enum WideAreaGatewayDiscovery { } var seen = Set() - let ordered = ips.filter { value in + return ips.filter { value in guard self.isTailnetIPv4(value) else { return false } if seen.contains(value) { return false } seen.insert(value) return true } - return ordered } private static func readTailscaleStatus() -> String? { @@ -370,5 +369,7 @@ private struct TailscaleStatus: Decodable { } extension Collection { - fileprivate var nonEmpty: Self? { isEmpty ? nil : self } + fileprivate var nonEmpty: Self? { + isEmpty ? nil : self + } } diff --git a/apps/macos/Sources/OpenClawIPC/IPC.swift b/apps/macos/Sources/OpenClawIPC/IPC.swift index 9560699d47fcd..13fbe8756ab15 100644 --- a/apps/macos/Sources/OpenClawIPC/IPC.swift +++ b/apps/macos/Sources/OpenClawIPC/IPC.swift @@ -407,11 +407,10 @@ extension Request: Codable { } } -// Shared transport settings +/// Shared transport settings public let controlSocketPath: String = { let home = FileManager().homeDirectoryForCurrentUser - let preferred = home + return home .appendingPathComponent("Library/Application Support/OpenClaw/control.sock") .path - return preferred }() diff --git a/apps/macos/Sources/OpenClawMacCLI/ConnectCommand.swift b/apps/macos/Sources/OpenClawMacCLI/ConnectCommand.swift index 1c31ce3b05161..0989164a01e60 100644 --- a/apps/macos/Sources/OpenClawMacCLI/ConnectCommand.swift +++ b/apps/macos/Sources/OpenClawMacCLI/ConnectCommand.swift @@ -1,9 +1,7 @@ +import Foundation +import OpenClawDiscovery import OpenClawKit import OpenClawProtocol -import Foundation -#if canImport(Darwin) -import Darwin -#endif struct ConnectOptions { var url: String? @@ -301,7 +299,7 @@ private func resolvedPassword(opts: ConnectOptions, mode: String, config: Gatewa private func resolveLocalHost(bind: String?) -> String { let normalized = (bind ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - let tailnetIP = detectTailnetIPv4() + let tailnetIP = TailscaleNetwork.detectTailnetIPv4() switch normalized { case "tailnet": return tailnetIP ?? "127.0.0.1" @@ -309,45 +307,3 @@ private func resolveLocalHost(bind: String?) -> String { return "127.0.0.1" } } - -private func detectTailnetIPv4() -> String? { - var addrList: UnsafeMutablePointer? - guard getifaddrs(&addrList) == 0, let first = addrList else { return nil } - defer { freeifaddrs(addrList) } - - for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) { - let flags = Int32(ptr.pointee.ifa_flags) - let isUp = (flags & IFF_UP) != 0 - let isLoopback = (flags & IFF_LOOPBACK) != 0 - let family = ptr.pointee.ifa_addr.pointee.sa_family - if !isUp || isLoopback || family != UInt8(AF_INET) { continue } - - var addr = ptr.pointee.ifa_addr.pointee - var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) - let result = getnameinfo( - &addr, - socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), - &buffer, - socklen_t(buffer.count), - nil, - 0, - NI_NUMERICHOST) - guard result == 0 else { continue } - let len = buffer.prefix { $0 != 0 } - let bytes = len.map { UInt8(bitPattern: $0) } - guard let ip = String(bytes: bytes, encoding: .utf8) else { continue } - if isTailnetIPv4(ip) { return ip } - } - - return nil -} - -private func isTailnetIPv4(_ address: String) -> Bool { - let parts = address.split(separator: ".") - guard parts.count == 4 else { return false } - let octets = parts.compactMap { Int($0) } - guard octets.count == 4 else { return false } - let a = octets[0] - let b = octets[1] - return a == 100 && b >= 64 && b <= 127 -} diff --git a/apps/macos/Sources/OpenClawMacCLI/DiscoverCommand.swift b/apps/macos/Sources/OpenClawMacCLI/DiscoverCommand.swift index 09ef2bbc051b2..b039ecdf41159 100644 --- a/apps/macos/Sources/OpenClawMacCLI/DiscoverCommand.swift +++ b/apps/macos/Sources/OpenClawMacCLI/DiscoverCommand.swift @@ -1,5 +1,5 @@ -import OpenClawDiscovery import Foundation +import OpenClawDiscovery struct DiscoveryOptions { var timeoutMs: Int = 2000 diff --git a/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift b/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift index 898a8a31cfa3d..0a73fc2108c2b 100644 --- a/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift +++ b/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift @@ -1,7 +1,7 @@ -import OpenClawKit -import OpenClawProtocol import Darwin import Foundation +import OpenClawKit +import OpenClawProtocol struct WizardCliOptions { var url: String? diff --git a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift index a134b4fd5b4d2..661d5dc11fd0c 100644 --- a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift @@ -296,6 +296,7 @@ public struct Snapshot: Codable, Sendable { public let statedir: String? public let sessiondefaults: [String: AnyCodable]? public let authmode: AnyCodable? + public let updateavailable: [String: AnyCodable]? public init( presence: [PresenceEntry], @@ -305,7 +306,8 @@ public struct Snapshot: Codable, Sendable { configpath: String?, statedir: String?, sessiondefaults: [String: AnyCodable]?, - authmode: AnyCodable? + authmode: AnyCodable?, + updateavailable: [String: AnyCodable]? ) { self.presence = presence self.health = health @@ -315,6 +317,7 @@ public struct Snapshot: Codable, Sendable { self.statedir = statedir self.sessiondefaults = sessiondefaults self.authmode = authmode + self.updateavailable = updateavailable } private enum CodingKeys: String, CodingKey { case presence @@ -325,6 +328,7 @@ public struct Snapshot: Codable, Sendable { case statedir = "stateDir" case sessiondefaults = "sessionDefaults" case authmode = "authMode" + case updateavailable = "updateAvailable" } } @@ -394,6 +398,7 @@ public struct SendParams: Codable, Sendable { public let gifplayback: Bool? public let channel: String? public let accountid: String? + public let threadid: String? public let sessionkey: String? public let idempotencykey: String @@ -405,6 +410,7 @@ public struct SendParams: Codable, Sendable { gifplayback: Bool?, channel: String?, accountid: String?, + threadid: String?, sessionkey: String?, idempotencykey: String ) { @@ -415,6 +421,7 @@ public struct SendParams: Codable, Sendable { self.gifplayback = gifplayback self.channel = channel self.accountid = accountid + self.threadid = threadid self.sessionkey = sessionkey self.idempotencykey = idempotencykey } @@ -426,6 +433,7 @@ public struct SendParams: Codable, Sendable { case gifplayback = "gifPlayback" case channel case accountid = "accountId" + case threadid = "threadId" case sessionkey = "sessionKey" case idempotencykey = "idempotencyKey" } @@ -436,7 +444,11 @@ public struct PollParams: Codable, Sendable { public let question: String public let options: [String] public let maxselections: Int? + public let durationseconds: Int? public let durationhours: Int? + public let silent: Bool? + public let isanonymous: Bool? + public let threadid: String? public let channel: String? public let accountid: String? public let idempotencykey: String @@ -446,7 +458,11 @@ public struct PollParams: Codable, Sendable { question: String, options: [String], maxselections: Int?, + durationseconds: Int?, durationhours: Int?, + silent: Bool?, + isanonymous: Bool?, + threadid: String?, channel: String?, accountid: String?, idempotencykey: String @@ -455,7 +471,11 @@ public struct PollParams: Codable, Sendable { self.question = question self.options = options self.maxselections = maxselections + self.durationseconds = durationseconds self.durationhours = durationhours + self.silent = silent + self.isanonymous = isanonymous + self.threadid = threadid self.channel = channel self.accountid = accountid self.idempotencykey = idempotencykey @@ -465,7 +485,11 @@ public struct PollParams: Codable, Sendable { case question case options case maxselections = "maxSelections" + case durationseconds = "durationSeconds" case durationhours = "durationHours" + case silent + case isanonymous = "isAnonymous" + case threadid = "threadId" case channel case accountid = "accountId" case idempotencykey = "idempotencyKey" @@ -905,6 +929,68 @@ public struct NodeInvokeRequestEvent: Codable, Sendable { } } +public struct PushTestParams: Codable, Sendable { + public let nodeid: String + public let title: String? + public let body: String? + public let environment: String? + + public init( + nodeid: String, + title: String?, + body: String?, + environment: String? + ) { + self.nodeid = nodeid + self.title = title + self.body = body + self.environment = environment + } + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + case title + case body + case environment + } +} + +public struct PushTestResult: Codable, Sendable { + public let ok: Bool + public let status: Int + public let apnsid: String? + public let reason: String? + public let tokensuffix: String + public let topic: String + public let environment: String + + public init( + ok: Bool, + status: Int, + apnsid: String?, + reason: String?, + tokensuffix: String, + topic: String, + environment: String + ) { + self.ok = ok + self.status = status + self.apnsid = apnsid + self.reason = reason + self.tokensuffix = tokensuffix + self.topic = topic + self.environment = environment + } + private enum CodingKeys: String, CodingKey { + case ok + case status + case apnsid = "apnsId" + case reason + case tokensuffix = "tokenSuffix" + case topic + case environment + } +} + public struct SessionsListParams: Codable, Sendable { public let limit: Int? public let activeminutes: Int? @@ -1026,6 +1112,7 @@ public struct SessionsPatchParams: Codable, Sendable { public let execnode: AnyCodable? public let model: AnyCodable? public let spawnedby: AnyCodable? + public let spawndepth: AnyCodable? public let sendpolicy: AnyCodable? public let groupactivation: AnyCodable? @@ -1043,6 +1130,7 @@ public struct SessionsPatchParams: Codable, Sendable { execnode: AnyCodable?, model: AnyCodable?, spawnedby: AnyCodable?, + spawndepth: AnyCodable?, sendpolicy: AnyCodable?, groupactivation: AnyCodable? ) { @@ -1059,6 +1147,7 @@ public struct SessionsPatchParams: Codable, Sendable { self.execnode = execnode self.model = model self.spawnedby = spawnedby + self.spawndepth = spawndepth self.sendpolicy = sendpolicy self.groupactivation = groupactivation } @@ -1076,6 +1165,7 @@ public struct SessionsPatchParams: Codable, Sendable { case execnode = "execNode" case model case spawnedby = "spawnedBy" + case spawndepth = "spawnDepth" case sendpolicy = "sendPolicy" case groupactivation = "groupActivation" } @@ -1083,14 +1173,18 @@ public struct SessionsPatchParams: Codable, Sendable { public struct SessionsResetParams: Codable, Sendable { public let key: String + public let reason: AnyCodable? public init( - key: String + key: String, + reason: AnyCodable? ) { self.key = key + self.reason = reason } private enum CodingKeys: String, CodingKey { case key + case reason } } @@ -2060,6 +2154,7 @@ public struct SkillsUpdateParams: Codable, Sendable { public struct CronJob: Codable, Sendable { public let id: String public let agentid: String? + public let sessionkey: String? public let name: String public let description: String? public let enabled: Bool @@ -2070,12 +2165,13 @@ public struct CronJob: Codable, Sendable { public let sessiontarget: AnyCodable public let wakemode: AnyCodable public let payload: AnyCodable - public let delivery: [String: AnyCodable]? + public let delivery: AnyCodable? public let state: [String: AnyCodable] public init( id: String, agentid: String?, + sessionkey: String?, name: String, description: String?, enabled: Bool, @@ -2086,11 +2182,12 @@ public struct CronJob: Codable, Sendable { sessiontarget: AnyCodable, wakemode: AnyCodable, payload: AnyCodable, - delivery: [String: AnyCodable]?, + delivery: AnyCodable?, state: [String: AnyCodable] ) { self.id = id self.agentid = agentid + self.sessionkey = sessionkey self.name = name self.description = description self.enabled = enabled @@ -2107,6 +2204,7 @@ public struct CronJob: Codable, Sendable { private enum CodingKeys: String, CodingKey { case id case agentid = "agentId" + case sessionkey = "sessionKey" case name case description case enabled @@ -2141,6 +2239,7 @@ public struct CronStatusParams: Codable, Sendable { public struct CronAddParams: Codable, Sendable { public let name: String public let agentid: AnyCodable? + public let sessionkey: AnyCodable? public let description: String? public let enabled: Bool? public let deleteafterrun: Bool? @@ -2148,11 +2247,12 @@ public struct CronAddParams: Codable, Sendable { public let sessiontarget: AnyCodable public let wakemode: AnyCodable public let payload: AnyCodable - public let delivery: [String: AnyCodable]? + public let delivery: AnyCodable? public init( name: String, agentid: AnyCodable?, + sessionkey: AnyCodable?, description: String?, enabled: Bool?, deleteafterrun: Bool?, @@ -2160,10 +2260,11 @@ public struct CronAddParams: Codable, Sendable { sessiontarget: AnyCodable, wakemode: AnyCodable, payload: AnyCodable, - delivery: [String: AnyCodable]? + delivery: AnyCodable? ) { self.name = name self.agentid = agentid + self.sessionkey = sessionkey self.description = description self.enabled = enabled self.deleteafterrun = deleteafterrun @@ -2176,6 +2277,7 @@ public struct CronAddParams: Codable, Sendable { private enum CodingKeys: String, CodingKey { case name case agentid = "agentId" + case sessionkey = "sessionKey" case description case enabled case deleteafterrun = "deleteAfterRun" @@ -2472,6 +2574,19 @@ public struct DevicePairRejectParams: Codable, Sendable { } } +public struct DevicePairRemoveParams: Codable, Sendable { + public let deviceid: String + + public init( + deviceid: String + ) { + self.deviceid = deviceid + } + private enum CodingKeys: String, CodingKey { + case deviceid = "deviceId" + } +} + public struct DeviceTokenRotateParams: Codable, Sendable { public let deviceid: String public let role: String diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift index 272fd81c11dfe..fc7b399353db2 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift @@ -103,18 +103,22 @@ public final class OpenClawChatViewModel { let now = Date().timeIntervalSince1970 * 1000 let cutoff = now - (24 * 60 * 60 * 1000) let sorted = self.sessions.sorted { ($0.updatedAt ?? 0) > ($1.updatedAt ?? 0) } - var seen = Set() - var recent: [OpenClawChatSessionEntry] = [] - for entry in sorted { - guard !seen.contains(entry.key) else { continue } - seen.insert(entry.key) - guard (entry.updatedAt ?? 0) >= cutoff else { continue } - recent.append(entry) - } var result: [OpenClawChatSessionEntry] = [] var included = Set() - for entry in recent where !included.contains(entry.key) { + + // Always show the main session first, even if it hasn't been updated recently. + if let main = sorted.first(where: { $0.key == "main" }) { + result.append(main) + included.insert(main.key) + } else { + result.append(self.placeholderSession(key: "main")) + included.insert("main") + } + + for entry in sorted { + guard !included.contains(entry.key) else { continue } + guard (entry.updatedAt ?? 0) >= cutoff else { continue } result.append(entry) included.insert(entry.key) } @@ -166,7 +170,9 @@ public final class OpenClawChatViewModel { } let payload = try await self.transport.requestHistory(sessionKey: self.sessionKey) - self.messages = Self.decodeMessages(payload.messages ?? []) + self.messages = Self.reconcileMessageIDs( + previous: self.messages, + incoming: Self.decodeMessages(payload.messages ?? [])) self.sessionId = payload.sessionId if let level = payload.thinkingLevel, !level.isEmpty { self.thinkingLevel = level @@ -187,6 +193,70 @@ public final class OpenClawChatViewModel { return Self.dedupeMessages(decoded) } + private static func messageIdentityKey(for message: OpenClawChatMessage) -> String? { + let role = message.role.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + guard !role.isEmpty else { return nil } + + let timestamp: String = { + guard let value = message.timestamp, value.isFinite else { return "" } + return String(format: "%.3f", value) + }() + + let contentFingerprint = message.content.map { item in + let type = (item.type ?? "text").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let text = (item.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let id = (item.id ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let name = (item.name ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let fileName = (item.fileName ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + return [type, text, id, name, fileName].joined(separator: "\\u{001F}") + }.joined(separator: "\\u{001E}") + + let toolCallId = (message.toolCallId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let toolName = (message.toolName ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + if timestamp.isEmpty, contentFingerprint.isEmpty, toolCallId.isEmpty, toolName.isEmpty { + return nil + } + return [role, timestamp, toolCallId, toolName, contentFingerprint].joined(separator: "|") + } + + private static func reconcileMessageIDs( + previous: [OpenClawChatMessage], + incoming: [OpenClawChatMessage]) -> [OpenClawChatMessage] + { + guard !previous.isEmpty, !incoming.isEmpty else { return incoming } + + var idsByKey: [String: [UUID]] = [:] + for message in previous { + guard let key = Self.messageIdentityKey(for: message) else { continue } + idsByKey[key, default: []].append(message.id) + } + + return incoming.map { message in + guard let key = Self.messageIdentityKey(for: message), + var ids = idsByKey[key], + let reusedId = ids.first + else { + return message + } + ids.removeFirst() + if ids.isEmpty { + idsByKey.removeValue(forKey: key) + } else { + idsByKey[key] = ids + } + guard reusedId != message.id else { return message } + return OpenClawChatMessage( + id: reusedId, + role: message.role, + content: message.content, + timestamp: message.timestamp, + toolCallId: message.toolCallId, + toolName: message.toolName, + usage: message.usage, + stopReason: message.stopReason) + } + } + private static func dedupeMessages(_ messages: [OpenClawChatMessage]) -> [OpenClawChatMessage] { var result: [OpenClawChatMessage] = [] result.reserveCapacity(messages.count) @@ -371,11 +441,18 @@ public final class OpenClawChatViewModel { } private func handleChatEvent(_ chat: OpenClawChatEventPayload) { - if let sessionKey = chat.sessionKey, sessionKey != self.sessionKey { + let isOurRun = chat.runId.flatMap { self.pendingRuns.contains($0) } ?? false + + // Gateway may publish canonical session keys (for example "agent:main:main") + // even when this view currently uses an alias key (for example "main"). + // Never drop events for our own pending run on key mismatch, or the UI can stay + // stuck at "thinking" until the user reopens and forces a history reload. + if let sessionKey = chat.sessionKey, + !Self.matchesCurrentSessionKey(incoming: sessionKey, current: self.sessionKey), + !isOurRun + { return } - - let isOurRun = chat.runId.flatMap { self.pendingRuns.contains($0) } ?? false if !isOurRun { // Keep multiple clients in sync: if another client finishes a run for our session, refresh history. switch chat.state { @@ -407,6 +484,21 @@ public final class OpenClawChatViewModel { } } + private static func matchesCurrentSessionKey(incoming: String, current: String) -> Bool { + let incomingNormalized = incoming.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let currentNormalized = current.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + if incomingNormalized == currentNormalized { + return true + } + // Common alias pair in operator clients: UI uses "main" while gateway emits canonical. + if (incomingNormalized == "agent:main:main" && currentNormalized == "main") || + (incomingNormalized == "main" && currentNormalized == "agent:main:main") + { + return true + } + return false + } + private func handleAgentEvent(_ evt: OpenClawAgentEventPayload) { if let sessionId, evt.runId != sessionId { return @@ -440,7 +532,9 @@ public final class OpenClawChatViewModel { private func refreshHistoryAfterRun() async { do { let payload = try await self.transport.requestHistory(sessionKey: self.sessionKey) - self.messages = Self.decodeMessages(payload.messages ?? []) + self.messages = Self.reconcileMessageIDs( + previous: self.messages, + incoming: Self.decodeMessages(payload.messages ?? [])) self.sessionId = payload.sessionId if let level = payload.thinkingLevel, !level.isEmpty { self.thinkingLevel = level diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable.swift index ef522447f43c8..02b53e3c392f1 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable.swift @@ -1,93 +1,4 @@ -import Foundation +import OpenClawProtocol -/// Lightweight `Codable` wrapper that round-trips heterogeneous JSON payloads. -/// -/// Marked `@unchecked Sendable` because it can hold reference types. -public struct AnyCodable: Codable, @unchecked Sendable, Hashable { - public let value: Any +public typealias AnyCodable = OpenClawProtocol.AnyCodable - public init(_ value: Any) { self.value = value } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let intVal = try? container.decode(Int.self) { self.value = intVal; return } - if let doubleVal = try? container.decode(Double.self) { self.value = doubleVal; return } - if let boolVal = try? container.decode(Bool.self) { self.value = boolVal; return } - if let stringVal = try? container.decode(String.self) { self.value = stringVal; return } - if container.decodeNil() { self.value = NSNull(); return } - if let dict = try? container.decode([String: AnyCodable].self) { self.value = dict; return } - if let array = try? container.decode([AnyCodable].self) { self.value = array; return } - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type") - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self.value { - case let intVal as Int: try container.encode(intVal) - case let doubleVal as Double: try container.encode(doubleVal) - case let boolVal as Bool: try container.encode(boolVal) - case let stringVal as String: try container.encode(stringVal) - case is NSNull: try container.encodeNil() - case let dict as [String: AnyCodable]: try container.encode(dict) - case let array as [AnyCodable]: try container.encode(array) - case let dict as [String: Any]: - try container.encode(dict.mapValues { AnyCodable($0) }) - case let array as [Any]: - try container.encode(array.map { AnyCodable($0) }) - case let dict as NSDictionary: - var converted: [String: AnyCodable] = [:] - for (k, v) in dict { - guard let key = k as? String else { continue } - converted[key] = AnyCodable(v) - } - try container.encode(converted) - case let array as NSArray: - try container.encode(array.map { AnyCodable($0) }) - default: - let context = EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unsupported type") - throw EncodingError.invalidValue(self.value, context) - } - } - - public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { - switch (lhs.value, rhs.value) { - case let (l as Int, r as Int): l == r - case let (l as Double, r as Double): l == r - case let (l as Bool, r as Bool): l == r - case let (l as String, r as String): l == r - case (_ as NSNull, _ as NSNull): true - case let (l as [String: AnyCodable], r as [String: AnyCodable]): l == r - case let (l as [AnyCodable], r as [AnyCodable]): l == r - default: - false - } - } - - public func hash(into hasher: inout Hasher) { - switch self.value { - case let v as Int: - hasher.combine(0); hasher.combine(v) - case let v as Double: - hasher.combine(1); hasher.combine(v) - case let v as Bool: - hasher.combine(2); hasher.combine(v) - case let v as String: - hasher.combine(3); hasher.combine(v) - case _ as NSNull: - hasher.combine(4) - case let v as [String: AnyCodable]: - hasher.combine(5) - for (k, val) in v.sorted(by: { $0.key < $1.key }) { - hasher.combine(k) - hasher.combine(val) - } - case let v as [AnyCodable]: - hasher.combine(6) - for item in v { - hasher.combine(item) - } - default: - hasher.combine(999) - } - } -} diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/Capabilities.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/Capabilities.swift index d5c5e3c439cdf..49f9efe996bf8 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/Capabilities.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/Capabilities.swift @@ -7,6 +7,7 @@ public enum OpenClawCapability: String, Codable, Sendable { case voiceWake case location case device + case watch case photos case contacts case calendar diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift index 10dd7ea05368e..30606ca26712c 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift @@ -2,6 +2,56 @@ import Foundation public enum DeepLinkRoute: Sendable, Equatable { case agent(AgentDeepLink) + case gateway(GatewayConnectDeepLink) +} + +public struct GatewayConnectDeepLink: Codable, Sendable, Equatable { + public let host: String + public let port: Int + public let tls: Bool + public let token: String? + public let password: String? + + public init(host: String, port: Int, tls: Bool, token: String?, password: String?) { + self.host = host + self.port = port + self.tls = tls + self.token = token + self.password = password + } + + public var websocketURL: URL? { + let scheme = self.tls ? "wss" : "ws" + return URL(string: "\(scheme)://\(self.host):\(self.port)") + } + + /// Parse a device-pair setup code (base64url-encoded JSON: `{url, token?, password?}`). + public static func fromSetupCode(_ code: String) -> GatewayConnectDeepLink? { + guard let data = Self.decodeBase64Url(code) else { return nil } + guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil } + guard let urlString = json["url"] as? String, + let parsed = URLComponents(string: urlString), + let hostname = parsed.host, !hostname.isEmpty + else { return nil } + + let scheme = (parsed.scheme ?? "ws").lowercased() + let tls = scheme == "wss" + let port = parsed.port ?? (tls ? 443 : 18789) + let token = json["token"] as? String + let password = json["password"] as? String + return GatewayConnectDeepLink(host: hostname, port: port, tls: tls, token: token, password: password) + } + + private static func decodeBase64Url(_ input: String) -> Data? { + var base64 = input + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + let remainder = base64.count % 4 + if remainder > 0 { + base64.append(contentsOf: String(repeating: "=", count: 4 - remainder)) + } + return Data(base64Encoded: base64) + } } public struct AgentDeepLink: Codable, Sendable, Equatable { @@ -69,6 +119,23 @@ public enum DeepLinkParser { channel: query["channel"], timeoutSeconds: timeoutSeconds, key: query["key"])) + + case "gateway": + guard let hostParam = query["host"], + !hostParam.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + else { + return nil + } + let port = query["port"].flatMap { Int($0) } ?? 18789 + let tls = (query["tls"] as NSString?)?.boolValue ?? false + return .gateway( + .init( + host: hostParam, + port: port, + tls: tls, + token: query["token"], + password: query["password"])) + default: return nil } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift index a255fc7a81daa..9682a31aa4650 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift @@ -133,10 +133,16 @@ public actor GatewayChannelActor { private var lastAuthSource: GatewayAuthSource = .none private let decoder = JSONDecoder() private let encoder = JSONEncoder() - private let connectTimeoutSeconds: Double = 6 - private let connectChallengeTimeoutSeconds: Double = 3.0 + // Remote gateways (tailscale/wan) can take a bit longer to deliver the connect.challenge event, + // and we must include the nonce once the gateway requires v2 signing. + private let connectTimeoutSeconds: Double = 12 + private let connectChallengeTimeoutSeconds: Double = 6.0 + // Some networks will silently drop idle TCP/TLS flows around ~30s. The gateway tick is server->client, + // but NATs/proxies often require outbound traffic to keep the connection alive. + private let keepaliveIntervalSeconds: Double = 15.0 private var watchdogTask: Task? private var tickTask: Task? + private var keepaliveTask: Task? private let defaultRequestTimeoutMs: Double = 15000 private let pushHandler: (@Sendable (GatewayPush) async -> Void)? private let connectOptions: GatewayConnectOptions? @@ -175,6 +181,9 @@ public actor GatewayChannelActor { self.tickTask?.cancel() self.tickTask = nil + self.keepaliveTask?.cancel() + self.keepaliveTask = nil + self.task?.cancel(with: .goingAway, reason: nil) self.task = nil @@ -257,6 +266,7 @@ public actor GatewayChannelActor { self.connected = true self.backoffMs = 500 self.lastSeq = nil + self.startKeepalive() let waiters = self.connectWaiters self.connectWaiters.removeAll() @@ -265,6 +275,29 @@ public actor GatewayChannelActor { } } + private func startKeepalive() { + self.keepaliveTask?.cancel() + self.keepaliveTask = Task { [weak self] in + guard let self else { return } + await self.keepaliveLoop() + } + } + + private func keepaliveLoop() async { + while self.shouldReconnect { + try? await Task.sleep(nanoseconds: UInt64(self.keepaliveIntervalSeconds * 1_000_000_000)) + guard self.shouldReconnect else { return } + guard self.connected else { continue } + // Best-effort outbound message to keep intermediate NAT/proxy state alive. + // We intentionally ignore the response. + do { + try await self.send(method: "health", params: nil) + } catch { + // Avoid spamming logs; the reconnect paths will surface meaningful errors. + } + } + } + private func sendConnect() async throws { let platform = InstanceIdentity.platformString let primaryLocale = Locale.preferredLanguages.first ?? Locale.current.identifier @@ -458,6 +491,8 @@ public actor GatewayChannelActor { let wrapped = self.wrap(err, context: "gateway receive") self.logger.error("gateway ws receive failed \(wrapped.localizedDescription, privacy: .public)") self.connected = false + self.keepaliveTask?.cancel() + self.keepaliveTask = nil await self.disconnectHandler?("receive failed: \(wrapped.localizedDescription)") await self.failPending(wrapped) await self.scheduleReconnect() diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayDiscoveryStatusText.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayDiscoveryStatusText.swift new file mode 100644 index 0000000000000..e15baf17fdb1a --- /dev/null +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayDiscoveryStatusText.swift @@ -0,0 +1,39 @@ +import Foundation +import Network + +public enum GatewayDiscoveryStatusText { + public static func make(states: [NWBrowser.State], hasBrowsers: Bool) -> String { + if states.isEmpty { + return hasBrowsers ? "Setup" : "Idle" + } + + if let failed = states.first(where: { state in + if case .failed = state { return true } + return false + }) { + if case let .failed(err) = failed { + return "Failed: \(err)" + } + } + + if let waiting = states.first(where: { state in + if case .waiting = state { return true } + return false + }) { + if case let .waiting(err) = waiting { + return "Waiting: \(err)" + } + } + + if states.contains(where: { if case .ready = $0 { true } else { false } }) { + return "Searching…" + } + + if states.contains(where: { if case .setup = $0 { true } else { false } }) { + return "Setup" + } + + return "Searching…" + } +} + diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift index 6311b4632cba7..d0303f7e9977b 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift @@ -85,7 +85,13 @@ public actor GatewayNodeSession { latch.resume(result) } timeoutTask = Task.detached { - try? await Task.sleep(nanoseconds: UInt64(timeout) * 1_000_000) + do { + try await Task.sleep(nanoseconds: UInt64(timeout) * 1_000_000) + } catch { + // Expected when invoke finishes first and cancels the timeout task. + return + } + guard !Task.isCancelled else { return } timeoutLogger.info("node invoke timeout fired id=\(request.id, privacy: .public)") latch.resume(BridgeInvokeResponse( id: request.id, diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift index 8672ab09f681f..139aa7d2942a8 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift @@ -2,14 +2,6 @@ import OpenClawProtocol import Foundation public enum GatewayPayloadDecoding { - public static func decode( - _ payload: OpenClawProtocol.AnyCodable, - as _: T.Type = T.self) throws -> T - { - let data = try JSONEncoder().encode(payload) - return try JSONDecoder().decode(T.self, from: data) - } - public static func decode( _ payload: AnyCodable, as _: T.Type = T.self) throws -> T @@ -18,14 +10,6 @@ public enum GatewayPayloadDecoding { return try JSONDecoder().decode(T.self, from: data) } - public static func decodeIfPresent( - _ payload: OpenClawProtocol.AnyCodable?, - as _: T.Type = T.self) throws -> T? - { - guard let payload else { return nil } - return try self.decode(payload, as: T.self) - } - public static func decodeIfPresent( _ payload: AnyCodable?, as _: T.Type = T.self) throws -> T? diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/NetworkInterfaces.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/NetworkInterfaces.swift new file mode 100644 index 0000000000000..3679ef5423444 --- /dev/null +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/NetworkInterfaces.swift @@ -0,0 +1,43 @@ +import Darwin +import Foundation + +public enum NetworkInterfaces { + public static func primaryIPv4Address() -> String? { + var addrList: UnsafeMutablePointer? + guard getifaddrs(&addrList) == 0, let first = addrList else { return nil } + defer { freeifaddrs(addrList) } + + var fallback: String? + var en0: String? + + for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) { + let flags = Int32(ptr.pointee.ifa_flags) + let isUp = (flags & IFF_UP) != 0 + let isLoopback = (flags & IFF_LOOPBACK) != 0 + let name = String(cString: ptr.pointee.ifa_name) + let family = ptr.pointee.ifa_addr.pointee.sa_family + if !isUp || isLoopback || family != UInt8(AF_INET) { continue } + + var addr = ptr.pointee.ifa_addr.pointee + var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + let result = getnameinfo( + &addr, + socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), + &buffer, + socklen_t(buffer.count), + nil, + 0, + NI_NUMERICHOST) + guard result == 0 else { continue } + let len = buffer.prefix { $0 != 0 } + let bytes = len.map { UInt8(bitPattern: $0) } + guard let ip = String(bytes: bytes, encoding: .utf8) else { continue } + + if name == "en0" { en0 = ip; break } + if fallback == nil { fallback = ip } + } + + return en0 ?? fallback + } +} + diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/OpenClawKitResources.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/OpenClawKitResources.swift index b19792ad7b813..5af33d1d35c28 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/OpenClawKitResources.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/OpenClawKitResources.swift @@ -52,18 +52,26 @@ public enum OpenClawKitResources { for candidate in candidates { guard let baseURL = candidate else { continue } - // Direct path - let directURL = baseURL.appendingPathComponent("\(bundleName).bundle") - if let bundle = Bundle(url: directURL) { - return bundle + // SwiftPM often places the resource bundle next to (or near) the test runner bundle, + // not inside it. Walk up a few levels and check common container paths. + var roots: [URL] = [] + roots.append(baseURL) + roots.append(baseURL.appendingPathComponent("Resources")) + roots.append(baseURL.appendingPathComponent("Contents/Resources")) + + var current = baseURL + for _ in 0 ..< 5 { + current = current.deletingLastPathComponent() + roots.append(current) + roots.append(current.appendingPathComponent("Resources")) + roots.append(current.appendingPathComponent("Contents/Resources")) } - // Inside Resources/ - let resourcesURL = baseURL - .appendingPathComponent("Resources") - .appendingPathComponent("\(bundleName).bundle") - if let bundle = Bundle(url: resourcesURL) { - return bundle + for root in roots { + let bundleURL = root.appendingPathComponent("\(bundleName).bundle") + if let bundle = Bundle(url: bundleURL) { + return bundle + } } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/PhotoCapture.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/PhotoCapture.swift new file mode 100644 index 0000000000000..b5f00d34751e1 --- /dev/null +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/PhotoCapture.swift @@ -0,0 +1,19 @@ +import Foundation + +public enum PhotoCapture { + public static func transcodeJPEGForGateway( + rawData: Data, + maxWidthPx: Int, + quality: Double, + maxPayloadBytes: Int = 5 * 1024 * 1024 + ) throws -> (data: Data, widthPx: Int, heightPx: Int) { + // Base64 inflates payloads by ~4/3; cap encoded bytes so the payload stays under maxPayloadBytes (API limit). + let maxEncodedBytes = (maxPayloadBytes / 4) * 3 + return try JPEGTranscoder.transcodeToJPEG( + imageData: rawData, + maxWidthPx: maxWidthPx, + quality: quality, + maxBytes: maxEncodedBytes) + } +} + diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareGatewayRelaySettings.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareGatewayRelaySettings.swift new file mode 100644 index 0000000000000..7b4c3864b37e8 --- /dev/null +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareGatewayRelaySettings.swift @@ -0,0 +1,62 @@ +import Foundation + +public struct ShareGatewayRelayConfig: Codable, Sendable, Equatable { + public let gatewayURLString: String + public let token: String? + public let password: String? + public let sessionKey: String + public let deliveryChannel: String? + public let deliveryTo: String? + + public init( + gatewayURLString: String, + token: String?, + password: String?, + sessionKey: String, + deliveryChannel: String? = nil, + deliveryTo: String? = nil) + { + self.gatewayURLString = gatewayURLString + self.token = token + self.password = password + self.sessionKey = sessionKey + self.deliveryChannel = deliveryChannel + self.deliveryTo = deliveryTo + } +} + +public enum ShareGatewayRelaySettings { + private static let suiteName = "group.ai.openclaw.shared" + private static let relayConfigKey = "share.gatewayRelay.config.v1" + private static let lastEventKey = "share.gatewayRelay.event.v1" + + private static var defaults: UserDefaults { + UserDefaults(suiteName: self.suiteName) ?? .standard + } + + public static func loadConfig() -> ShareGatewayRelayConfig? { + guard let data = self.defaults.data(forKey: self.relayConfigKey) else { return nil } + return try? JSONDecoder().decode(ShareGatewayRelayConfig.self, from: data) + } + + public static func saveConfig(_ config: ShareGatewayRelayConfig) { + guard let data = try? JSONEncoder().encode(config) else { return } + self.defaults.set(data, forKey: self.relayConfigKey) + } + + public static func clearConfig() { + self.defaults.removeObject(forKey: self.relayConfigKey) + } + + public static func saveLastEvent(_ message: String) { + let timestamp = ISO8601DateFormatter().string(from: Date()) + let payload = "[\(timestamp)] \(message)" + self.defaults.set(payload, forKey: self.lastEventKey) + } + + public static func loadLastEvent() -> String? { + let value = self.defaults.string(forKey: self.lastEventKey)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return value.isEmpty ? nil : value + } +} diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareToAgentDeepLink.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareToAgentDeepLink.swift new file mode 100644 index 0000000000000..08f0623433460 --- /dev/null +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareToAgentDeepLink.swift @@ -0,0 +1,62 @@ +import Foundation + +public struct SharedContentPayload: Sendable, Equatable { + public let title: String? + public let url: URL? + public let text: String? + + public init(title: String?, url: URL?, text: String?) { + self.title = title + self.url = url + self.text = text + } +} + +public enum ShareToAgentDeepLink { + public static func buildURL(from payload: SharedContentPayload, instruction: String? = nil) -> URL? { + let message = self.buildMessage(from: payload, instruction: instruction) + guard !message.isEmpty else { return nil } + + var components = URLComponents() + components.scheme = "openclaw" + components.host = "agent" + components.queryItems = [ + URLQueryItem(name: "message", value: message), + URLQueryItem(name: "thinking", value: "low"), + ] + return components.url + } + + public static func buildMessage(from payload: SharedContentPayload, instruction: String? = nil) -> String { + let title = self.clean(payload.title) + let text = self.clean(payload.text) + let urlText = payload.url?.absoluteString.trimmingCharacters(in: .whitespacesAndNewlines) + let resolvedInstruction = self.clean(instruction) ?? ShareToAgentSettings.loadDefaultInstruction() + + var lines: [String] = ["Shared from iOS."] + if let title, !title.isEmpty { + lines.append("Title: \(title)") + } + if let urlText, !urlText.isEmpty { + lines.append("URL: \(urlText)") + } + if let text, !text.isEmpty { + lines.append("Text:\n\(text)") + } + lines.append(resolvedInstruction) + + let message = lines.joined(separator: "\n\n") + return self.limit(message, maxCharacters: 2400) + } + + private static func clean(_ value: String?) -> String? { + guard let value else { return nil } + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } + + private static func limit(_ value: String, maxCharacters: Int) -> String { + guard value.count > maxCharacters else { return value } + return String(value.prefix(maxCharacters)) + } +} diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareToAgentSettings.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareToAgentSettings.swift new file mode 100644 index 0000000000000..9034dcfe1b667 --- /dev/null +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareToAgentSettings.swift @@ -0,0 +1,29 @@ +import Foundation + +public enum ShareToAgentSettings { + private static let suiteName = "group.ai.openclaw.shared" + private static let defaultInstructionKey = "share.defaultInstruction" + private static let fallbackInstruction = "Please help me with this." + + private static var defaults: UserDefaults { + UserDefaults(suiteName: suiteName) ?? .standard + } + + public static func loadDefaultInstruction() -> String { + let raw = self.defaults.string(forKey: self.defaultInstructionKey)? + .trimmingCharacters(in: .whitespacesAndNewlines) + if let raw, !raw.isEmpty { + return raw + } + return self.fallbackInstruction + } + + public static func saveDefaultInstruction(_ value: String?) { + let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if trimmed.isEmpty { + self.defaults.removeObject(forKey: self.defaultInstructionKey) + return + } + self.defaults.set(trimmed, forKey: self.defaultInstructionKey) + } +} diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPromptBuilder.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPromptBuilder.swift index c63f40e9d3a7d..2a2e39d68cf69 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPromptBuilder.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPromptBuilder.swift @@ -1,10 +1,19 @@ public enum TalkPromptBuilder: Sendable { - public static func build(transcript: String, interruptedAtSeconds: Double?) -> String { + public static func build( + transcript: String, + interruptedAtSeconds: Double?, + includeVoiceDirectiveHint: Bool = true + ) -> String { var lines: [String] = [ "Talk Mode active. Reply in a concise, spoken tone.", - "You may optionally prefix the response with JSON (first line) to set ElevenLabs voice (id or alias), e.g. {\"voice\":\"\",\"once\":true}.", ] + if includeVoiceDirectiveHint { + lines.append( + "You may optionally prefix the response with JSON (first line) to set ElevenLabs voice (id or alias), e.g. {\"voice\":\"\",\"once\":true}." + ) + } + if let interruptedAtSeconds { let formatted = String(format: "%.1f", interruptedAtSeconds) lines.append("Assistant speech interrupted at \(formatted)s.") diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/WatchCommands.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/WatchCommands.swift new file mode 100644 index 0000000000000..814efe68a886a --- /dev/null +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/WatchCommands.swift @@ -0,0 +1,52 @@ +import Foundation + +public enum OpenClawWatchCommand: String, Codable, Sendable { + case status = "watch.status" + case notify = "watch.notify" +} + +public struct OpenClawWatchStatusPayload: Codable, Sendable, Equatable { + public var supported: Bool + public var paired: Bool + public var appInstalled: Bool + public var reachable: Bool + public var activationState: String + + public init( + supported: Bool, + paired: Bool, + appInstalled: Bool, + reachable: Bool, + activationState: String) + { + self.supported = supported + self.paired = paired + self.appInstalled = appInstalled + self.reachable = reachable + self.activationState = activationState + } +} + +public struct OpenClawWatchNotifyParams: Codable, Sendable, Equatable { + public var title: String + public var body: String + public var priority: OpenClawNotificationPriority? + + public init(title: String, body: String, priority: OpenClawNotificationPriority? = nil) { + self.title = title + self.body = body + self.priority = priority + } +} + +public struct OpenClawWatchNotifyPayload: Codable, Sendable, Equatable { + public var deliveredImmediately: Bool + public var queuedForDelivery: Bool + public var transport: String + + public init(deliveredImmediately: Bool, queuedForDelivery: Bool, transport: String) { + self.deliveredImmediately = deliveredImmediately + self.queuedForDelivery = queuedForDelivery + self.transport = transport + } +} diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/AnyCodable.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/AnyCodable.swift index ad0c338729677..4315bb073efca 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/AnyCodable.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/AnyCodable.swift @@ -1,33 +1,34 @@ import Foundation /// Lightweight `Codable` wrapper that round-trips heterogeneous JSON payloads. +/// /// Marked `@unchecked Sendable` because it can hold reference types. -public struct AnyCodable: Codable, @unchecked Sendable { +public struct AnyCodable: Codable, @unchecked Sendable, Hashable { public let value: Any - public init(_ value: Any) { self.value = value } + public init(_ value: Any) { self.value = Self.normalize(value) } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() + if let boolVal = try? container.decode(Bool.self) { self.value = boolVal; return } if let intVal = try? container.decode(Int.self) { self.value = intVal; return } if let doubleVal = try? container.decode(Double.self) { self.value = doubleVal; return } - if let boolVal = try? container.decode(Bool.self) { self.value = boolVal; return } if let stringVal = try? container.decode(String.self) { self.value = stringVal; return } if container.decodeNil() { self.value = NSNull(); return } if let dict = try? container.decode([String: AnyCodable].self) { self.value = dict; return } if let array = try? container.decode([AnyCodable].self) { self.value = array; return } - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Unsupported type") + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type") } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self.value { + case let boolVal as Bool: try container.encode(boolVal) case let intVal as Int: try container.encode(intVal) case let doubleVal as Double: try container.encode(doubleVal) - case let boolVal as Bool: try container.encode(boolVal) case let stringVal as String: try container.encode(stringVal) + case let number as NSNumber where CFGetTypeID(number) == CFBooleanGetTypeID(): + try container.encode(number.boolValue) case is NSNull: try container.encodeNil() case let dict as [String: AnyCodable]: try container.encode(dict) case let array as [AnyCodable]: try container.encode(array) @@ -51,4 +52,53 @@ public struct AnyCodable: Codable, @unchecked Sendable { throw EncodingError.invalidValue(self.value, context) } } + + private static func normalize(_ value: Any) -> Any { + if let number = value as? NSNumber, CFGetTypeID(number) == CFBooleanGetTypeID() { + return number.boolValue + } + return value + } + + public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { + switch (lhs.value, rhs.value) { + case let (l as Bool, r as Bool): l == r + case let (l as Int, r as Int): l == r + case let (l as Double, r as Double): l == r + case let (l as String, r as String): l == r + case (_ as NSNull, _ as NSNull): true + case let (l as [String: AnyCodable], r as [String: AnyCodable]): l == r + case let (l as [AnyCodable], r as [AnyCodable]): l == r + default: + false + } + } + + public func hash(into hasher: inout Hasher) { + switch self.value { + case let v as Bool: + hasher.combine(2); hasher.combine(v) + case let v as Int: + hasher.combine(0); hasher.combine(v) + case let v as Double: + hasher.combine(1); hasher.combine(v) + case let v as String: + hasher.combine(3); hasher.combine(v) + case _ as NSNull: + hasher.combine(4) + case let v as [String: AnyCodable]: + hasher.combine(5) + for (k, val) in v.sorted(by: { $0.key < $1.key }) { + hasher.combine(k) + hasher.combine(val) + } + case let v as [AnyCodable]: + hasher.combine(6) + for item in v { + hasher.combine(item) + } + default: + hasher.combine(999) + } + } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index a134b4fd5b4d2..661d5dc11fd0c 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -296,6 +296,7 @@ public struct Snapshot: Codable, Sendable { public let statedir: String? public let sessiondefaults: [String: AnyCodable]? public let authmode: AnyCodable? + public let updateavailable: [String: AnyCodable]? public init( presence: [PresenceEntry], @@ -305,7 +306,8 @@ public struct Snapshot: Codable, Sendable { configpath: String?, statedir: String?, sessiondefaults: [String: AnyCodable]?, - authmode: AnyCodable? + authmode: AnyCodable?, + updateavailable: [String: AnyCodable]? ) { self.presence = presence self.health = health @@ -315,6 +317,7 @@ public struct Snapshot: Codable, Sendable { self.statedir = statedir self.sessiondefaults = sessiondefaults self.authmode = authmode + self.updateavailable = updateavailable } private enum CodingKeys: String, CodingKey { case presence @@ -325,6 +328,7 @@ public struct Snapshot: Codable, Sendable { case statedir = "stateDir" case sessiondefaults = "sessionDefaults" case authmode = "authMode" + case updateavailable = "updateAvailable" } } @@ -394,6 +398,7 @@ public struct SendParams: Codable, Sendable { public let gifplayback: Bool? public let channel: String? public let accountid: String? + public let threadid: String? public let sessionkey: String? public let idempotencykey: String @@ -405,6 +410,7 @@ public struct SendParams: Codable, Sendable { gifplayback: Bool?, channel: String?, accountid: String?, + threadid: String?, sessionkey: String?, idempotencykey: String ) { @@ -415,6 +421,7 @@ public struct SendParams: Codable, Sendable { self.gifplayback = gifplayback self.channel = channel self.accountid = accountid + self.threadid = threadid self.sessionkey = sessionkey self.idempotencykey = idempotencykey } @@ -426,6 +433,7 @@ public struct SendParams: Codable, Sendable { case gifplayback = "gifPlayback" case channel case accountid = "accountId" + case threadid = "threadId" case sessionkey = "sessionKey" case idempotencykey = "idempotencyKey" } @@ -436,7 +444,11 @@ public struct PollParams: Codable, Sendable { public let question: String public let options: [String] public let maxselections: Int? + public let durationseconds: Int? public let durationhours: Int? + public let silent: Bool? + public let isanonymous: Bool? + public let threadid: String? public let channel: String? public let accountid: String? public let idempotencykey: String @@ -446,7 +458,11 @@ public struct PollParams: Codable, Sendable { question: String, options: [String], maxselections: Int?, + durationseconds: Int?, durationhours: Int?, + silent: Bool?, + isanonymous: Bool?, + threadid: String?, channel: String?, accountid: String?, idempotencykey: String @@ -455,7 +471,11 @@ public struct PollParams: Codable, Sendable { self.question = question self.options = options self.maxselections = maxselections + self.durationseconds = durationseconds self.durationhours = durationhours + self.silent = silent + self.isanonymous = isanonymous + self.threadid = threadid self.channel = channel self.accountid = accountid self.idempotencykey = idempotencykey @@ -465,7 +485,11 @@ public struct PollParams: Codable, Sendable { case question case options case maxselections = "maxSelections" + case durationseconds = "durationSeconds" case durationhours = "durationHours" + case silent + case isanonymous = "isAnonymous" + case threadid = "threadId" case channel case accountid = "accountId" case idempotencykey = "idempotencyKey" @@ -905,6 +929,68 @@ public struct NodeInvokeRequestEvent: Codable, Sendable { } } +public struct PushTestParams: Codable, Sendable { + public let nodeid: String + public let title: String? + public let body: String? + public let environment: String? + + public init( + nodeid: String, + title: String?, + body: String?, + environment: String? + ) { + self.nodeid = nodeid + self.title = title + self.body = body + self.environment = environment + } + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + case title + case body + case environment + } +} + +public struct PushTestResult: Codable, Sendable { + public let ok: Bool + public let status: Int + public let apnsid: String? + public let reason: String? + public let tokensuffix: String + public let topic: String + public let environment: String + + public init( + ok: Bool, + status: Int, + apnsid: String?, + reason: String?, + tokensuffix: String, + topic: String, + environment: String + ) { + self.ok = ok + self.status = status + self.apnsid = apnsid + self.reason = reason + self.tokensuffix = tokensuffix + self.topic = topic + self.environment = environment + } + private enum CodingKeys: String, CodingKey { + case ok + case status + case apnsid = "apnsId" + case reason + case tokensuffix = "tokenSuffix" + case topic + case environment + } +} + public struct SessionsListParams: Codable, Sendable { public let limit: Int? public let activeminutes: Int? @@ -1026,6 +1112,7 @@ public struct SessionsPatchParams: Codable, Sendable { public let execnode: AnyCodable? public let model: AnyCodable? public let spawnedby: AnyCodable? + public let spawndepth: AnyCodable? public let sendpolicy: AnyCodable? public let groupactivation: AnyCodable? @@ -1043,6 +1130,7 @@ public struct SessionsPatchParams: Codable, Sendable { execnode: AnyCodable?, model: AnyCodable?, spawnedby: AnyCodable?, + spawndepth: AnyCodable?, sendpolicy: AnyCodable?, groupactivation: AnyCodable? ) { @@ -1059,6 +1147,7 @@ public struct SessionsPatchParams: Codable, Sendable { self.execnode = execnode self.model = model self.spawnedby = spawnedby + self.spawndepth = spawndepth self.sendpolicy = sendpolicy self.groupactivation = groupactivation } @@ -1076,6 +1165,7 @@ public struct SessionsPatchParams: Codable, Sendable { case execnode = "execNode" case model case spawnedby = "spawnedBy" + case spawndepth = "spawnDepth" case sendpolicy = "sendPolicy" case groupactivation = "groupActivation" } @@ -1083,14 +1173,18 @@ public struct SessionsPatchParams: Codable, Sendable { public struct SessionsResetParams: Codable, Sendable { public let key: String + public let reason: AnyCodable? public init( - key: String + key: String, + reason: AnyCodable? ) { self.key = key + self.reason = reason } private enum CodingKeys: String, CodingKey { case key + case reason } } @@ -2060,6 +2154,7 @@ public struct SkillsUpdateParams: Codable, Sendable { public struct CronJob: Codable, Sendable { public let id: String public let agentid: String? + public let sessionkey: String? public let name: String public let description: String? public let enabled: Bool @@ -2070,12 +2165,13 @@ public struct CronJob: Codable, Sendable { public let sessiontarget: AnyCodable public let wakemode: AnyCodable public let payload: AnyCodable - public let delivery: [String: AnyCodable]? + public let delivery: AnyCodable? public let state: [String: AnyCodable] public init( id: String, agentid: String?, + sessionkey: String?, name: String, description: String?, enabled: Bool, @@ -2086,11 +2182,12 @@ public struct CronJob: Codable, Sendable { sessiontarget: AnyCodable, wakemode: AnyCodable, payload: AnyCodable, - delivery: [String: AnyCodable]?, + delivery: AnyCodable?, state: [String: AnyCodable] ) { self.id = id self.agentid = agentid + self.sessionkey = sessionkey self.name = name self.description = description self.enabled = enabled @@ -2107,6 +2204,7 @@ public struct CronJob: Codable, Sendable { private enum CodingKeys: String, CodingKey { case id case agentid = "agentId" + case sessionkey = "sessionKey" case name case description case enabled @@ -2141,6 +2239,7 @@ public struct CronStatusParams: Codable, Sendable { public struct CronAddParams: Codable, Sendable { public let name: String public let agentid: AnyCodable? + public let sessionkey: AnyCodable? public let description: String? public let enabled: Bool? public let deleteafterrun: Bool? @@ -2148,11 +2247,12 @@ public struct CronAddParams: Codable, Sendable { public let sessiontarget: AnyCodable public let wakemode: AnyCodable public let payload: AnyCodable - public let delivery: [String: AnyCodable]? + public let delivery: AnyCodable? public init( name: String, agentid: AnyCodable?, + sessionkey: AnyCodable?, description: String?, enabled: Bool?, deleteafterrun: Bool?, @@ -2160,10 +2260,11 @@ public struct CronAddParams: Codable, Sendable { sessiontarget: AnyCodable, wakemode: AnyCodable, payload: AnyCodable, - delivery: [String: AnyCodable]? + delivery: AnyCodable? ) { self.name = name self.agentid = agentid + self.sessionkey = sessionkey self.description = description self.enabled = enabled self.deleteafterrun = deleteafterrun @@ -2176,6 +2277,7 @@ public struct CronAddParams: Codable, Sendable { private enum CodingKeys: String, CodingKey { case name case agentid = "agentId" + case sessionkey = "sessionKey" case description case enabled case deleteafterrun = "deleteAfterRun" @@ -2472,6 +2574,19 @@ public struct DevicePairRejectParams: Codable, Sendable { } } +public struct DevicePairRemoveParams: Codable, Sendable { + public let deviceid: String + + public init( + deviceid: String + ) { + self.deviceid = deviceid + } + private enum CodingKeys: String, CodingKey { + case deviceid = "deviceId" + } +} + public struct DeviceTokenRotateParams: Codable, Sendable { public let deviceid: String public let role: String diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/AnyCodableTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/AnyCodableTests.swift new file mode 100644 index 0000000000000..3835f1186c077 --- /dev/null +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/AnyCodableTests.swift @@ -0,0 +1,40 @@ +import Foundation +import Testing +import OpenClawProtocol + +struct AnyCodableTests { + @Test + func encodesNSNumberBooleansAsJSONBooleans() throws { + let trueData = try JSONEncoder().encode(AnyCodable(NSNumber(value: true))) + let falseData = try JSONEncoder().encode(AnyCodable(NSNumber(value: false))) + + #expect(String(data: trueData, encoding: .utf8) == "true") + #expect(String(data: falseData, encoding: .utf8) == "false") + } + + @Test + func preservesBooleanLiteralsFromJSONSerializationBridge() throws { + let raw = try #require( + JSONSerialization.jsonObject(with: Data(#"{"enabled":true,"nested":{"active":false}}"#.utf8)) + as? [String: Any] + ) + let enabled = try #require(raw["enabled"]) + let nested = try #require(raw["nested"]) + + struct RequestEnvelope: Codable { + let params: [String: AnyCodable] + } + + let envelope = RequestEnvelope( + params: [ + "enabled": AnyCodable(enabled), + "nested": AnyCodable(nested), + ] + ) + let data = try JSONEncoder().encode(envelope) + let json = try #require(String(data: data, encoding: .utf8)) + + #expect(json.contains(#""enabled":true"#)) + #expect(json.contains(#""active":false"#)) + } +} diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift index 3babe8b9a30c7..ff7caabf381c0 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift @@ -215,6 +215,153 @@ extension TestChatTransportState { #expect(await MainActor.run { vm.pendingToolCalls.isEmpty }) } + @Test func acceptsCanonicalSessionKeyEventsForOwnPendingRun() async throws { + let history1 = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [], + thinkingLevel: "off") + let history2 = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [ + AnyCodable([ + "role": "assistant", + "content": [["type": "text", "text": "from history"]], + "timestamp": Date().timeIntervalSince1970 * 1000, + ]), + ], + thinkingLevel: "off") + + let transport = TestChatTransport(historyResponses: [history1, history2]) + let vm = await MainActor.run { OpenClawChatViewModel(sessionKey: "main", transport: transport) } + + await MainActor.run { vm.load() } + try await waitUntil("bootstrap") { await MainActor.run { vm.healthOK } } + + await MainActor.run { + vm.input = "hi" + vm.send() + } + try await waitUntil("pending run starts") { await MainActor.run { vm.pendingRunCount == 1 } } + + let runId = try #require(await transport.lastSentRunId()) + transport.emit( + .chat( + OpenClawChatEventPayload( + runId: runId, + sessionKey: "agent:main:main", + state: "final", + message: nil, + errorMessage: nil))) + + try await waitUntil("pending run clears") { await MainActor.run { vm.pendingRunCount == 0 } } + try await waitUntil("history refresh") { + await MainActor.run { vm.messages.contains(where: { $0.role == "assistant" }) } + } + } + + @Test func acceptsCanonicalSessionKeyEventsForExternalRuns() async throws { + let now = Date().timeIntervalSince1970 * 1000 + let history1 = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [ + AnyCodable([ + "role": "user", + "content": [["type": "text", "text": "first"]], + "timestamp": now, + ]), + ], + thinkingLevel: "off") + let history2 = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [ + AnyCodable([ + "role": "user", + "content": [["type": "text", "text": "first"]], + "timestamp": now, + ]), + AnyCodable([ + "role": "assistant", + "content": [["type": "text", "text": "from external run"]], + "timestamp": now + 1, + ]), + ], + thinkingLevel: "off") + + let transport = TestChatTransport(historyResponses: [history1, history2]) + let vm = await MainActor.run { OpenClawChatViewModel(sessionKey: "main", transport: transport) } + + await MainActor.run { vm.load() } + try await waitUntil("bootstrap") { await MainActor.run { vm.messages.count == 1 } } + + transport.emit( + .chat( + OpenClawChatEventPayload( + runId: "external-run", + sessionKey: "agent:main:main", + state: "final", + message: nil, + errorMessage: nil))) + + try await waitUntil("history refresh after canonical external event") { + await MainActor.run { vm.messages.count == 2 } + } + } + + @Test func preservesMessageIDsAcrossHistoryRefreshes() async throws { + let now = Date().timeIntervalSince1970 * 1000 + let history1 = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [ + AnyCodable([ + "role": "user", + "content": [["type": "text", "text": "hello"]], + "timestamp": now, + ]), + ], + thinkingLevel: "off") + let history2 = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [ + AnyCodable([ + "role": "user", + "content": [["type": "text", "text": "hello"]], + "timestamp": now, + ]), + AnyCodable([ + "role": "assistant", + "content": [["type": "text", "text": "world"]], + "timestamp": now + 1, + ]), + ], + thinkingLevel: "off") + + let transport = TestChatTransport(historyResponses: [history1, history2]) + let vm = await MainActor.run { OpenClawChatViewModel(sessionKey: "main", transport: transport) } + + await MainActor.run { vm.load() } + try await waitUntil("bootstrap") { await MainActor.run { vm.messages.count == 1 } } + let firstIdBefore = try #require(await MainActor.run { vm.messages.first?.id }) + + transport.emit( + .chat( + OpenClawChatEventPayload( + runId: "other-run", + sessionKey: "main", + state: "final", + message: nil, + errorMessage: nil))) + + try await waitUntil("history refresh") { await MainActor.run { vm.messages.count == 2 } } + let firstIdAfter = try #require(await MainActor.run { vm.messages.first?.id }) + #expect(firstIdAfter == firstIdBefore) + } + @Test func clearsStreamingOnExternalFinalEvent() async throws { let sessionId = "sess-main" let history = OpenClawChatHistoryPayload( diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkPromptBuilderTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkPromptBuilderTests.swift index 1ca18fdf32d9d..513b60d047aaf 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkPromptBuilderTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkPromptBuilderTests.swift @@ -12,4 +12,18 @@ final class TalkPromptBuilderTests: XCTestCase { let prompt = TalkPromptBuilder.build(transcript: "Hi", interruptedAtSeconds: 1.234) XCTAssertTrue(prompt.contains("Assistant speech interrupted at 1.2s.")) } + + func testBuildIncludesVoiceDirectiveHintByDefault() { + let prompt = TalkPromptBuilder.build(transcript: "Hello", interruptedAtSeconds: nil) + XCTAssertTrue(prompt.contains("ElevenLabs voice")) + } + + func testBuildExcludesVoiceDirectiveHintWhenDisabled() { + let prompt = TalkPromptBuilder.build( + transcript: "Hello", + interruptedAtSeconds: nil, + includeVoiceDirectiveHint: false) + XCTAssertFalse(prompt.contains("ElevenLabs voice")) + XCTAssertTrue(prompt.contains("Talk Mode active.")) + } } diff --git a/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js b/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js index 563adcc3b1d4a..a9cb659876a5c 100644 --- a/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js +++ b/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js @@ -451,7 +451,6 @@ class OpenClawA2UIHost extends LitElement { if (this.surfaces.length === 0) { return html`
Canvas (A2UI)
-
Waiting for A2UI messages…
`; } diff --git a/assets/chrome-extension/README.md b/assets/chrome-extension/README.md index 2a2a11a3be5c7..4ee072c1f2bb5 100644 --- a/assets/chrome-extension/README.md +++ b/assets/chrome-extension/README.md @@ -20,3 +20,4 @@ Purpose: attach OpenClaw to an existing Chrome tab so the Gateway can automate i ## Options - `Relay port`: defaults to `18792`. +- `Gateway token`: required. Set this to `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`). diff --git a/assets/chrome-extension/background.js b/assets/chrome-extension/background.js index 31ba401bddca6..7a1754e06c96b 100644 --- a/assets/chrome-extension/background.js +++ b/assets/chrome-extension/background.js @@ -42,6 +42,12 @@ async function getRelayPort() { return n } +async function getGatewayToken() { + const stored = await chrome.storage.local.get(['gatewayToken']) + const token = String(stored.gatewayToken || '').trim() + return token || '' +} + function setBadge(tabId, kind) { const cfg = BADGE[kind] void chrome.action.setBadgeText({ tabId, text: cfg.text }) @@ -55,8 +61,11 @@ async function ensureRelayConnection() { relayConnectPromise = (async () => { const port = await getRelayPort() + const gatewayToken = await getGatewayToken() const httpBase = `http://127.0.0.1:${port}` - const wsUrl = `ws://127.0.0.1:${port}/extension` + const wsUrl = gatewayToken + ? `ws://127.0.0.1:${port}/extension?token=${encodeURIComponent(gatewayToken)}` + : `ws://127.0.0.1:${port}/extension` // Fast preflight: is the relay server up? try { @@ -65,6 +74,12 @@ async function ensureRelayConnection() { throw new Error(`Relay server not reachable at ${httpBase} (${String(err)})`) } + if (!gatewayToken) { + throw new Error( + 'Missing gatewayToken in extension settings (chrome.storage.local.gatewayToken)', + ) + } + const ws = new WebSocket(wsUrl) relayWs = ws diff --git a/assets/chrome-extension/options.html b/assets/chrome-extension/options.html index 14704d65cf0d7..17fc6a79eed33 100644 --- a/assets/chrome-extension/options.html +++ b/assets/chrome-extension/options.html @@ -176,15 +176,19 @@

Getting started

-

Relay port

+

Relay connection

+
+ +
+
- Default: 18792. Extension connects to: http://127.0.0.1:<port>/. - Only change this if your OpenClaw profile uses a different cdpUrl port. + Default port: 18792. Extension connects to: http://127.0.0.1:<port>/. + Gateway token must match gateway.auth.token (or OPENCLAW_GATEWAY_TOKEN).
diff --git a/assets/chrome-extension/options.js b/assets/chrome-extension/options.js index 5b558ddccf2d5..e4252ccae4c2b 100644 --- a/assets/chrome-extension/options.js +++ b/assets/chrome-extension/options.js @@ -13,6 +13,12 @@ function updateRelayUrl(port) { el.textContent = `http://127.0.0.1:${port}/` } +function relayHeaders(token) { + const t = String(token || '').trim() + if (!t) return {} + return { 'x-openclaw-relay-token': t } +} + function setStatus(kind, message) { const status = document.getElementById('status') if (!status) return @@ -20,18 +26,31 @@ function setStatus(kind, message) { status.textContent = message || '' } -async function checkRelayReachable(port) { - const url = `http://127.0.0.1:${port}/` +async function checkRelayReachable(port, token) { + const url = `http://127.0.0.1:${port}/json/version` + const trimmedToken = String(token || '').trim() + if (!trimmedToken) { + setStatus('error', 'Gateway token required. Save your gateway token to connect.') + return + } const ctrl = new AbortController() - const t = setTimeout(() => ctrl.abort(), 900) + const t = setTimeout(() => ctrl.abort(), 1200) try { - const res = await fetch(url, { method: 'HEAD', signal: ctrl.signal }) + const res = await fetch(url, { + method: 'GET', + headers: relayHeaders(trimmedToken), + signal: ctrl.signal, + }) + if (res.status === 401) { + setStatus('error', 'Gateway token rejected. Check token and save again.') + return + } if (!res.ok) throw new Error(`HTTP ${res.status}`) - setStatus('ok', `Relay reachable at ${url}`) + setStatus('ok', `Relay reachable and authenticated at http://127.0.0.1:${port}/`) } catch { setStatus( 'error', - `Relay not reachable at ${url}. Start OpenClaw’s browser relay on this machine, then click the toolbar button again.`, + `Relay not reachable/authenticated at http://127.0.0.1:${port}/. Start OpenClaw browser relay and verify token.`, ) } finally { clearTimeout(t) @@ -39,20 +58,25 @@ async function checkRelayReachable(port) { } async function load() { - const stored = await chrome.storage.local.get(['relayPort']) + const stored = await chrome.storage.local.get(['relayPort', 'gatewayToken']) const port = clampPort(stored.relayPort) + const token = String(stored.gatewayToken || '').trim() document.getElementById('port').value = String(port) + document.getElementById('token').value = token updateRelayUrl(port) - await checkRelayReachable(port) + await checkRelayReachable(port, token) } async function save() { - const input = document.getElementById('port') - const port = clampPort(input.value) - await chrome.storage.local.set({ relayPort: port }) - input.value = String(port) + const portInput = document.getElementById('port') + const tokenInput = document.getElementById('token') + const port = clampPort(portInput.value) + const token = String(tokenInput.value || '').trim() + await chrome.storage.local.set({ relayPort: port, gatewayToken: token }) + portInput.value = String(port) + tokenInput.value = token updateRelayUrl(port) - await checkRelayReachable(port) + await checkRelayReachable(port, token) } document.getElementById('save').addEventListener('click', () => void save()) diff --git a/docker-setup.sh b/docker-setup.sh index 1d2f5e53fd126..00c3cf1924fd4 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -8,6 +8,11 @@ IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}" EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}" HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}" +fail() { + echo "ERROR: $*" >&2 + exit 1 +} + require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "Missing dependency: $1" >&2 @@ -15,6 +20,44 @@ require_cmd() { fi } +contains_disallowed_chars() { + local value="$1" + [[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *$'\t'* ]] +} + +validate_mount_path_value() { + local label="$1" + local value="$2" + if [[ -z "$value" ]]; then + fail "$label cannot be empty." + fi + if contains_disallowed_chars "$value"; then + fail "$label contains unsupported control characters." + fi + if [[ "$value" =~ [[:space:]] ]]; then + fail "$label cannot contain whitespace." + fi +} + +validate_named_volume() { + local value="$1" + if [[ ! "$value" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then + fail "OPENCLAW_HOME_VOLUME must match [A-Za-z0-9][A-Za-z0-9_.-]* when using a named volume." + fi +} + +validate_mount_spec() { + local mount="$1" + if contains_disallowed_chars "$mount"; then + fail "OPENCLAW_EXTRA_MOUNTS entries cannot contain control characters." + fi + # Keep mount specs strict to avoid YAML structure injection. + # Expected format: source:target[:options] + if [[ ! "$mount" =~ ^[^[:space:],:]+:[^[:space:],:]+(:[^[:space:],:]+)?$ ]]; then + fail "Invalid mount format '$mount'. Expected source:target[:options] without spaces." + fi +} + require_cmd docker if ! docker compose version >/dev/null 2>&1; then echo "Docker Compose not available (try: docker compose version)" >&2 @@ -24,6 +67,19 @@ fi OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}" OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}" +validate_mount_path_value "OPENCLAW_CONFIG_DIR" "$OPENCLAW_CONFIG_DIR" +validate_mount_path_value "OPENCLAW_WORKSPACE_DIR" "$OPENCLAW_WORKSPACE_DIR" +if [[ -n "$HOME_VOLUME_NAME" ]]; then + if [[ "$HOME_VOLUME_NAME" == *"/"* ]]; then + validate_mount_path_value "OPENCLAW_HOME_VOLUME" "$HOME_VOLUME_NAME" + else + validate_named_volume "$HOME_VOLUME_NAME" + fi +fi +if contains_disallowed_chars "$EXTRA_MOUNTS"; then + fail "OPENCLAW_EXTRA_MOUNTS cannot contain control characters." +fi + mkdir -p "$OPENCLAW_CONFIG_DIR" mkdir -p "$OPENCLAW_WORKSPACE_DIR" @@ -57,6 +113,9 @@ write_extra_compose() { local home_volume="$1" shift local mount + local gateway_home_mount + local gateway_config_mount + local gateway_workspace_mount cat >"$EXTRA_COMPOSE_FILE" <<'YAML' services: @@ -65,12 +124,19 @@ services: YAML if [[ -n "$home_volume" ]]; then - printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s:/home/node/.openclaw\n' "$OPENCLAW_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s:/home/node/.openclaw/workspace\n' "$OPENCLAW_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE" + gateway_home_mount="${home_volume}:/home/node" + gateway_config_mount="${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw" + gateway_workspace_mount="${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace" + validate_mount_spec "$gateway_home_mount" + validate_mount_spec "$gateway_config_mount" + validate_mount_spec "$gateway_workspace_mount" + printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE" fi for mount in "$@"; do + validate_mount_spec "$mount" printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE" done @@ -80,16 +146,18 @@ YAML YAML if [[ -n "$home_volume" ]]; then - printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s:/home/node/.openclaw\n' "$OPENCLAW_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s:/home/node/.openclaw/workspace\n' "$OPENCLAW_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE" fi for mount in "$@"; do + validate_mount_spec "$mount" printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE" done if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then + validate_named_volume "$home_volume" cat >>"$EXTRA_COMPOSE_FILE" <`, with delivery (announce by default or none). - Wakeups are first-class: a job can request “wake now” vs “next heartbeat”. +- Webhook posting is per job via `delivery.mode = "webhook"` + `delivery.to = ""`. +- Legacy fallback remains for stored jobs with `notify: true` when `cron.webhook` is set, migrate those jobs to webhook delivery mode. ## Quick start (actionable) @@ -99,7 +101,7 @@ A cron job is a stored record with: - a **schedule** (when it should run), - a **payload** (what it should do), -- optional **delivery mode** (announce or none). +- optional **delivery mode** (`announce`, `webhook`, or `none`). - optional **agent binding** (`agentId`): run the job under a specific agent; if missing or unknown, the gateway falls back to the default agent. @@ -113,11 +115,22 @@ Cron supports three schedule kinds: - `at`: one-shot timestamp via `schedule.at` (ISO 8601). - `every`: fixed interval (ms). -- `cron`: 5-field cron expression with optional IANA timezone. +- `cron`: 5-field cron expression (or 6-field with seconds) with optional IANA timezone. Cron expressions use `croner`. If a timezone is omitted, the Gateway host’s local timezone is used. +To reduce top-of-hour load spikes across many gateways, OpenClaw applies a +deterministic per-job stagger window of up to 5 minutes for recurring +top-of-hour expressions (for example `0 * * * *`, `0 */2 * * *`). Fixed-hour +expressions such as `0 7 * * *` remain exact. + +For any cron schedule, you can set an explicit stagger window with `schedule.staggerMs` +(`0` keeps exact timing). CLI shortcuts: + +- `--stagger 30s` (or `1m`, `5m`) to set an explicit stagger window. +- `--exact` to force `staggerMs = 0`. + ### Main vs isolated execution #### Main session jobs (system events) @@ -140,8 +153,9 @@ Key behaviors: - Prompt is prefixed with `[cron: ]` for traceability. - Each run starts a **fresh session id** (no prior conversation carry-over). - Default behavior: if `delivery` is omitted, isolated jobs announce a summary (`delivery.mode = "announce"`). -- `delivery.mode` (isolated-only) chooses what happens: +- `delivery.mode` chooses what happens: - `announce`: deliver a summary to the target channel and post a brief summary to the main session. + - `webhook`: POST the finished event payload to `delivery.to` when the finished event includes a summary. - `none`: internal only (no delivery, no main-session summary). - `wakeMode` controls when the main-session summary posts: - `now`: immediate heartbeat. @@ -163,11 +177,11 @@ Common `agentTurn` fields: - `model` / `thinking`: optional overrides (see below). - `timeoutSeconds`: optional timeout override. -Delivery config (isolated jobs only): +Delivery config: -- `delivery.mode`: `none` | `announce`. +- `delivery.mode`: `none` | `announce` | `webhook`. - `delivery.channel`: `last` or a specific channel. -- `delivery.to`: channel-specific target (phone/chat/channel id). +- `delivery.to`: channel-specific target (announce) or webhook URL (webhook mode). - `delivery.bestEffort`: avoid failing the job if announce delivery fails. Announce delivery suppresses messaging tool sends for the run; use `delivery.channel`/`delivery.to` @@ -192,6 +206,18 @@ Behavior details: - The main-session summary respects `wakeMode`: `now` triggers an immediate heartbeat and `next-heartbeat` waits for the next scheduled heartbeat. +#### Webhook delivery flow + +When `delivery.mode = "webhook"`, cron posts the finished event payload to `delivery.to` when the finished event includes a summary. + +Behavior details: + +- The endpoint must be a valid HTTP(S) URL. +- No channel delivery is attempted in webhook mode. +- No main-session summary is posted in webhook mode. +- If `cron.webhookToken` is set, auth header is `Authorization: Bearer `. +- Deprecated fallback: stored legacy jobs with `notify: true` still post to `cron.webhook` (if configured), with a warning so you can migrate to `delivery.mode = "webhook"`. + ### Model and thinking overrides Isolated jobs (`agentTurn`) can override the model and thinking level: @@ -213,11 +239,12 @@ Resolution priority: Isolated jobs can deliver output to a channel via the top-level `delivery` config: -- `delivery.mode`: `announce` (deliver a summary) or `none`. +- `delivery.mode`: `announce` (channel delivery), `webhook` (HTTP POST), or `none`. - `delivery.channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last`. - `delivery.to`: channel-specific recipient target. -Delivery config is only valid for isolated jobs (`sessionTarget: "isolated"`). +`announce` delivery is only valid for isolated jobs (`sessionTarget: "isolated"`). +`webhook` delivery is valid for both main and isolated jobs. If `delivery.channel` or `delivery.to` is omitted, cron can fall back to the main session’s “last route” (the last place the agent replied). @@ -333,10 +360,21 @@ Notes: enabled: true, // default true store: "~/.openclaw/cron/jobs.json", maxConcurrentRuns: 1, // default 1 + webhook: "https://example.invalid/legacy", // deprecated fallback for stored notify:true jobs + webhookToken: "replace-with-dedicated-webhook-token", // optional bearer token for webhook mode }, } ``` +Webhook behavior: + +- Preferred: set `delivery.mode: "webhook"` with `delivery.to: "https://..."` per job. +- Webhook URLs must be valid `http://` or `https://` URLs. +- When posted, payload is the cron finished event JSON. +- If `cron.webhookToken` is set, auth header is `Authorization: Bearer `. +- If `cron.webhookToken` is not set, no `Authorization` header is sent. +- Deprecated fallback: stored legacy jobs with `notify: true` still use `cron.webhook` when present. + Disable cron entirely: - `cron.enabled: false` (config) @@ -381,6 +419,19 @@ openclaw cron add \ --to "+15551234567" ``` +Recurring cron job with explicit 30-second stagger: + +```bash +openclaw cron add \ + --name "Minute watcher" \ + --cron "0 * * * * *" \ + --tz "UTC" \ + --stagger 30s \ + --session isolated \ + --message "Run minute watcher checks." \ + --announce +``` + Recurring isolated job (deliver to a Telegram topic): ```bash @@ -438,6 +489,12 @@ openclaw cron edit \ --thinking low ``` +Force an existing cron job to run exactly on schedule (no stagger): + +```bash +openclaw cron edit --exact +``` + Run history: ```bash @@ -476,3 +533,10 @@ openclaw system event --mode now --text "Next heartbeat: check battery." - For forum topics, use `-100…:topic:` so it’s explicit and unambiguous. - If you see `telegram:...` prefixes in logs or stored “last route” targets, that’s normal; cron delivery accepts them and still parses topic IDs correctly. + +### Subagent announce delivery retries + +- When a subagent run completes, the gateway announces the result to the requester session. +- If the announce flow returns `false` (e.g. requester session is busy), the gateway retries up to 3 times with tracking via `announceRetryCount`. +- Announces older than 5 minutes past `endedAt` are force-expired to prevent stale entries from looping indefinitely. +- If you see repeated announce deliveries in logs, check the subagent registry for entries with high `announceRetryCount` values. diff --git a/docs/automation/cron-vs-heartbeat.md b/docs/automation/cron-vs-heartbeat.md index 423565d4f3268..c25cbcb80dbc9 100644 --- a/docs/automation/cron-vs-heartbeat.md +++ b/docs/automation/cron-vs-heartbeat.md @@ -74,7 +74,9 @@ See [Heartbeat](/gateway/heartbeat) for full configuration. ## Cron: Precise Scheduling -Cron jobs run at **exact times** and can run in isolated sessions without affecting main context. +Cron jobs run at precise times and can run in isolated sessions without affecting main context. +Recurring top-of-hour schedules are automatically spread by a deterministic +per-job offset in a 0-5 minute window. ### When to use cron @@ -87,7 +89,9 @@ Cron jobs run at **exact times** and can run in isolated sessions without affect ### Cron advantages -- **Exact timing**: 5-field cron expressions with timezone support. +- **Precise timing**: 5-field or 6-field (seconds) cron expressions with timezone support. +- **Built-in load spreading**: recurring top-of-hour schedules are staggered by up to 5 minutes by default. +- **Per-job control**: override stagger with `--stagger ` or force exact timing with `--exact`. - **Session isolation**: Runs in `cron:` without polluting main history. - **Model overrides**: Use a cheaper or more powerful model per job. - **Delivery control**: Isolated jobs default to `announce` (summary); choose `none` as needed. @@ -207,7 +211,7 @@ For ad-hoc workflows, call Lobster directly. - Lobster runs as a **local subprocess** (`lobster` CLI) in tool mode and returns a **JSON envelope**. - If the tool returns `needs_approval`, you resume with a `resumeToken` and `approve` flag. - The tool is an **optional plugin**; enable it additively via `tools.alsoAllow: ["lobster"]` (recommended). -- If you pass `lobsterPath`, it must be an **absolute path**. +- Lobster expects the `lobster` CLI to be available on `PATH`. See [Lobster](/tools/lobster) for full usage and examples. diff --git a/docs/automation/hooks.md b/docs/automation/hooks.md index ffdf32ab79b00..66b96cd1e9e96 100644 --- a/docs/automation/hooks.md +++ b/docs/automation/hooks.md @@ -119,6 +119,8 @@ Example `package.json`: Each entry points to a hook directory containing `HOOK.md` and `handler.ts` (or `index.ts`). Hook packs can ship dependencies; they will be installed under `~/.openclaw/hooks/`. +Each `openclaw.hooks` entry must stay inside the package directory after symlink +resolution; entries that escape are rejected. Security note: `openclaw hooks install` installs dependencies with `npm install --ignore-scripts` (no lifecycle scripts). Keep hook pack dependency trees "pure JS/TS" and avoid packages that rely @@ -207,12 +209,13 @@ Each event includes: ```typescript { - type: 'command' | 'session' | 'agent' | 'gateway', - action: string, // e.g., 'new', 'reset', 'stop' + type: 'command' | 'session' | 'agent' | 'gateway' | 'message', + action: string, // e.g., 'new', 'reset', 'stop', 'received', 'sent' sessionKey: string, // Session identifier timestamp: Date, // When the event occurred messages: string[], // Push messages here to send to user context: { + // Command events: sessionEntry?: SessionEntry, sessionId?: string, sessionFile?: string, @@ -220,7 +223,13 @@ Each event includes: senderId?: string, workspaceDir?: string, bootstrapFiles?: WorkspaceBootstrapFile[], - cfg?: OpenClawConfig + cfg?: OpenClawConfig, + // Message events (see Message Events section for full details): + from?: string, // message:received + to?: string, // message:sent + content?: string, + channelId?: string, + success?: boolean, // message:sent } } ``` @@ -246,6 +255,70 @@ Triggered when the gateway starts: - **`gateway:startup`**: After channels start and hooks are loaded +### Message Events + +Triggered when messages are received or sent: + +- **`message`**: All message events (general listener) +- **`message:received`**: When an inbound message is received from any channel +- **`message:sent`**: When an outbound message is successfully sent + +#### Message Event Context + +Message events include rich context about the message: + +```typescript +// message:received context +{ + from: string, // Sender identifier (phone number, user ID, etc.) + content: string, // Message content + timestamp?: number, // Unix timestamp when received + channelId: string, // Channel (e.g., "whatsapp", "telegram", "discord") + accountId?: string, // Provider account ID for multi-account setups + conversationId?: string, // Chat/conversation ID + messageId?: string, // Message ID from the provider + metadata?: { // Additional provider-specific data + to?: string, + provider?: string, + surface?: string, + threadId?: string, + senderId?: string, + senderName?: string, + senderUsername?: string, + senderE164?: string, + } +} + +// message:sent context +{ + to: string, // Recipient identifier + content: string, // Message content that was sent + success: boolean, // Whether the send succeeded + error?: string, // Error message if sending failed + channelId: string, // Channel (e.g., "whatsapp", "telegram", "discord") + accountId?: string, // Provider account ID + conversationId?: string, // Chat/conversation ID + messageId?: string, // Message ID returned by the provider +} +``` + +#### Example: Message Logger Hook + +```typescript +import type { HookHandler } from "../../src/hooks/hooks.js"; +import { isMessageReceivedEvent, isMessageSentEvent } from "../../src/hooks/internal-hooks.js"; + +const handler: HookHandler = async (event) => { + if (isMessageReceivedEvent(event)) { + console.log(`[message-logger] Received from ${event.context.from}: ${event.context.content}`); + } else if (isMessageSentEvent(event)) { + console.log(`[message-logger] Sent to ${event.context.to}: ${event.context.content}`); + } +}; + +export default handler; +``` + ### Tool Result Hooks (Plugin API) These hooks are not event-stream listeners; they let plugins synchronously adjust tool results before OpenClaw persists them. @@ -259,8 +332,6 @@ Planned event types: - **`session:start`**: When a new session begins - **`session:end`**: When a session ends - **`agent:error`**: When an agent encounters an error -- **`message:sent`**: When a message is sent -- **`message:received`**: When a message is received ## Creating Custom Hooks diff --git a/docs/automation/troubleshooting.md b/docs/automation/troubleshooting.md index 51f2aa209cf41..a189d805221f9 100644 --- a/docs/automation/troubleshooting.md +++ b/docs/automation/troubleshooting.md @@ -89,7 +89,8 @@ Common signatures: - `heartbeat skipped` with `reason=quiet-hours` → outside `activeHours`. - `requests-in-flight` → main lane busy; heartbeat deferred. -- `empty-heartbeat-file` → `HEARTBEAT.md` exists but has no actionable content. +- `empty-heartbeat-file` → interval heartbeat skipped because `HEARTBEAT.md` has no actionable content and no tagged cron event is queued. +- `no-heartbeat-file` → interval heartbeat skipped because `HEARTBEAT.md` is missing and no tagged cron event is queued. - `alerts-disabled` → visibility settings suppress outbound heartbeat messages. ## Timezone and activeHours gotchas diff --git a/docs/channels/bluebubbles.md b/docs/channels/bluebubbles.md index a63d2f1d35ec2..fd677a1d585df 100644 --- a/docs/channels/bluebubbles.md +++ b/docs/channels/bluebubbles.md @@ -44,6 +44,10 @@ Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **R 4. Point BlueBubbles webhooks to your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=`). 5. Start the gateway; it will register the webhook handler and start pairing. +Security note: + +- Always set a webhook password. If you expose the gateway through a reverse proxy (Tailscale Serve/Funnel, nginx, Cloudflare Tunnel, ngrok), the proxy may connect to the gateway over loopback. The BlueBubbles webhook handler treats requests with forwarding headers as proxied and will not accept passwordless webhooks. + ## Keeping Messages.app alive (VM / headless setups) Some macOS VM / always-on setups can end up with Messages.app going “idle” (incoming events stop until the app is opened/foregrounded). A simple workaround is to **poke Messages every 5 minutes** using an AppleScript + LaunchAgent. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 29d99253fa45e..774a0eba1a8e0 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -23,16 +23,98 @@ Status: ready for DMs and guild channels via the official Discord gateway. ## Quick setup +You will need to create a new application with a bot, add the bot to your server, and pair it to OpenClaw. We recommend adding your bot to your own private server. If you don't have one yet, [create one first](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server) (choose **Create My Own > For me and my friends**). + - - Create an application in the Discord Developer Portal, add a bot, then enable: + + Go to the [Discord Developer Portal](https://discord.com/developers/applications) and click **New Application**. Name it something like "OpenClaw". + + Click **Bot** on the sidebar. Set the **Username** to whatever you call your OpenClaw agent. + + + + + Still on the **Bot** page, scroll down to **Privileged Gateway Intents** and enable: + + - **Message Content Intent** (required) + - **Server Members Intent** (recommended; required for role allowlists and name-to-ID matching) + - **Presence Intent** (optional; only needed for presence updates) + + + + + Scroll back up on the **Bot** page and click **Reset Token**. + + + Despite the name, this generates your first token — nothing is being "reset." + - - **Message Content Intent** - - **Server Members Intent** (required for role allowlists and role-based routing; recommended for name-to-ID allowlist matching) + Copy the token and save it somewhere. This is your **Bot Token** and you will need it shortly. - + + Click **OAuth2** on the sidebar. You'll generate an invite URL with the right permissions to add the bot to your server. + + Scroll down to **OAuth2 URL Generator** and enable: + + - `bot` + - `applications.commands` + + A **Bot Permissions** section will appear below. Enable: + + - View Channels + - Send Messages + - Read Message History + - Embed Links + - Attach Files + - Add Reactions (optional) + + Copy the generated URL at the bottom, paste it into your browser, select your server, and click **Continue** to connect. You should now see your bot in the Discord server. + + + + + Back in the Discord app, you need to enable Developer Mode so you can copy internal IDs. + + 1. Click **User Settings** (gear icon next to your avatar) → **Advanced** → toggle on **Developer Mode** + 2. Right-click your **server icon** in the sidebar → **Copy Server ID** + 3. Right-click your **own avatar** → **Copy User ID** + + Save your **Server ID** and **User ID** alongside your Bot Token — you'll send all three to OpenClaw in the next step. + + + + + For pairing to work, Discord needs to allow your bot to DM you. Right-click your **server icon** → **Privacy Settings** → toggle on **Direct Messages**. + + This lets server members (including bots) send you DMs. Keep this enabled if you want to use Discord DMs with OpenClaw. If you only plan to use guild channels, you can disable DMs after pairing. + + + + + Your Discord bot token is a secret (like a password). Set it on the machine running OpenClaw before messaging your agent. + +```bash +openclaw config set channels.discord.token '"YOUR_BOT_TOKEN"' --json +openclaw config set channels.discord.enabled true --json +openclaw gateway +``` + + If OpenClaw is already running as a background service, use `openclaw gateway restart` instead. + + + + + + + + Chat with your OpenClaw agent on any existing channel (e.g. Telegram) and tell it. If Discord is your first channel, use the CLI / config tab instead. + + > "I already set my Discord bot token in config. Please finish Discord setup with User ID `` and Server ID ``." + + + If you prefer file-based config, set: ```json5 { @@ -45,32 +127,40 @@ Status: ready for DMs and guild channels via the official Discord gateway. } ``` - Env fallback for the default account: + Env fallback for the default account: ```bash DISCORD_BOT_TOKEN=... ``` - - - - Invite the bot to your server with message permissions. - -```bash -openclaw gateway -``` + + + Wait until the gateway is running, then DM your bot in Discord. It will respond with a pairing code. + + + + Send the pairing code to your agent on your existing channel: + + > "Approve this Discord pairing code: ``" + + ```bash openclaw pairing list discord openclaw pairing approve discord ``` + + + Pairing codes expire after 1 hour. + You should now be able to chat with your agent in Discord via DM. + @@ -78,6 +168,87 @@ openclaw pairing approve discord Token resolution is account-aware. Config token values win over env fallback. `DISCORD_BOT_TOKEN` is only used for the default account. +## Recommended: Set up a guild workspace + +Once DMs are working, you can set up your Discord server as a full workspace where each channel gets its own agent session with its own context. This is recommended for private servers where it's just you and your bot. + + + + This enables your agent to respond in any channel on your server, not just DMs. + + + + > "Add my Discord Server ID `` to the guild allowlist" + + + +```json5 +{ + channels: { + discord: { + groupPolicy: "allowlist", + guilds: { + YOUR_SERVER_ID: { + requireMention: true, + users: ["YOUR_USER_ID"], + }, + }, + }, + }, +} +``` + + + + + + + + By default, your agent only responds in guild channels when @mentioned. For a private server, you probably want it to respond to every message. + + + + > "Allow my agent to respond on this server without having to be @mentioned" + + + Set `requireMention: false` in your guild config: + +```json5 +{ + channels: { + discord: { + guilds: { + YOUR_SERVER_ID: { + requireMention: false, + }, + }, + }, + }, +} +``` + + + + + + + + By default, long-term memory (MEMORY.md) only loads in DM sessions. Guild channels do not auto-load MEMORY.md. + + + + > "When I ask questions in Discord channels, use memory_search or memory_get if you need long-term context from MEMORY.md." + + + If you need shared context in every channel, put the stable instructions in `AGENTS.md` or `USER.md` (they are injected for every session). Keep long-term notes in `MEMORY.md` and access them on demand with memory tools. + + + + + + +Now create some channels on your Discord server and start chatting. Your agent can see the channel name, and each channel gets its own isolated session — so you can set up `#coding`, `#home`, `#research`, or whatever fits your workflow. + ## Runtime model - Gateway owns the Discord connection. @@ -87,15 +258,95 @@ Token resolution is account-aware. Config token values win over env fallback. `D - Group DMs are ignored by default (`channels.discord.dm.groupEnabled=false`). - Native slash commands run in isolated command sessions (`agent::discord:slash:`), while still carrying `CommandTargetSessionKey` to the routed conversation session. +## Interactive components + +OpenClaw supports Discord components v2 containers for agent messages. Use the message tool with a `components` payload. Interaction results are routed back to the agent as normal inbound messages and follow the existing Discord `replyToMode` settings. + +Supported blocks: + +- `text`, `section`, `separator`, `actions`, `media-gallery`, `file` +- Action rows allow up to 5 buttons or a single select menu +- Select types: `string`, `user`, `role`, `mentionable`, `channel` + +By default, components are single use. Set `components.reusable=true` to allow buttons, selects, and forms to be used multiple times until they expire. + +To restrict who can click a button, set `allowedUsers` on that button (Discord user IDs, tags, or `*`). When configured, unmatched users receive an ephemeral denial. + +File attachments: + +- `file` blocks must point to an attachment reference (`attachment://`) +- Provide the attachment via `media`/`path`/`filePath` (single file); use `media-gallery` for multiple files +- Use `filename` to override the upload name when it should match the attachment reference + +Modal forms: + +- Add `components.modal` with up to 5 fields +- Field types: `text`, `checkbox`, `radio`, `select`, `role-select`, `user-select` +- OpenClaw adds a trigger button automatically + +Example: + +```json5 +{ + channel: "discord", + action: "send", + to: "channel:123456789012345678", + message: "Optional fallback text", + components: { + reusable: true, + text: "Choose a path", + blocks: [ + { + type: "actions", + buttons: [ + { + label: "Approve", + style: "success", + allowedUsers: ["123456789012345678"], + }, + { label: "Decline", style: "danger" }, + ], + }, + { + type: "actions", + select: { + type: "string", + placeholder: "Pick an option", + options: [ + { label: "Option A", value: "a" }, + { label: "Option B", value: "b" }, + ], + }, + }, + ], + modal: { + title: "Details", + triggerLabel: "Open form", + fields: [ + { type: "text", label: "Requester" }, + { + type: "select", + label: "Priority", + options: [ + { label: "Low", value: "low" }, + { label: "High", value: "high" }, + ], + }, + ], + }, + }, +} +``` + ## Access control and routing - `channels.discord.dm.policy` controls DM access: + `channels.discord.dmPolicy` controls DM access (legacy: `channels.discord.dm.policy`): - `pairing` (default) - `allowlist` - - `open` (requires `channels.discord.dm.allowFrom` to include `"*"`) + - `open` (requires `channels.discord.allowFrom` to include `"*"`; legacy: `channels.discord.dm.allowFrom`) - `disabled` If DM policy is not open, unknown users are blocked (or prompted for pairing in `pairing` mode). @@ -313,6 +564,23 @@ See [Slash commands](/tools/slash-commands) for command catalog and behavior. + + `ackReaction` sends an acknowledgement emoji while OpenClaw is processing an inbound message. + + Resolution order: + + - `channels.discord.accounts..ackReaction` + - `channels.discord.ackReaction` + - `messages.ackReaction` + - agent identity emoji fallback (`agents.list[].identity.emoji`, else "👀") + + Notes: + + - Discord accepts unicode emoji or custom emoji names. + - Use `""` to disable the reaction for a channel or account. + + + Channel-initiated config writes are enabled by default. @@ -333,7 +601,7 @@ See [Slash commands](/tools/slash-commands) for command catalog and behavior. - Route Discord gateway WebSocket traffic through an HTTP(S) proxy with `channels.discord.proxy`. + Route Discord gateway WebSocket traffic and startup REST lookups (application ID + allowlist resolution) through an HTTP(S) proxy with `channels.discord.proxy`. ```json5 { @@ -482,6 +750,30 @@ Default gate behavior: | moderation | disabled | | presence | disabled | +## Components v2 UI + +OpenClaw uses Discord components v2 for exec approvals and cross-context markers. Discord message actions can also accept `components` for custom UI (advanced; requires Carbon component instances), while legacy `embeds` remain available but are not recommended. + +- `channels.discord.ui.components.accentColor` sets the accent color used by Discord component containers (hex). +- Set per account with `channels.discord.accounts..ui.components.accentColor`. +- `embeds` are ignored when components v2 are present. + +Example: + +```json5 +{ + channels: { + discord: { + ui: { + components: { + accentColor: "#5865F2", + }, + }, + }, + }, +} +``` + ## Voice messages Discord voice messages show a waveform preview and require OGG/Opus audio plus metadata. OpenClaw generates the waveform automatically, but it needs `ffmpeg` and `ffprobe` available on the gateway host to inspect and convert audio files. @@ -545,7 +837,7 @@ openclaw logs --follow - DM disabled: `channels.discord.dm.enabled=false` - - DM policy disabled: `channels.discord.dm.policy="disabled"` + - DM policy disabled: `channels.discord.dmPolicy="disabled"` (legacy: `channels.discord.dm.policy`) - awaiting pairing approval in `pairing` mode @@ -574,6 +866,7 @@ High-signal Discord fields: - media/retry: `mediaMaxMb`, `retry` - actions: `actions.*` - presence: `activity`, `status`, `activityType`, `activityUrl` +- UI: `ui.components.accentColor` - features: `pluralkit`, `execApprovals`, `intents`, `agentComponents`, `heartbeat`, `responsePrefix` ## Safety and operations @@ -586,5 +879,6 @@ High-signal Discord fields: - [Pairing](/channels/pairing) - [Channel routing](/channels/channel-routing) +- [Multi-agent routing](/concepts/multi-agent) - [Troubleshooting](/channels/troubleshooting) - [Slash commands](/tools/slash-commands) diff --git a/docs/channels/feishu.md b/docs/channels/feishu.md index 461facdbb2730..e92f84460d386 100644 --- a/docs/channels/feishu.md +++ b/docs/channels/feishu.md @@ -193,6 +193,8 @@ Edit `~/.openclaw/openclaw.json`: } ``` +If you use `connectionMode: "webhook"`, set `verificationToken`. The Feishu webhook server binds to `127.0.0.1` by default; set `webhookHost` only if you intentionally need a different bind address. + ### Configure via environment variables ```bash @@ -527,23 +529,28 @@ Full configuration: [Gateway configuration](/gateway/configuration) Key options: -| Setting | Description | Default | -| ------------------------------------------------- | ------------------------------- | --------- | -| `channels.feishu.enabled` | Enable/disable channel | `true` | -| `channels.feishu.domain` | API domain (`feishu` or `lark`) | `feishu` | -| `channels.feishu.accounts..appId` | App ID | - | -| `channels.feishu.accounts..appSecret` | App Secret | - | -| `channels.feishu.accounts..domain` | Per-account API domain override | `feishu` | -| `channels.feishu.dmPolicy` | DM policy | `pairing` | -| `channels.feishu.allowFrom` | DM allowlist (open_id list) | - | -| `channels.feishu.groupPolicy` | Group policy | `open` | -| `channels.feishu.groupAllowFrom` | Group allowlist | - | -| `channels.feishu.groups..requireMention` | Require @mention | `true` | -| `channels.feishu.groups..enabled` | Enable group | `true` | -| `channels.feishu.textChunkLimit` | Message chunk size | `2000` | -| `channels.feishu.mediaMaxMb` | Media size limit | `30` | -| `channels.feishu.streaming` | Enable streaming card output | `true` | -| `channels.feishu.blockStreaming` | Enable block streaming | `true` | +| Setting | Description | Default | +| ------------------------------------------------- | ------------------------------- | ---------------- | +| `channels.feishu.enabled` | Enable/disable channel | `true` | +| `channels.feishu.domain` | API domain (`feishu` or `lark`) | `feishu` | +| `channels.feishu.connectionMode` | Event transport mode | `websocket` | +| `channels.feishu.verificationToken` | Required for webhook mode | - | +| `channels.feishu.webhookPath` | Webhook route path | `/feishu/events` | +| `channels.feishu.webhookHost` | Webhook bind host | `127.0.0.1` | +| `channels.feishu.webhookPort` | Webhook bind port | `3000` | +| `channels.feishu.accounts..appId` | App ID | - | +| `channels.feishu.accounts..appSecret` | App Secret | - | +| `channels.feishu.accounts..domain` | Per-account API domain override | `feishu` | +| `channels.feishu.dmPolicy` | DM policy | `pairing` | +| `channels.feishu.allowFrom` | DM allowlist (open_id list) | - | +| `channels.feishu.groupPolicy` | Group policy | `open` | +| `channels.feishu.groupAllowFrom` | Group allowlist | - | +| `channels.feishu.groups..requireMention` | Require @mention | `true` | +| `channels.feishu.groups..enabled` | Enable group | `true` | +| `channels.feishu.textChunkLimit` | Message chunk size | `2000` | +| `channels.feishu.mediaMaxMb` | Media size limit | `30` | +| `channels.feishu.streaming` | Enable streaming card output | `true` | +| `channels.feishu.blockStreaming` | Enable block streaming | `true` | --- diff --git a/docs/channels/grammy.md b/docs/channels/grammy.md index c2891d1a2eeb3..ae92c5292b02c 100644 --- a/docs/channels/grammy.md +++ b/docs/channels/grammy.md @@ -21,7 +21,7 @@ title: grammY - **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `channels.telegram.webhookUrl` + `channels.telegram.webhookSecret` are set (otherwise it long-polls). - **Sessions:** direct chats collapse into the agent main session (`agent::`); groups use `agent::telegram:group:`; replies route back to the same channel. - **Config knobs:** `channels.telegram.botToken`, `channels.telegram.dmPolicy`, `channels.telegram.groups` (allowlist + mention defaults), `channels.telegram.allowFrom`, `channels.telegram.groupAllowFrom`, `channels.telegram.groupPolicy`, `channels.telegram.mediaMaxMb`, `channels.telegram.linkPreview`, `channels.telegram.proxy`, `channels.telegram.webhookSecret`, `channels.telegram.webhookUrl`, `channels.telegram.webhookHost`. -- **Draft streaming:** optional `channels.telegram.streamMode` uses `sendMessageDraft` in private topic chats (Bot API 9.3+). This is separate from channel block streaming. +- **Live stream preview:** optional `channels.telegram.streamMode` sends a temporary message and updates it with `editMessageText`. This is separate from channel block streaming. - **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome. Open questions diff --git a/docs/channels/groups.md b/docs/channels/groups.md index 1b3fb0394a332..6bd278846c5bd 100644 --- a/docs/channels/groups.md +++ b/docs/channels/groups.md @@ -105,7 +105,7 @@ Want “groups can only see folder X” instead of “no host access”? Keep `w docker: { binds: [ // hostPath:containerPath:mode - "~/FriendsShared:/data:ro", + "/home/user/FriendsShared:/data:ro", ], }, }, diff --git a/docs/channels/imessage.md b/docs/channels/imessage.md index 2876be3137280..d7a1b6335977a 100644 --- a/docs/channels/imessage.md +++ b/docs/channels/imessage.md @@ -97,12 +97,19 @@ exec ssh -T gateway-host imsg "$@" cliPath: "~/.openclaw/scripts/imsg-ssh", remoteHost: "user@gateway-host", // used for SCP attachment fetches includeAttachments: true, + // Optional: override allowed attachment roots. + // Defaults include /Users/*/Library/Messages/Attachments + attachmentRoots: ["/Users/*/Library/Messages/Attachments"], + remoteAttachmentRoots: ["/Users/*/Library/Messages/Attachments"], }, }, } ``` If `remoteHost` is not set, OpenClaw attempts to auto-detect it by parsing the SSH wrapper script. + `remoteHost` must be `host` or `user@host` (no spaces or SSH options). + OpenClaw uses strict host-key checking for SCP, so the relay host key must already exist in `~/.ssh/known_hosts`. + Attachment paths are validated against allowed roots (`attachmentRoots` / `remoteAttachmentRoots`). @@ -224,13 +231,14 @@ exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@" ``` Use SSH keys so both SSH and SCP are non-interactive. + Ensure the host key is trusted first (for example `ssh bot@mac-mini.tailnet-1234.ts.net`) so `known_hosts` is populated. iMessage supports per-account config under `channels.imessage.accounts`. - Each account can override fields such as `cliPath`, `dbPath`, `allowFrom`, `groupPolicy`, `mediaMaxMb`, and history settings. + Each account can override fields such as `cliPath`, `dbPath`, `allowFrom`, `groupPolicy`, `mediaMaxMb`, history settings, and attachment root allowlists. @@ -241,6 +249,11 @@ exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@" - inbound attachment ingestion is optional: `channels.imessage.includeAttachments` - remote attachment paths can be fetched via SCP when `remoteHost` is set + - attachment paths must match allowed roots: + - `channels.imessage.attachmentRoots` (local) + - `channels.imessage.remoteAttachmentRoots` (remote SCP mode) + - default root pattern: `/Users/*/Library/Messages/Attachments` + - SCP uses strict host-key checking (`StrictHostKeyChecking=yes`) - outbound media size uses `channels.imessage.mediaMaxMb` (default 16 MB) @@ -325,7 +338,9 @@ openclaw channels status --probe Check: - `channels.imessage.remoteHost` + - `channels.imessage.remoteAttachmentRoots` - SSH/SCP key auth from the gateway host + - host key exists in `~/.ssh/known_hosts` on the gateway host - remote path readability on the Mac running Messages diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index 93bcaada5680b..04205d9497110 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -190,6 +190,7 @@ Notes: - `openclaw pairing approve matrix ` - Public DMs: `channels.matrix.dm.policy="open"` plus `channels.matrix.dm.allowFrom=["*"]`. - `channels.matrix.dm.allowFrom` accepts full Matrix user IDs (example: `@user:server`). The wizard resolves display names to user IDs when directory search finds a single exact match. +- Do not use display names or bare localparts (example: `"Alice"` or `"alice"`). They are ambiguous and are ignored for allowlist matching. Use full `@user:server` IDs. ## Rooms (groups) diff --git a/docs/channels/mattermost.md b/docs/channels/mattermost.md index f4353180e2af1..fa0d9393e0f7d 100644 --- a/docs/channels/mattermost.md +++ b/docs/channels/mattermost.md @@ -114,6 +114,26 @@ Use these target formats with `openclaw message send` or cron/webhooks: Bare IDs are treated as channels. +## Reactions (message tool) + +- Use `message action=react` with `channel=mattermost`. +- `messageId` is the Mattermost post id. +- `emoji` accepts names like `thumbsup` or `:+1:` (colons are optional). +- Set `remove=true` (boolean) to remove a reaction. +- Reaction add/remove events are forwarded as system events to the routed agent session. + +Examples: + +``` +message action=react channel=mattermost target=channel: messageId= emoji=thumbsup +message action=react channel=mattermost target=channel: messageId= emoji=thumbsup remove=true +``` + +Config: + +- `channels.mattermost.actions.reactions`: enable/disable reaction actions (default true). +- Per-account override: `channels.mattermost.accounts..actions.reactions`. + ## Multi-account Mattermost supports multiple accounts under `channels.mattermost.accounts`: diff --git a/docs/channels/slack.md b/docs/channels/slack.md index 46ce2f7fe229a..9fdd3fb89a2e4 100644 --- a/docs/channels/slack.md +++ b/docs/channels/slack.md @@ -137,17 +137,18 @@ For actions/directory reads, user token can be preferred when configured. For wr - `channels.slack.dm.policy` controls DM access: + `channels.slack.dmPolicy` controls DM access (legacy: `channels.slack.dm.policy`): - `pairing` (default) - `allowlist` - - `open` (requires `dm.allowFrom` to include `"*"`) + - `open` (requires `channels.slack.allowFrom` to include `"*"`; legacy: `channels.slack.dm.allowFrom`) - `disabled` DM flags: - `dm.enabled` (default true) - - `dm.allowFrom` + - `channels.slack.allowFrom` (preferred) + - `dm.allowFrom` (legacy) - `dm.groupEnabled` (group DMs default false) - `dm.groupChannels` (optional MPIM allowlist) @@ -200,6 +201,12 @@ For actions/directory reads, user token can be preferred when configured. For wr - Enable native Slack command handlers with `channels.slack.commands.native: true` (or global `commands.native: true`). - When native commands are enabled, register matching slash commands in Slack (`/` names). - If native commands are not enabled, you can run a single configured slash command via `channels.slack.slashCommand`. +- Native arg menus now adapt their rendering strategy: + - up to 5 options: button blocks + - 6-100 options: static select menu + - more than 100 options: external select with async option filtering when interactivity options handlers are available + - if encoded option values exceed Slack limits, the flow falls back to buttons +- For long option payloads, Slash command argument menus use a confirm dialog before dispatching a selected value. Default slash command settings: @@ -283,8 +290,28 @@ Available action groups in current Slack tooling: - Message edits/deletes/thread broadcasts are mapped into system events. - Reaction add/remove events are mapped into system events. - Member join/leave, channel created/renamed, and pin add/remove events are mapped into system events. +- Assistant thread status updates (for "is typing..." indicators in threads) use `assistant.threads.setStatus` and require bot scope `assistant:write`. - `channel_id_changed` can migrate channel config keys when `configWrites` is enabled. - Channel topic/purpose metadata is treated as untrusted context and can be injected into routing context. +- Block actions and modal interactions emit structured `Slack interaction: ...` system events with rich payload fields: + - block actions: selected values, labels, picker values, and `workflow_*` metadata + - modal `view_submission` and `view_closed` events with routed channel metadata and form inputs + +## Ack reactions + +`ackReaction` sends an acknowledgement emoji while OpenClaw is processing an inbound message. + +Resolution order: + +- `channels.slack.accounts..ackReaction` +- `channels.slack.ackReaction` +- `messages.ackReaction` +- agent identity emoji fallback (`agents.list[].identity.emoji`, else "👀") + +Notes: + +- Slack expects shortcodes (for example `"eyes"`). +- Use `""` to disable the reaction for a channel or account. ## Manifest and scope checklist @@ -325,6 +352,7 @@ Available action groups in current Slack tooling: "mpim:history", "users:read", "app_mentions:read", + "assistant:write", "reactions:read", "reactions:write", "pins:read", @@ -399,7 +427,7 @@ openclaw doctor Check: - `channels.slack.dm.enabled` - - `channels.slack.dm.policy` + - `channels.slack.dmPolicy` (or legacy `channels.slack.dm.policy`) - pairing approvals / allowlist entries ```bash @@ -433,20 +461,45 @@ openclaw pairing list slack +## Text streaming + +OpenClaw supports Slack native text streaming via the Agents and AI Apps API. + +By default, streaming is enabled. Disable it per account: + +```yaml +channels: + slack: + streaming: false +``` + +### Requirements + +1. Enable **Agents and AI Apps** in your Slack app settings. +2. Ensure the app has the `assistant:write` scope. +3. A reply thread must be available for that message. Thread selection still follows `replyToMode`. + +### Behavior + +- First text chunk starts a stream (`chat.startStream`). +- Later text chunks append to the same stream (`chat.appendStream`). +- End of reply finalizes stream (`chat.stopStream`). +- Media and non-text payloads fall back to normal delivery. +- If streaming fails mid-reply, OpenClaw falls back to normal delivery for remaining payloads. + ## Configuration reference pointers Primary reference: - [Configuration reference - Slack](/gateway/configuration-reference#slack) -High-signal Slack fields: - -- mode/auth: `mode`, `botToken`, `appToken`, `signingSecret`, `webhookPath`, `accounts.*` -- DM access: `dm.enabled`, `dm.policy`, `dm.allowFrom`, `dm.groupEnabled`, `dm.groupChannels` -- channel access: `groupPolicy`, `channels.*`, `channels.*.users`, `channels.*.requireMention` -- threading/history: `replyToMode`, `replyToModeByChatType`, `thread.*`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` -- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb` -- ops/features: `configWrites`, `commands.native`, `slashCommand.*`, `actions.*`, `userToken`, `userTokenReadOnly` + High-signal Slack fields: + - mode/auth: `mode`, `botToken`, `appToken`, `signingSecret`, `webhookPath`, `accounts.*` + - DM access: `dm.enabled`, `dmPolicy`, `allowFrom` (legacy: `dm.policy`, `dm.allowFrom`), `dm.groupEnabled`, `dm.groupChannels` + - channel access: `groupPolicy`, `channels.*`, `channels.*.users`, `channels.*.requireMention` + - threading/history: `replyToMode`, `replyToModeByChatType`, `thread.*`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` + - delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb` + - ops/features: `configWrites`, `commands.native`, `slashCommand.*`, `actions.*`, `userToken`, `userTokenReadOnly` ## Related diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 630bf5f4b0da1..7e1d95d2febb9 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -221,23 +221,20 @@ curl "https://api.telegram.org/bot/getUpdates" ## Feature reference - - OpenClaw can stream partial replies with Telegram draft bubbles (`sendMessageDraft`). + + OpenClaw can stream partial replies by sending a temporary Telegram message and editing it as text arrives. - Requirements: + Requirement: - `channels.telegram.streamMode` is not `"off"` (default: `"partial"`) - - private chat - - inbound update includes `message_thread_id` - - bot topics are enabled (`getMe().has_topics_enabled`) Modes: - - `off`: no draft streaming - - `partial`: frequent draft updates from partial text - - `block`: chunked draft updates using `channels.telegram.draftChunk` + - `off`: no live preview + - `partial`: frequent preview updates from partial text + - `block`: chunked preview updates using `channels.telegram.draftChunk` - `draftChunk` defaults for block mode: + `draftChunk` defaults for `streamMode: "block"`: - `minChars: 200` - `maxChars: 800` @@ -245,13 +242,17 @@ curl "https://api.telegram.org/bot/getUpdates" `maxChars` is clamped by `channels.telegram.textChunkLimit`. - Draft streaming is DM-only; groups/channels do not use draft bubbles. + This works in direct chats and groups/topics. - If you want early real Telegram messages instead of draft updates, use block streaming (`channels.telegram.blockStreaming: true`). + For text-only replies, OpenClaw keeps the same preview message and performs a final edit in place (no second message). + + For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message. + + `streamMode` is separate from block streaming. When block streaming is explicitly enabled for Telegram, OpenClaw skips the preview stream to avoid double-streaming. Telegram-only reasoning stream: - - `/reasoning stream` sends reasoning to the draft bubble while generating + - `/reasoning stream` sends reasoning to the live preview while generating - final answer is sent without reasoning text @@ -570,6 +571,23 @@ curl "https://api.telegram.org/bot/getUpdates" + + `ackReaction` sends an acknowledgement emoji while OpenClaw is processing an inbound message. + + Resolution order: + + - `channels.telegram.accounts..ackReaction` + - `channels.telegram.ackReaction` + - `messages.ackReaction` + - agent identity emoji fallback (`agents.list[].identity.emoji`, else "👀") + + Notes: + + - Telegram expects unicode emoji (for example "👀"). + - Use `""` to disable the reaction for a channel or account. + + + Channel config writes are enabled by default (`configWrites !== false`). @@ -703,7 +721,7 @@ Primary reference: - `channels.telegram.textChunkLimit`: outbound chunk size (chars). - `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. - `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true). -- `channels.telegram.streamMode`: `off | partial | block` (draft streaming). +- `channels.telegram.streamMode`: `off | partial | block` (live stream preview). - `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB). - `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter). - `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to disabled on Node 22 to avoid Happy Eyeballs timeouts. @@ -727,7 +745,7 @@ Telegram-specific high-signal fields: - access control: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`, `groups.*.topics.*` - command/menu: `commands.native`, `customCommands` - threading/replies: `replyToMode` -- streaming: `streamMode`, `draftChunk`, `blockStreaming` +- streaming: `streamMode` (preview), `draftChunk`, `blockStreaming` - formatting/delivery: `textChunkLimit`, `chunkMode`, `linkPreview`, `responsePrefix` - media/network: `mediaMaxMb`, `timeoutSeconds`, `retry`, `network.autoSelectFamily`, `proxy` - webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost` @@ -739,4 +757,5 @@ Telegram-specific high-signal fields: - [Pairing](/channels/pairing) - [Channel routing](/channels/channel-routing) +- [Multi-agent routing](/concepts/multi-agent) - [Troubleshooting](/channels/troubleshooting) diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index 23bbb38f747cf..a6fb427bdc2d5 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -144,6 +144,8 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch `allowFrom` accepts E.164-style numbers (normalized internally). + Multi-account override: `channels.whatsapp.accounts..dmPolicy` (and `allowFrom`) take precedence over channel-level defaults for that account. + Runtime behavior details: - pairings are persisted in channel allow-store and merged with configured `allowFrom` @@ -167,6 +169,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch Sender allowlist fallback: - if `groupAllowFrom` is unset, runtime falls back to `allowFrom` when available + - sender allowlists are evaluated before mention/reply activation Note: if no `channels.whatsapp` block exists at all, runtime group-policy fallback is effectively `open`. @@ -181,6 +184,11 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch - configured mention regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`) - implicit reply-to-bot detection (reply sender matches bot identity) + Security note: + + - quote/reply only satisfies mention gating; it does **not** grant sender authorization + - with `groupPolicy: "allowlist"`, non-allowlisted senders are still blocked even if they reply to an allowlisted user's message + Session-level activation command: - `/activation mention` @@ -405,6 +413,7 @@ Behavior notes: - `groupAllowFrom` / `allowFrom` - `groups` allowlist entries - mention gating (`requireMention` + mention patterns) + - duplicate keys in `openclaw.json` (JSON5): later entries override earlier ones, so keep a single `groupPolicy` per scope @@ -431,4 +440,5 @@ High-signal WhatsApp fields: - [Pairing](/channels/pairing) - [Channel routing](/channels/channel-routing) +- [Multi-agent routing](/concepts/multi-agent) - [Troubleshooting](/channels/troubleshooting) diff --git a/docs/channels/zalo.md b/docs/channels/zalo.md index c595c5e6dde83..cda126f564913 100644 --- a/docs/channels/zalo.md +++ b/docs/channels/zalo.md @@ -115,6 +115,9 @@ Multi-account support: use `channels.zalo.accounts` with per-account tokens and - Webhook URL must use HTTPS. - Zalo sends events with `X-Bot-Api-Secret-Token` header for verification. - Gateway HTTP handles webhook requests at `channels.zalo.webhookPath` (defaults to the webhook URL path). + - Requests must use `Content-Type: application/json` (or `+json` media types). + - Duplicate events (`event_name + message_id`) are ignored for a short replay window. + - Burst traffic is rate-limited per path/source and may return HTTP 429. **Note:** getUpdates (polling) and webhook are mutually exclusive per Zalo API docs. diff --git a/docs/ci.md b/docs/ci.md index cdf5b126a2889..64d4df0ec1c47 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -34,12 +34,11 @@ Jobs are ordered so cheap checks fail before expensive ones run: ## Runners -| Runner | Jobs | -| ------------------------------- | ----------------------------- | -| `blacksmith-4vcpu-ubuntu-2404` | Most Linux jobs | -| `blacksmith-4vcpu-windows-2025` | `checks-windows` | -| `macos-latest` | `macos`, `ios` | -| `ubuntu-latest` | Scope detection (lightweight) | +| Runner | Jobs | +| -------------------------------- | ------------------------------------------ | +| `blacksmith-16vcpu-ubuntu-2404` | Most Linux jobs, including scope detection | +| `blacksmith-16vcpu-windows-2025` | `checks-windows` | +| `macos-latest` | `macos`, `ios` | ## Local Equivalents diff --git a/docs/cli/acp.md b/docs/cli/acp.md index 46b78cce6f51d..9535509016d23 100644 --- a/docs/cli/acp.md +++ b/docs/cli/acp.md @@ -21,6 +21,9 @@ openclaw acp # Remote Gateway openclaw acp --url wss://gateway-host:18789 --token +# Remote Gateway (token from file) +openclaw acp --url wss://gateway-host:18789 --token-file ~/.openclaw/gateway.token + # Attach to an existing session key openclaw acp --session agent:main:main @@ -40,7 +43,7 @@ It spawns the ACP bridge and lets you type prompts interactively. openclaw acp client # Point the spawned bridge at a remote Gateway -openclaw acp client --server-args --url wss://gateway-host:18789 --token +openclaw acp client --server-args --url wss://gateway-host:18789 --token-file ~/.openclaw/gateway.token # Override the server command (default: openclaw) openclaw acp client --server "node" --server-args openclaw.mjs acp --url ws://127.0.0.1:19001 @@ -66,6 +69,8 @@ Example direct run (no config write): ```bash openclaw acp --url wss://gateway-host:18789 --token +# preferred for local process safety +openclaw acp --url wss://gateway-host:18789 --token-file ~/.openclaw/gateway.token ``` ## Selecting agents @@ -153,7 +158,9 @@ Learn more about session keys at [/concepts/session](/concepts/session). - `--url `: Gateway WebSocket URL (defaults to gateway.remote.url when configured). - `--token `: Gateway auth token. +- `--token-file `: read Gateway auth token from file. - `--password `: Gateway auth password. +- `--password-file `: read Gateway auth password from file. - `--session `: default session key. - `--session-label