Compare commits
9 Commits
runtime-la
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e705c69ba8 | |||
| 8f0a15d906 | |||
|
|
849972ab5c | ||
|
|
d396760a4e | ||
|
|
6ec2c10924 | ||
|
|
d5f0e9f437 | ||
|
|
baddb2f13d | ||
|
|
1fe544c984 | ||
| 48c05db842 |
38
.github/workflows/ci.yml
vendored
38
.github/workflows/ci.yml
vendored
@ -1,38 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
run: |
|
|
||||||
corepack enable
|
|
||||||
corepack prepare pnpm@10.28.2 --activate
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: pnpm test
|
|
||||||
|
|
||||||
- name: Typecheck
|
|
||||||
run: pnpm typecheck
|
|
||||||
|
|
||||||
- name: Verify npm package contents
|
|
||||||
run: pnpm pack:check
|
|
||||||
@ -1,25 +1,24 @@
|
|||||||
name: Deploy
|
name: Pipeline
|
||||||
|
|
||||||
env:
|
|
||||||
VAULT_ADDR: https://vault.svc.plus
|
|
||||||
|
|
||||||
|
# 单一流水线,三个串联 stage:build -> publish(npm) -> deploy。
|
||||||
|
# build : 安装/测试/类型检查/包内容校验(PR 与 push 都跑)。
|
||||||
|
# publish : 发布到 npm(仅 release / 版本 tag / 手动触发;needs build)。
|
||||||
|
# deploy : SSH 安装到 ubuntu@openclaw.svc.plus(needs publish)。
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
tags:
|
tags:
|
||||||
- "[0-9]+.[0-9]+.[0-9]+"
|
- "[0-9]+.[0-9]+.[0-9]+"
|
||||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||||
|
pull_request:
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- Publish
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: "Plugin version to install (e.g. 2026.6.1). Leave blank to use the release tag."
|
description: "Plugin version to install (e.g. 2026.6.1). Leave blank to use package.json."
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
force:
|
force:
|
||||||
@ -31,27 +30,150 @@ on:
|
|||||||
- "false"
|
- "false"
|
||||||
- "true"
|
- "true"
|
||||||
|
|
||||||
|
env:
|
||||||
|
VAULT_ADDR: https://vault.svc.plus
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: openclaw-deploy
|
group: pipeline-${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
install-on-host:
|
# ───────────────────────── Stage 1: build ─────────────────────────
|
||||||
name: Update plugin on ubuntu@openclaw.svc.plus
|
build:
|
||||||
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'release')
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
run: |
|
||||||
|
corepack enable
|
||||||
|
corepack prepare pnpm@10.28.2 --activate
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm test
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: pnpm typecheck
|
||||||
|
|
||||||
|
- name: Verify npm package contents
|
||||||
|
run: pnpm pack:check
|
||||||
|
|
||||||
|
# ──────────────────────── Stage 2: publish ────────────────────────
|
||||||
|
publish:
|
||||||
|
name: Publish to npm
|
||||||
|
needs: build
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'release' ||
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.meta.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
|
|
||||||
|
- name: Load Vault secrets
|
||||||
|
id: vault
|
||||||
|
uses: hashicorp/vault-action@v2
|
||||||
|
with:
|
||||||
|
url: ${{ env.VAULT_ADDR }}
|
||||||
|
method: jwt
|
||||||
|
role: github-actions-openclaw-multi-session-plugins
|
||||||
|
jwtGithubAudience: vault
|
||||||
|
secrets: |
|
||||||
|
kv/data/github-actions/openclaw-multi-session-plugins NPM_TOKEN | NPM_TOKEN
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
registry-url: https://registry.npmjs.org/
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
run: |
|
||||||
|
corepack enable
|
||||||
|
corepack prepare pnpm@10.28.2 --activate
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Resolve package metadata
|
||||||
|
id: meta
|
||||||
|
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Verify npm publish access
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ steps.vault.outputs.NPM_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
name="$(node -p "require('./package.json').name")"
|
||||||
|
version="$(node -p "require('./package.json').version")"
|
||||||
|
user="$(npm whoami 2>/dev/null || true)"
|
||||||
|
if [ -z "${user}" ]; then
|
||||||
|
echo "::error::NPM_TOKEN is not valid for npm publish. Create an npm automation token for an account that can publish ${name}, then store it in Vault as NPM_TOKEN."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if npm view "${name}" name >/dev/null 2>&1; then
|
||||||
|
echo "::notice::Publishing ${name}@${version} as npm user ${user}; package already exists."
|
||||||
|
else
|
||||||
|
echo "::notice::Publishing ${name}@${version} as npm user ${user}; npm will create this public package on first publish."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check published version
|
||||||
|
id: published
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
name="$(node -p "require('./package.json').name")"
|
||||||
|
version="$(node -p "require('./package.json').version")"
|
||||||
|
if npm view "${name}@${version}" version >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "${name}@${version} is already published; skipping npm publish."
|
||||||
|
else
|
||||||
|
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
if: steps.published.outputs.exists != 'true'
|
||||||
|
run: npm publish --provenance --access public
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ steps.vault.outputs.NPM_TOKEN }}
|
||||||
|
|
||||||
|
# ───────────────────────── Stage 3: deploy ────────────────────────
|
||||||
|
deploy:
|
||||||
|
name: Update plugin on ubuntu@openclaw.svc.plus
|
||||||
|
needs: publish
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: openclaw-deploy
|
||||||
|
cancel-in-progress: false
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
env:
|
env:
|
||||||
SSH_HOST: ubuntu@openclaw.svc.plus
|
SSH_HOST: ubuntu@openclaw.svc.plus
|
||||||
PLUGIN_NAME: openclaw-multi-session-plugins
|
PLUGIN_NAME: openclaw-multi-session-plugins
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source
|
- name: Checkout source
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
|
||||||
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.ref }}
|
|
||||||
|
|
||||||
- name: Load Vault secrets
|
- name: Load Vault secrets
|
||||||
id: vault
|
id: vault
|
||||||
@ -69,19 +191,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Resolve target version
|
- name: Resolve target version
|
||||||
id: version
|
id: version
|
||||||
|
env:
|
||||||
|
INPUT_VERSION: ${{ inputs.version }}
|
||||||
|
PUBLISH_VERSION: ${{ needs.publish.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
if [ -n "${{ inputs.version }}" ]; then
|
if [ -n "${INPUT_VERSION}" ]; then
|
||||||
value="${{ inputs.version }}"
|
value="${INPUT_VERSION}"
|
||||||
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
|
elif [ -n "${PUBLISH_VERSION}" ]; then
|
||||||
if [ ! -f package.json ]; then
|
value="${PUBLISH_VERSION}"
|
||||||
echo "::error::package.json not found after checking out workflow_run source"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
value="$(node -p "require('./package.json').version")"
|
|
||||||
else
|
else
|
||||||
ref="${GITHUB_REF_NAME:-}"
|
value="$(node -p "require('./package.json').version")"
|
||||||
value="${ref}"
|
|
||||||
fi
|
fi
|
||||||
value="${value##*/}"
|
value="${value##*/}"
|
||||||
value="${value#v}"
|
value="${value#v}"
|
||||||
@ -89,10 +209,6 @@ jobs:
|
|||||||
echo "::error::Resolved value '${value}' is not a valid X.Y.Z version"
|
echo "::error::Resolved value '${value}' is not a valid X.Y.Z version"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ -z "${value}" ]; then
|
|
||||||
echo "::error::Could not resolve plugin version from inputs or GITHUB_REF_NAME"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "value=${value}" >> "$GITHUB_OUTPUT"
|
echo "value=${value}" >> "$GITHUB_OUTPUT"
|
||||||
echo "Resolved plugin version: ${value}"
|
echo "Resolved plugin version: ${value}"
|
||||||
|
|
||||||
@ -136,8 +252,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
pnpm test
|
|
||||||
pnpm typecheck
|
|
||||||
pnpm pack
|
pnpm pack
|
||||||
tarball="${PLUGIN_NAME}-${VERSION}.tgz"
|
tarball="${PLUGIN_NAME}-${VERSION}.tgz"
|
||||||
test -f "${tarball}"
|
test -f "${tarball}"
|
||||||
91
.github/workflows/publish.yml
vendored
91
.github/workflows/publish.yml
vendored
@ -1,91 +0,0 @@
|
|||||||
name: Publish
|
|
||||||
|
|
||||||
env:
|
|
||||||
VAULT_ADDR: https://vault.svc.plus
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- published
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
name: Publish to npm
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
||||||
|
|
||||||
- name: Load Vault secrets
|
|
||||||
id: vault
|
|
||||||
uses: hashicorp/vault-action@v2
|
|
||||||
with:
|
|
||||||
url: ${{ env.VAULT_ADDR }}
|
|
||||||
method: jwt
|
|
||||||
role: github-actions-openclaw-multi-session-plugins
|
|
||||||
jwtGithubAudience: vault
|
|
||||||
secrets: |
|
|
||||||
kv/data/github-actions/openclaw-multi-session-plugins NPM_TOKEN | NPM_TOKEN
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
registry-url: https://registry.npmjs.org/
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
run: |
|
|
||||||
corepack enable
|
|
||||||
corepack prepare pnpm@10.28.2 --activate
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: pnpm test
|
|
||||||
|
|
||||||
- name: Typecheck
|
|
||||||
run: pnpm typecheck
|
|
||||||
|
|
||||||
- name: Verify npm publish access
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
name="$(node -p "require('./package.json').name")"
|
|
||||||
version="$(node -p "require('./package.json').version")"
|
|
||||||
user="$(npm whoami 2>/dev/null || true)"
|
|
||||||
if [ -z "${user}" ]; then
|
|
||||||
echo "::error::NPM_TOKEN is not valid for npm publish. Create an npm automation token for an account that can publish ${name}, then save it as the repository secret NPM_TOKEN."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if npm view "${name}" name >/dev/null 2>&1; then
|
|
||||||
echo "::notice::Publishing ${name}@${version} as npm user ${user}; package already exists."
|
|
||||||
else
|
|
||||||
echo "::notice::Publishing ${name}@${version} as npm user ${user}; npm will create this public package on first publish."
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ steps.vault.outputs.NPM_TOKEN }}
|
|
||||||
|
|
||||||
- name: Check published version
|
|
||||||
id: published
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
name="$(node -p "require('./package.json').name")"
|
|
||||||
version="$(node -p "require('./package.json').version")"
|
|
||||||
if npm view "${name}@${version}" version >/dev/null 2>&1; then
|
|
||||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "${name}@${version} is already published; skipping npm publish."
|
|
||||||
else
|
|
||||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Publish
|
|
||||||
if: steps.published.outputs.exists != 'true'
|
|
||||||
run: npm publish --provenance --access public
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ steps.vault.outputs.NPM_TOKEN }}
|
|
||||||
53
.github/workflows/runtime-release.yaml
vendored
53
.github/workflows/runtime-release.yaml
vendored
@ -6,7 +6,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "**/*.ts"
|
- "**/*.ts"
|
||||||
- package.json
|
- package.json
|
||||||
- pnpm-lock.yaml
|
- package-lock.json
|
||||||
- openclaw.plugin.json
|
- openclaw.plugin.json
|
||||||
- .github/workflows/runtime-release.yaml
|
- .github/workflows/runtime-release.yaml
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@ -25,31 +25,26 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v3
|
|
||||||
with:
|
|
||||||
version: 9
|
|
||||||
|
|
||||||
- name: Use Node.js 22
|
- name: Use Node.js 22
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: pnpm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Build runtime asset
|
- name: Build runtime asset
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
root="dist/runtime/openclaw-multi-session-plugins"
|
root="dist/runtime/openclaw-multi-session-plugins"
|
||||||
mkdir -p "${root}" dist/assets
|
mkdir -p "${root}/dist" dist/assets
|
||||||
|
|
||||||
# Maintain necessary file structure required by openclaw loader
|
# Maintain necessary file structure required by openclaw loader
|
||||||
cp -a dist "${root}/"
|
cp -a dist/index.js dist/index.d.ts dist/src "${root}/dist/"
|
||||||
cp openclaw.plugin.json package.json "${root}/"
|
cp openclaw.plugin.json package.json "${root}/"
|
||||||
|
|
||||||
tar -czf "dist/assets/openclaw-multi-session-plugins-runtime-all.tar.gz" \
|
tar -czf "dist/assets/openclaw-multi-session-plugins-runtime-all.tar.gz" \
|
||||||
@ -83,16 +78,30 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
tag="runtime-${GITHUB_SHA::12}"
|
|
||||||
cat dist/SHA256SUMS-* | sort -u > dist/SHA256SUMS || true
|
cat dist/SHA256SUMS-* | sort -u > dist/SHA256SUMS || true
|
||||||
rm -f dist/SHA256SUMS-*
|
rm -f dist/SHA256SUMS-*
|
||||||
if gh release view "${tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
|
|
||||||
gh release upload "${tag}" dist/*.tar.gz dist/SHA256SUMS \
|
# Publish (or refresh) a release with the runtime tarball + checksums.
|
||||||
--repo "${GITHUB_REPOSITORY}" --clobber
|
# --latest=false keeps GitHub's "Latest release" pointer free for the
|
||||||
else
|
# human-facing v* tags; deployments pull via explicit tag URLs instead.
|
||||||
gh release create "${tag}" dist/*.tar.gz dist/SHA256SUMS \
|
publish_release() {
|
||||||
--repo "${GITHUB_REPOSITORY}" \
|
local tag="$1" title="$2"
|
||||||
--target "${GITHUB_SHA}" \
|
if gh release view "${tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
|
||||||
--title "OpenClaw Plugin runtime ${GITHUB_SHA::12}" \
|
gh release upload "${tag}" dist/*.tar.gz dist/SHA256SUMS \
|
||||||
--notes "Prebuilt Plugin assets. No target-host build or Nix profile installation required."
|
--repo "${GITHUB_REPOSITORY}" --clobber
|
||||||
fi
|
else
|
||||||
|
gh release create "${tag}" dist/*.tar.gz dist/SHA256SUMS \
|
||||||
|
--repo "${GITHUB_REPOSITORY}" \
|
||||||
|
--target "${GITHUB_SHA}" \
|
||||||
|
--latest=false \
|
||||||
|
--title "${title}" \
|
||||||
|
--notes "Prebuilt Plugin assets. No target-host build or Nix profile installation required."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Immutable per-commit release for traceability.
|
||||||
|
publish_release "runtime-${GITHUB_SHA::12}" "OpenClaw Plugin runtime ${GITHUB_SHA::12}"
|
||||||
|
# Stable moving release so deployments resolve a deterministic URL
|
||||||
|
# (releases/download/runtime-latest/...) instead of the mutable
|
||||||
|
# /releases/latest/ pointer, which collides with other release tracks.
|
||||||
|
publish_release "runtime-latest" "OpenClaw Plugin runtime (latest)"
|
||||||
|
|||||||
44
.github/workflows/validate-release-pr.yml
vendored
Normal file
44
.github/workflows/validate-release-pr.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
name: Validate Release PR
|
||||||
|
|
||||||
|
# release/* 分支的发布策略门禁:仅接受 hotfix/* 或带 cherry-pick/backport 标签的 PR。
|
||||||
|
# 详见 iac_modules/docs/tldr-github-branch-model.md
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-release-source:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.base_ref, 'release/')
|
||||||
|
steps:
|
||||||
|
- name: Check PR source branch
|
||||||
|
run: |
|
||||||
|
SRC="${{ github.head_ref }}"
|
||||||
|
TGT="${{ github.base_ref }}"
|
||||||
|
LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}"
|
||||||
|
|
||||||
|
echo "🔍 Validating PR into release branch"
|
||||||
|
echo " source: $SRC"
|
||||||
|
echo " target: $TGT"
|
||||||
|
echo " labels: $LABELS"
|
||||||
|
|
||||||
|
if [[ "$SRC" =~ ^hotfix/ ]]; then
|
||||||
|
echo "✅ Allowed: hotfix/* branch"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$LABELS" =~ (^|,)(cherry-pick|backport)(,|$) ]]; then
|
||||||
|
echo "✅ Allowed: cherry-pick/backport labeled PR"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "❌ Rejected."
|
||||||
|
echo "release/* 仅接受:"
|
||||||
|
echo " - 来自 hotfix/* 的 PR"
|
||||||
|
echo " - 带 cherry-pick 或 backport 标签的 PR(已验证 feature 的 backport/cherry-pick)"
|
||||||
|
echo "禁止从 main / develop / feature/* 直接合并到 release/*。"
|
||||||
|
exit 1
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -3,3 +3,9 @@ coverage/
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
repomix-output.xml
|
repomix-output.xml
|
||||||
openclaw-multi-session-plugins-*.tgz
|
openclaw-multi-session-plugins-*.tgz
|
||||||
|
|
||||||
|
# Runtime release build artifacts (produced by the runtime-release workflow /
|
||||||
|
# local packaging; published to GitHub Releases, not committed). The compiled
|
||||||
|
# library under dist/ and dist/src/ stays tracked.
|
||||||
|
dist/assets/
|
||||||
|
dist/runtime/
|
||||||
|
|||||||
1
dist/index.d.ts
vendored
1
dist/index.d.ts
vendored
@ -1,3 +1,4 @@
|
|||||||
|
export declare function lastAssistantText(messages: unknown): string | undefined;
|
||||||
declare const plugin: {
|
declare const plugin: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
58
dist/index.js
vendored
58
dist/index.js
vendored
@ -1,7 +1,7 @@
|
|||||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||||
import { getPluginRuntimeGatewayRequestScope } from "openclaw/plugin-sdk/plugin-runtime";
|
import { getPluginRuntimeGatewayRequestScope } from "openclaw/plugin-sdk/plugin-runtime";
|
||||||
import { collectAndSnapshotXWorkmateArtifacts, exportXWorkmateArtifacts, prepareXWorkmateArtifacts, readXWorkmateArtifact, formatArtifactManifestMarkdown, } from "./src/exportArtifacts.js";
|
import { collectAndSnapshotXWorkmateArtifacts, exportXWorkmateArtifacts, prepareXWorkmateArtifacts, readXWorkmateArtifact, formatArtifactManifestMarkdown, } from "./src/exportArtifacts.js";
|
||||||
import { getXWorkmateTaskSnapshot, recordXWorkmateSessionMapping, registerXWorkmateSessionExtension, } from "./src/taskState.js";
|
import { getXWorkmateTaskSnapshot, recordXWorkmateSessionMapping, recordXWorkmateTaskRunStarted, recordXWorkmateTaskRunTerminal, registerXWorkmateSessionExtension, } from "./src/taskState.js";
|
||||||
function scopedGatewayParams(params) {
|
function scopedGatewayParams(params) {
|
||||||
const sessionScope = getPluginRuntimeGatewayRequestScope()?.sessionScope;
|
const sessionScope = getPluginRuntimeGatewayRequestScope()?.sessionScope;
|
||||||
const runScope = resolveRunScope({ sessionScope });
|
const runScope = resolveRunScope({ sessionScope });
|
||||||
@ -33,6 +33,37 @@ function resolveRunScope(ctx) {
|
|||||||
function stringParam(value) {
|
function stringParam(value) {
|
||||||
return typeof value === "string" ? value.trim() : "";
|
return typeof value === "string" ? value.trim() : "";
|
||||||
}
|
}
|
||||||
|
export function lastAssistantText(messages) {
|
||||||
|
if (!Array.isArray(messages))
|
||||||
|
return undefined;
|
||||||
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||||
|
const message = messages[index];
|
||||||
|
if (!message || typeof message !== "object")
|
||||||
|
continue;
|
||||||
|
const record = message;
|
||||||
|
if (stringParam(record.role).toLowerCase() !== "assistant")
|
||||||
|
continue;
|
||||||
|
const content = record.content;
|
||||||
|
if (typeof content === "string" && content.trim())
|
||||||
|
return content.trim();
|
||||||
|
if (!Array.isArray(content))
|
||||||
|
continue;
|
||||||
|
const text = content
|
||||||
|
.map((block) => {
|
||||||
|
if (!block || typeof block !== "object")
|
||||||
|
return "";
|
||||||
|
const item = block;
|
||||||
|
const type = stringParam(item.type).toLowerCase();
|
||||||
|
return type === "text" || type === "output_text" ? stringParam(item.text) : "";
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
if (text)
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const plugin = definePluginEntry({
|
const plugin = definePluginEntry({
|
||||||
id: "openclaw-multi-session-plugins",
|
id: "openclaw-multi-session-plugins",
|
||||||
name: "openclaw-multi-session-plugins",
|
name: "openclaw-multi-session-plugins",
|
||||||
@ -65,6 +96,26 @@ function register(api) {
|
|||||||
api.logger?.warn?.(`xworkmate session_start preparation failed: ${String(error)}`);
|
api.logger?.warn?.(`xworkmate session_start preparation failed: ${String(error)}`);
|
||||||
}
|
}
|
||||||
}, { name: "openclaw-multi-session-plugins.session-start" });
|
}, { name: "openclaw-multi-session-plugins.session-start" });
|
||||||
|
api.on("agent_end", async (event, ctx) => {
|
||||||
|
try {
|
||||||
|
const openclawSessionKey = stringParam(ctx?.sessionKey ?? event?.sessionKey);
|
||||||
|
const runId = stringParam(event?.runId ?? ctx?.runId);
|
||||||
|
if (!openclawSessionKey || !runId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await recordXWorkmateTaskRunTerminal({
|
||||||
|
api,
|
||||||
|
openclawSessionKey,
|
||||||
|
runId,
|
||||||
|
success: event?.success === true,
|
||||||
|
output: lastAssistantText(event?.messages),
|
||||||
|
error: event?.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
api.logger?.warn?.(`xworkmate agent_end state capture failed: ${String(error)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
api.registerGatewayMethod("xworkmate.session.prepare", async (opts) => {
|
api.registerGatewayMethod("xworkmate.session.prepare", async (opts) => {
|
||||||
try {
|
try {
|
||||||
const params = scopedGatewayParams(opts.params);
|
const params = scopedGatewayParams(opts.params);
|
||||||
@ -82,6 +133,11 @@ function register(api) {
|
|||||||
config: api.config,
|
config: api.config,
|
||||||
pluginConfig: api.pluginConfig,
|
pluginConfig: api.pluginConfig,
|
||||||
});
|
});
|
||||||
|
await recordXWorkmateTaskRunStarted({
|
||||||
|
api,
|
||||||
|
openclawSessionKey: mapping.openclawSessionKey,
|
||||||
|
runId: stringParam(params.runId),
|
||||||
|
});
|
||||||
opts.respond(true, {
|
opts.respond(true, {
|
||||||
...payload,
|
...payload,
|
||||||
mapping,
|
mapping,
|
||||||
|
|||||||
25
dist/src/taskState.d.ts
vendored
25
dist/src/taskState.d.ts
vendored
@ -1,5 +1,6 @@
|
|||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||||
export declare const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
export declare const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
||||||
|
export declare const XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE = "xworkmate.taskRuns";
|
||||||
export type XWorkmateTaskMetadataV1 = {
|
export type XWorkmateTaskMetadataV1 = {
|
||||||
schemaVersion: 1;
|
schemaVersion: 1;
|
||||||
appThreadKey: string;
|
appThreadKey: string;
|
||||||
@ -27,6 +28,17 @@ export type XWorkmateTaskLookupError = {
|
|||||||
mapping?: XWorkmateSessionMappingV1;
|
mapping?: XWorkmateSessionMappingV1;
|
||||||
expectedArtifactDirs?: string[];
|
expectedArtifactDirs?: string[];
|
||||||
};
|
};
|
||||||
|
export type XWorkmateRecordedTaskRunV1 = {
|
||||||
|
schemaVersion: 1;
|
||||||
|
runId: string;
|
||||||
|
status: "running" | "completed" | "failed";
|
||||||
|
success: boolean;
|
||||||
|
startedAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
completedAt?: string;
|
||||||
|
output?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
export declare function registerXWorkmateSessionExtension(api: OpenClawPluginApi): void;
|
export declare function registerXWorkmateSessionExtension(api: OpenClawPluginApi): void;
|
||||||
export declare function recordXWorkmateSessionMapping(input: {
|
export declare function recordXWorkmateSessionMapping(input: {
|
||||||
api: OpenClawPluginApi;
|
api: OpenClawPluginApi;
|
||||||
@ -34,6 +46,19 @@ export declare function recordXWorkmateSessionMapping(input: {
|
|||||||
artifactScope?: string;
|
artifactScope?: string;
|
||||||
source?: XWorkmateSessionMappingSource;
|
source?: XWorkmateSessionMappingSource;
|
||||||
}): Promise<XWorkmateSessionMappingV1>;
|
}): Promise<XWorkmateSessionMappingV1>;
|
||||||
|
export declare function recordXWorkmateTaskRunStarted(input: {
|
||||||
|
api: OpenClawPluginApi;
|
||||||
|
openclawSessionKey: string;
|
||||||
|
runId: string;
|
||||||
|
}): Promise<XWorkmateRecordedTaskRunV1>;
|
||||||
|
export declare function recordXWorkmateTaskRunTerminal(input: {
|
||||||
|
api: OpenClawPluginApi;
|
||||||
|
openclawSessionKey: string;
|
||||||
|
runId: string;
|
||||||
|
success: boolean;
|
||||||
|
output?: unknown;
|
||||||
|
error?: unknown;
|
||||||
|
}): Promise<XWorkmateRecordedTaskRunV1>;
|
||||||
export declare function getXWorkmateTaskSnapshot(input: {
|
export declare function getXWorkmateTaskSnapshot(input: {
|
||||||
api: OpenClawPluginApi;
|
api: OpenClawPluginApi;
|
||||||
params: Record<string, unknown>;
|
params: Record<string, unknown>;
|
||||||
|
|||||||
169
dist/src/taskState.js
vendored
169
dist/src/taskState.js
vendored
@ -2,6 +2,8 @@ import { exportXWorkmateArtifacts } from "./exportArtifacts.js";
|
|||||||
import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js";
|
import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js";
|
||||||
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||||
export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
||||||
|
export const XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE = "xworkmate.taskRuns";
|
||||||
|
const MAX_RECORDED_TASK_RUNS = 32;
|
||||||
export function registerXWorkmateSessionExtension(api) {
|
export function registerXWorkmateSessionExtension(api) {
|
||||||
const registerExtension = api.session?.state?.registerSessionExtension ?? api.registerSessionExtension;
|
const registerExtension = api.session?.state?.registerSessionExtension ?? api.registerSessionExtension;
|
||||||
if (typeof registerExtension !== "function") {
|
if (typeof registerExtension !== "function") {
|
||||||
@ -29,6 +31,30 @@ export async function recordXWorkmateSessionMapping(input) {
|
|||||||
source: input.source ?? "bridge_prepare",
|
source: input.source ?? "bridge_prepare",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
export async function recordXWorkmateTaskRunStarted(input) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return upsertXWorkmateTaskRun(input.api, {
|
||||||
|
openclawSessionKey: requiredString(input.openclawSessionKey, "openclawSessionKey required"),
|
||||||
|
runId: requiredString(input.runId, "runId required"),
|
||||||
|
status: "running",
|
||||||
|
success: false,
|
||||||
|
startedAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function recordXWorkmateTaskRunTerminal(input) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return upsertXWorkmateTaskRun(input.api, {
|
||||||
|
openclawSessionKey: requiredString(input.openclawSessionKey, "openclawSessionKey required"),
|
||||||
|
runId: requiredString(input.runId, "runId required"),
|
||||||
|
status: input.success ? "completed" : "failed",
|
||||||
|
success: input.success,
|
||||||
|
updatedAt: now,
|
||||||
|
completedAt: now,
|
||||||
|
output: sanitizeTaskRunOutput(input.output),
|
||||||
|
error: sanitizeTaskRunError(input.error),
|
||||||
|
});
|
||||||
|
}
|
||||||
function normalizeXWorkmateTaskMetadataV1(input) {
|
function normalizeXWorkmateTaskMetadataV1(input) {
|
||||||
const envelope = asRecord(input.xworkmate) ?? asRecord(input.xworkmateMetadata) ?? input;
|
const envelope = asRecord(input.xworkmate) ?? asRecord(input.xworkmateMetadata) ?? input;
|
||||||
const schemaVersion = Number(envelope.schemaVersion ?? 1);
|
const schemaVersion = Number(envelope.schemaVersion ?? 1);
|
||||||
@ -139,9 +165,52 @@ export async function getXWorkmateTaskSnapshot(input) {
|
|||||||
});
|
});
|
||||||
const includeArtifacts = params.includeArtifacts !== false;
|
const includeArtifacts = params.includeArtifacts !== false;
|
||||||
if (!task) {
|
if (!task) {
|
||||||
|
const recordedRun = runId
|
||||||
|
? readXWorkmateTaskRun(input.api, openclawSessionKey, runId)
|
||||||
|
: undefined;
|
||||||
const exported = includeArtifacts && runId
|
const exported = includeArtifacts && runId
|
||||||
? await exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping)
|
? await exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
if (recordedRun) {
|
||||||
|
return {
|
||||||
|
success: recordedRun.status === "running" ? true : recordedRun.success,
|
||||||
|
status: recordedRun.status,
|
||||||
|
taskStatus: recordedRun.status,
|
||||||
|
terminal: recordedRun.status !== "running",
|
||||||
|
terminalSource: recordedRun.status === "running" ? "session_prepare" : "agent_end",
|
||||||
|
mode: "gateway-chat",
|
||||||
|
mapping,
|
||||||
|
appThreadKey: mapping?.appThreadKey ?? appThreadKey,
|
||||||
|
openclawSessionKey,
|
||||||
|
runId,
|
||||||
|
taskId: taskId || runId,
|
||||||
|
task: {
|
||||||
|
taskId: taskId || runId,
|
||||||
|
runId,
|
||||||
|
status: recordedRun.status,
|
||||||
|
success: recordedRun.success,
|
||||||
|
source: "xworkmate_run_state",
|
||||||
|
startedAt: recordedRun.startedAt,
|
||||||
|
updatedAt: recordedRun.updatedAt,
|
||||||
|
completedAt: recordedRun.completedAt,
|
||||||
|
error: recordedRun.error,
|
||||||
|
},
|
||||||
|
output: recordedRun.output,
|
||||||
|
resultSummary: recordedRun.output,
|
||||||
|
error: recordedRun.error,
|
||||||
|
message: recordedRun.output ?? recordedRun.error,
|
||||||
|
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? [],
|
||||||
|
artifactScope: exported?.artifactScope,
|
||||||
|
remoteWorkingDirectory: exported?.remoteWorkingDirectory,
|
||||||
|
remoteWorkspaceRefKind: exported?.remoteWorkspaceRefKind,
|
||||||
|
scopeKind: exported?.scopeKind,
|
||||||
|
artifacts: exported?.artifacts ?? [],
|
||||||
|
constraintSatisfied: exported?.constraintSatisfied,
|
||||||
|
missingRequiredExtensions: exported?.missingRequiredExtensions,
|
||||||
|
warnings: exported?.warnings ?? [],
|
||||||
|
artifactCount: exported?.artifacts.length ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (exported?.artifacts.length) {
|
if (exported?.artifacts.length) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -205,6 +274,106 @@ export async function getXWorkmateTaskSnapshot(input) {
|
|||||||
artifactCount: exported?.artifacts.length ?? 0,
|
artifactCount: exported?.artifacts.length ?? 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
async function upsertXWorkmateTaskRun(api, input) {
|
||||||
|
const patchSessionEntry = resolvePatchSessionEntry(api);
|
||||||
|
if (!patchSessionEntry) {
|
||||||
|
throw new Error("OpenClaw runtime session patch API is unavailable");
|
||||||
|
}
|
||||||
|
let recorded;
|
||||||
|
await patchSessionEntry({
|
||||||
|
sessionKey: input.openclawSessionKey,
|
||||||
|
fallbackEntry: {
|
||||||
|
sessionId: input.openclawSessionKey,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
},
|
||||||
|
preserveActivity: true,
|
||||||
|
update: (entry) => {
|
||||||
|
const runs = readTaskRunsFromEntry(entry);
|
||||||
|
const existing = runs[input.runId];
|
||||||
|
recorded = compactObject({
|
||||||
|
schemaVersion: 1,
|
||||||
|
runId: input.runId,
|
||||||
|
status: input.status,
|
||||||
|
success: input.success,
|
||||||
|
startedAt: existing?.startedAt ?? input.startedAt ?? input.updatedAt,
|
||||||
|
updatedAt: input.updatedAt,
|
||||||
|
completedAt: input.completedAt,
|
||||||
|
output: input.output,
|
||||||
|
error: input.error,
|
||||||
|
});
|
||||||
|
runs[input.runId] = recorded;
|
||||||
|
const boundedRuns = Object.fromEntries(Object.entries(runs)
|
||||||
|
.sort((left, right) => right[1].updatedAt.localeCompare(left[1].updatedAt))
|
||||||
|
.slice(0, MAX_RECORDED_TASK_RUNS));
|
||||||
|
return {
|
||||||
|
pluginExtensions: {
|
||||||
|
...(entry.pluginExtensions ?? {}),
|
||||||
|
[XWORKMATE_PLUGIN_ID]: {
|
||||||
|
...(entry.pluginExtensions?.[XWORKMATE_PLUGIN_ID] ?? {}),
|
||||||
|
[XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE]: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
runs: boundedRuns,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!recorded) {
|
||||||
|
throw new Error("failed to write xworkmate task run state");
|
||||||
|
}
|
||||||
|
return recorded;
|
||||||
|
}
|
||||||
|
function readXWorkmateTaskRun(api, openclawSessionKey, runId) {
|
||||||
|
const entry = resolveGetSessionEntry(api)?.({ sessionKey: openclawSessionKey });
|
||||||
|
return readTaskRunsFromEntry(entry)[runId];
|
||||||
|
}
|
||||||
|
function readTaskRunsFromEntry(entry) {
|
||||||
|
const pluginState = asRecord(entry?.pluginExtensions?.[XWORKMATE_PLUGIN_ID]);
|
||||||
|
const store = asRecord(pluginState?.[XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE]);
|
||||||
|
if (store?.schemaVersion !== 1) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const runs = asRecord(store.runs) ?? {};
|
||||||
|
const result = {};
|
||||||
|
for (const [key, rawValue] of Object.entries(runs)) {
|
||||||
|
const raw = asRecord(rawValue);
|
||||||
|
const runId = optionalString(raw?.runId) || key;
|
||||||
|
const status = optionalString(raw?.status);
|
||||||
|
if (!runId || (status !== "running" && status !== "completed" && status !== "failed")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result[runId] = compactObject({
|
||||||
|
schemaVersion: 1,
|
||||||
|
runId,
|
||||||
|
status,
|
||||||
|
success: raw?.success === true,
|
||||||
|
startedAt: optionalString(raw?.startedAt) || new Date(0).toISOString(),
|
||||||
|
updatedAt: optionalString(raw?.updatedAt) || new Date(0).toISOString(),
|
||||||
|
completedAt: optionalString(raw?.completedAt),
|
||||||
|
output: optionalString(raw?.output),
|
||||||
|
error: optionalString(raw?.error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function sanitizeTaskRunOutput(value) {
|
||||||
|
const raw = optionalString(value);
|
||||||
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return raw.slice(0, 16 * 1024);
|
||||||
|
}
|
||||||
|
function sanitizeTaskRunError(value) {
|
||||||
|
const raw = optionalString(value);
|
||||||
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
.replace(/\b(sk|nvapi)-[A-Za-z0-9._-]+\b/gi, "$1-<redacted>")
|
||||||
|
.replace(/(api[_ -]?key\s*[:=]\s*)[^\s,;]+/gi, "$1<redacted>")
|
||||||
|
.slice(0, 2048);
|
||||||
|
}
|
||||||
async function exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping) {
|
async function exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping) {
|
||||||
return exportXWorkmateArtifacts({
|
return exportXWorkmateArtifacts({
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import plugin from "./index.js";
|
import plugin, { lastAssistantText } from "./index.js";
|
||||||
import { prepareXWorkmateArtifacts } from "./src/exportArtifacts.js";
|
import { prepareXWorkmateArtifacts } from "./src/exportArtifacts.js";
|
||||||
|
|
||||||
type GatewayMethodHandler = Parameters<OpenClawPluginApi["registerGatewayMethod"]>[1];
|
type GatewayMethodHandler = Parameters<OpenClawPluginApi["registerGatewayMethod"]>[1];
|
||||||
@ -14,6 +14,12 @@ type GatewayMethodResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("plugin registration", () => {
|
describe("plugin registration", () => {
|
||||||
|
it("extracts only the final assistant display text", () => {
|
||||||
|
expect(lastAssistantText([
|
||||||
|
{ role: "user", content: "secret prompt" },
|
||||||
|
{ role: "assistant", content: [{ type: "tool_call", text: "ignored" }, { type: "text", text: "完成并已保存。" }] },
|
||||||
|
])).toBe("完成并已保存。");
|
||||||
|
});
|
||||||
it("declares registered agent tools in the manifest contract", () => {
|
it("declares registered agent tools in the manifest contract", () => {
|
||||||
const manifest = JSON.parse(fs.readFileSync("openclaw.plugin.json", "utf8")) as {
|
const manifest = JSON.parse(fs.readFileSync("openclaw.plugin.json", "utf8")) as {
|
||||||
contracts?: { tools?: string[]; sessionScopedTools?: string[] };
|
contracts?: { tools?: string[]; sessionScopedTools?: string[] };
|
||||||
@ -42,6 +48,7 @@ describe("plugin registration", () => {
|
|||||||
tools.push({ tool, options });
|
tools.push({ tool, options });
|
||||||
},
|
},
|
||||||
registerHook: () => undefined,
|
registerHook: () => undefined,
|
||||||
|
on: () => undefined,
|
||||||
} as unknown as OpenClawPluginApi;
|
} as unknown as OpenClawPluginApi;
|
||||||
|
|
||||||
plugin.register(api);
|
plugin.register(api);
|
||||||
@ -73,6 +80,7 @@ describe("plugin registration", () => {
|
|||||||
},
|
},
|
||||||
registerTool: () => undefined,
|
registerTool: () => undefined,
|
||||||
registerHook: () => undefined,
|
registerHook: () => undefined,
|
||||||
|
on: () => undefined,
|
||||||
runtime: {
|
runtime: {
|
||||||
agent: {
|
agent: {
|
||||||
session: {
|
session: {
|
||||||
@ -139,7 +147,7 @@ describe("plugin registration", () => {
|
|||||||
it("registers xworkmate task state against the native session extension and task runtime seams", async () => {
|
it("registers xworkmate task state against the native session extension and task runtime seams", async () => {
|
||||||
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-task-state-"));
|
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-task-state-"));
|
||||||
const methods = new Map<string, GatewayMethodHandler>();
|
const methods = new Map<string, GatewayMethodHandler>();
|
||||||
const hooks = new Map<string, (event: unknown) => Promise<void>>();
|
const hooks = new Map<string, (event: unknown, ctx?: unknown) => Promise<void>>();
|
||||||
const sessionExtensions: Array<Record<string, unknown>> = [];
|
const sessionExtensions: Array<Record<string, unknown>> = [];
|
||||||
const sessionExtensionPatches: Array<Record<string, unknown>> = [];
|
const sessionExtensionPatches: Array<Record<string, unknown>> = [];
|
||||||
const detachedRuntimes: Array<Record<string, unknown>> = [];
|
const detachedRuntimes: Array<Record<string, unknown>> = [];
|
||||||
@ -197,7 +205,10 @@ describe("plugin registration", () => {
|
|||||||
methods.set(method, handler);
|
methods.set(method, handler);
|
||||||
},
|
},
|
||||||
registerTool: () => undefined,
|
registerTool: () => undefined,
|
||||||
registerHook: (event: string, handler: (payload: unknown) => Promise<void>) => {
|
registerHook: (event: string, handler: (payload: unknown, ctx?: unknown) => Promise<void>) => {
|
||||||
|
hooks.set(event, handler);
|
||||||
|
},
|
||||||
|
on: (event: string, handler: (payload: unknown, ctx?: unknown) => Promise<void>) => {
|
||||||
hooks.set(event, handler);
|
hooks.set(event, handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -257,6 +268,15 @@ describe("plugin registration", () => {
|
|||||||
});
|
});
|
||||||
expect(snapshot.payload?.task).toMatchObject({ taskId: "native-task", status: "running" });
|
expect(snapshot.payload?.task).toMatchObject({ taskId: "native-task", status: "running" });
|
||||||
expect(snapshot.payload?.artifacts).toMatchObject([{ relativePath: "reports/final.md" }]);
|
expect(snapshot.payload?.artifacts).toMatchObject([{ relativePath: "reports/final.md" }]);
|
||||||
|
|
||||||
|
await hooks.get("agent_end")?.(
|
||||||
|
{ runId: "turn-1", success: false, error: "401 authentication failed", messages: [{ role: "assistant", content: [{ type: "text", text: "上游认证失败。" }] }] },
|
||||||
|
{ sessionKey: "draft:1780636411666238-3", runId: "turn-1" },
|
||||||
|
);
|
||||||
|
expect(sessionExtensionPatches.at(-1)).toMatchObject({
|
||||||
|
sessionKey: "draft:1780636411666238-3",
|
||||||
|
preserveActivity: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not invent default session or run ids for the optional agent tool", async () => {
|
it("does not invent default session or run ids for the optional agent tool", async () => {
|
||||||
@ -266,6 +286,7 @@ describe("plugin registration", () => {
|
|||||||
pluginConfig: { workspaceDir: path.join(os.tmpdir(), "openclaw-multi-session-tool-test") },
|
pluginConfig: { workspaceDir: path.join(os.tmpdir(), "openclaw-multi-session-tool-test") },
|
||||||
registerGatewayMethod: () => undefined,
|
registerGatewayMethod: () => undefined,
|
||||||
registerHook: () => undefined,
|
registerHook: () => undefined,
|
||||||
|
on: () => undefined,
|
||||||
registerTool: (tool: unknown, options: unknown) => {
|
registerTool: (tool: unknown, options: unknown) => {
|
||||||
tools.push({ tool, options });
|
tools.push({ tool, options });
|
||||||
},
|
},
|
||||||
@ -295,6 +316,7 @@ describe("plugin registration", () => {
|
|||||||
pluginConfig: {},
|
pluginConfig: {},
|
||||||
registerGatewayMethod: () => undefined,
|
registerGatewayMethod: () => undefined,
|
||||||
registerHook: () => undefined,
|
registerHook: () => undefined,
|
||||||
|
on: () => undefined,
|
||||||
registerTool: (tool: unknown, options: { names?: string[] }) => {
|
registerTool: (tool: unknown, options: { names?: string[] }) => {
|
||||||
tools.push({ tool, options });
|
tools.push({ tool, options });
|
||||||
},
|
},
|
||||||
@ -325,6 +347,7 @@ describe("plugin registration", () => {
|
|||||||
pluginConfig: {},
|
pluginConfig: {},
|
||||||
registerGatewayMethod: () => undefined,
|
registerGatewayMethod: () => undefined,
|
||||||
registerHook: () => undefined,
|
registerHook: () => undefined,
|
||||||
|
on: () => undefined,
|
||||||
registerTool: (tool: unknown, options: unknown) => {
|
registerTool: (tool: unknown, options: unknown) => {
|
||||||
tools.push({ tool, options });
|
tools.push({ tool, options });
|
||||||
},
|
},
|
||||||
|
|||||||
55
index.ts
55
index.ts
@ -15,6 +15,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
getXWorkmateTaskSnapshot,
|
getXWorkmateTaskSnapshot,
|
||||||
recordXWorkmateSessionMapping,
|
recordXWorkmateSessionMapping,
|
||||||
|
recordXWorkmateTaskRunStarted,
|
||||||
|
recordXWorkmateTaskRunTerminal,
|
||||||
registerXWorkmateSessionExtension,
|
registerXWorkmateSessionExtension,
|
||||||
} from "./src/taskState.js";
|
} from "./src/taskState.js";
|
||||||
|
|
||||||
@ -84,6 +86,31 @@ function stringParam(value: unknown): string {
|
|||||||
return typeof value === "string" ? value.trim() : "";
|
return typeof value === "string" ? value.trim() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function lastAssistantText(messages: unknown): string | undefined {
|
||||||
|
if (!Array.isArray(messages)) return undefined;
|
||||||
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||||
|
const message = messages[index];
|
||||||
|
if (!message || typeof message !== "object") continue;
|
||||||
|
const record = message as Record<string, unknown>;
|
||||||
|
if (stringParam(record.role).toLowerCase() !== "assistant") continue;
|
||||||
|
const content = record.content;
|
||||||
|
if (typeof content === "string" && content.trim()) return content.trim();
|
||||||
|
if (!Array.isArray(content)) continue;
|
||||||
|
const text = content
|
||||||
|
.map((block) => {
|
||||||
|
if (!block || typeof block !== "object") return "";
|
||||||
|
const item = block as Record<string, unknown>;
|
||||||
|
const type = stringParam(item.type).toLowerCase();
|
||||||
|
return type === "text" || type === "output_text" ? stringParam(item.text) : "";
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
if (text) return text;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const plugin = definePluginEntry({
|
const plugin = definePluginEntry({
|
||||||
id: "openclaw-multi-session-plugins",
|
id: "openclaw-multi-session-plugins",
|
||||||
name: "openclaw-multi-session-plugins",
|
name: "openclaw-multi-session-plugins",
|
||||||
@ -123,6 +150,29 @@ function register(api: OpenClawPluginApi) {
|
|||||||
{ name: "openclaw-multi-session-plugins.session-start" },
|
{ name: "openclaw-multi-session-plugins.session-start" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
api.on(
|
||||||
|
"agent_end",
|
||||||
|
async (event: any, ctx: any) => {
|
||||||
|
try {
|
||||||
|
const openclawSessionKey = stringParam(ctx?.sessionKey ?? event?.sessionKey);
|
||||||
|
const runId = stringParam(event?.runId ?? ctx?.runId);
|
||||||
|
if (!openclawSessionKey || !runId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await recordXWorkmateTaskRunTerminal({
|
||||||
|
api,
|
||||||
|
openclawSessionKey,
|
||||||
|
runId,
|
||||||
|
success: event?.success === true,
|
||||||
|
output: lastAssistantText(event?.messages),
|
||||||
|
error: event?.error,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
api.logger?.warn?.(`xworkmate agent_end state capture failed: ${String(error)}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
api.registerGatewayMethod("xworkmate.session.prepare", async (opts: GatewayRequestHandlerOptions) => {
|
api.registerGatewayMethod("xworkmate.session.prepare", async (opts: GatewayRequestHandlerOptions) => {
|
||||||
try {
|
try {
|
||||||
const params = scopedGatewayParams(opts.params);
|
const params = scopedGatewayParams(opts.params);
|
||||||
@ -140,6 +190,11 @@ function register(api: OpenClawPluginApi) {
|
|||||||
config: api.config,
|
config: api.config,
|
||||||
pluginConfig: api.pluginConfig,
|
pluginConfig: api.pluginConfig,
|
||||||
});
|
});
|
||||||
|
await recordXWorkmateTaskRunStarted({
|
||||||
|
api,
|
||||||
|
openclawSessionKey: mapping.openclawSessionKey,
|
||||||
|
runId: stringParam(params.runId),
|
||||||
|
});
|
||||||
opts.respond(
|
opts.respond(
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import {
|
|||||||
XWORKMATE_SESSION_EXTENSION_NAMESPACE,
|
XWORKMATE_SESSION_EXTENSION_NAMESPACE,
|
||||||
getXWorkmateTaskSnapshot,
|
getXWorkmateTaskSnapshot,
|
||||||
recordXWorkmateSessionMapping,
|
recordXWorkmateSessionMapping,
|
||||||
|
recordXWorkmateTaskRunStarted,
|
||||||
|
recordXWorkmateTaskRunTerminal,
|
||||||
} from "./taskState.js";
|
} from "./taskState.js";
|
||||||
|
|
||||||
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||||
@ -252,6 +254,95 @@ describe("xworkmate task state mapping", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns a durable failed agent terminal state when the native task record is absent", async () => {
|
||||||
|
const workspaceDir = await createWorkspaceFixture();
|
||||||
|
const { api } = createApiFixture({}, { workspaceDir });
|
||||||
|
await recordXWorkmateSessionMapping({
|
||||||
|
api,
|
||||||
|
params: {
|
||||||
|
appThreadKey: "draft:failed-run",
|
||||||
|
openclawSessionKey: "agent:main:draft:failed-run",
|
||||||
|
runId: "turn-failed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await recordXWorkmateTaskRunStarted({
|
||||||
|
api,
|
||||||
|
openclawSessionKey: "agent:main:draft:failed-run",
|
||||||
|
runId: "turn-failed",
|
||||||
|
});
|
||||||
|
await recordXWorkmateTaskRunTerminal({
|
||||||
|
api,
|
||||||
|
openclawSessionKey: "agent:main:draft:failed-run",
|
||||||
|
runId: "turn-failed",
|
||||||
|
success: false,
|
||||||
|
output: "任务执行失败前的说明",
|
||||||
|
error: "401 Authentication Fails, api_key=sk-secret-value",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
getXWorkmateTaskSnapshot({
|
||||||
|
api,
|
||||||
|
params: {
|
||||||
|
appThreadKey: "draft:failed-run",
|
||||||
|
runId: "turn-failed",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
success: false,
|
||||||
|
status: "failed",
|
||||||
|
taskStatus: "failed",
|
||||||
|
terminal: true,
|
||||||
|
terminalSource: "agent_end",
|
||||||
|
output: "任务执行失败前的说明",
|
||||||
|
resultSummary: "任务执行失败前的说明",
|
||||||
|
message: "任务执行失败前的说明",
|
||||||
|
task: {
|
||||||
|
runId: "turn-failed",
|
||||||
|
status: "failed",
|
||||||
|
source: "xworkmate_run_state",
|
||||||
|
},
|
||||||
|
error: "401 Authentication Fails, api_key=<redacted>",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a recorded running state while the agent turn is still active", async () => {
|
||||||
|
const { api } = createApiFixture();
|
||||||
|
await recordXWorkmateSessionMapping({
|
||||||
|
api,
|
||||||
|
params: {
|
||||||
|
appThreadKey: "draft:running-run",
|
||||||
|
openclawSessionKey: "agent:main:draft:running-run",
|
||||||
|
runId: "turn-running",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await recordXWorkmateTaskRunStarted({
|
||||||
|
api,
|
||||||
|
openclawSessionKey: "agent:main:draft:running-run",
|
||||||
|
runId: "turn-running",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
getXWorkmateTaskSnapshot({
|
||||||
|
api,
|
||||||
|
params: {
|
||||||
|
appThreadKey: "draft:running-run",
|
||||||
|
runId: "turn-running",
|
||||||
|
includeArtifacts: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
success: true,
|
||||||
|
status: "running",
|
||||||
|
terminal: false,
|
||||||
|
terminalSource: "session_prepare",
|
||||||
|
task: {
|
||||||
|
runId: "turn-running",
|
||||||
|
status: "running",
|
||||||
|
source: "xworkmate_run_state",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("does not accept legacy sessionKey as a task lookup alias", async () => {
|
it("does not accept legacy sessionKey as a task lookup alias", async () => {
|
||||||
const { api } = createApiFixture({
|
const { api } = createApiFixture({
|
||||||
"draft:legacy:run-1": {
|
"draft:legacy:run-1": {
|
||||||
|
|||||||
211
src/taskState.ts
211
src/taskState.ts
@ -4,6 +4,8 @@ import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js";
|
|||||||
|
|
||||||
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||||
export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
||||||
|
export const XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE = "xworkmate.taskRuns";
|
||||||
|
const MAX_RECORDED_TASK_RUNS = 32;
|
||||||
|
|
||||||
export type XWorkmateTaskMetadataV1 = {
|
export type XWorkmateTaskMetadataV1 = {
|
||||||
schemaVersion: 1;
|
schemaVersion: 1;
|
||||||
@ -44,6 +46,18 @@ export type XWorkmateTaskLookupError = {
|
|||||||
expectedArtifactDirs?: string[];
|
expectedArtifactDirs?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type XWorkmateRecordedTaskRunV1 = {
|
||||||
|
schemaVersion: 1;
|
||||||
|
runId: string;
|
||||||
|
status: "running" | "completed" | "failed";
|
||||||
|
success: boolean;
|
||||||
|
startedAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
completedAt?: string;
|
||||||
|
output?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type SessionEntry = Record<string, unknown> & {
|
type SessionEntry = Record<string, unknown> & {
|
||||||
pluginExtensions?: Record<string, Record<string, unknown>>;
|
pluginExtensions?: Record<string, Record<string, unknown>>;
|
||||||
};
|
};
|
||||||
@ -102,6 +116,43 @@ export async function recordXWorkmateSessionMapping(input: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function recordXWorkmateTaskRunStarted(input: {
|
||||||
|
api: OpenClawPluginApi;
|
||||||
|
openclawSessionKey: string;
|
||||||
|
runId: string;
|
||||||
|
}): Promise<XWorkmateRecordedTaskRunV1> {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return upsertXWorkmateTaskRun(input.api, {
|
||||||
|
openclawSessionKey: requiredString(input.openclawSessionKey, "openclawSessionKey required"),
|
||||||
|
runId: requiredString(input.runId, "runId required"),
|
||||||
|
status: "running",
|
||||||
|
success: false,
|
||||||
|
startedAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function recordXWorkmateTaskRunTerminal(input: {
|
||||||
|
api: OpenClawPluginApi;
|
||||||
|
openclawSessionKey: string;
|
||||||
|
runId: string;
|
||||||
|
success: boolean;
|
||||||
|
output?: unknown;
|
||||||
|
error?: unknown;
|
||||||
|
}): Promise<XWorkmateRecordedTaskRunV1> {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return upsertXWorkmateTaskRun(input.api, {
|
||||||
|
openclawSessionKey: requiredString(input.openclawSessionKey, "openclawSessionKey required"),
|
||||||
|
runId: requiredString(input.runId, "runId required"),
|
||||||
|
status: input.success ? "completed" : "failed",
|
||||||
|
success: input.success,
|
||||||
|
updatedAt: now,
|
||||||
|
completedAt: now,
|
||||||
|
output: sanitizeTaskRunOutput(input.output),
|
||||||
|
error: sanitizeTaskRunError(input.error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeXWorkmateTaskMetadataV1(input: Record<string, unknown>): XWorkmateTaskMetadataV1 {
|
function normalizeXWorkmateTaskMetadataV1(input: Record<string, unknown>): XWorkmateTaskMetadataV1 {
|
||||||
const envelope = asRecord(input.xworkmate) ?? asRecord(input.xworkmateMetadata) ?? input;
|
const envelope = asRecord(input.xworkmate) ?? asRecord(input.xworkmateMetadata) ?? input;
|
||||||
const schemaVersion = Number(envelope.schemaVersion ?? 1);
|
const schemaVersion = Number(envelope.schemaVersion ?? 1);
|
||||||
@ -233,9 +284,52 @@ export async function getXWorkmateTaskSnapshot(input: {
|
|||||||
});
|
});
|
||||||
const includeArtifacts = params.includeArtifacts !== false;
|
const includeArtifacts = params.includeArtifacts !== false;
|
||||||
if (!task) {
|
if (!task) {
|
||||||
|
const recordedRun = runId
|
||||||
|
? readXWorkmateTaskRun(input.api, openclawSessionKey, runId)
|
||||||
|
: undefined;
|
||||||
const exported = includeArtifacts && runId
|
const exported = includeArtifacts && runId
|
||||||
? await exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping)
|
? await exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
if (recordedRun) {
|
||||||
|
return {
|
||||||
|
success: recordedRun.status === "running" ? true : recordedRun.success,
|
||||||
|
status: recordedRun.status,
|
||||||
|
taskStatus: recordedRun.status,
|
||||||
|
terminal: recordedRun.status !== "running",
|
||||||
|
terminalSource: recordedRun.status === "running" ? "session_prepare" : "agent_end",
|
||||||
|
mode: "gateway-chat",
|
||||||
|
mapping,
|
||||||
|
appThreadKey: mapping?.appThreadKey ?? appThreadKey,
|
||||||
|
openclawSessionKey,
|
||||||
|
runId,
|
||||||
|
taskId: taskId || runId,
|
||||||
|
task: {
|
||||||
|
taskId: taskId || runId,
|
||||||
|
runId,
|
||||||
|
status: recordedRun.status,
|
||||||
|
success: recordedRun.success,
|
||||||
|
source: "xworkmate_run_state",
|
||||||
|
startedAt: recordedRun.startedAt,
|
||||||
|
updatedAt: recordedRun.updatedAt,
|
||||||
|
completedAt: recordedRun.completedAt,
|
||||||
|
error: recordedRun.error,
|
||||||
|
},
|
||||||
|
output: recordedRun.output,
|
||||||
|
resultSummary: recordedRun.output,
|
||||||
|
error: recordedRun.error,
|
||||||
|
message: recordedRun.output ?? recordedRun.error,
|
||||||
|
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? [],
|
||||||
|
artifactScope: exported?.artifactScope,
|
||||||
|
remoteWorkingDirectory: exported?.remoteWorkingDirectory,
|
||||||
|
remoteWorkspaceRefKind: exported?.remoteWorkspaceRefKind,
|
||||||
|
scopeKind: exported?.scopeKind,
|
||||||
|
artifacts: exported?.artifacts ?? [],
|
||||||
|
constraintSatisfied: exported?.constraintSatisfied,
|
||||||
|
missingRequiredExtensions: exported?.missingRequiredExtensions,
|
||||||
|
warnings: exported?.warnings ?? [],
|
||||||
|
artifactCount: exported?.artifacts.length ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (exported?.artifacts.length) {
|
if (exported?.artifacts.length) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -308,6 +402,123 @@ export async function getXWorkmateTaskSnapshot(input: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function upsertXWorkmateTaskRun(
|
||||||
|
api: OpenClawPluginApi,
|
||||||
|
input: Omit<XWorkmateRecordedTaskRunV1, "schemaVersion" | "startedAt"> & {
|
||||||
|
openclawSessionKey: string;
|
||||||
|
startedAt?: string;
|
||||||
|
},
|
||||||
|
): Promise<XWorkmateRecordedTaskRunV1> {
|
||||||
|
const patchSessionEntry = resolvePatchSessionEntry(api);
|
||||||
|
if (!patchSessionEntry) {
|
||||||
|
throw new Error("OpenClaw runtime session patch API is unavailable");
|
||||||
|
}
|
||||||
|
let recorded: XWorkmateRecordedTaskRunV1 | undefined;
|
||||||
|
await patchSessionEntry({
|
||||||
|
sessionKey: input.openclawSessionKey,
|
||||||
|
fallbackEntry: {
|
||||||
|
sessionId: input.openclawSessionKey,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
},
|
||||||
|
preserveActivity: true,
|
||||||
|
update: (entry) => {
|
||||||
|
const runs = readTaskRunsFromEntry(entry);
|
||||||
|
const existing = runs[input.runId];
|
||||||
|
recorded = compactObject({
|
||||||
|
schemaVersion: 1 as const,
|
||||||
|
runId: input.runId,
|
||||||
|
status: input.status,
|
||||||
|
success: input.success,
|
||||||
|
startedAt: existing?.startedAt ?? input.startedAt ?? input.updatedAt,
|
||||||
|
updatedAt: input.updatedAt,
|
||||||
|
completedAt: input.completedAt,
|
||||||
|
output: input.output,
|
||||||
|
error: input.error,
|
||||||
|
}) as XWorkmateRecordedTaskRunV1;
|
||||||
|
runs[input.runId] = recorded;
|
||||||
|
const boundedRuns = Object.fromEntries(
|
||||||
|
Object.entries(runs)
|
||||||
|
.sort((left, right) => right[1].updatedAt.localeCompare(left[1].updatedAt))
|
||||||
|
.slice(0, MAX_RECORDED_TASK_RUNS),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
pluginExtensions: {
|
||||||
|
...(entry.pluginExtensions ?? {}),
|
||||||
|
[XWORKMATE_PLUGIN_ID]: {
|
||||||
|
...(entry.pluginExtensions?.[XWORKMATE_PLUGIN_ID] ?? {}),
|
||||||
|
[XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE]: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
runs: boundedRuns,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!recorded) {
|
||||||
|
throw new Error("failed to write xworkmate task run state");
|
||||||
|
}
|
||||||
|
return recorded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readXWorkmateTaskRun(
|
||||||
|
api: OpenClawPluginApi,
|
||||||
|
openclawSessionKey: string,
|
||||||
|
runId: string,
|
||||||
|
): XWorkmateRecordedTaskRunV1 | undefined {
|
||||||
|
const entry = resolveGetSessionEntry(api)?.({ sessionKey: openclawSessionKey });
|
||||||
|
return readTaskRunsFromEntry(entry)[runId];
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTaskRunsFromEntry(entry: SessionEntry | undefined | null): Record<string, XWorkmateRecordedTaskRunV1> {
|
||||||
|
const pluginState = asRecord(entry?.pluginExtensions?.[XWORKMATE_PLUGIN_ID]);
|
||||||
|
const store = asRecord(pluginState?.[XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE]);
|
||||||
|
if (store?.schemaVersion !== 1) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const runs = asRecord(store.runs) ?? {};
|
||||||
|
const result: Record<string, XWorkmateRecordedTaskRunV1> = {};
|
||||||
|
for (const [key, rawValue] of Object.entries(runs)) {
|
||||||
|
const raw = asRecord(rawValue);
|
||||||
|
const runId = optionalString(raw?.runId) || key;
|
||||||
|
const status = optionalString(raw?.status);
|
||||||
|
if (!runId || (status !== "running" && status !== "completed" && status !== "failed")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result[runId] = compactObject({
|
||||||
|
schemaVersion: 1 as const,
|
||||||
|
runId,
|
||||||
|
status,
|
||||||
|
success: raw?.success === true,
|
||||||
|
startedAt: optionalString(raw?.startedAt) || new Date(0).toISOString(),
|
||||||
|
updatedAt: optionalString(raw?.updatedAt) || new Date(0).toISOString(),
|
||||||
|
completedAt: optionalString(raw?.completedAt),
|
||||||
|
output: optionalString(raw?.output),
|
||||||
|
error: optionalString(raw?.error),
|
||||||
|
}) as XWorkmateRecordedTaskRunV1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeTaskRunOutput(value: unknown): string | undefined {
|
||||||
|
const raw = optionalString(value);
|
||||||
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return raw.slice(0, 16 * 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeTaskRunError(value: unknown): string | undefined {
|
||||||
|
const raw = optionalString(value);
|
||||||
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
.replace(/\b(sk|nvapi)-[A-Za-z0-9._-]+\b/gi, "$1-<redacted>")
|
||||||
|
.replace(/(api[_ -]?key\s*[:=]\s*)[^\s,;]+/gi, "$1<redacted>")
|
||||||
|
.slice(0, 2048);
|
||||||
|
}
|
||||||
|
|
||||||
async function exportArtifactsForTaskLookup(
|
async function exportArtifactsForTaskLookup(
|
||||||
input: { api: OpenClawPluginApi; params: Record<string, unknown> },
|
input: { api: OpenClawPluginApi; params: Record<string, unknown> },
|
||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user