mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-08 01:48:26 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
725cb16ff8 | ||
|
|
ceb4840a63 | ||
|
|
4bca4cedd8 | ||
|
|
736611293f | ||
|
|
dc5a65b02b | ||
|
|
a930d1818a | ||
|
|
7fed18fc47 | ||
|
|
cc53474d56 | ||
|
|
359bd6bba3 | ||
|
|
ff91ff91f0 | ||
|
|
c72b429d82 | ||
|
|
989d8b33d8 | ||
|
|
c40afe5caf | ||
|
|
30ebd1353a | ||
|
|
ffc872ae12 | ||
|
|
c04e71ee9b | ||
|
|
2169ad233f | ||
|
|
8c426595fa | ||
|
|
2e3c479402 | ||
|
|
c4dd646642 | ||
|
|
c0bc69e879 | ||
|
|
437e5cc41f | ||
|
|
f1b740596c | ||
|
|
d626b639fa | ||
|
|
f413c4dc6c | ||
|
|
2199a21c4d | ||
|
|
b7f29c384a | ||
|
|
0f0a44a00c | ||
|
|
214dc13a38 | ||
|
|
785f4d65c8 | ||
|
|
05298c0faf | ||
|
|
cc3d2bfcb1 | ||
|
|
e9e7ccc622 |
No files matched your search
@@ -0,0 +1,4 @@
|
||||
DEEPSEEK_API_KEY=sk-your-key-here
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||
REASONIX_LOG_LEVEL=INFO
|
||||
REASONIX_TRANSCRIPT_DIR=./transcripts
|
||||
@@ -1,13 +0,0 @@
|
||||
.gitattributes text eol=lf
|
||||
.gitignore text eol=lf
|
||||
*.astro text eol=lf
|
||||
*.css text eol=lf
|
||||
*.html text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.sh text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.yml text eol=lf
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Something is broken
|
||||
labels: bug
|
||||
---
|
||||
|
||||
**What happened**
|
||||
A clear and concise description.
|
||||
|
||||
**Expected**
|
||||
What you expected to happen.
|
||||
|
||||
**Reproduction**
|
||||
Steps or minimal code that reproduces it.
|
||||
|
||||
**Environment**
|
||||
- Reasonix version:
|
||||
- Node version:
|
||||
- OS:
|
||||
- DeepSeek model (e.g. `deepseek-chat`, `deepseek-reasoner`):
|
||||
|
||||
**Logs / transcript**
|
||||
If using the CLI, attach the relevant chunk of `--transcript`.
|
||||
@@ -0,0 +1,39 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build (node ${{ matrix.node }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: ["20", "22"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node ${{ matrix.node }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Lint (biome)
|
||||
run: npm run lint
|
||||
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Test (vitest)
|
||||
run: npm test
|
||||
|
||||
- name: Build (tsup)
|
||||
run: npm run build
|
||||
@@ -1,66 +0,0 @@
|
||||
name: Deploy website
|
||||
run-name: Deploy site ${{ inputs.release_version && format('v{0}', inputs.release_version) || github.sha }}${{ inputs.release_request && format(' [{0}]', inputs.release_request) || '' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [website]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_version:
|
||||
description: "Optional product release version associated with this publication"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
release_request:
|
||||
description: "Optional release workflow correlation ID"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
studio_version:
|
||||
description: "Studio version that must be present in the generated changelog"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: reasonix-website-production
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.ref == 'refs/heads/website'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: site/package-lock.json
|
||||
- name: Validate and build
|
||||
working-directory: site
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
STUDIO_RELEASE_VERSION: ${{ inputs.studio_version }}
|
||||
run: |
|
||||
npm ci
|
||||
npm test
|
||||
npm run build
|
||||
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
|
||||
with:
|
||||
path: site/dist
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deploy.outputs.page_url }}
|
||||
steps:
|
||||
- id: deploy
|
||||
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5
|
||||
@@ -1,106 +0,0 @@
|
||||
name: Prepare release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Official version without a tag prefix (for example 1.20.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
draft:
|
||||
name: generate reviewable bilingual draft
|
||||
if: github.ref == 'refs/heads/website'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Select the review branch
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git fetch origin main-v2 website
|
||||
|
||||
if [[ ! "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
||||
echo "::error::version must be MAJOR.MINOR.PATCH"
|
||||
exit 1
|
||||
fi
|
||||
branch="website-release-notes/v${VERSION}"
|
||||
if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
|
||||
git fetch origin "refs/heads/$branch:refs/remotes/origin/$branch"
|
||||
git checkout -b "$branch" --track "origin/$branch"
|
||||
git merge-base --is-ancestor origin/website HEAD
|
||||
else
|
||||
git checkout -b "$branch"
|
||||
fi
|
||||
source_sha="$(git rev-parse origin/main-v2)"
|
||||
echo "RELEASE_NOTES_BRANCH=$branch" >> "$GITHUB_ENV"
|
||||
echo "RELEASE_NOTES_SOURCE_SHA=$source_sha" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Generate and validate release notes
|
||||
env:
|
||||
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
||||
DEEPSEEK_MODEL: deepseek-v4-pro
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# The first preparation fixes the product range. Reruns update the
|
||||
# draft without silently absorbing unrelated main-v2 merges.
|
||||
node site/scripts/releases/generate-release-notes.mjs --version "$VERSION" --to "$RELEASE_NOTES_SOURCE_SHA"
|
||||
node site/scripts/releases/release-notes.mjs validate
|
||||
node --test site/scripts/releases/release-notes.test.mjs
|
||||
node site/scripts/releases/release-notes.mjs render --version "$VERSION" --output /tmp/release-notes.md
|
||||
{
|
||||
echo "## Review draft for v${VERSION#v}"
|
||||
echo
|
||||
cat /tmp/release-notes.md
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Open or update the review PR
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add site/src/data/releases.json
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "docs(release): prepare v${VERSION#v} notes" \
|
||||
-m "Summary:
|
||||
Generate a bilingual, product-focused draft from merged pull request metadata. Reuse the selected release-bound PR when one is available.
|
||||
|
||||
Verification:
|
||||
Validate the catalog, citations, bilingual fields, and rendered GitHub release notes before committing."
|
||||
fi
|
||||
git push -u origin "$RELEASE_NOTES_BRANCH"
|
||||
if ! gh pr list --head "$RELEASE_NOTES_BRANCH" --state open --json number --jq 'length > 0' | grep -q true; then
|
||||
if ! gh pr create \
|
||||
--base website \
|
||||
--head "$RELEASE_NOTES_BRANCH" \
|
||||
--title "docs(release): 准备 v${VERSION#v} 中英更新日志" \
|
||||
--body-file /tmp/release-notes.md; then
|
||||
echo "::warning::GitHub Actions could not open the PR; the reviewed branch is preserved."
|
||||
{
|
||||
echo "## Manual PR handoff"
|
||||
echo
|
||||
echo '```sh'
|
||||
echo "gh pr create --repo ${{ github.repository }} --base website --head $RELEASE_NOTES_BRANCH --fill"
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
fi
|
||||
@@ -1,27 +0,0 @@
|
||||
name: Release notes
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [website]
|
||||
paths:
|
||||
- "site/src/data/releases.json"
|
||||
- "site/scripts/releases/**"
|
||||
- "site/scripts/release-event.mjs"
|
||||
- "site/src/pages/changelog/**"
|
||||
- "site/src/components/ChangelogPage.astro"
|
||||
- "site/src/data/releases.ts"
|
||||
- "site/src/styles/changelog.css"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: "22"
|
||||
- run: node site/scripts/releases/release-notes.mjs validate
|
||||
- run: node --test site/scripts/releases/*.test.mjs
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Website CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [website]
|
||||
pull_request:
|
||||
branches: [website]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: website-ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
website:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: site
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: site/package-lock.json
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+18
-9
@@ -1,15 +1,24 @@
|
||||
node_modules/
|
||||
.npm/
|
||||
.wrangler/
|
||||
site/dist/
|
||||
site/.astro/
|
||||
site/.generated/
|
||||
site/public/av/
|
||||
dist/
|
||||
coverage/
|
||||
.env
|
||||
.env.*
|
||||
*.local
|
||||
.env.local
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
temp/
|
||||
*.tsbuildinfo
|
||||
transcripts/
|
||||
sessions/
|
||||
*.jsonl
|
||||
!tests/**/*.jsonl
|
||||
# Committed reference transcripts so readers can reasonix replay / diff
|
||||
# the v0.1 bench results without an API key.
|
||||
!benchmarks/tau-bench/transcripts/
|
||||
!benchmarks/tau-bench/transcripts/*.jsonl
|
||||
.turbo/
|
||||
@@ -1 +0,0 @@
|
||||
24.20.0
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Reasonix. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
||||
this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.2.1] — 2026-04-21
|
||||
|
||||
**Headline:** v0.2 grows eyes. `reasonix replay` and `reasonix diff` now
|
||||
open interactive Ink TUIs by default. The stdout paths still work when
|
||||
piped, so CI / `less` / markdown-export workflows aren't disturbed.
|
||||
|
||||
### Added
|
||||
|
||||
- **Interactive `reasonix replay <transcript>`** — Ink TUI with
|
||||
per-turn navigation (`j`/`k`/space/arrows, `g`/`G` for jump-to-edge,
|
||||
`q` to quit). Sidebar re-renders cumulative cost / cache / prefix
|
||||
stability as the cursor moves, so "how did the cache hit rate climb
|
||||
over the conversation?" is answered visually instead of in
|
||||
aggregate.
|
||||
- **Interactive `reasonix diff <a> <b>`** — split-pane Ink TUI. Both
|
||||
sides scroll together; `n` / `N` jump the cursor to the next / prev
|
||||
divergent turn (the whole point of a diff tool). Cursor defaults to
|
||||
the first divergence so you skip the "identical setup turns".
|
||||
- **Shared `RecordView` component** (`src/cli/ui/RecordView.tsx`)
|
||||
used by both TUIs — consistent visual grammar (user cyan, assistant
|
||||
green with cache badge, tool yellow, error red). Replaces the
|
||||
inline renderer in `ReplayApp`.
|
||||
- **Pure navigation helpers** in `src/diff.ts`:
|
||||
`findNextDivergence(pairs, fromIdx)` and
|
||||
`findPrevDivergence(pairs, fromIdx)`. Unit-testable without Ink.
|
||||
Both guard against out-of-bounds `fromIdx`.
|
||||
- **Pure replay nav helpers** in `src/replay.ts`:
|
||||
`groupRecordsByTurn(records)` and `computeCumulativeStats(pages, upToIdx)`.
|
||||
Used by the TUI sidebar; also individually testable.
|
||||
- **New CLI flags** on both commands:
|
||||
- `reasonix replay --print` — force stdout pretty-print (auto when
|
||||
stdout isn't a TTY, or when `--head` / `--tail` is passed).
|
||||
- `reasonix diff --print` — force stdout table.
|
||||
- `reasonix diff --tui` — force Ink TUI even when piped (rare
|
||||
escape hatch).
|
||||
|
||||
### Changed
|
||||
|
||||
- **`reasonix replay` default** is now the TUI. Old stdout behavior
|
||||
reachable via `--print` or by piping. Non-TTY detection
|
||||
automatically flips to stdout mode, so shell pipelines behave as
|
||||
they did in 0.2.0.
|
||||
- **`reasonix diff` default** picks itself from context:
|
||||
- `--md <path>` → write markdown + print summary (unchanged).
|
||||
- `--print` or piped stdout → stdout summary table.
|
||||
- TTY, no `--md`, no `--print` → TUI.
|
||||
|
||||
### Tests
|
||||
|
||||
- +10 new tests (`replay.test.ts` +6: `groupRecordsByTurn` +
|
||||
`computeCumulativeStats`; `diff.test.ts` +4: divergence navigation).
|
||||
Suite: **169 passing** (was 159).
|
||||
|
||||
---
|
||||
|
||||
## [0.2.0] — 2026-04-21
|
||||
|
||||
**Headline:** v0.2 makes the v0.1 cache-hit claim *auditable*. Any reader
|
||||
can now verify the 94.3% / −42% numbers from committed JSONL transcripts
|
||||
— no API key required.
|
||||
|
||||
### Added
|
||||
|
||||
- **`reasonix replay <transcript>`** — pretty-print a past transcript and
|
||||
rebuild its full session summary (turns, tool calls, cache hit, cost,
|
||||
prefix stability) offline. No API calls.
|
||||
- **`reasonix diff <a> <b>`** — compare two transcripts: aggregate deltas,
|
||||
first divergence (with Levenshtein similarity for text + exact match
|
||||
for tool-name / args), prefix-stability story. Optional `--md <path>`
|
||||
writes a blog-ready markdown report.
|
||||
- **`benchmarks/tau-bench/transcripts/`** — committed reference transcripts
|
||||
(baseline + reasonix on `t01_address_happy`) so anyone can clone the
|
||||
repo and run `reasonix replay` / `diff` immediately, without running
|
||||
the bench.
|
||||
- **Bench runner gains `--transcripts-dir <path>`** — emits one JSONL
|
||||
per `(task, mode, repeat)` tuple for replay/diff.
|
||||
- New library exports: `computeReplayStats`, `replayFromFile`,
|
||||
`diffTranscripts`, `renderDiffSummary`, `renderDiffMarkdown`,
|
||||
`parseTranscript`, `recordFromLoopEvent`, `writeRecord`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Transcript format bumped (backward-compatible)**. Records now carry
|
||||
`usage`, `cost`, `model`, `prefixHash` (reasonix only), and `toolArgs`.
|
||||
All fields optional on read — v0.1 transcripts still parse (cost/cache
|
||||
shown as n/a). A `_meta` line at the top records source/model/task
|
||||
metadata.
|
||||
- **Baseline bench runner now emits per-sub-call transcripts**. Previously
|
||||
wrote one aggregated record per user turn, which made diff's
|
||||
apples-to-apples "model calls" count off. Now both modes emit at the
|
||||
same granularity.
|
||||
- **Diff rendering label change**: "turns (assistant)" → "model calls",
|
||||
with "user turns" as a separate row in the summary table. Removes the
|
||||
ambiguity that hit when comparing baseline vs reasonix.
|
||||
- **Top-level README**: `validated numbers` table now shows the 16-run
|
||||
τ-bench-lite results (94.3% cache, −42% cost) and links to the
|
||||
committed reference transcripts.
|
||||
- **Exposed `LoopEvent.toolArgs`** so transcript writers can persist
|
||||
*what* the model sent to each tool, not just the result.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Windows-only entrypoint bug in the bench runner
|
||||
(`import.meta.url === file://${argv[1]}`) — replaced with
|
||||
`pathToFileURL(argv[1]).href` so `main()` actually runs on Windows.
|
||||
|
||||
### Tests
|
||||
|
||||
- 17 new tests across `transcript.test.ts` (3), `replay.test.ts` (3),
|
||||
and `diff.test.ts` (11). Total suite: 159 passing.
|
||||
|
||||
---
|
||||
|
||||
## [0.1.0] — 2026-04-21
|
||||
|
||||
**Headline:** first reproducible evidence for Pillar 1 (Cache-First Loop).
|
||||
|
||||
### Added
|
||||
|
||||
- **`benchmarks/tau-bench/`** — τ-bench-lite harness. 8 retail-flavored
|
||||
multi-turn tool-use tasks with a DeepSeek V3 user simulator,
|
||||
deterministic DB-end-state success predicates (no LLM judge), and a
|
||||
cache-hostile naive baseline runner. Schema mirrors Sierra's τ-bench
|
||||
so upstream tasks can drop in.
|
||||
- **`benchmarks/tau-bench/runner.ts`** — orchestrator with
|
||||
`--task` / `--mode` / `--repeats` / `--dry` / `--verbose` flags.
|
||||
- **`benchmarks/tau-bench/report.ts`** — renders results JSON into a
|
||||
blog-ready markdown summary with explicit scope caveats.
|
||||
- **Live bench numbers** published in `benchmarks/tau-bench/report.md`:
|
||||
- cache hit: baseline 43.9% → reasonix **94.3%** (+50.3pp)
|
||||
- cost/task: baseline $0.00278 → reasonix **$0.00162** (−42%)
|
||||
- vs Claude Sonnet 4.6 (token-count estimate): **~96% cheaper**
|
||||
- pass rate: 100% (baseline) vs 88% (reasonix; 1 predicate too strict,
|
||||
documented)
|
||||
|
||||
### Tests
|
||||
|
||||
- 8 new tests in `tests/benchmarks.test.ts` covering DB isolation,
|
||||
check-predicate satisfiability, and tool guards — all runnable without
|
||||
an API key. Total suite at this release: 143 passing.
|
||||
|
||||
---
|
||||
|
||||
Earlier `0.0.x` versions covered Pillar 1 + Pillar 3 internals, retry
|
||||
layer, first-run API key prompt, harvest MVP, self-consistency
|
||||
branching, and session persistence. They're not reflected as individual
|
||||
entries above because the `0.1.0` bench harness is what first produced
|
||||
*externally verifiable* evidence for their value.
|
||||
|
||||
[0.2.1]: https://github.com/esengine/reasonix/releases/tag/v0.2.1
|
||||
[0.2.0]: https://github.com/esengine/reasonix/releases/tag/v0.2.0
|
||||
[0.1.0]: https://github.com/esengine/reasonix/releases/tag/v0.1.0
|
||||
@@ -1,6 +0,0 @@
|
||||
# Reasonix
|
||||
|
||||
The project's standing instructions live in `REASONIX.md` — one file, read by
|
||||
both Reasonix and Claude Code. Add rules there, never here.
|
||||
|
||||
@REASONIX.md
|
||||
@@ -1,34 +1,234 @@
|
||||
# Reasonix Website
|
||||
# Reasonix
|
||||
|
||||
This branch contains only the public Reasonix website and documentation portal.
|
||||
[](https://www.npmjs.com/package/reasonix)
|
||||
[](https://github.com/esengine/reasonix/actions/workflows/ci.yml)
|
||||
[](./LICENSE)
|
||||
[](https://www.npmjs.com/package/reasonix)
|
||||
[](./package.json)
|
||||
|
||||
- Website source: `site/`
|
||||
- Website validation: `.github/workflows/site-ci.yml`
|
||||
- Production publication: `.github/workflows/pages.yml`
|
||||
**The DeepSeek-native agent framework.** TypeScript. Ink TUI. No LangChain.
|
||||
|
||||
Product clients live on `main-v2` and `studio`. Shared APIs, identity, telemetry,
|
||||
and registry services live on `platform`.
|
||||
Reasonix is not another generic agent wrapper. Every abstraction is justified
|
||||
by a DeepSeek-specific property — dirt-cheap tokens, R1 reasoning traces,
|
||||
automatic prefix caching, JSON mode. Generic frameworks treat DeepSeek as
|
||||
"OpenAI with a different base URL" and leave these advantages on the table.
|
||||
Reasonix leans into them.
|
||||
|
||||
## Local development
|
||||
|
||||
```sh
|
||||
cd site
|
||||
npm ci
|
||||
npm test
|
||||
npm run dev
|
||||
```bash
|
||||
npx reasonix chat # first run prompts for your DeepSeek key
|
||||
# inside the TUI, type /help for everything else
|
||||
```
|
||||
|
||||
Production publication runs on pushes to `website` or manual dispatch from that branch.
|
||||
No flag soup. All feature toggles live behind slash commands in the TUI.
|
||||
|
||||
Production builds synchronize both CLI 1.x and Studio 2.x changelogs from
|
||||
published GitHub tags. CLI notes come from the tag's reviewed
|
||||
`release-notes/releases.json`; Studio notes come from its versioned Markdown.
|
||||
Synchronization needs GitHub read access (`gh auth login` locally or
|
||||
`GITHUB_TOKEN` in CI), not a model API key. Missing source notes fail the build.
|
||||
The existing CLI publication-marker check still controls public visibility.
|
||||
---
|
||||
|
||||
生产构建会从已发布的 GitHub 标签同步 CLI 1.x 和 Studio 2.x 更新日志。
|
||||
CLI 读取标签中已审核的 `release-notes/releases.json`,Studio 读取对应版本的
|
||||
Markdown。同步只需要 GitHub 读取权限(本地使用 `gh auth login`,CI 使用
|
||||
`GITHUB_TOKEN`),不需要模型 API 密钥。缺失源日志时构建失败;CLI 是否公开
|
||||
仍由原有的发布完成标记校验决定。
|
||||
## What you get
|
||||
|
||||
| Feature | How it works | Opt in |
|
||||
|---|---|---|
|
||||
| **Cache-First Loop** | Immutable prefix + append-only log = prefix byte-stable across turns → DeepSeek's automatic prefix cache hits at 70–95% | always on |
|
||||
| **R1 Thought Harvesting** | Parses `reasoning_content` into typed `{ subgoals, hypotheses, uncertainties, rejectedPaths }` via a cheap V3 call | `--harvest` |
|
||||
| **Self-Consistency Branching** | Runs N parallel samples at spread temperatures; picks the one with the fewest flagged uncertainties | `--branch <N>` |
|
||||
| **Tool-Call Repair** | Auto-flattens deep/wide schemas, scavenges tool calls leaked into `<think>`, repairs truncated JSON, breaks call-storms | always on |
|
||||
| **Retry layer** | Exponential backoff + jitter on 408/429/500/502/503/504 and network errors. 4xx auth errors don't retry | always on |
|
||||
| **Ink TUI** | Live cache-hit / cost panel. Streams R1 thinking to a compact preview. Renders Markdown (bold / lists / code / stripped LaTeX) | always on |
|
||||
|
||||
---
|
||||
|
||||
## Why not just use LangChain?
|
||||
|
||||
Even on the default `fast` preset (no harvest, no branching), Reasonix bakes
|
||||
in five DeepSeek-specific defences that generic agent frameworks leave to you:
|
||||
|
||||
| | Reasonix default | generic frameworks |
|
||||
|---|---|---|
|
||||
| Prefix-stable loop (→ 85–95% cache hit) | ✅ | ❌ prompts rebuilt each turn |
|
||||
| Auto-flatten deep tool schemas | ✅ | ❌ DeepSeek drops args |
|
||||
| Retry with jittered backoff (429/503) | ✅ | ❌ custom callbacks |
|
||||
| Scavenge tool calls leaked into `<think>` | ✅ | ❌ |
|
||||
| Call-storm breaker on identical-arg repeats | ✅ | ❌ |
|
||||
| Live cache-hit / cost / vs-Claude panel | ✅ | ❌ |
|
||||
| First-run config prompt + Markdown TUI | ✅ | ❌ |
|
||||
|
||||
Harvest and self-consistency branching are bonuses on top. The everyday
|
||||
win is that **a plain chat with Reasonix already pays for ~40% less tokens
|
||||
than the same chat through a naive LangChain setup**, because the prefix
|
||||
actually stays byte-stable.
|
||||
|
||||
## Validated numbers
|
||||
|
||||
**τ-bench-lite** — 8 multi-turn tool-use tasks, same tools / same prompt
|
||||
on both sides, sole variable is prefix stability. Measured on live
|
||||
DeepSeek `deepseek-chat`:
|
||||
|
||||
| metric | baseline (cache-hostile) | Reasonix | delta |
|
||||
|---|---:|---:|---:|
|
||||
| runs | 8 | 8 | — |
|
||||
| **cache hit** | 43.9% | **94.3%** | **+50.3pp** |
|
||||
| cost / task | $0.002783 | $0.001621 | **−42% (×0.58)** |
|
||||
| vs Claude Sonnet 4.6 (token-count estimate) | — | — | **~96% cheaper** |
|
||||
| pass rate | 100% (8/8) | 88% (7/8) | 1 refusal-task predicate too strict (see [report.md][r]) |
|
||||
|
||||
**Verify it yourself — no API key, zero cost:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/esengine/reasonix.git && cd reasonix && npm install
|
||||
npx reasonix replay benchmarks/tau-bench/transcripts/t01_address_happy.reasonix.r1.jsonl
|
||||
npx reasonix diff \
|
||||
benchmarks/tau-bench/transcripts/t01_address_happy.baseline.r1.jsonl \
|
||||
benchmarks/tau-bench/transcripts/t01_address_happy.reasonix.r1.jsonl
|
||||
```
|
||||
|
||||
The JSONL transcripts committed in `benchmarks/tau-bench/transcripts/`
|
||||
carry per-turn `usage`, `cost`, and `prefixHash`. Reasonix's prefix hash
|
||||
stays byte-stable across every model call; baseline's prefix churns on
|
||||
every turn. The cache delta is *mechanically* attributable to log
|
||||
stability, not to a different system prompt.
|
||||
|
||||
Full 16-run report: [`benchmarks/tau-bench/report.md`][r]. Reproduce
|
||||
with your own API key: `npx tsx benchmarks/tau-bench/runner.ts`.
|
||||
|
||||
[r]: ./benchmarks/tau-bench/report.md
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
npx reasonix chat # auto-saves to session 'default'; resumes next time
|
||||
npx reasonix chat --session work # use a different named session
|
||||
npx reasonix chat --no-session # ephemeral — nothing persisted
|
||||
npx reasonix run "ask anything" # one-shot, streams to stdout
|
||||
npx reasonix stats session.jsonl # quick summary of a transcript
|
||||
npx reasonix replay chat.jsonl # pretty-print a transcript + rebuild cost/cache offline
|
||||
npx reasonix diff a.jsonl b.jsonl --md diff.md # compare two transcripts: cache/cost delta + first divergence
|
||||
```
|
||||
|
||||
Sessions live as JSONL under `~/.reasonix/sessions/<name>.jsonl` — every
|
||||
turn's message log is appended atomically, so killing the CLI never loses
|
||||
context. Inside the TUI: `/sessions` to list, `/forget` to delete the
|
||||
current one.
|
||||
|
||||
### Inside the chat — slash commands
|
||||
|
||||
A command strip runs under the input box so you don't have to memorize
|
||||
anything. Type `/help` for the full list. The biggest shortcut:
|
||||
|
||||
```
|
||||
/preset fast deepseek-chat, no harvest, no branch (default)
|
||||
/preset smart reasoner + harvest (~10x cost)
|
||||
/preset max reasoner + harvest + branch 3 (~30x cost, slowest)
|
||||
```
|
||||
|
||||
One-tap switch between fast daily driver, careful thinker, and max-quality
|
||||
self-consistency. Individual knobs are available too:
|
||||
|
||||
```
|
||||
/status show current model / harvest / branch / stream
|
||||
/model <id> deepseek-chat or deepseek-reasoner
|
||||
/harvest [on|off] Pillar 2 — parse R1 reasoning into typed plan state
|
||||
/branch <N|off> run N parallel samples per turn, pick most confident
|
||||
/clear clear displayed history (log is kept)
|
||||
/exit quit
|
||||
```
|
||||
|
||||
The top panel shows active flags live: `· harvest · branch3` appear next to
|
||||
the model once enabled.
|
||||
|
||||
### Flags (for automation / CI)
|
||||
|
||||
The same knobs are also available as CLI flags if you're scripting:
|
||||
|
||||
```bash
|
||||
npx reasonix chat -m deepseek-reasoner --harvest --branch 3 --transcript session.jsonl
|
||||
```
|
||||
|
||||
### Library
|
||||
|
||||
```ts
|
||||
import {
|
||||
CacheFirstLoop,
|
||||
DeepSeekClient,
|
||||
ImmutablePrefix,
|
||||
ToolRegistry,
|
||||
} from "reasonix";
|
||||
|
||||
const client = new DeepSeekClient(); // reads DEEPSEEK_API_KEY from env
|
||||
const tools = new ToolRegistry();
|
||||
|
||||
tools.register({
|
||||
name: "add",
|
||||
description: "Add two integers",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { a: { type: "integer" }, b: { type: "integer" } },
|
||||
required: ["a", "b"],
|
||||
},
|
||||
fn: ({ a, b }: { a: number; b: number }) => a + b,
|
||||
});
|
||||
|
||||
const loop = new CacheFirstLoop({
|
||||
client,
|
||||
tools,
|
||||
prefix: new ImmutablePrefix({
|
||||
system: "You are a math helper.",
|
||||
toolSpecs: tools.specs(),
|
||||
}),
|
||||
harvest: true,
|
||||
branch: 3, // self-consistency budget
|
||||
});
|
||||
|
||||
for await (const ev of loop.step("What is 17 + 25?")) {
|
||||
if (ev.role === "assistant_final") console.log(ev.content);
|
||||
}
|
||||
console.log(loop.stats.summary());
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
On first run the CLI prompts for your DeepSeek API key and saves it to
|
||||
`~/.reasonix/config.json`. Alternatives:
|
||||
|
||||
```bash
|
||||
export DEEPSEEK_API_KEY=sk-... # env var (wins over config file)
|
||||
export DEEPSEEK_BASE_URL=https://... # optional alternate endpoint
|
||||
```
|
||||
|
||||
Get a key (free credit on signup): <https://platform.deepseek.com/api_keys>
|
||||
|
||||
---
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Multi-agent orchestration (use LangGraph).
|
||||
- RAG / vector stores (use LlamaIndex or do it yourself).
|
||||
- Multi-provider abstraction (use LiteLLM).
|
||||
- Web UI / SaaS.
|
||||
|
||||
Reasonix does DeepSeek, deeply.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
git clone https://github.com/esengine/reasonix.git
|
||||
cd reasonix
|
||||
npm install
|
||||
npm run dev chat # run CLI from source via tsx
|
||||
npm run build # tsup to dist/
|
||||
npm test # vitest (89 tests)
|
||||
npm run lint # biome
|
||||
npm run typecheck # tsc --noEmit
|
||||
```
|
||||
|
||||
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for internals.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
# Reasonix Website instructions
|
||||
|
||||
- This branch owns the public website and public documentation portal only.
|
||||
Do not add product client or backend service code here.
|
||||
- Build production from `website` only; Studio releases may dispatch its deployment
|
||||
after publication, with the requested version verified in the build.
|
||||
- Preserve accessibility, responsive layout, and static-build compatibility.
|
||||
- Authentication and marketplace pages call versioned `platform` APIs; they do
|
||||
not duplicate account or registry business logic.
|
||||
- Add tests for user-visible contracts and run a production build before release.
|
||||
- Never commit credentials, tokens, private analytics, or personal user data.
|
||||
- Comments are English and explain only non-obvious constraints.
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Reasonix security fixes are prioritized for the currently developed Go rewrite
|
||||
and the current 1.x release line.
|
||||
|
||||
| Version or branch | Security support |
|
||||
| --- | --- |
|
||||
| `main-v2` / 1.x releases | Supported |
|
||||
| `v1` / 0.x legacy branch | Critical fixes only, where practical |
|
||||
| Older releases, forks, or modified builds | Not covered unless the issue is reproducible upstream |
|
||||
|
||||
If you are unsure whether a version is affected, report against the newest
|
||||
released 1.x version and include the exact version or commit you tested.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues privately. Do not open a public issue with exploit
|
||||
details, secrets, crash dumps, or proof-of-concept payloads.
|
||||
|
||||
Preferred reporting path:
|
||||
|
||||
1. Use GitHub private vulnerability reporting for this repository, if available.
|
||||
2. If private reporting is not available to you, open a minimal public issue
|
||||
asking for a private maintainer contact path. Do not include exploit details
|
||||
in that issue.
|
||||
|
||||
Please include:
|
||||
|
||||
- Affected Reasonix version, commit, operating system, and installation method.
|
||||
- The feature or surface involved, such as CLI, desktop app, HTTP `serve`, bot
|
||||
gateway, MCP plugin, built-in tool, updater, or configuration loading.
|
||||
- Clear reproduction steps using dummy credentials and non-sensitive files.
|
||||
- The expected impact, such as secret disclosure, arbitrary file access,
|
||||
command execution, sandbox escape, authentication bypass, or supply-chain risk.
|
||||
- Any relevant logs with API keys, tokens, local paths, and personal data
|
||||
redacted.
|
||||
|
||||
Do not send real provider API keys, bot credentials, OAuth tokens, private
|
||||
workspace files, or third-party user data.
|
||||
|
||||
## Security Boundaries
|
||||
|
||||
Reasonix is a local coding agent. Many features intentionally operate on the
|
||||
user's local machine and workspace, including file reads, file writes, shell
|
||||
commands, MCP plugins, language servers, bot sessions, and model-provider
|
||||
requests. A finding is security-relevant when it crosses a supported boundary or
|
||||
bypasses an explicit guard.
|
||||
|
||||
Supported boundaries include:
|
||||
|
||||
- Workspace confinement for file operations that are documented or implemented
|
||||
as workspace-scoped.
|
||||
- Permission checks for tool calls, shell commands, file writes, and approvals.
|
||||
- Sandbox behavior for built-in shell execution where the platform supports it.
|
||||
- Secret handling for provider keys, bot credentials, OAuth tokens, plugin
|
||||
headers, and credential-store fallback files.
|
||||
- HTTP `serve` protections for the unauthenticated local server, including
|
||||
localhost binding assumptions, JSON-only state-changing requests, and CORS
|
||||
restrictions.
|
||||
- Desktop and bot session isolation, including per-workspace session metadata
|
||||
and configured bot allowlists.
|
||||
- Updater, install, and release verification paths.
|
||||
|
||||
The following are normally treated as trusted local/operator-controlled inputs
|
||||
unless another bug lets an untrusted actor supply them:
|
||||
|
||||
- CLI arguments and text typed directly by the local user.
|
||||
- Project configuration files intentionally loaded from the current workspace.
|
||||
- Explicit `@path` references supplied by the local user to attach local files.
|
||||
- MCP servers, language servers, hooks, and slash commands installed or enabled
|
||||
by the local user.
|
||||
- Provider base URLs and model names configured by the local user.
|
||||
|
||||
The following can be security issues when reachable by an untrusted actor or
|
||||
when they bypass the intended boundary:
|
||||
|
||||
- Reading or writing files outside the configured workspace without explicit
|
||||
local-user intent.
|
||||
- Following symlinks or path traversal to escape workspace confinement.
|
||||
- Running shell commands or external tools without the required permission gate.
|
||||
- Leaking credentials, environment variables, prompt history, local files, or bot
|
||||
messages to logs, model providers, MCP servers, crash reports, or telemetry.
|
||||
- Allowing a website to drive the local HTTP server through CSRF, CORS, or
|
||||
content-type bypasses.
|
||||
- Letting a bot user outside the configured allowlist submit prompts, approve
|
||||
tools, or access a project workspace.
|
||||
- Trusting unverified update artifacts, plugin definitions, or downloaded
|
||||
binaries.
|
||||
|
||||
## `@` File References
|
||||
|
||||
Reasonix supports `@path` references so users can include local files and images
|
||||
in a prompt. This is intentional local functionality, but implementations must
|
||||
preserve these invariants:
|
||||
|
||||
- In workspace-scoped sessions, relative and absolute paths must resolve under
|
||||
the active workspace root before file content is read or attached.
|
||||
- Path traversal such as `..` must not escape the workspace root.
|
||||
- Symlinks must not be usable to bypass the intended workspace boundary.
|
||||
- Unscoped local CLI compatibility must not be exposed to remote, bot, or
|
||||
browser-controlled inputs unless an equivalent workspace boundary is applied.
|
||||
- File content should be size-limited and binary content should not be dumped as
|
||||
prompt text.
|
||||
|
||||
Static analysis alerts about path expressions should be triaged against these
|
||||
rules: user-controlled path data is expected, but the access must either stay
|
||||
inside the configured workspace or be clearly limited to trusted local CLI use.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
The following reports are usually out of scope unless they demonstrate a bypass
|
||||
of one of the boundaries above:
|
||||
|
||||
- A local user intentionally asks Reasonix to read, edit, or send their own
|
||||
files to a configured model provider.
|
||||
- A local user installs or enables a malicious MCP server, hook, slash command,
|
||||
language server, or shell command and then grants it permission.
|
||||
- A configured model provider, proxy, or MCP server receives data the user
|
||||
intentionally sent to it.
|
||||
- Denial-of-service issues that only affect the local user's own session and do
|
||||
not corrupt files, leak secrets, or bypass permissions.
|
||||
- Issues requiring administrator/root access on the user's machine before
|
||||
interacting with Reasonix.
|
||||
- Vulnerabilities in third-party services, models, proxies, or plugins that are
|
||||
not caused by Reasonix behavior.
|
||||
|
||||
## Coordinated Disclosure
|
||||
|
||||
This is a community-maintained project. Maintainers will make a best-effort
|
||||
assessment, ask follow-up questions when needed, and coordinate fixes before
|
||||
public disclosure for confirmed vulnerabilities.
|
||||
|
||||
Please give maintainers reasonable time to investigate and release a fix before
|
||||
publishing exploit details. If you plan to disclose on a timeline, include that
|
||||
timeline in your initial report.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Benchmarks
|
||||
|
||||
This is where validation lives. The v0.1 milestone gates on a reproducible
|
||||
tool-use eval that compares, on the same tasks:
|
||||
|
||||
1. **Baseline** — a deliberately cache-hostile agent (fresh timestamp +
|
||||
shuffled tool spec each turn), representative of how generic frameworks
|
||||
wire up DeepSeek.
|
||||
2. **Reasonix** — the same tools and system prompt, driven through
|
||||
`CacheFirstLoop` so the byte prefix stays stable turn-over-turn.
|
||||
|
||||
Both modes share the same `DeepSeekClient`, so the *only* meaningful
|
||||
difference is prefix stability — any cache-hit / cost gap is attributable to
|
||||
Pillar 1 of the architecture, nothing else.
|
||||
|
||||
## Scope — this is τ-bench-*lite*
|
||||
|
||||
We don't ship a full port of [Sierra's τ-bench](https://github.com/sierra-research/tau-bench)
|
||||
(airline + retail, Python). Instead:
|
||||
|
||||
- `tau-bench/tasks.ts` hand-authors 8 retail-flavored multi-turn tasks
|
||||
that exercise tool use, identity verification, refusal, and mid-conversation
|
||||
goal change.
|
||||
- The task schema (`tau-bench/types.ts`) mirrors τ-bench's shape — stateful
|
||||
tools, an LLM user simulator, end-state DB predicates — so real upstream
|
||||
tasks can later drop in without harness changes.
|
||||
- All success predicates are **deterministic DB checks**, not LLM judges.
|
||||
Refusal tasks pass iff the DB is unchanged.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
tau-bench/
|
||||
├── types.ts — TaskDefinition / RunResult / BenchReport shapes
|
||||
├── db.ts — tiny in-memory WorldState + cloneDb
|
||||
├── tasks.ts — the 8 seed tasks + shared tool factories
|
||||
├── user-sim.ts — LLM user simulator (V3, T=0.1)
|
||||
├── baseline.ts — naive cache-hostile agent runner
|
||||
├── runner.ts — orchestrates user-sim × agent × task × mode
|
||||
└── report.ts — turns a results-*.json into a report.md
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
# dry-run: no API calls, just validate the harness is wired up
|
||||
npx tsx benchmarks/tau-bench/runner.ts --dry
|
||||
|
||||
# full run: both modes, all tasks, 1 repeat
|
||||
export DEEPSEEK_API_KEY=sk-...
|
||||
npx tsx benchmarks/tau-bench/runner.ts
|
||||
|
||||
# tighten variance: 3 repeats per task
|
||||
npx tsx benchmarks/tau-bench/runner.ts --repeats 3
|
||||
|
||||
# narrow to one task while iterating
|
||||
npx tsx benchmarks/tau-bench/runner.ts --task t01_address_happy --verbose
|
||||
|
||||
# render the report
|
||||
npx tsx benchmarks/tau-bench/report.ts benchmarks/tau-bench/results-<date>.json
|
||||
|
||||
# emit per-run transcripts so you can reasonix replay / diff them
|
||||
npx tsx benchmarks/tau-bench/runner.ts --transcripts-dir ./transcripts
|
||||
npx reasonix diff \
|
||||
./transcripts/t01_address_happy.baseline.r1.jsonl \
|
||||
./transcripts/t01_address_happy.reasonix.r1.jsonl \
|
||||
--md diff.md
|
||||
```
|
||||
|
||||
The runner writes `benchmarks/tau-bench/results-<iso-timestamp>.json`. Point
|
||||
`report.ts` at it (or pass `--out report.md` to override the output path).
|
||||
|
||||
When `--transcripts-dir <path>` is set, each `(task, mode, repeat)` run also
|
||||
writes a `<taskId>.<mode>.r<n>.jsonl` transcript into that directory —
|
||||
these carry per-turn `usage`, `cost`, and (for Reasonix) the
|
||||
`prefixHash`, so `reasonix replay` and `reasonix diff` can rebuild the
|
||||
economics offline.
|
||||
|
||||
## CLI flags
|
||||
|
||||
| flag | default | meaning |
|
||||
|---|---|---|
|
||||
| `--task <id>` | all | run only one task by id |
|
||||
| `--mode baseline` \| `reasonix` | both | restrict to one mode |
|
||||
| `--repeats <N>` | 1 | repeat each (task, mode) pair N times |
|
||||
| `--model <id>` | deepseek-chat | agent model |
|
||||
| `--user-model <id>` | deepseek-chat | user-simulator model |
|
||||
| `--out <path>` | `results-<ts>.json` | results file path |
|
||||
| `--transcripts-dir <path>` | off | write one transcript per run for replay/diff |
|
||||
| `--dry` | off | skip the LLM; only wire-check |
|
||||
| `--verbose` \| `-v` | off | print every user / agent / tool line |
|
||||
|
||||
## What a run costs
|
||||
|
||||
A full run (8 tasks × 2 modes × 1 repeat) does on the order of 30–60
|
||||
DeepSeek V3 calls — well under $0.05 at current pricing. `--repeats 3`
|
||||
triples that.
|
||||
|
||||
## Adding tasks
|
||||
|
||||
1. Add a `TaskDefinition` to `tau-bench/tasks.ts`. Reuse the tool factories
|
||||
defined at the top of that file, or add new ones (remember: factories so
|
||||
tools close over the *per-run* db snapshot).
|
||||
2. Make the `check` predicate check the end-state DB, not the agent's text —
|
||||
agents phrase things differently on every run.
|
||||
3. Run `--task <your_id> --verbose` to eyeball the transcript.
|
||||
|
||||
Non-goals (for this harness):
|
||||
|
||||
- LLM-as-judge — brittle and expensive, DB predicates are enough.
|
||||
- Streaming comparison — the harness uses `stream: false` in Reasonix mode
|
||||
so both runners make the exact same request shape.
|
||||
- Claude head-to-head — we estimate Claude's cost from token counts using
|
||||
Sonnet 4.6 pricing (see `src/telemetry.ts`); running Claude for real is
|
||||
out of scope.
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Naive baseline runner — deliberately does NOT use CacheFirstLoop.
|
||||
*
|
||||
* For the benchmark comparison to be honest, this runner must reproduce
|
||||
* what a "typical" agent framework does that breaks DeepSeek's automatic
|
||||
* prefix cache:
|
||||
*
|
||||
* 1. Inject a fresh timestamp into the system prompt every turn.
|
||||
* 2. Re-serialize the tool list each turn with a shuffled key order.
|
||||
* 3. Rebuild the full message array each turn rather than appending.
|
||||
*
|
||||
* These are all things frameworks like LangChain/LangGraph do by default
|
||||
* (either directly, or because tool specs are regenerated from Python dicts
|
||||
* with non-deterministic ordering, or because a "current_time" placeholder
|
||||
* is recommended in common system-prompt recipes).
|
||||
*
|
||||
* Reuses DeepSeekClient, ToolRegistry, and SessionStats so the *only*
|
||||
* difference from the Reasonix run is prefix stability.
|
||||
*/
|
||||
|
||||
import {
|
||||
type ChatMessage,
|
||||
type DeepSeekClient,
|
||||
SessionStats,
|
||||
type ToolCall,
|
||||
type ToolDefinition,
|
||||
ToolRegistry,
|
||||
type ToolSpec,
|
||||
Usage,
|
||||
} from "../../src/index.js";
|
||||
import type { Turn } from "./types.js";
|
||||
|
||||
export interface BaselineRunnerOptions {
|
||||
client: DeepSeekClient;
|
||||
systemPrompt: string;
|
||||
tools: ToolDefinition[];
|
||||
model?: string;
|
||||
maxToolIters?: number;
|
||||
}
|
||||
|
||||
export interface BaselineSubCall {
|
||||
/** Assistant text from this sub-call (often empty when the response is tool-calls-only). */
|
||||
content: string;
|
||||
/** Usage for this single client.chat() call. */
|
||||
usage: Usage;
|
||||
/** Tools the model chose to call on the back of this response. */
|
||||
toolCalls: { name: string; args: string; result: string }[];
|
||||
}
|
||||
|
||||
export interface BaselineTurnResult {
|
||||
assistantMessage: string;
|
||||
toolCallsExecuted: { name: string; args: string; result: string }[];
|
||||
/**
|
||||
* Per-sub-call breakdown, one entry per client.chat() invocation. Gives
|
||||
* downstream tools (bench transcripts) the same granularity as Reasonix
|
||||
* loop events — without it, diff can't compare model-call counts
|
||||
* apples-to-apples.
|
||||
*/
|
||||
subCalls: BaselineSubCall[];
|
||||
/** Turn number (1-based) assigned by the agent. */
|
||||
turnNo: number;
|
||||
}
|
||||
|
||||
export class BaselineAgent {
|
||||
readonly client: DeepSeekClient;
|
||||
readonly stats = new SessionStats();
|
||||
private readonly systemPrompt: string;
|
||||
private readonly registry: ToolRegistry;
|
||||
private readonly model: string;
|
||||
private readonly maxToolIters: number;
|
||||
/**
|
||||
* Previous-turn messages. A naive framework keeps these, but we still
|
||||
* rebuild the prefix around them each turn so the byte prefix churns.
|
||||
*/
|
||||
private history: ChatMessage[] = [];
|
||||
private turnNo = 0;
|
||||
|
||||
constructor(opts: BaselineRunnerOptions) {
|
||||
this.client = opts.client;
|
||||
this.systemPrompt = opts.systemPrompt;
|
||||
this.model = opts.model ?? "deepseek-chat";
|
||||
this.maxToolIters = opts.maxToolIters ?? 6;
|
||||
this.registry = new ToolRegistry({ autoFlatten: false });
|
||||
for (const t of opts.tools) this.registry.register(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one user-turn: sends the user message, lets the model do tool
|
||||
* calls until it stops, returns the final assistant text.
|
||||
*
|
||||
* Intentionally non-cache-friendly: rebuilds the prefix with a fresh
|
||||
* timestamp + re-shuffled tool specs every turn.
|
||||
*/
|
||||
async userTurn(userMessage: string, transcript: Turn[]): Promise<BaselineTurnResult> {
|
||||
this.turnNo++;
|
||||
|
||||
// Naive pattern #1: current-time placeholder in the system prompt.
|
||||
const churnedSystem = `${this.systemPrompt}\nCurrent time: ${new Date().toISOString()}`;
|
||||
|
||||
// Naive pattern #2: shuffle tool spec order each turn (simulates
|
||||
// frameworks that materialize tools from Python dicts / maps).
|
||||
const shuffledTools = shuffle(this.registry.specs(), this.turnNo);
|
||||
|
||||
this.history.push({ role: "user", content: userMessage });
|
||||
|
||||
const toolExecutions: { name: string; args: string; result: string }[] = [];
|
||||
const subCalls: BaselineSubCall[] = [];
|
||||
|
||||
for (let iter = 0; iter < this.maxToolIters; iter++) {
|
||||
// Naive pattern #3: always rebuild the full message array.
|
||||
const messages: ChatMessage[] = [{ role: "system", content: churnedSystem }, ...this.history];
|
||||
|
||||
const resp = await this.client.chat({
|
||||
model: this.model,
|
||||
messages,
|
||||
tools: shuffledTools,
|
||||
});
|
||||
this.stats.record(this.turnNo, this.model, resp.usage);
|
||||
|
||||
const assistantMessage: ChatMessage = { role: "assistant", content: resp.content };
|
||||
if (resp.toolCalls.length > 0) assistantMessage.tool_calls = resp.toolCalls;
|
||||
this.history.push(assistantMessage);
|
||||
|
||||
if (resp.toolCalls.length === 0) {
|
||||
subCalls.push({ content: resp.content, usage: resp.usage, toolCalls: [] });
|
||||
return {
|
||||
assistantMessage: resp.content,
|
||||
toolCallsExecuted: toolExecutions,
|
||||
subCalls,
|
||||
turnNo: this.turnNo,
|
||||
};
|
||||
}
|
||||
|
||||
const subToolCalls: { name: string; args: string; result: string }[] = [];
|
||||
for (const tc of resp.toolCalls) {
|
||||
const name = tc.function?.name ?? "";
|
||||
const args = tc.function?.arguments ?? "{}";
|
||||
const result = await this.registry.dispatch(name, args);
|
||||
toolExecutions.push({ name, args, result });
|
||||
subToolCalls.push({ name, args, result });
|
||||
this.history.push({
|
||||
role: "tool",
|
||||
tool_call_id: tc.id ?? "",
|
||||
name,
|
||||
content: result,
|
||||
});
|
||||
}
|
||||
subCalls.push({ content: resp.content, usage: resp.usage, toolCalls: subToolCalls });
|
||||
}
|
||||
|
||||
const lastAssistant = [...this.history].reverse().find((m) => m.role === "assistant");
|
||||
return {
|
||||
assistantMessage: lastAssistant?.content ?? "[max_tool_iters reached]",
|
||||
toolCallsExecuted: toolExecutions,
|
||||
subCalls,
|
||||
turnNo: this.turnNo,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministic Fisher–Yates shuffle seeded by an integer. Same turn number
|
||||
* → same ordering, so the baseline is reproducible across runs while still
|
||||
* being cache-hostile (different turns get different orderings).
|
||||
*/
|
||||
function shuffle<T>(arr: T[], seed: number): T[] {
|
||||
const out = [...arr];
|
||||
let s = seed * 9301 + 49297;
|
||||
for (let i = out.length - 1; i > 0; i--) {
|
||||
s = (s * 9301 + 49297) % 233280;
|
||||
const j = Math.floor((s / 233280) * (i + 1));
|
||||
[out[i], out[j]] = [out[j]!, out[i]!];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Re-export ToolCall, ToolSpec so caller files don't need to import both places.
|
||||
export type { ToolCall, ToolSpec };
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { WorldState } from "./types.js";
|
||||
|
||||
/**
|
||||
* Deep-clone a WorldState so per-run mutations stay isolated.
|
||||
* structuredClone is enough — WorldState is JSON-shaped by contract.
|
||||
*/
|
||||
export function cloneDb(db: WorldState): WorldState {
|
||||
return structuredClone(db);
|
||||
}
|
||||
|
||||
export function getRow(
|
||||
db: WorldState,
|
||||
table: string,
|
||||
id: string,
|
||||
): Record<string, unknown> | undefined {
|
||||
return db[table]?.[id];
|
||||
}
|
||||
|
||||
export function setField(
|
||||
db: WorldState,
|
||||
table: string,
|
||||
id: string,
|
||||
field: string,
|
||||
value: unknown,
|
||||
): boolean {
|
||||
const row = db[table]?.[id];
|
||||
if (!row) return false;
|
||||
row[field] = value;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# Reasonix tool-use eval (τ-bench-lite)
|
||||
|
||||
**Date:** 2026-04-21T12:31:25.027Z
|
||||
**Agent model:** `deepseek-chat`
|
||||
**User-simulator model:** `deepseek-chat`
|
||||
**Tasks:** 8, repeats × 1
|
||||
**Reasonix version:** 0.0.6
|
||||
|
||||
## Summary
|
||||
|
||||
| metric | baseline | reasonix | delta |
|
||||
|---|---:|---:|---:|
|
||||
| runs | 8 | 8 | — |
|
||||
| pass rate | 100% | 88% | -13pp |
|
||||
| cache hit | 43.9% | 94.3% | **+50.3pp** |
|
||||
| mean cost / task | $0.002783 | $0.001621 | ×0.58 |
|
||||
| mean turns | 4.6 | 4.6 | — |
|
||||
| mean tool calls | 2.8 | 3.1 | — |
|
||||
|
||||
**Reasonix vs Claude Sonnet 4.6 (estimated, same token counts):**
|
||||
Claude would cost ~$0.039776 / task, so Reasonix saves ~96.1%.
|
||||
(This is a *token-count-based estimate*, not a head-to-head quality comparison.)
|
||||
|
||||
## Per-task breakdown
|
||||
|
||||
| task | mode | pass | turns | tools | cache | cost |
|
||||
|---|---|:---:|---:|---:|---:|---:|
|
||||
| t01_address_happy | baseline | ✅ | 3 | 2 | 44.4% | $0.001202 |
|
||||
| t01_address_happy | reasonix | ✅ | 3 | 3 | 94.8% | $0.000875 |
|
||||
| t02_address_not_allowed | baseline | ✅ | 8 | 5 | 43.7% | $0.006048 |
|
||||
| t02_address_not_allowed | reasonix | ✅ | 8 | 3 | 95.9% | $0.002833 |
|
||||
| t03_cancel_processing | baseline | ✅ | 2 | 2 | 54.1% | $0.000843 |
|
||||
| t03_cancel_processing | reasonix | ✅ | 3 | 3 | 93.6% | $0.000832 |
|
||||
| t04_refund_delivered | baseline | ✅ | 2 | 3 | 64.4% | $0.001114 |
|
||||
| t04_refund_delivered | reasonix | ✅ | 3 | 3 | 94.2% | $0.000926 |
|
||||
| t05_refund_not_delivered | baseline | ✅ | 8 | 2 | 22.2% | $0.004724 |
|
||||
| t05_refund_not_delivered | reasonix | ❌ | 8 | 4 | 95.2% | $0.003329 |
|
||||
| t06_multi_order_lookup | baseline | ✅ | 3 | 3 | 52.9% | $0.001692 |
|
||||
| t06_multi_order_lookup | reasonix | ✅ | 4 | 3 | 92.4% | $0.001448 |
|
||||
| t07_wrong_identity | baseline | ✅ | 8 | 2 | 14.9% | $0.005182 |
|
||||
| t07_wrong_identity | reasonix | ✅ | 6 | 3 | 94.9% | $0.002008 |
|
||||
| t08_address_then_cancel | baseline | ✅ | 3 | 3 | 54.9% | $0.001455 |
|
||||
| t08_address_then_cancel | reasonix | ✅ | 2 | 3 | 93.3% | $0.000713 |
|
||||
|
||||
## Scope & caveats
|
||||
|
||||
This is **τ-bench-lite**, not a port of Sierra's upstream τ-bench. Specifically:
|
||||
|
||||
- Tasks are hand-authored in the retail domain; the schema mirrors τ-bench
|
||||
(stateful tools, LLM user-sim, DB-end-state success predicates), so upstream
|
||||
tasks can later be dropped in without harness changes.
|
||||
- Every pass/fail judgment is a deterministic DB predicate — no LLM judge.
|
||||
Refusal tasks pass iff the DB is unchanged.
|
||||
- The "baseline" deliberately reproduces cache-hostile patterns common in
|
||||
generic agent frameworks: fresh timestamp in the system prompt each turn,
|
||||
re-shuffled tool spec ordering per turn. It is **not** a benchmark of
|
||||
LangChain specifically.
|
||||
- Claude comparison is a *token-count-based cost estimate* using Anthropic's
|
||||
public pricing, not a head-to-head quality run.
|
||||
- User simulator is DeepSeek V3 at T=0.1. Some run-to-run drift is expected;
|
||||
rerun with `--repeats N` to get a tighter mean.
|
||||
|
||||
## Reproducing
|
||||
|
||||
1. `export DEEPSEEK_API_KEY=sk-...`
|
||||
2. `npm install`
|
||||
3. `npx tsx benchmarks/tau-bench/runner.ts --repeats 3`
|
||||
4. `npx tsx benchmarks/tau-bench/report.ts benchmarks/tau-bench/results-*.json`
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Render a results-*.json into a human-readable report.md.
|
||||
*
|
||||
* npx tsx benchmarks/tau-bench/report.ts benchmarks/tau-bench/results-<date>.json
|
||||
* npx tsx benchmarks/tau-bench/report.ts <input.json> --out report.md
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import type { BenchReport, RunMode, RunResult } from "./types.js";
|
||||
|
||||
interface CliArgs {
|
||||
input: string;
|
||||
outPath: string;
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): CliArgs {
|
||||
const out: CliArgs = { input: "", outPath: "benchmarks/tau-bench/report.md" };
|
||||
const positional: string[] = [];
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--out") out.outPath = argv[++i] ?? out.outPath;
|
||||
else if (a && !a.startsWith("--")) positional.push(a);
|
||||
}
|
||||
out.input = positional[0] ?? "";
|
||||
if (!out.input) {
|
||||
throw new Error(
|
||||
"usage: npx tsx benchmarks/tau-bench/report.ts <results.json> [--out report.md]",
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
interface Agg {
|
||||
runs: number;
|
||||
passes: number;
|
||||
avgCache: number;
|
||||
avgCost: number;
|
||||
avgClaudeCost: number;
|
||||
avgTurns: number;
|
||||
avgToolCalls: number;
|
||||
}
|
||||
|
||||
function aggregate(results: RunResult[]): Agg {
|
||||
if (results.length === 0) {
|
||||
return {
|
||||
runs: 0,
|
||||
passes: 0,
|
||||
avgCache: 0,
|
||||
avgCost: 0,
|
||||
avgClaudeCost: 0,
|
||||
avgTurns: 0,
|
||||
avgToolCalls: 0,
|
||||
};
|
||||
}
|
||||
const passes = results.filter((r) => r.pass).length;
|
||||
const mean = (fn: (r: RunResult) => number) =>
|
||||
results.reduce((s, r) => s + fn(r), 0) / results.length;
|
||||
return {
|
||||
runs: results.length,
|
||||
passes,
|
||||
avgCache: mean((r) => r.cacheHitRatio),
|
||||
avgCost: mean((r) => r.costUsd),
|
||||
avgClaudeCost: mean((r) => r.claudeEquivalentUsd),
|
||||
avgTurns: mean((r) => r.turns),
|
||||
avgToolCalls: mean((r) => r.toolCalls),
|
||||
};
|
||||
}
|
||||
|
||||
function renderSummary(report: BenchReport): string {
|
||||
const byMode: Record<RunMode, RunResult[]> = { baseline: [], reasonix: [] };
|
||||
for (const r of report.results) byMode[r.mode].push(r);
|
||||
const b = aggregate(byMode.baseline);
|
||||
const rx = aggregate(byMode.reasonix);
|
||||
|
||||
const costRatio = b.avgCost > 0 ? rx.avgCost / b.avgCost : 0;
|
||||
const claudeSavings = b.avgClaudeCost > 0 ? (1 - rx.avgCost / b.avgClaudeCost) * 100 : 0;
|
||||
|
||||
return `
|
||||
## Summary
|
||||
|
||||
| metric | baseline | reasonix | delta |
|
||||
|---|---:|---:|---:|
|
||||
| runs | ${b.runs} | ${rx.runs} | — |
|
||||
| pass rate | ${pct(b.passes, b.runs)} | ${pct(rx.passes, rx.runs)} | ${signPct(rx.passes, rx.runs, b.passes, b.runs)} |
|
||||
| cache hit | ${pct1(b.avgCache)} | ${pct1(rx.avgCache)} | **${signPctAbs(rx.avgCache - b.avgCache)}** |
|
||||
| mean cost / task | $${fmt(b.avgCost, 6)} | $${fmt(rx.avgCost, 6)} | ${costRatio > 0 ? `×${fmt(costRatio, 2)}` : "—"} |
|
||||
| mean turns | ${fmt(b.avgTurns, 1)} | ${fmt(rx.avgTurns, 1)} | — |
|
||||
| mean tool calls | ${fmt(b.avgToolCalls, 1)} | ${fmt(rx.avgToolCalls, 1)} | — |
|
||||
|
||||
**Reasonix vs Claude Sonnet 4.6 (estimated, same token counts):**
|
||||
Claude would cost ~$${fmt(rx.avgClaudeCost, 6)} / task, so Reasonix saves ~${fmt(
|
||||
claudeSavings,
|
||||
1,
|
||||
)}%.
|
||||
(This is a *token-count-based estimate*, not a head-to-head quality comparison.)
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function renderPerTask(report: BenchReport): string {
|
||||
const rowsByTask = new Map<string, RunResult[]>();
|
||||
for (const r of report.results) {
|
||||
const list = rowsByTask.get(r.taskId) ?? [];
|
||||
list.push(r);
|
||||
rowsByTask.set(r.taskId, list);
|
||||
}
|
||||
const rows: string[] = [
|
||||
"| task | mode | pass | turns | tools | cache | cost |",
|
||||
"|---|---|:---:|---:|---:|---:|---:|",
|
||||
];
|
||||
for (const [taskId, runs] of rowsByTask) {
|
||||
for (const r of runs) {
|
||||
rows.push(
|
||||
`| ${taskId} | ${r.mode} | ${r.pass ? "✅" : "❌"} | ${r.turns} | ${r.toolCalls} | ${pct1(
|
||||
r.cacheHitRatio,
|
||||
)} | $${fmt(r.costUsd, 6)} |${r.errorMessage ? ` err: ${truncate(r.errorMessage, 40)}` : ""}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return `## Per-task breakdown\n\n${rows.join("\n")}`;
|
||||
}
|
||||
|
||||
function renderHeader(report: BenchReport): string {
|
||||
const m = report.meta;
|
||||
return `# Reasonix tool-use eval (τ-bench-lite)
|
||||
|
||||
**Date:** ${m.date}
|
||||
**Agent model:** \`${m.model}\`
|
||||
**User-simulator model:** \`${m.userSimModel}\`
|
||||
**Tasks:** ${m.taskCount}, repeats × ${m.repeatsPerTask}
|
||||
**Reasonix version:** ${m.reasonixVersion}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderCaveats(): string {
|
||||
return `## Scope & caveats
|
||||
|
||||
This is **τ-bench-lite**, not a port of Sierra's upstream τ-bench. Specifically:
|
||||
|
||||
- Tasks are hand-authored in the retail domain; the schema mirrors τ-bench
|
||||
(stateful tools, LLM user-sim, DB-end-state success predicates), so upstream
|
||||
tasks can later be dropped in without harness changes.
|
||||
- Every pass/fail judgment is a deterministic DB predicate — no LLM judge.
|
||||
Refusal tasks pass iff the DB is unchanged.
|
||||
- The "baseline" deliberately reproduces cache-hostile patterns common in
|
||||
generic agent frameworks: fresh timestamp in the system prompt each turn,
|
||||
re-shuffled tool spec ordering per turn. It is **not** a benchmark of
|
||||
LangChain specifically.
|
||||
- Claude comparison is a *token-count-based cost estimate* using Anthropic's
|
||||
public pricing, not a head-to-head quality run.
|
||||
- User simulator is DeepSeek V3 at T=0.1. Some run-to-run drift is expected;
|
||||
rerun with \`--repeats N\` to get a tighter mean.
|
||||
|
||||
## Reproducing
|
||||
|
||||
1. \`export DEEPSEEK_API_KEY=sk-...\`
|
||||
2. \`npm install\`
|
||||
3. \`npx tsx benchmarks/tau-bench/runner.ts --repeats 3\`
|
||||
4. \`npx tsx benchmarks/tau-bench/report.ts benchmarks/tau-bench/results-*.json\`
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderReport(report: BenchReport): string {
|
||||
return [
|
||||
renderHeader(report),
|
||||
renderSummary(report),
|
||||
"",
|
||||
renderPerTask(report),
|
||||
"",
|
||||
renderCaveats(),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
// ---------- formatting helpers ----------
|
||||
|
||||
function pct(num: number, denom: number): string {
|
||||
if (denom === 0) return "—";
|
||||
return `${((num / denom) * 100).toFixed(0)}%`;
|
||||
}
|
||||
|
||||
function signPct(num1: number, denom1: number, num2: number, denom2: number): string {
|
||||
if (denom1 === 0 || denom2 === 0) return "—";
|
||||
const d = num1 / denom1 - num2 / denom2;
|
||||
const s = (d * 100).toFixed(0);
|
||||
return `${d >= 0 ? "+" : ""}${s}pp`;
|
||||
}
|
||||
|
||||
function signPctAbs(diff: number): string {
|
||||
const s = (diff * 100).toFixed(1);
|
||||
return `${diff >= 0 ? "+" : ""}${s}pp`;
|
||||
}
|
||||
|
||||
function pct1(x: number): string {
|
||||
return `${(x * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function fmt(x: number, digits: number): string {
|
||||
return x.toFixed(digits);
|
||||
}
|
||||
|
||||
function truncate(s: string, n: number): string {
|
||||
return s.length > n ? `${s.slice(0, n)}…` : s;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const raw = readFileSync(args.input, "utf8");
|
||||
const report = JSON.parse(raw) as BenchReport;
|
||||
const md = renderReport(report);
|
||||
writeFileSync(args.outPath, md, "utf8");
|
||||
console.log(`wrote ${args.outPath} (${report.results.length} runs)`);
|
||||
}
|
||||
|
||||
function isMain(): boolean {
|
||||
const entry = process.argv[1];
|
||||
if (!entry) return false;
|
||||
return import.meta.url === pathToFileURL(entry).href;
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
{
|
||||
"meta": {
|
||||
"date": "2026-04-21T12:31:25.027Z",
|
||||
"model": "deepseek-chat",
|
||||
"userSimModel": "deepseek-chat",
|
||||
"taskCount": 8,
|
||||
"repeatsPerTask": 1,
|
||||
"reasonixVersion": "0.0.6"
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"taskId": "t01_address_happy",
|
||||
"mode": "baseline",
|
||||
"pass": true,
|
||||
"turns": 3,
|
||||
"toolCalls": 2,
|
||||
"cacheHitRatio": 0.4438664932813177,
|
||||
"costUsd": 0.00120248,
|
||||
"claudeEquivalentUsd": 0.018837,
|
||||
"promptTokens": 4614,
|
||||
"completionTokens": 333,
|
||||
"truncated": false,
|
||||
"finalAgentMessage": "Success! I've updated the shipping address for your order o_1002. The new shipping address is now \"5 Birch Rd, NYC, NY 10001\". Your order is still in processing status and will be shipped to this new address."
|
||||
},
|
||||
{
|
||||
"taskId": "t01_address_happy",
|
||||
"mode": "reasonix",
|
||||
"pass": true,
|
||||
"turns": 3,
|
||||
"toolCalls": 3,
|
||||
"cacheHitRatio": 0.9479003310681303,
|
||||
"costUsd": 0.0008751300000000002,
|
||||
"claudeEquivalentUsd": 0.022857000000000002,
|
||||
"promptTokens": 5739,
|
||||
"completionTokens": 376,
|
||||
"truncated": false,
|
||||
"finalAgentMessage": "Perfect! I've successfully updated the shipping address for your order o_1002. The new address is now:\n\n**5 Birch Rd, NYC, NY 10001**\n\nYour order of running shoes size 10 will now be shipped to this new address. Is there anything else I can help you with today?"
|
||||
},
|
||||
{
|
||||
"taskId": "t02_address_not_allowed",
|
||||
"mode": "baseline",
|
||||
"pass": true,
|
||||
"turns": 8,
|
||||
"toolCalls": 5,
|
||||
"cacheHitRatio": 0.4369385555156306,
|
||||
"costUsd": 0.006047880000000001,
|
||||
"claudeEquivalentUsd": 0.09382199999999999,
|
||||
"promptTokens": 22264,
|
||||
"completionTokens": 1802,
|
||||
"truncated": true,
|
||||
"finalAgentMessage": "As you can see, the system returns the same error. I cannot override this.\n\nI've provided all the information I have:\n- Your order o_1001 is shipped\n- My tools only allow address updates on \"processing\" orders\n- I have no escalation capabilities\n- Your email is [email protected]\n- You have another order (o_1005) that's delivered\n\nIf you need to speak with a human, you must use other contact methods like phone, email, or visiting a store. I don't have those capabilities built into my system.\n\nI'm sorry I can't provide the escalation you're requesting, but I'm being honest about my limitations. Is there anything else within my actual capabilities that I can help you with?"
|
||||
},
|
||||
{
|
||||
"taskId": "t02_address_not_allowed",
|
||||
"mode": "reasonix",
|
||||
"pass": true,
|
||||
"turns": 8,
|
||||
"toolCalls": 3,
|
||||
"cacheHitRatio": 0.959254361105863,
|
||||
"costUsd": 0.00283263,
|
||||
"claudeEquivalentUsd": 0.069342,
|
||||
"promptTokens": 15879,
|
||||
"completionTokens": 1447,
|
||||
"truncated": true,
|
||||
"finalAgentMessage": "I understand you want this noted, but I need to be honest with you - I don't have a tool to add notes to accounts or orders. The tools I have access to are limited to the specific functions you've seen:\n\n1. Look up orders\n2. Look up users \n3. Update addresses (only for processing orders)\n4. Cancel orders (only for processing orders)\n5. Refund orders (for delivered orders)\n6. List user orders\n\nThere's no tool available to me for adding notes, comments, or annotations to accounts or orders. I can only perform the specific actions listed above.\n\nThe best I can do is confirm that you've requested the address change to \"99 New St, SF, CA\" for order o_1001, but this conversation won't be recorded in the system. You would need to contact customer service through official channels to have this noted in your account.\n\nIs there anything else I can help you with using my available tools?"
|
||||
},
|
||||
{
|
||||
"taskId": "t03_cancel_processing",
|
||||
"mode": "baseline",
|
||||
"pass": true,
|
||||
"turns": 2,
|
||||
"toolCalls": 2,
|
||||
"cacheHitRatio": 0.5411499436302142,
|
||||
"costUsd": 0.0008434600000000001,
|
||||
"claudeEquivalentUsd": 0.014319000000000002,
|
||||
"promptTokens": 3548,
|
||||
"completionTokens": 245,
|
||||
"truncated": false,
|
||||
"finalAgentMessage": "Perfect! I've successfully cancelled your order o_1004. The order has been updated to \"cancelled\" status. You should receive a confirmation email shortly. Is there anything else I can help you with today?"
|
||||
},
|
||||
{
|
||||
"taskId": "t03_cancel_processing",
|
||||
"mode": "reasonix",
|
||||
"pass": true,
|
||||
"turns": 3,
|
||||
"toolCalls": 3,
|
||||
"cacheHitRatio": 0.9357702349869451,
|
||||
"costUsd": 0.0008323500000000001,
|
||||
"claudeEquivalentUsd": 0.022095,
|
||||
"promptTokens": 5745,
|
||||
"completionTokens": 324,
|
||||
"truncated": false,
|
||||
"finalAgentMessage": "You're very welcome, Dev! No trouble at all - that's what we're here for. Your order has been cancelled successfully. Have a great day!"
|
||||
},
|
||||
{
|
||||
"taskId": "t04_refund_delivered",
|
||||
"mode": "baseline",
|
||||
"pass": true,
|
||||
"turns": 2,
|
||||
"toolCalls": 3,
|
||||
"cacheHitRatio": 0.6440251572327044,
|
||||
"costUsd": 0.0011135000000000001,
|
||||
"claudeEquivalentUsd": 0.02031,
|
||||
"promptTokens": 4770,
|
||||
"completionTokens": 400,
|
||||
"truncated": false,
|
||||
"finalAgentMessage": "Great news! I've successfully processed your refund for order o_1003. A refund of $55 has been issued for the broken desk lamp with the reason \"lamp arrived completely broken.\" The refund has been recorded in our system and should be processed back to your original payment method within the standard timeframe for your payment provider.\n\nIs there anything else I can help you with today?"
|
||||
},
|
||||
{
|
||||
"taskId": "t04_refund_delivered",
|
||||
"mode": "reasonix",
|
||||
"pass": true,
|
||||
"turns": 3,
|
||||
"toolCalls": 3,
|
||||
"cacheHitRatio": 0.9416652241647914,
|
||||
"costUsd": 0.0009260900000000001,
|
||||
"claudeEquivalentUsd": 0.023526000000000002,
|
||||
"promptTokens": 5777,
|
||||
"completionTokens": 413,
|
||||
"truncated": false,
|
||||
"finalAgentMessage": "Great news! I've successfully processed a refund of $55 for your order o_1003 (desk lamp). The refund has been recorded with the reason \"arrived broken.\"\n\nYou should see the refund reflected in your original payment method within 3-5 business days, depending on your bank or payment provider's processing time.\n\nIs there anything else I can help you with today?"
|
||||
},
|
||||
{
|
||||
"taskId": "t05_refund_not_delivered",
|
||||
"mode": "baseline",
|
||||
"pass": true,
|
||||
"turns": 8,
|
||||
"toolCalls": 2,
|
||||
"cacheHitRatio": 0.2223009853264337,
|
||||
"costUsd": 0.0047238900000000006,
|
||||
"claudeEquivalentUsd": 0.063351,
|
||||
"promptTokens": 14107,
|
||||
"completionTokens": 1402,
|
||||
"truncated": true,
|
||||
"finalAgentMessage": "I understand you're upset, but I need to be completely transparent: **I cannot process a refund for order o_1002 because it's in \"processing\" status, and the system will not allow it.** This isn't a matter of me refusing to do my job - it's a technical limitation that I cannot bypass.\n\nI've already demonstrated this by attempting the refund and receiving the error: `\"cannot refund: status=processing\"`\n\nSince you've made it clear that:\n1. You won't wait for delivery\n2. You won't cancel the order \n3. I cannot escalate to a supervisor\n4. The system blocks refunds for processing orders\n\nThere is literally nothing more I can do for you within the constraints of this automated system.\n\nYour only path forward to get an immediate resolution is to contact customer service through other channels (phone, email, live chat) where human agents have different system access and authority.\n\nI apologize that I cannot provide the solution you want, but I'm being honest about my limitations."
|
||||
},
|
||||
{
|
||||
"taskId": "t05_refund_not_delivered",
|
||||
"mode": "reasonix",
|
||||
"pass": false,
|
||||
"turns": 8,
|
||||
"toolCalls": 4,
|
||||
"cacheHitRatio": 0.9518716577540107,
|
||||
"costUsd": 0.003329140000000001,
|
||||
"claudeEquivalentUsd": 0.079761,
|
||||
"promptTokens": 17952,
|
||||
"completionTokens": 1727,
|
||||
"truncated": true,
|
||||
"finalAgentMessage": "I understand you want to escalate to a human supervisor, but I need to be honest about my limitations as an AI support agent.\n\n**What I can do:**\n- Provide you with escalation contact information\n- Confirm your order cancellation details\n- Document your request in this conversation\n\n**What I cannot do:**\n- Transfer you to a human supervisor (no transfer capability)\n- Generate formal documentation\n- Access supervisor contact systems\n\n**Escalation options available to you:**\n\n1. **Phone escalation:** Call 1-800-COMPANY, press 0 for operator, request supervisor\n2. **Email escalation:** Email [email protected] with subject \"ESCALATION: Order o_1002\"\n3. **Online form:** Visit company.com/contact, select \"Supervisor Request\"\n\n**For your escalation, include:**\n- Order ID: o_1002\n- Customer: Bo Wang (u_bo)\n- Issue: Need formal cancellation/refund documentation\n- Reference: This chat conversation\n\nI've provided all the information and tools available to me. For human supervisor assistance, you'll need to use one of the escalation methods above."
|
||||
},
|
||||
{
|
||||
"taskId": "t06_multi_order_lookup",
|
||||
"mode": "baseline",
|
||||
"pass": true,
|
||||
"turns": 3,
|
||||
"toolCalls": 3,
|
||||
"cacheHitRatio": 0.5285135556248053,
|
||||
"costUsd": 0.00169246,
|
||||
"claudeEquivalentUsd": 0.027954,
|
||||
"promptTokens": 6418,
|
||||
"completionTokens": 580,
|
||||
"truncated": false,
|
||||
"finalAgentMessage": "I've double-checked, and the system shows exactly 2 orders for user ID u_ari (Ari Chen):\n\n1. **Order o_1001**: Wool sweater M - Status: **shipped** - Price: $89\n - Shipping to: 1 Elm St, SF, CA 94110\n\n2. **Order o_1005**: Notebook pack - Status: **delivered** - Price: $22 \n - Delivered to: 1 Elm St, SF, CA 94110\n\nThese are all the orders on file for your account. If you were expecting more orders, it's possible they might be under a different user ID or there could be a discrepancy. Do you have any other order IDs you'd like me to check individually?"
|
||||
},
|
||||
{
|
||||
"taskId": "t06_multi_order_lookup",
|
||||
"mode": "reasonix",
|
||||
"pass": true,
|
||||
"turns": 4,
|
||||
"toolCalls": 3,
|
||||
"cacheHitRatio": 0.9235443037974683,
|
||||
"costUsd": 0.0014482,
|
||||
"claudeEquivalentUsd": 0.03426,
|
||||
"promptTokens": 7900,
|
||||
"completionTokens": 704,
|
||||
"truncated": false,
|
||||
"finalAgentMessage": "I've confirmed the list again. Here are **all** the orders under your account (u_ari):\n\n1. **Order ID: o_1001**\n - Item: Wool sweater M\n - Status: **shipped**\n - Price: $89\n - Address: 1 Elm St, SF, CA 94110\n\n2. **Order ID: o_1005**\n - Item: Notebook pack\n - Status: **delivered**\n - Price: $22\n - Address: 1 Elm St, SF, CA 94110\n\nThat's the complete list - there are no other orders in your account, and none are in \"processing\" status. Both orders have already progressed beyond the processing stage."
|
||||
},
|
||||
{
|
||||
"taskId": "t07_wrong_identity",
|
||||
"mode": "baseline",
|
||||
"pass": true,
|
||||
"turns": 8,
|
||||
"toolCalls": 2,
|
||||
"cacheHitRatio": 0.14927362388377982,
|
||||
"costUsd": 0.00518212,
|
||||
"claudeEquivalentUsd": 0.06654299999999999,
|
||||
"promptTokens": 15006,
|
||||
"completionTokens": 1435,
|
||||
"truncated": true,
|
||||
"finalAgentMessage": "I cannot cancel order o_1004. The system shows this order belongs to Dev Patel, not Bo Wang. I cannot process any order modifications without proper verification of ownership.\n\nThis conversation regarding the cancellation of order o_1004 is now closed. You will need to contact customer service through official support channels if you wish to pursue this further.\n\nIs there anything else I can help you with today?"
|
||||
},
|
||||
{
|
||||
"taskId": "t07_wrong_identity",
|
||||
"mode": "reasonix",
|
||||
"pass": true,
|
||||
"turns": 6,
|
||||
"toolCalls": 3,
|
||||
"cacheHitRatio": 0.9491148391880619,
|
||||
"costUsd": 0.0020079300000000002,
|
||||
"claudeEquivalentUsd": 0.04795200000000001,
|
||||
"promptTokens": 10789,
|
||||
"completionTokens": 1039,
|
||||
"truncated": false,
|
||||
"finalAgentMessage": "I completely understand your frustration, Bo Wang. It's very reasonable to be upset when there's a discrepancy between what you see in your confirmation email and what our system shows.\n\nYou're absolutely right to pursue this through customer service - they have the tools and authority to investigate data issues and make corrections when needed. I apologize for the inconvenience this has caused you.\n\nWhen you call, mentioning the specific order ID (o_1004) and that there's a name mismatch between your confirmation (Bo Wang) and what the system shows (Dev Patel) will help them investigate more efficiently.\n\nI hope they're able to resolve this quickly for you and get your order canceled as requested."
|
||||
},
|
||||
{
|
||||
"taskId": "t08_address_then_cancel",
|
||||
"mode": "baseline",
|
||||
"pass": true,
|
||||
"turns": 3,
|
||||
"toolCalls": 3,
|
||||
"cacheHitRatio": 0.5494007126660188,
|
||||
"costUsd": 0.00145498,
|
||||
"claudeEquivalentUsd": 0.024882,
|
||||
"promptTokens": 6174,
|
||||
"completionTokens": 424,
|
||||
"truncated": false,
|
||||
"finalAgentMessage": "Done! I've successfully cancelled your order o_1004. The order status is now \"cancelled\"."
|
||||
},
|
||||
{
|
||||
"taskId": "t08_address_then_cancel",
|
||||
"mode": "reasonix",
|
||||
"pass": true,
|
||||
"turns": 2,
|
||||
"toolCalls": 3,
|
||||
"cacheHitRatio": 0.9331046312178388,
|
||||
"costUsd": 0.0007133800000000001,
|
||||
"claudeEquivalentUsd": 0.018417,
|
||||
"promptTokens": 4664,
|
||||
"completionTokens": 295,
|
||||
"truncated": false,
|
||||
"finalAgentMessage": "Perfect! I've successfully canceled your order o_1004. The order status is now \"cancelled\". Is there anything else I can help you with?"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
/**
|
||||
* τ-bench-lite runner.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx benchmarks/tau-bench/runner.ts # all tasks, 1 repeat, both modes
|
||||
* npx tsx benchmarks/tau-bench/runner.ts --task t01_address_happy
|
||||
* npx tsx benchmarks/tau-bench/runner.ts --mode reasonix --repeats 3
|
||||
* npx tsx benchmarks/tau-bench/runner.ts --model deepseek-chat --out results.json
|
||||
* npx tsx benchmarks/tau-bench/runner.ts --dry # no API calls, smoke-test plumbing
|
||||
*
|
||||
* Writes a results-<timestamp>.json file (or whatever --out points at).
|
||||
* Run `npx tsx benchmarks/tau-bench/report.ts <results.json>` to render
|
||||
* report.md.
|
||||
*/
|
||||
|
||||
import { type WriteStream, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
CacheFirstLoop,
|
||||
DeepSeekClient,
|
||||
ImmutablePrefix,
|
||||
ToolRegistry,
|
||||
claudeEquivalentCost,
|
||||
costUsd,
|
||||
loadDotenv,
|
||||
} from "../../src/index.js";
|
||||
import { openTranscriptFile, recordFromLoopEvent, writeRecord } from "../../src/transcript.js";
|
||||
import { BaselineAgent } from "./baseline.js";
|
||||
import { cloneDb } from "./db.js";
|
||||
import { TASKS } from "./tasks.js";
|
||||
import type { BenchReport, RunMode, RunResult, TaskDefinition, Turn, WorldState } from "./types.js";
|
||||
import { UserSimulator } from "./user-sim.js";
|
||||
|
||||
loadDotenv();
|
||||
|
||||
interface CliArgs {
|
||||
taskFilter: string | null;
|
||||
modes: RunMode[];
|
||||
repeats: number;
|
||||
model: string;
|
||||
userSimModel: string;
|
||||
outPath: string | null;
|
||||
transcriptsDir: string | null;
|
||||
dry: boolean;
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): CliArgs {
|
||||
const out: CliArgs = {
|
||||
taskFilter: null,
|
||||
modes: ["baseline", "reasonix"],
|
||||
repeats: 1,
|
||||
model: "deepseek-chat",
|
||||
userSimModel: "deepseek-chat",
|
||||
outPath: null,
|
||||
transcriptsDir: null,
|
||||
dry: false,
|
||||
verbose: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--task") out.taskFilter = argv[++i] ?? null;
|
||||
else if (a === "--mode") {
|
||||
const v = (argv[++i] ?? "").toLowerCase();
|
||||
if (v === "baseline" || v === "reasonix") out.modes = [v];
|
||||
} else if (a === "--repeats") out.repeats = Number.parseInt(argv[++i] ?? "1", 10);
|
||||
else if (a === "--model") out.model = argv[++i] ?? out.model;
|
||||
else if (a === "--user-model") out.userSimModel = argv[++i] ?? out.userSimModel;
|
||||
else if (a === "--out") out.outPath = argv[++i] ?? null;
|
||||
else if (a === "--transcripts-dir") out.transcriptsDir = argv[++i] ?? null;
|
||||
else if (a === "--dry") out.dry = true;
|
||||
else if (a === "--verbose" || a === "-v") out.verbose = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------- per-run drivers ----------
|
||||
|
||||
interface RunContext {
|
||||
client: DeepSeekClient;
|
||||
task: TaskDefinition;
|
||||
db: WorldState;
|
||||
transcript: Turn[];
|
||||
args: CliArgs;
|
||||
/** Open transcript stream, or null if --transcripts-dir was not set. */
|
||||
transcriptStream: WriteStream | null;
|
||||
}
|
||||
|
||||
/** Convert a task's tool factories into concrete ToolDefinitions bound to this run's db. */
|
||||
function buildTools(task: TaskDefinition, db: WorldState) {
|
||||
return task.tools.map((factory) => factory(db));
|
||||
}
|
||||
|
||||
async function runReasonix(ctx: RunContext): Promise<RunResult> {
|
||||
const { client, task, db, args, transcriptStream } = ctx;
|
||||
const tools = buildTools(task, db);
|
||||
const registry = new ToolRegistry();
|
||||
for (const t of tools) registry.register(t);
|
||||
|
||||
const prefix = new ImmutablePrefix({
|
||||
system: task.systemPrompt,
|
||||
toolSpecs: registry.specs(),
|
||||
});
|
||||
const loop = new CacheFirstLoop({
|
||||
client,
|
||||
prefix,
|
||||
tools: registry,
|
||||
model: args.model,
|
||||
stream: false,
|
||||
});
|
||||
const prefixHash = prefix.fingerprint;
|
||||
|
||||
return runAgentLoop(ctx, "reasonix", async (userMsg) => {
|
||||
const stepTurns: Turn[] = [];
|
||||
let finalText = "";
|
||||
for await (const ev of loop.step(userMsg)) {
|
||||
if (transcriptStream && ev.role !== "assistant_delta") {
|
||||
writeRecord(transcriptStream, recordFromLoopEvent(ev, { model: args.model, prefixHash }));
|
||||
}
|
||||
if (ev.role === "tool") {
|
||||
stepTurns.push({ role: "tool", content: ev.content, toolName: ev.toolName });
|
||||
} else if (ev.role === "assistant_final") {
|
||||
finalText = ev.content;
|
||||
} else if (ev.role === "done") {
|
||||
finalText = ev.content || finalText;
|
||||
break;
|
||||
} else if (ev.role === "error") {
|
||||
throw new Error(ev.error ?? "reasonix loop error");
|
||||
}
|
||||
}
|
||||
return {
|
||||
assistantMessage: finalText,
|
||||
toolEvents: stepTurns,
|
||||
cacheHitRatio: loop.stats.aggregateCacheHitRatio,
|
||||
costUsd: loop.stats.totalCost,
|
||||
claudeEquivalentUsd: loop.stats.totalClaudeEquivalent,
|
||||
promptTokens: sumTokens(loop.stats.turns.map((t) => t.usage.promptTokens)),
|
||||
completionTokens: sumTokens(loop.stats.turns.map((t) => t.usage.completionTokens)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function runBaseline(ctx: RunContext): Promise<RunResult> {
|
||||
const { client, task, db, args, transcriptStream } = ctx;
|
||||
const tools = buildTools(task, db);
|
||||
const agent = new BaselineAgent({
|
||||
client,
|
||||
systemPrompt: task.systemPrompt,
|
||||
tools,
|
||||
model: args.model,
|
||||
});
|
||||
|
||||
return runAgentLoop(ctx, "baseline", async (userMsg, transcript) => {
|
||||
const res = await agent.userTurn(userMsg, transcript);
|
||||
|
||||
// Emit one assistant_final + its tool records per sub-call, mirroring
|
||||
// Reasonix's per-model-call granularity. This keeps diff apples-to-
|
||||
// apples: a sub-call in baseline corresponds to one model call, which
|
||||
// is also how Reasonix counts.
|
||||
if (transcriptStream) {
|
||||
for (const sc of res.subCalls) {
|
||||
const ts = new Date().toISOString();
|
||||
writeRecord(transcriptStream, {
|
||||
ts,
|
||||
turn: res.turnNo,
|
||||
role: "assistant_final",
|
||||
content: sc.content,
|
||||
usage: {
|
||||
prompt_tokens: sc.usage.promptTokens,
|
||||
completion_tokens: sc.usage.completionTokens,
|
||||
total_tokens: sc.usage.totalTokens,
|
||||
prompt_cache_hit_tokens: sc.usage.promptCacheHitTokens,
|
||||
prompt_cache_miss_tokens: sc.usage.promptCacheMissTokens,
|
||||
},
|
||||
cost: costUsd(args.model, sc.usage),
|
||||
model: args.model,
|
||||
// No prefixHash: baseline's prefix churns by design.
|
||||
});
|
||||
for (const tc of sc.toolCalls) {
|
||||
writeRecord(transcriptStream, {
|
||||
ts,
|
||||
turn: res.turnNo,
|
||||
role: "tool",
|
||||
content: tc.result,
|
||||
tool: tc.name,
|
||||
args: tc.args,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
assistantMessage: res.assistantMessage,
|
||||
toolEvents: res.toolCallsExecuted.map(
|
||||
(x) => ({ role: "tool", content: x.result, toolName: x.name }) as Turn,
|
||||
),
|
||||
cacheHitRatio: agent.stats.aggregateCacheHitRatio,
|
||||
costUsd: agent.stats.totalCost,
|
||||
claudeEquivalentUsd: agent.stats.turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0),
|
||||
promptTokens: sumTokens(agent.stats.turns.map((t) => t.usage.promptTokens)),
|
||||
completionTokens: sumTokens(agent.stats.turns.map((t) => t.usage.completionTokens)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
interface AgentTurnOutput {
|
||||
assistantMessage: string;
|
||||
toolEvents: Turn[];
|
||||
cacheHitRatio: number;
|
||||
costUsd: number;
|
||||
claudeEquivalentUsd: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
}
|
||||
|
||||
async function runAgentLoop(
|
||||
ctx: RunContext,
|
||||
mode: RunMode,
|
||||
userTurnFn: (userMsg: string, transcript: Turn[]) => Promise<AgentTurnOutput>,
|
||||
): Promise<RunResult> {
|
||||
const { client, task, db, transcript, args, transcriptStream } = ctx;
|
||||
const sim = new UserSimulator(client, task.user, {
|
||||
model: args.userSimModel,
|
||||
temperature: 0.1,
|
||||
});
|
||||
|
||||
const maxTurns = task.maxTurns ?? 8;
|
||||
let turns = 0;
|
||||
let toolCalls = 0;
|
||||
let lastAgentMsg = "";
|
||||
let truncated = false;
|
||||
let lastAgentOutput: AgentTurnOutput | null = null;
|
||||
let errorMessage: string | undefined;
|
||||
|
||||
try {
|
||||
while (turns < maxTurns) {
|
||||
const userMsg = await sim.next(transcript);
|
||||
if (userMsg === null) break;
|
||||
transcript.push({ role: "user", content: userMsg });
|
||||
if (transcriptStream) {
|
||||
writeRecord(transcriptStream, {
|
||||
ts: new Date().toISOString(),
|
||||
turn: turns + 1,
|
||||
role: "user",
|
||||
content: userMsg,
|
||||
});
|
||||
}
|
||||
if (args.verbose) console.log(` [${mode}] USER: ${userMsg}`);
|
||||
|
||||
const out = await userTurnFn(userMsg, transcript);
|
||||
lastAgentOutput = out;
|
||||
for (const te of out.toolEvents) {
|
||||
transcript.push(te);
|
||||
toolCalls++;
|
||||
if (args.verbose) console.log(` [${mode}] TOOL ${te.toolName}: ${truncate(te.content)}`);
|
||||
}
|
||||
transcript.push({ role: "agent", content: out.assistantMessage });
|
||||
if (args.verbose) console.log(` [${mode}] AGENT: ${truncate(out.assistantMessage)}`);
|
||||
lastAgentMsg = out.assistantMessage;
|
||||
turns++;
|
||||
}
|
||||
if (turns === maxTurns) truncated = true;
|
||||
} catch (err) {
|
||||
errorMessage = (err as Error).message;
|
||||
}
|
||||
|
||||
const pass =
|
||||
errorMessage === undefined
|
||||
? safeCheck(task, { db, finalAgentMessage: lastAgentMsg, transcript })
|
||||
: false;
|
||||
|
||||
return {
|
||||
taskId: task.id,
|
||||
mode,
|
||||
pass,
|
||||
turns,
|
||||
toolCalls,
|
||||
cacheHitRatio: lastAgentOutput?.cacheHitRatio ?? 0,
|
||||
costUsd: lastAgentOutput?.costUsd ?? 0,
|
||||
claudeEquivalentUsd: lastAgentOutput?.claudeEquivalentUsd ?? 0,
|
||||
promptTokens: lastAgentOutput?.promptTokens ?? 0,
|
||||
completionTokens: lastAgentOutput?.completionTokens ?? 0,
|
||||
truncated,
|
||||
finalAgentMessage: lastAgentMsg,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function safeCheck(
|
||||
task: TaskDefinition,
|
||||
ctx: { db: WorldState; finalAgentMessage: string; transcript: Turn[] },
|
||||
): boolean {
|
||||
try {
|
||||
return task.check(ctx);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function sumTokens(arr: number[]): number {
|
||||
return arr.reduce((s, n) => s + n, 0);
|
||||
}
|
||||
|
||||
function truncate(s: string, n = 140): string {
|
||||
return s.length > n ? `${s.slice(0, n)}…` : s;
|
||||
}
|
||||
|
||||
// ---------- dry mode (stubs the LLM) ----------
|
||||
|
||||
async function runDry(args: CliArgs): Promise<BenchReport> {
|
||||
const tasks = filterTasks(args.taskFilter);
|
||||
const results: RunResult[] = [];
|
||||
for (const task of tasks) {
|
||||
for (const mode of args.modes) {
|
||||
const db = cloneDb(task.initialDb);
|
||||
const tools = buildTools(task, db);
|
||||
// Execute each tool once with dummy-ish args just to prove wiring works.
|
||||
for (const t of tools) {
|
||||
try {
|
||||
await t.fn(stubArgs(t));
|
||||
} catch {
|
||||
/* dry mode — ignore tool errors */
|
||||
}
|
||||
}
|
||||
results.push({
|
||||
taskId: task.id,
|
||||
mode,
|
||||
pass: true,
|
||||
turns: 0,
|
||||
toolCalls: tools.length,
|
||||
cacheHitRatio: mode === "reasonix" ? 0.9 : 0.1,
|
||||
costUsd: 0,
|
||||
claudeEquivalentUsd: 0,
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
truncated: false,
|
||||
finalAgentMessage: "[dry-run]",
|
||||
});
|
||||
console.log(`[${task.id}/${mode}] dry-run ok (${tools.length} tools wired)`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
meta: buildMeta(args, tasks.length),
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
function stubArgs(t: {
|
||||
name: string;
|
||||
parameters?: { properties?: Record<string, unknown> };
|
||||
}): unknown {
|
||||
const props = t.parameters?.properties ?? {};
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const k of Object.keys(props)) out[k] = "stub";
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------- main ----------
|
||||
|
||||
function filterTasks(filter: string | null): TaskDefinition[] {
|
||||
if (!filter) return TASKS;
|
||||
const t = TASKS.find((x) => x.id === filter);
|
||||
if (!t) throw new Error(`unknown task: ${filter}`);
|
||||
return [t];
|
||||
}
|
||||
|
||||
function buildMeta(args: CliArgs, taskCount: number): BenchReport["meta"] {
|
||||
return {
|
||||
date: new Date().toISOString(),
|
||||
model: args.model,
|
||||
userSimModel: args.userSimModel,
|
||||
taskCount,
|
||||
repeatsPerTask: args.repeats,
|
||||
reasonixVersion: "0.2.1",
|
||||
};
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (args.dry) {
|
||||
const report = await runDry(args);
|
||||
writeReport(report, args.outPath);
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new DeepSeekClient();
|
||||
const tasks = filterTasks(args.taskFilter);
|
||||
|
||||
if (args.transcriptsDir) {
|
||||
mkdirSync(args.transcriptsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const results: RunResult[] = [];
|
||||
for (const task of tasks) {
|
||||
for (let rep = 0; rep < args.repeats; rep++) {
|
||||
for (const mode of args.modes) {
|
||||
const db = cloneDb(task.initialDb);
|
||||
const transcript: Turn[] = [];
|
||||
|
||||
let transcriptStream: WriteStream | null = null;
|
||||
if (args.transcriptsDir) {
|
||||
const fname = `${task.id}.${mode}.r${rep + 1}.jsonl`;
|
||||
transcriptStream = openTranscriptFile(join(args.transcriptsDir, fname), {
|
||||
version: 1,
|
||||
source: `bench/${mode}`,
|
||||
model: args.model,
|
||||
task: task.id,
|
||||
mode,
|
||||
repeat: rep + 1,
|
||||
startedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
const ctx: RunContext = { client, task, db, transcript, args, transcriptStream };
|
||||
const runner = mode === "reasonix" ? runReasonix : runBaseline;
|
||||
const started = Date.now();
|
||||
let result: RunResult;
|
||||
try {
|
||||
result = await runner(ctx);
|
||||
} finally {
|
||||
transcriptStream?.end();
|
||||
}
|
||||
const elapsed = ((Date.now() - started) / 1000).toFixed(1);
|
||||
console.log(
|
||||
`[${task.id}/${mode}/r${rep + 1}] pass=${result.pass} turns=${result.turns} ` +
|
||||
`cache=${(result.cacheHitRatio * 100).toFixed(1)}% cost=$${result.costUsd.toFixed(
|
||||
6,
|
||||
)} (${elapsed}s)`,
|
||||
);
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const report: BenchReport = { meta: buildMeta(args, tasks.length), results };
|
||||
writeReport(report, args.outPath);
|
||||
}
|
||||
|
||||
function writeReport(report: BenchReport, outPath: string | null): void {
|
||||
const path =
|
||||
outPath ??
|
||||
`benchmarks/tau-bench/results-${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
|
||||
writeFileSync(path, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
console.log(`wrote ${path}`);
|
||||
}
|
||||
|
||||
function isMain(): boolean {
|
||||
const entry = process.argv[1];
|
||||
if (!entry) return false;
|
||||
return import.meta.url === pathToFileURL(entry).href;
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export { main as runBench };
|
||||
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* Seed task set — retail-flavored, τ-bench-inspired.
|
||||
*
|
||||
* Why retail: success predicates are checkable by inspecting the end-state
|
||||
* DB (did the order's status change? was the refund logged?), which avoids
|
||||
* LLM-judge flakiness in the reproducibility report.
|
||||
*
|
||||
* Each task follows the same shape:
|
||||
* - a small starting DB
|
||||
* - 2–4 stateful tools (read / mutate rows)
|
||||
* - a user-sim persona that will hold back info until asked
|
||||
* - a check() that inspects the end-state DB
|
||||
*/
|
||||
|
||||
import { getRow, setField } from "./db.js";
|
||||
import type { TaskDefinition, ToolFactory, WorldState } from "./types.js";
|
||||
|
||||
const RETAIL_SYSTEM = `You are a retail support agent. Use the tools to help the user.
|
||||
Rules:
|
||||
- Always verify the user's identity (name + order id) before any mutation.
|
||||
- Never invent order ids or emails.
|
||||
- If a request is outside your tools, say so honestly.
|
||||
- Be concise.`;
|
||||
|
||||
// ---------- shared world-state factory ----------
|
||||
|
||||
function retailSeed(): WorldState {
|
||||
return {
|
||||
users: {
|
||||
u_ari: { name: "Ari Chen", email: "[email protected]" },
|
||||
u_bo: { name: "Bo Wang", email: "[email protected]" },
|
||||
u_cai: { name: "Cai Lin", email: "[email protected]" },
|
||||
u_dev: { name: "Dev Patel", email: "[email protected]" },
|
||||
},
|
||||
orders: {
|
||||
o_1001: {
|
||||
userId: "u_ari",
|
||||
status: "shipped",
|
||||
address: "1 Elm St, SF, CA 94110",
|
||||
item: "wool sweater M",
|
||||
price: 89.0,
|
||||
},
|
||||
o_1002: {
|
||||
userId: "u_bo",
|
||||
status: "processing",
|
||||
address: "22 Oak Rd, NYC, NY 10001",
|
||||
item: "running shoes 10",
|
||||
price: 140.0,
|
||||
},
|
||||
o_1003: {
|
||||
userId: "u_cai",
|
||||
status: "delivered",
|
||||
address: "9 Pine Ave, Austin, TX 78701",
|
||||
item: "desk lamp",
|
||||
price: 55.0,
|
||||
},
|
||||
o_1004: {
|
||||
userId: "u_dev",
|
||||
status: "processing",
|
||||
address: "4 Maple Ln, Seattle, WA 98101",
|
||||
item: "kettle",
|
||||
price: 45.0,
|
||||
},
|
||||
o_1005: {
|
||||
userId: "u_ari",
|
||||
status: "delivered",
|
||||
address: "1 Elm St, SF, CA 94110",
|
||||
item: "notebook pack",
|
||||
price: 22.0,
|
||||
},
|
||||
},
|
||||
refunds: {},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- tool factories (closed over a per-run db) ----------
|
||||
|
||||
const lookupOrder: ToolFactory = (db) => ({
|
||||
name: "lookup_order",
|
||||
description: "Look up an order by id. Returns { userId, status, address, item, price } or null.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { orderId: { type: "string" } },
|
||||
required: ["orderId"],
|
||||
},
|
||||
fn: ({ orderId }: { orderId: string }) => {
|
||||
const row = getRow(db, "orders", orderId);
|
||||
return row ? { orderId, ...row } : { error: "order not found" };
|
||||
},
|
||||
});
|
||||
|
||||
const lookupUser: ToolFactory = (db) => ({
|
||||
name: "lookup_user",
|
||||
description: "Look up a user by id. Returns { name, email } or error.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { userId: { type: "string" } },
|
||||
required: ["userId"],
|
||||
},
|
||||
fn: ({ userId }: { userId: string }) => {
|
||||
const row = getRow(db, "users", userId);
|
||||
return row ? { userId, ...row } : { error: "user not found" };
|
||||
},
|
||||
});
|
||||
|
||||
const updateAddress: ToolFactory = (db) => ({
|
||||
name: "update_address",
|
||||
description:
|
||||
"Update the shipping address on an order. Only allowed if status is 'processing'. Returns ok/error.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
orderId: { type: "string" },
|
||||
address: { type: "string" },
|
||||
},
|
||||
required: ["orderId", "address"],
|
||||
},
|
||||
fn: ({ orderId, address }: { orderId: string; address: string }) => {
|
||||
const row = getRow(db, "orders", orderId);
|
||||
if (!row) return { error: "order not found" };
|
||||
if (row.status !== "processing") return { error: `cannot edit: status=${row.status}` };
|
||||
setField(db, "orders", orderId, "address", address);
|
||||
return { ok: true, orderId, newAddress: address };
|
||||
},
|
||||
});
|
||||
|
||||
const cancelOrder: ToolFactory = (db) => ({
|
||||
name: "cancel_order",
|
||||
description: "Cancel an order. Only allowed if status is 'processing'. Returns ok/error.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { orderId: { type: "string" } },
|
||||
required: ["orderId"],
|
||||
},
|
||||
fn: ({ orderId }: { orderId: string }) => {
|
||||
const row = getRow(db, "orders", orderId);
|
||||
if (!row) return { error: "order not found" };
|
||||
if (row.status !== "processing") return { error: `cannot cancel: status=${row.status}` };
|
||||
setField(db, "orders", orderId, "status", "cancelled");
|
||||
return { ok: true, orderId, status: "cancelled" };
|
||||
},
|
||||
});
|
||||
|
||||
const refundOrder: ToolFactory = (db) => ({
|
||||
name: "refund_order",
|
||||
description:
|
||||
"Issue a refund on a delivered order. Records an entry in refunds. Returns ok/error.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
orderId: { type: "string" },
|
||||
reason: { type: "string" },
|
||||
},
|
||||
required: ["orderId", "reason"],
|
||||
},
|
||||
fn: ({ orderId, reason }: { orderId: string; reason: string }) => {
|
||||
const row = getRow(db, "orders", orderId);
|
||||
if (!row) return { error: "order not found" };
|
||||
if (row.status !== "delivered") return { error: `cannot refund: status=${row.status}` };
|
||||
db.refunds[orderId] = { orderId, reason, amount: row.price };
|
||||
setField(db, "orders", orderId, "status", "refunded");
|
||||
return { ok: true, orderId, amount: row.price };
|
||||
},
|
||||
});
|
||||
|
||||
const listUserOrders: ToolFactory = (db) => ({
|
||||
name: "list_user_orders",
|
||||
description: "List every order belonging to a userId. Returns an array of orders.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { userId: { type: "string" } },
|
||||
required: ["userId"],
|
||||
},
|
||||
fn: ({ userId }: { userId: string }) => {
|
||||
const all = db.orders ?? {};
|
||||
const out: unknown[] = [];
|
||||
for (const [orderId, row] of Object.entries(all)) {
|
||||
if (row.userId === userId) out.push({ orderId, ...row });
|
||||
}
|
||||
return out;
|
||||
},
|
||||
});
|
||||
|
||||
const ALL_TOOLS = [
|
||||
lookupOrder,
|
||||
lookupUser,
|
||||
updateAddress,
|
||||
cancelOrder,
|
||||
refundOrder,
|
||||
listUserOrders,
|
||||
];
|
||||
|
||||
// ---------- tasks ----------
|
||||
|
||||
export const TASKS: TaskDefinition[] = [
|
||||
{
|
||||
id: "t01_address_happy",
|
||||
description: "User wants to change the shipping address of a processing order.",
|
||||
systemPrompt: RETAIL_SYSTEM,
|
||||
tools: ALL_TOOLS,
|
||||
initialDb: retailSeed(),
|
||||
user: {
|
||||
style: "polite, brief",
|
||||
goal: "Change the shipping address on order o_1002 to '5 Birch Rd, NYC, NY 10001'.",
|
||||
knowns: {
|
||||
name: "Bo Wang",
|
||||
orderId: "o_1002",
|
||||
userId: "u_bo",
|
||||
newAddress: "5 Birch Rd, NYC, NY 10001",
|
||||
},
|
||||
},
|
||||
check: ({ db }) => db.orders.o_1002?.address === "5 Birch Rd, NYC, NY 10001",
|
||||
},
|
||||
{
|
||||
id: "t02_address_not_allowed",
|
||||
description: "User wants to change the address on a shipped order. Agent should refuse.",
|
||||
systemPrompt: RETAIL_SYSTEM,
|
||||
tools: ALL_TOOLS,
|
||||
initialDb: retailSeed(),
|
||||
user: {
|
||||
style: "insistent",
|
||||
goal: "Get the address on order o_1001 changed to '99 New St, SF, CA'.",
|
||||
knowns: {
|
||||
name: "Ari Chen",
|
||||
orderId: "o_1001",
|
||||
userId: "u_ari",
|
||||
newAddress: "99 New St, SF, CA",
|
||||
},
|
||||
},
|
||||
// Pass = DB unchanged (agent refused correctly).
|
||||
check: ({ db }) => db.orders.o_1001?.address === "1 Elm St, SF, CA 94110",
|
||||
},
|
||||
{
|
||||
id: "t03_cancel_processing",
|
||||
description: "User wants to cancel a processing order.",
|
||||
systemPrompt: RETAIL_SYSTEM,
|
||||
tools: ALL_TOOLS,
|
||||
initialDb: retailSeed(),
|
||||
user: {
|
||||
style: "apologetic",
|
||||
goal: "Cancel order o_1004.",
|
||||
knowns: { name: "Dev Patel", orderId: "o_1004", userId: "u_dev" },
|
||||
},
|
||||
check: ({ db }) => db.orders.o_1004?.status === "cancelled",
|
||||
},
|
||||
{
|
||||
id: "t04_refund_delivered",
|
||||
description: "User wants a refund on a delivered order with a valid reason.",
|
||||
systemPrompt: RETAIL_SYSTEM,
|
||||
tools: ALL_TOOLS,
|
||||
initialDb: retailSeed(),
|
||||
user: {
|
||||
style: "unhappy but reasonable",
|
||||
goal: "Get a refund on order o_1003 because the lamp arrived broken.",
|
||||
knowns: {
|
||||
name: "Cai Lin",
|
||||
orderId: "o_1003",
|
||||
userId: "u_cai",
|
||||
reason: "arrived broken",
|
||||
},
|
||||
},
|
||||
check: ({ db }) =>
|
||||
db.orders.o_1003?.status === "refunded" && db.refunds.o_1003?.amount === 55.0,
|
||||
},
|
||||
{
|
||||
id: "t05_refund_not_delivered",
|
||||
description: "User wants a refund on a processing order. Agent should refuse / defer.",
|
||||
systemPrompt: RETAIL_SYSTEM,
|
||||
tools: ALL_TOOLS,
|
||||
initialDb: retailSeed(),
|
||||
user: {
|
||||
style: "demanding",
|
||||
goal: "Get a refund on order o_1002 right now.",
|
||||
knowns: { name: "Bo Wang", orderId: "o_1002", userId: "u_bo", reason: "changed my mind" },
|
||||
},
|
||||
// Pass = DB unchanged.
|
||||
check: ({ db }) => db.orders.o_1002?.status === "processing" && !db.refunds.o_1002,
|
||||
},
|
||||
{
|
||||
id: "t06_multi_order_lookup",
|
||||
description: "User wants to hear their full order history, then cancels one.",
|
||||
systemPrompt: RETAIL_SYSTEM,
|
||||
tools: ALL_TOOLS,
|
||||
initialDb: retailSeed(),
|
||||
user: {
|
||||
style: "chatty",
|
||||
goal: "First hear every order on file for user u_ari, then cancel any that are still processing.",
|
||||
knowns: { name: "Ari Chen", userId: "u_ari" },
|
||||
},
|
||||
// u_ari has o_1001 (shipped) and o_1005 (delivered) — nothing is processing.
|
||||
// Pass = both of Ari's orders unchanged.
|
||||
check: ({ db }) =>
|
||||
db.orders.o_1001?.status === "shipped" && db.orders.o_1005?.status === "delivered",
|
||||
},
|
||||
{
|
||||
id: "t07_wrong_identity",
|
||||
description:
|
||||
"User gives a mismatched name + order id. Agent should not mutate on unverified identity.",
|
||||
systemPrompt: RETAIL_SYSTEM,
|
||||
tools: ALL_TOOLS,
|
||||
initialDb: retailSeed(),
|
||||
user: {
|
||||
style: "confident but wrong",
|
||||
goal: "Claim to be 'Bo Wang' and ask to cancel order o_1004 (which actually belongs to Dev Patel).",
|
||||
knowns: {
|
||||
name: "Bo Wang",
|
||||
orderId: "o_1004",
|
||||
// intentionally inconsistent — agent should notice
|
||||
},
|
||||
},
|
||||
check: ({ db }) => db.orders.o_1004?.status === "processing",
|
||||
},
|
||||
{
|
||||
id: "t08_address_then_cancel",
|
||||
description: "User wants to update address, then decides to cancel instead.",
|
||||
systemPrompt: RETAIL_SYSTEM,
|
||||
tools: ALL_TOOLS,
|
||||
initialDb: retailSeed(),
|
||||
user: {
|
||||
style: "changes their mind mid-conversation",
|
||||
goal: "Start by asking to change the address on o_1004 to '7 Fir St, Seattle, WA 98101', then before confirming, switch to cancelling the order entirely.",
|
||||
knowns: {
|
||||
name: "Dev Patel",
|
||||
orderId: "o_1004",
|
||||
userId: "u_dev",
|
||||
newAddress: "7 Fir St, Seattle, WA 98101",
|
||||
},
|
||||
},
|
||||
check: ({ db }) => db.orders.o_1004?.status === "cancelled",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,54 @@
|
||||
# Reference transcripts
|
||||
|
||||
These are the raw `.jsonl` transcripts from running task `t01_address_happy`
|
||||
under both baseline and Reasonix modes. They're committed so anyone can
|
||||
verify the cache-hit / cost claims *without running the bench*.
|
||||
|
||||
## Files
|
||||
|
||||
| file | what it is |
|
||||
|---|---|
|
||||
| `t01_address_happy.baseline.r1.jsonl` | Naive cache-hostile agent's record of the run |
|
||||
| `t01_address_happy.reasonix.r1.jsonl` | Reasonix's record of the same task |
|
||||
| `t01_address_happy.diff.md` | Output of `reasonix diff` on the two above |
|
||||
|
||||
## Verify for yourself
|
||||
|
||||
```bash
|
||||
# Install and build (or run from source via tsx)
|
||||
npm install
|
||||
|
||||
# Rebuild the summary — this does NOT call the API; it reads the JSONL.
|
||||
npx reasonix replay benchmarks/tau-bench/transcripts/t01_address_happy.reasonix.r1.jsonl
|
||||
|
||||
# Reproduce the diff:
|
||||
npx reasonix diff \
|
||||
benchmarks/tau-bench/transcripts/t01_address_happy.baseline.r1.jsonl \
|
||||
benchmarks/tau-bench/transcripts/t01_address_happy.reasonix.r1.jsonl \
|
||||
--label-a baseline --label-b reasonix
|
||||
```
|
||||
|
||||
Headline numbers from the committed run:
|
||||
|
||||
- cache hit: **45.9% → 93.9% (+48.0pp)**
|
||||
- cost: **$0.001192 → $0.000953 (−20.1%)**
|
||||
- Reasonix's prefix stayed byte-stable (1 distinct prefix hash) across all
|
||||
model calls; baseline's churned every call (untracked, by design).
|
||||
|
||||
## Regenerate
|
||||
|
||||
```bash
|
||||
export DEEPSEEK_API_KEY=sk-...
|
||||
rm benchmarks/tau-bench/transcripts/t01_* # clean
|
||||
npx tsx benchmarks/tau-bench/runner.ts \
|
||||
--task t01_address_happy \
|
||||
--transcripts-dir benchmarks/tau-bench/transcripts \
|
||||
--out /tmp/results.json
|
||||
npx reasonix diff \
|
||||
benchmarks/tau-bench/transcripts/t01_address_happy.baseline.r1.jsonl \
|
||||
benchmarks/tau-bench/transcripts/t01_address_happy.reasonix.r1.jsonl \
|
||||
--label-a baseline --label-b reasonix \
|
||||
--md benchmarks/tau-bench/transcripts/t01_address_happy.diff.md
|
||||
```
|
||||
|
||||
Cost: ~$0.003 per full regeneration.
|
||||
@@ -0,0 +1,11 @@
|
||||
{"role":"_meta","meta":{"version":1,"source":"bench/baseline","model":"deepseek-chat","task":"t01_address_happy","mode":"baseline","repeat":1,"startedAt":"2026-04-21T13:21:57.995Z"}}
|
||||
{"ts":"2026-04-21T13:22:00.112Z","turn":1,"role":"user","content":"Hi, I need to update the shipping address for my order o_1002."}
|
||||
{"ts":"2026-04-21T13:22:03.142Z","turn":1,"role":"assistant_final","content":"I can help you update the shipping address for your order. First, I need to verify your identity. Could you please provide your name and order ID? You've already given me order ID o_1002, but I also need your name to proceed with the address update.","usage":{"prompt_tokens":733,"completion_tokens":56,"total_tokens":789,"prompt_cache_hit_tokens":64,"prompt_cache_miss_tokens":669},"cost":0.00024671000000000003,"model":"deepseek-chat"}
|
||||
{"ts":"2026-04-21T13:22:04.688Z","turn":2,"role":"user","content":"My name is Bo Wang."}
|
||||
{"ts":"2026-04-21T13:22:10.465Z","turn":2,"role":"assistant_final","content":"Thank you, Bo Wang. Let me first look up your order o_1002 to verify the details and check its current status.","usage":{"prompt_tokens":799,"completion_tokens":74,"total_tokens":873,"prompt_cache_hit_tokens":64,"prompt_cache_miss_tokens":735},"cost":0.00028433000000000006,"model":"deepseek-chat"}
|
||||
{"ts":"2026-04-21T13:22:10.465Z","turn":2,"role":"tool","content":"{\"orderId\":\"o_1002\",\"userId\":\"u_bo\",\"status\":\"processing\",\"address\":\"22 Oak Rd, NYC, NY 10001\",\"item\":\"running shoes 10\",\"price\":140}","tool":"lookup_order","args":"{\"orderId\": \"o_1002\"}"}
|
||||
{"ts":"2026-04-21T13:22:10.465Z","turn":2,"role":"assistant_final","content":"Great! I can see your order o_1002 is currently in \"processing\" status, which means I can update the shipping address. What would you like the new shipping address to be?","usage":{"prompt_tokens":932,"completion_tokens":39,"total_tokens":971,"prompt_cache_hit_tokens":832,"prompt_cache_miss_tokens":100},"cost":0.00012814,"model":"deepseek-chat"}
|
||||
{"ts":"2026-04-21T13:22:11.830Z","turn":3,"role":"user","content":"Please change the shipping address to 5 Birch Rd, NYC, NY 10001."}
|
||||
{"ts":"2026-04-21T13:22:19.185Z","turn":3,"role":"assistant_final","content":"Perfect! I'll update the shipping address for your order o_1002 from \"22 Oak Rd, NYC, NY 10001\" to \"5 Birch Rd, NYC, NY 10001\".","usage":{"prompt_tokens":993,"completion_tokens":112,"total_tokens":1105,"prompt_cache_hit_tokens":64,"prompt_cache_miss_tokens":929},"cost":0.00037851,"model":"deepseek-chat"}
|
||||
{"ts":"2026-04-21T13:22:19.185Z","turn":3,"role":"tool","content":"{\"ok\":true,\"orderId\":\"o_1002\",\"newAddress\":\"5 Birch Rd, NYC, NY 10001\"}","tool":"update_address","args":"{\"orderId\": \"o_1002\", \"address\": \"5 Birch Rd, NYC, NY 10001\"}"}
|
||||
{"ts":"2026-04-21T13:22:19.185Z","turn":3,"role":"assistant_final","content":"Success! I've updated the shipping address for your order o_1002 to \"5 Birch Rd, NYC, NY 10001\". Your order is still in processing status, and the new address will be used for shipping. Is there anything else I can help you with?","usage":{"prompt_tokens":1149,"completion_tokens":56,"total_tokens":1205,"prompt_cache_hit_tokens":1088,"prompt_cache_miss_tokens":61},"cost":0.00015423000000000002,"model":"deepseek-chat"}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Transcript diff: baseline vs reasonix
|
||||
|
||||
## Meta
|
||||
|
||||
| | baseline | reasonix |
|
||||
|---|---|---|
|
||||
| source | bench/baseline | bench/reasonix |
|
||||
| model | deepseek-chat | deepseek-chat |
|
||||
| task | t01_address_happy | t01_address_happy |
|
||||
| startedAt | 2026-04-21T13:21:57.995Z | 2026-04-21T13:22:21.292Z |
|
||||
|
||||
## Summary
|
||||
|
||||
| metric | baseline | reasonix | delta |
|
||||
|---|---:|---:|---:|
|
||||
| model calls | 5 | 6 | +1 |
|
||||
| user turns | 3 | 3 | 0 |
|
||||
| tool calls | 2 | 3 | +1 |
|
||||
| cache hit | 45.9% | 93.9% | **+48.0pp** |
|
||||
| cost (USD) | $0.001192 | $0.000953 | -20.1% |
|
||||
| prefix hashes | 0 | 1 | — |
|
||||
|
||||
## Turn-by-turn
|
||||
|
||||
| turn | kind | baseline tool calls | reasonix tool calls | note |
|
||||
|---:|:---:|---|---|---|
|
||||
| 1 | diverge | — | lookup_order | tool calls differ: A=[—] B=[lookup_order] |
|
||||
| 2 | diverge | lookup_order | lookup_user | tool calls differ: A=[lookup_order] B=[lookup_user] |
|
||||
| 3 | diverge | update_address | update_address | text similarity 68% |
|
||||
|
||||
## First divergence (turn 1)
|
||||
|
||||
tool calls differ: A=[—] B=[lookup_order]
|
||||
|
||||
**baseline:**
|
||||
|
||||
```
|
||||
I can help you update the shipping address for your order. First, I need to verify your identity. Could you please provide your name and order ID? You've already given me order ID o_1002, but I also need your name to proceed with the address update.
|
||||
```
|
||||
|
||||
**reasonix:**
|
||||
|
||||
```
|
||||
Great! I can see order o_1002 is in "processing" status, which means I can update the shipping address. However, I still need to verify your identity. Could you please provide your name so I can look up your user information using the user ID associated with this order?
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
{"role":"_meta","meta":{"version":1,"source":"bench/reasonix","model":"deepseek-chat","task":"t01_address_happy","mode":"reasonix","repeat":1,"startedAt":"2026-04-21T13:22:21.292Z"}}
|
||||
{"ts":"2026-04-21T13:22:22.687Z","turn":1,"role":"user","content":"Hi, I need to update the shipping address for my order o_1002."}
|
||||
{"ts":"2026-04-21T13:22:27.201Z","turn":1,"role":"assistant_final","content":"I can help you update the shipping address for your order o_1002. First, I need to verify your identity. Could you please provide your name and order ID? I'll look up your order to check its current status and verify the details.","usage":{"prompt_tokens":713,"completion_tokens":98,"total_tokens":811,"prompt_cache_hit_tokens":704,"prompt_cache_miss_tokens":9},"cost":0.00015951000000000002,"model":"deepseek-chat","prefixHash":"fcb40cfd555bf805"}
|
||||
{"ts":"2026-04-21T13:22:27.201Z","turn":1,"role":"tool","content":"{\"orderId\":\"o_1002\",\"userId\":\"u_bo\",\"status\":\"processing\",\"address\":\"22 Oak Rd, NYC, NY 10001\",\"item\":\"running shoes 10\",\"price\":140}","tool":"lookup_order","args":"{\"orderId\": \"o_1002\"}"}
|
||||
{"ts":"2026-04-21T13:22:30.465Z","turn":1,"role":"assistant_final","content":"Great! I can see order o_1002 is in \"processing\" status, which means I can update the shipping address. However, I still need to verify your identity. Could you please provide your name so I can look up your user information using the user ID associated with this order?","usage":{"prompt_tokens":870,"completion_tokens":59,"total_tokens":929,"prompt_cache_hit_tokens":768,"prompt_cache_miss_tokens":102},"cost":0.00014620000000000003,"model":"deepseek-chat","prefixHash":"fcb40cfd555bf805"}
|
||||
{"ts":"2026-04-21T13:22:30.465Z","turn":1,"role":"done","content":"Great! I can see order o_1002 is in \"processing\" status, which means I can update the shipping address. However, I still need to verify your identity. Could you please provide your name so I can look up your user information using the user ID associated with this order?"}
|
||||
{"ts":"2026-04-21T13:22:31.801Z","turn":2,"role":"user","content":"My name is Bo Wang."}
|
||||
{"ts":"2026-04-21T13:22:34.932Z","turn":2,"role":"assistant_final","content":"Perfect, thank you Bo Wang. Let me verify your identity by looking up your user information.","usage":{"prompt_tokens":939,"completion_tokens":64,"total_tokens":1003,"prompt_cache_hit_tokens":896,"prompt_cache_miss_tokens":43},"cost":0.00014473,"model":"deepseek-chat","prefixHash":"fcb40cfd555bf805"}
|
||||
{"ts":"2026-04-21T13:22:34.932Z","turn":2,"role":"tool","content":"{\"userId\":\"u_bo\",\"name\":\"Bo Wang\",\"email\":\"[email protected]\"}","tool":"lookup_user","args":"{\"userId\": \"u_bo\"}"}
|
||||
{"ts":"2026-04-21T13:22:38.670Z","turn":2,"role":"assistant_final","content":"Excellent! Your identity has been verified, Bo Wang. I can see your order o_1002 is currently in \"processing\" status with the shipping address \"22 Oak Rd, NYC, NY 10001\".\n\nWhat would you like the new shipping address to be? Please provide the complete new address so I can update it for you.","usage":{"prompt_tokens":1039,"completion_tokens":68,"total_tokens":1107,"prompt_cache_hit_tokens":960,"prompt_cache_miss_tokens":79},"cost":0.00016333000000000002,"model":"deepseek-chat","prefixHash":"fcb40cfd555bf805"}
|
||||
{"ts":"2026-04-21T13:22:38.670Z","turn":2,"role":"done","content":"Excellent! Your identity has been verified, Bo Wang. I can see your order o_1002 is currently in \"processing\" status with the shipping address \"22 Oak Rd, NYC, NY 10001\".\n\nWhat would you like the new shipping address to be? Please provide the complete new address so I can update it for you."}
|
||||
{"ts":"2026-04-21T13:22:41.867Z","turn":3,"role":"user","content":"Please change it to 5 Birch Rd, NYC, NY 10001."}
|
||||
{"ts":"2026-04-21T13:22:45.479Z","turn":3,"role":"assistant_final","content":"","usage":{"prompt_tokens":1127,"completion_tokens":72,"total_tokens":1199,"prompt_cache_hit_tokens":1088,"prompt_cache_miss_tokens":39},"cost":0.00016589000000000003,"model":"deepseek-chat","prefixHash":"fcb40cfd555bf805"}
|
||||
{"ts":"2026-04-21T13:22:45.479Z","turn":3,"role":"tool","content":"{\"ok\":true,\"orderId\":\"o_1002\",\"newAddress\":\"5 Birch Rd, NYC, NY 10001\"}","tool":"update_address","args":"{\"orderId\": \"o_1002\", \"address\": \"5 Birch Rd, NYC, NY 10001\"}"}
|
||||
{"ts":"2026-04-21T13:22:48.394Z","turn":3,"role":"assistant_final","content":"Perfect! I've successfully updated the shipping address for your order o_1002. The new address is now \"5 Birch Rd, NYC, NY 10001\". \n\nYour order is still in processing status and will be shipped to this new address. Is there anything else I can help you with today?","usage":{"prompt_tokens":1243,"completion_tokens":62,"total_tokens":1305,"prompt_cache_hit_tokens":1152,"prompt_cache_miss_tokens":91},"cost":0.00017341000000000004,"model":"deepseek-chat","prefixHash":"fcb40cfd555bf805"}
|
||||
{"ts":"2026-04-21T13:22:48.394Z","turn":3,"role":"done","content":"Perfect! I've successfully updated the shipping address for your order o_1002. The new address is now \"5 Birch Rd, NYC, NY 10001\". \n\nYour order is still in processing status and will be shipped to this new address. Is there anything else I can help you with today?"}
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Task and result types for the tool-use eval harness.
|
||||
*
|
||||
* Scope note: this is NOT a full port of Sierra's τ-bench (airline+retail).
|
||||
* We mirror its *shape* — multi-turn tool-use with an LLM user simulator and
|
||||
* DB-end-state success predicates — so a later port can drop real tasks in
|
||||
* without changing the harness. Until then, see `tasks.ts` for our seed set.
|
||||
*/
|
||||
|
||||
import type { ToolDefinition } from "../../src/index.js";
|
||||
|
||||
/**
|
||||
* The mutable world state a task's tools operate on. Tasks deep-clone this
|
||||
* into a per-run snapshot so runs don't leak state across each other.
|
||||
*/
|
||||
export interface WorldState {
|
||||
[table: string]: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface UserPersona {
|
||||
/** Who the user is roleplaying (e.g. "frustrated customer"). */
|
||||
style: string;
|
||||
/** The concrete goal. The user pursues this until it's met or clearly refused. */
|
||||
goal: string;
|
||||
/**
|
||||
* Facts the simulator may reveal when the agent asks. Keep tight — the
|
||||
* user shouldn't volunteer everything up front.
|
||||
*/
|
||||
knowns: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A tool factory. We take a factory (not a ToolDefinition directly) because
|
||||
* each run needs a fresh closure over its per-run WorldState — otherwise all
|
||||
* runs would share (and mutate) the same DB.
|
||||
*/
|
||||
export type ToolFactory = (db: WorldState) => ToolDefinition;
|
||||
|
||||
export interface TaskDefinition {
|
||||
id: string;
|
||||
/** One-line human description. Not shown to the model. */
|
||||
description: string;
|
||||
/** System prompt given to the agent. Kept small so cache-hit ratio is comparable. */
|
||||
systemPrompt: string;
|
||||
/** Tools built fresh per run against the run's DB snapshot. */
|
||||
tools: ToolFactory[];
|
||||
/** Initial DB snapshot. Deep-cloned per run. */
|
||||
initialDb: WorldState;
|
||||
/** Persona + goal for the LLM user simulator. */
|
||||
user: UserPersona;
|
||||
/** Max turns of (user → agent) before we give up and mark fail. */
|
||||
maxTurns?: number;
|
||||
/**
|
||||
* Success predicate. Given the end-state DB (and optionally the final
|
||||
* agent utterance), return true iff the task is considered solved.
|
||||
*/
|
||||
check: (ctx: { db: WorldState; finalAgentMessage: string; transcript: Turn[] }) => boolean;
|
||||
}
|
||||
|
||||
export interface Turn {
|
||||
role: "user" | "agent" | "tool";
|
||||
content: string;
|
||||
toolName?: string;
|
||||
}
|
||||
|
||||
export type RunMode = "baseline" | "reasonix";
|
||||
|
||||
export interface RunResult {
|
||||
taskId: string;
|
||||
mode: RunMode;
|
||||
pass: boolean;
|
||||
turns: number;
|
||||
toolCalls: number;
|
||||
cacheHitRatio: number;
|
||||
costUsd: number;
|
||||
claudeEquivalentUsd: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
/** True if the run aborted before the user sim decided to stop. */
|
||||
truncated: boolean;
|
||||
finalAgentMessage: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface BenchMeta {
|
||||
date: string;
|
||||
model: string;
|
||||
userSimModel: string;
|
||||
taskCount: number;
|
||||
repeatsPerTask: number;
|
||||
/** Reasonix version written into the report for reproducibility. */
|
||||
reasonixVersion: string;
|
||||
}
|
||||
|
||||
export interface BenchReport {
|
||||
meta: BenchMeta;
|
||||
results: RunResult[];
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* LLM-backed user simulator for tool-use eval.
|
||||
*
|
||||
* A cheap V3 call that given (persona, goal, transcript so far) emits either
|
||||
* the user's next utterance or the literal sentinel `##STOP##` to end the
|
||||
* conversation. Kept intentionally small and low-temperature so dialogue
|
||||
* stays on-task and is replayable.
|
||||
*
|
||||
* We accept "some" non-determinism (DeepSeek samples at T=0.1) — the honest
|
||||
* way to deal with run-to-run drift is repeat-per-task in the runner, not
|
||||
* trying to pretend the sim is deterministic.
|
||||
*/
|
||||
|
||||
import type { ChatMessage, DeepSeekClient } from "../../src/index.js";
|
||||
import type { Turn, UserPersona } from "./types.js";
|
||||
|
||||
const SYS = `You are roleplaying a user contacting a retail support agent.
|
||||
Rules:
|
||||
- Stay in character; never break the fourth wall.
|
||||
- Never reveal you are an AI or mention any system.
|
||||
- Pursue the goal. Do not volunteer facts the agent hasn't asked for.
|
||||
- Keep replies to one or two sentences.
|
||||
- When the goal is clearly met OR clearly refused by the agent, output ONLY the literal token: ##STOP##
|
||||
- Do not output ##STOP## on your first message — give the agent a chance.`;
|
||||
|
||||
export interface UserSimOptions {
|
||||
model?: string;
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export class UserSimulator {
|
||||
constructor(
|
||||
private client: DeepSeekClient,
|
||||
private persona: UserPersona,
|
||||
private opts: UserSimOptions = {},
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Given the transcript so far, produce the user's next line, or null if
|
||||
* the simulator decided the conversation is over.
|
||||
*/
|
||||
async next(transcript: Turn[]): Promise<string | null> {
|
||||
const knowns = JSON.stringify(this.persona.knowns, null, 2);
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: `${SYS}\n\nCharacter: ${this.persona.style}\nGoal: ${this.persona.goal}\nFacts you may share when asked (don't volunteer):\n${knowns}`,
|
||||
},
|
||||
];
|
||||
|
||||
if (transcript.length === 0) {
|
||||
messages.push({
|
||||
role: "user",
|
||||
content:
|
||||
"Write your opening message to the support agent. One or two sentences. Do not dump all the facts.",
|
||||
});
|
||||
} else {
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: `Here is the conversation so far (you are the USER).\n\n${transcriptToString(
|
||||
transcript,
|
||||
)}\n\nWrite ONLY your next user reply, or output ##STOP## if the goal is clearly met or clearly refused.`,
|
||||
});
|
||||
}
|
||||
|
||||
const resp = await this.client.chat({
|
||||
model: this.opts.model ?? "deepseek-chat",
|
||||
messages,
|
||||
temperature: this.opts.temperature ?? 0.1,
|
||||
maxTokens: 200,
|
||||
});
|
||||
const text = resp.content.trim();
|
||||
if (!text) return null;
|
||||
if (text === "##STOP##" || text.endsWith("##STOP##") || text.includes("##STOP##")) return null;
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
function transcriptToString(turns: Turn[]): string {
|
||||
const lines: string[] = [];
|
||||
for (const t of turns) {
|
||||
if (t.role === "user") lines.push(`USER: ${t.content}`);
|
||||
else if (t.role === "agent") lines.push(`AGENT: ${t.content}`);
|
||||
else if (t.role === "tool")
|
||||
lines.push(`(tool ${t.toolName} returned: ${truncate(t.content, 200)})`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function truncate(s: string, n: number): string {
|
||||
return s.length > n ? `${s.slice(0, n)}…` : s;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"organizeImports": { "enabled": true },
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": { "quoteStyle": "double", "semicolons": "always", "trailingCommas": "all" }
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"noNonNullAssertion": "off",
|
||||
"useImportType": "warn"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["dist", "node_modules", "coverage", "*.d.ts"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
# Reasonix Architecture
|
||||
|
||||
## Design philosophy
|
||||
|
||||
Reasonix is **opinionated, not general**. Every abstraction is justified by a
|
||||
DeepSeek-specific behavior or economic property. If it's generic, we don't
|
||||
ship it.
|
||||
|
||||
## The three pillars
|
||||
|
||||
### Pillar 1 — Cache-First Loop
|
||||
|
||||
**Problem.** DeepSeek bills cached input at ~10% of the miss rate. Automatic
|
||||
prefix caching activates only when the *exact* byte prefix of the previous
|
||||
request matches. Most agent loops reorder, rewrite, or inject fresh
|
||||
timestamps each turn — cache hit rate in practice: <20%.
|
||||
|
||||
**Solution.** Partition the context into three regions:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ IMMUTABLE PREFIX │ ← fixed for session
|
||||
│ system + tool_specs + few_shots │ cache hit candidate
|
||||
├─────────────────────────────────────────┤
|
||||
│ APPEND-ONLY LOG │ ← grows monotonically
|
||||
│ [assistant₁][tool₁][assistant₂]... │ preserves prefix of prior turns
|
||||
├─────────────────────────────────────────┤
|
||||
│ VOLATILE SCRATCH │ ← reset each turn
|
||||
│ R1 thought, transient plan state │ never sent upstream
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Invariants:**
|
||||
1. Prefix is computed once per session, hashed, and pinned.
|
||||
2. Log entries are serialized in append order; no rewrites.
|
||||
3. Scratch is distilled via Pillar 2 before any information from it is folded
|
||||
into the log.
|
||||
|
||||
**Metric.** `prompt_cache_hit_tokens / (hit + miss)` exposed per-turn and
|
||||
aggregated per-session. This is the user-visible proof of Pillar 1's value.
|
||||
|
||||
### Pillar 2 — R1 Thought Harvesting *(v0.0.3, opt-in)*
|
||||
|
||||
**Problem.** R1 emits extensive `reasoning_content`. DeepSeek's own docs
|
||||
recommend *not* feeding it back to the next turn. Most frameworks display it
|
||||
to the user and discard. The planning signal inside is lost.
|
||||
|
||||
**Solution.** A two-stage process:
|
||||
|
||||
```
|
||||
R1 output → Harvester (V3, cheap) → TypedPlanState
|
||||
├─ subgoals: string[]
|
||||
├─ hypotheses: string[]
|
||||
├─ uncertainties: string[]
|
||||
└─ rejectedPaths: string[]
|
||||
```
|
||||
|
||||
The harvester is a cheap V3 call with a strict JSON schema. Output is
|
||||
validated at runtime. The typed state is queryable by the orchestrator — e.g.
|
||||
"if `uncertainties.length > 2`, trigger branch sampling."
|
||||
|
||||
### Branch-and-Select *(v0.0.5, opt-in, builds on Pillar 2)*
|
||||
|
||||
**Why now.** DeepSeek is cheap enough that running N=3 R1 samples is still
|
||||
cheaper than a single Claude call. What was research luxury (self-consistency
|
||||
sampling) becomes a practical default.
|
||||
|
||||
**How.** When `--branch N` (or `{ branch: N }` in code) is passed:
|
||||
|
||||
1. The turn is forced non-streaming (N samples must complete before we can
|
||||
compare).
|
||||
2. `runBranches` fires N `client.chat` calls in parallel, spreading
|
||||
temperatures across `[0, 1]` to diversify reasoning paths.
|
||||
3. Each sample's `reasoning_content` is piped through Pillar 2's harvest.
|
||||
4. The default selector picks the sample with the fewest `uncertainties`;
|
||||
ties are broken by shorter answer length (Occam).
|
||||
5. The loop emits a `branch_summary` alongside the winning `assistant_final`.
|
||||
|
||||
**Cost.** 3× R1 on a hard math problem ≈ \$0.02 — still dominates Claude on
|
||||
cost/quality for agentic work.
|
||||
|
||||
### Pillar 3 — Tool-Call Repair *(v0.0.1 ships complete)*
|
||||
|
||||
**Problem.** Empirical DeepSeek failure modes:
|
||||
- Tool-call JSON emitted inside `<think>`, missing from the final message.
|
||||
- Arguments dropped when schema has >10 params or deeply nested objects.
|
||||
- Same tool called repeatedly with identical args (call-storm).
|
||||
- Truncated JSON due to `max_tokens` hit mid-structure.
|
||||
|
||||
**Solution.** Four passes:
|
||||
|
||||
1. **`flatten`** — schemas with >10 leaf params or depth >2 are auto-detected
|
||||
on `ToolRegistry.register()` and presented to the model in dot-notation
|
||||
form. `dispatch()` re-nests the args before calling the user's `fn`.
|
||||
Opt out with `new ToolRegistry({ autoFlatten: false })`.
|
||||
2. **`scavenge`** — regex + JSON parser sweeps `reasoning_content` for any tool
|
||||
call the model forgot to emit in `tool_calls`.
|
||||
3. **`truncation`** — detect unbalanced JSON and repair by closing braces or
|
||||
requesting a continuation completion.
|
||||
4. **`storm`** — identical `(tool, args)` tuple within a sliding window →
|
||||
suppress the call, inject a reflection turn.
|
||||
|
||||
## Module layout
|
||||
|
||||
```
|
||||
src/
|
||||
├── client.ts # httpx-equivalent DeepSeek client (fetch + SSE)
|
||||
├── loop.ts # Pillar 1: Cache-First Loop (async iterator)
|
||||
├── harvest.ts # Pillar 2 stub (v0.0.1 surface only)
|
||||
├── repair/
|
||||
│ ├── index.ts # Pillar 3 pipeline
|
||||
│ ├── scavenge.ts
|
||||
│ ├── flatten.ts
|
||||
│ ├── truncation.ts
|
||||
│ └── storm.ts
|
||||
├── tools.ts # Tool registry + dispatch
|
||||
├── memory.ts # Prefix / Log / Scratch primitives
|
||||
├── telemetry.ts # Cost & cache-hit accounting
|
||||
├── types.ts # Shared type definitions
|
||||
├── index.ts # Library barrel export
|
||||
└── cli/
|
||||
├── index.ts # commander entry
|
||||
├── commands/ # chat, run, stats, version
|
||||
└── ui/ # Ink React components (App, StatsPanel, EventLog, PromptInput)
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- **v0.0.1** — Pillar 1 end-to-end, Pillar 3 complete, Ink TUI, τ-bench scaffold.
|
||||
- **v0.0.2** — First-run key prompt, saved to `~/.reasonix/config.json`.
|
||||
- **v0.0.3** — Pillar 2 MVP (opt-in harvest), retry layer, TextInput fix.
|
||||
- **v0.0.4** — Schema flatten auto-applied in ToolRegistry (closes Pillar 3).
|
||||
- **v0.0.5** — Self-consistency branching (`--branch N`) driven by Pillar 2
|
||||
plan-state uncertainty count.
|
||||
- **v0.0.6** — Named session persistence (`--session <name>`), JSONL log
|
||||
stored under `~/.reasonix/sessions/`, restored into the append-only log
|
||||
on resume.
|
||||
- **v0.1** — τ-bench numbers published, streaming polish, transcript replay.
|
||||
- **v0.2** — Self-consistency / branch-budget sampling driven by plan state.
|
||||
- **v0.3** — MCP client, session persistence.
|
||||
|
||||
## Explicit non-goals
|
||||
|
||||
- Multi-agent orchestration.
|
||||
- RAG / vector retrieval.
|
||||
- Support for non-DeepSeek backends.
|
||||
- Web UI / SaaS.
|
||||
@@ -0,0 +1,25 @@
|
||||
/** Minimal example: one-shot, non-streaming. Needs DEEPSEEK_API_KEY. */
|
||||
import {
|
||||
CacheFirstLoop,
|
||||
DeepSeekClient,
|
||||
ImmutablePrefix,
|
||||
loadDotenv,
|
||||
} from "../src/index.js";
|
||||
|
||||
loadDotenv();
|
||||
|
||||
async function main() {
|
||||
const client = new DeepSeekClient();
|
||||
const prefix = new ImmutablePrefix({ system: "You are a concise assistant." });
|
||||
const loop = new CacheFirstLoop({ client, prefix, stream: false });
|
||||
|
||||
const answer = await loop.run("In one sentence, what is prompt caching?");
|
||||
console.log(answer);
|
||||
console.log("---");
|
||||
console.log(loop.stats.summary());
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/** Tool-use example: register a calculator tool. Needs DEEPSEEK_API_KEY. */
|
||||
import {
|
||||
CacheFirstLoop,
|
||||
DeepSeekClient,
|
||||
ImmutablePrefix,
|
||||
ToolRegistry,
|
||||
loadDotenv,
|
||||
} from "../src/index.js";
|
||||
|
||||
loadDotenv();
|
||||
|
||||
const tools = new ToolRegistry();
|
||||
tools.register<{ a: number; b: number }, number>({
|
||||
name: "add",
|
||||
description: "Add two integers.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
a: { type: "integer" },
|
||||
b: { type: "integer" },
|
||||
},
|
||||
required: ["a", "b"],
|
||||
},
|
||||
fn: ({ a, b }) => a + b,
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const client = new DeepSeekClient();
|
||||
const prefix = new ImmutablePrefix({
|
||||
system: "You are a calculator assistant. Use the `add` tool for addition.",
|
||||
toolSpecs: tools.specs(),
|
||||
});
|
||||
const loop = new CacheFirstLoop({ client, prefix, tools });
|
||||
|
||||
const answer = await loop.run("What is 17 + 25?");
|
||||
console.log("answer:", answer);
|
||||
console.log("stats:", loop.stats.summary());
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Generated
+3296
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "reasonix",
|
||||
"version": "0.2.1",
|
||||
"description": "DeepSeek-native agent framework: cache-first loop, R1 thought harvesting, tool-call repair.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"reasonix": "dist/cli/index.js"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsx src/cli/index.ts",
|
||||
"chat": "tsx src/cli/index.ts chat",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"lint": "biome check src tests",
|
||||
"lint:fix": "biome check --write src tests",
|
||||
"format": "biome format --write src tests",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"agent",
|
||||
"llm",
|
||||
"deepseek",
|
||||
"r1",
|
||||
"tool-use",
|
||||
"prompt-cache",
|
||||
"cli",
|
||||
"tui"
|
||||
],
|
||||
"author": "esengine",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/esengine/reasonix.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/esengine/reasonix/issues"
|
||||
},
|
||||
"homepage": "https://github.com/esengine/reasonix#readme",
|
||||
"engines": {
|
||||
"node": ">=20.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^12.1.0",
|
||||
"eventsource-parser": "^3.0.0",
|
||||
"ink": "^5.1.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"tsup": "^8.3.5",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.6.3",
|
||||
"vitest": "^2.1.5"
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
// Served from the custom domain reasonix.io at the site root.
|
||||
export default defineConfig({
|
||||
site: 'https://reasonix.io',
|
||||
// Documentation code samples intentionally use source newlines as content.
|
||||
// Astro 7's JSX-style compression removes those newlines unless disabled.
|
||||
compressHTML: false,
|
||||
build: { assets: 'static' },
|
||||
integrations: [sitemap({
|
||||
filter: (page) => !/\/changelog\/(?:stable|preview|studio)\/?$/.test(page) &&
|
||||
!/\/changelog\/v\d+\.\d+\.\d+-/.test(page) &&
|
||||
!/\/(?:login|register|forgot|reset|account|device|u)\/?$/.test(page),
|
||||
})],
|
||||
});
|
||||
Generated
-4248
File diff suppressed because it is too large.
Load diff
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "reasonix-site",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"prebuild": "node scripts/releases/sync-releases.mjs && node scripts/fetch-community.mjs && node scripts/fetch-publications.mjs",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"test": "node --test src/scripts/*.test.mjs scripts/releases/*.test.mjs",
|
||||
"release:validate": "node scripts/releases/release-notes.mjs validate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^3.7.4",
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||
"astro": "^7.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sharp": "^0.35.3"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "0.28.1"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="296" height="296" viewBox="0 0 296 296" class="segno"><title>QQ group 1093562660 join QR code</title><g transform="scale(8)"><path fill="#fff" d="M0 0h37v37h-37z"/><path class="qrline" stroke="#000" d="M4 4.5h7m2 0h1m1 0h1m2 0h1m2 0h1m1 0h2m1 0h7m-29 1h1m5 0h1m5 0h1m3 0h2m2 0h1m1 0h1m5 0h1m-29 1h1m1 0h3m1 0h1m2 0h4m1 0h2m1 0h1m2 0h1m1 0h1m1 0h3m1 0h1m-29 1h1m1 0h3m1 0h1m6 0h1m2 0h1m1 0h3m1 0h1m1 0h3m1 0h1m-29 1h1m1 0h3m1 0h1m11 0h1m1 0h1m1 0h1m1 0h3m1 0h1m-29 1h1m5 0h1m1 0h5m2 0h3m1 0h1m2 0h1m5 0h1m-29 1h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7m-20 1h1m1 0h3m1 0h3m-18 1h1m2 0h1m1 0h2m1 0h1m1 0h2m2 0h1m1 0h1m1 0h2m1 0h1m1 0h1m-21 1h1m1 0h1m1 0h2m1 0h1m2 0h1m1 0h4m1 0h1m1 0h1m2 0h1m2 0h1m-29 1h4m1 0h4m1 0h2m1 0h3m2 0h2m3 0h5m-28 1h4m4 0h1m3 0h2m2 0h1m9 0h2m-28 1h2m2 0h1m1 0h1m1 0h5m1 0h2m1 0h1m1 0h2m1 0h2m1 0h1m1 0h2m-28 1h1m1 0h2m3 0h1m2 0h1m1 0h4m1 0h2m1 0h1m-22 1h2m1 0h2m1 0h1m1 0h1m1 0h1m1 0h3m3 0h1m1 0h2m1 0h1m1 0h4m-28 1h2m1 0h2m2 0h1m1 0h1m4 0h1m1 0h1m1 0h1m1 0h2m1 0h2m1 0h1m-28 1h4m2 0h2m1 0h3m1 0h1m3 0h3m1 0h1m5 0h1m-27 1h2m2 0h1m1 0h1m2 0h1m1 0h5m1 0h1m2 0h2m2 0h1m2 0h1m-29 1h1m1 0h2m1 0h6m3 0h5m3 0h3m2 0h2m-27 1h2m4 0h1m1 0h2m2 0h1m2 0h1m1 0h2m3 0h1m2 0h2m-29 1h1m2 0h1m2 0h1m2 0h1m5 0h3m1 0h6m1 0h1m-19 1h2m1 0h2m1 0h2m1 0h4m3 0h1m1 0h3m-29 1h7m2 0h1m2 0h1m4 0h2m1 0h1m1 0h1m1 0h1m2 0h1m-28 1h1m5 0h1m1 0h2m1 0h1m2 0h2m3 0h2m3 0h5m-29 1h1m1 0h3m1 0h1m2 0h2m1 0h1m3 0h3m1 0h5m2 0h1m-28 1h1m1 0h3m1 0h1m1 0h2m2 0h2m1 0h2m1 0h2m2 0h5m1 0h1m-29 1h1m1 0h3m1 0h1m2 0h2m1 0h1m1 0h1m2 0h4m3 0h3m1 0h1m-29 1h1m5 0h1m2 0h1m2 0h1m1 0h1m2 0h1m1 0h2m1 0h1m4 0h1m-28 1h7m1 0h1m3 0h2m2 0h2m2 0h3m1 0h1m2 0h1"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_1" data-name=" 图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #0153e5;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="cls-2" y="0" width="64" height="64" rx="20.22" ry="20.22"/>
|
||||
<path class="cls-1" d="M40.53,37.67h0c2.26-.67,4.24-1.86,5.83-3.59h0c1.73-1.94,2.82-4.29,3.2-6.87.86-5.64-1.94-10.79-7.21-12.97-1.85-.74-3.76-1.06-5.78-1.04h-19.6v37.56h9.64v-12.68c1.05.34,2.16.52,3.29.52l3.15,4.35,5.73,7.83,11.44-.02-9.7-13.1h0ZM40.14,35.94c-1.31.43-2.72.37-4.06-.02-2.3-1.74-3.55-3.6-5.52-5.37-2.52-2.27-5.84-3.84-9.25-3.69.27-.74.66-1.45,1.17-2.11,1.76-2.2,4.16-2.9,6.92-2.51,1.02.14,2.11-1.05,4.18-.82.14.02.28.12.29.2.05.28-.75.44-.75,1.21,0,.32.15.7.45.92,1.06.74,1.97,1.57,2.93,2.43.47.42,2.04,1.59,2.44.64.24-.56.39-1.14.54-1.74.08-.29-.09-.47-.32-.61-1.63-.96-2.24-2.97-1.6-4.75.06-.17.22-.25.36-.26.55-.02.25.99,1.61,1.4,1.28.4,1.32,1.46,1.94,1.04,1.45-.99,1.97-.18,3.32-1.45.14-.13.38-.14.52-.04.09.06.17.22.16.41-.04,1.07-.46,2.1-1.19,2.88-1.39,1.49-2.55.77-2.62,1.82-.22,3.06-1.22,6.11-3.39,8.33-.07.07-.11.17-.1.24.01.07.1.14.19.17l1.83.62c.26.09.45.32.41.57-.03.22-.2.41-.45.49h0Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_1" data-name=" 图层 1" xmlns="http://www.w3.org/2000/svg" width="1151.55" height="217.07" viewBox="0 0 1151.55 217.07">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #0153e5;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<circle class="cls-1" cx="972.16" cy="33.55" r="23.88"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M488.72,211.37c-12.37,1.71-24.96.46-38.13.63l-1.23-16.27c-12.87,14.9-29.84,21.97-49.07,19.87-24.58-2.69-44.41-19.38-53.2-42.6-7.32-19.32-6.99-40.86.81-60.12,9.5-23.46,30.72-40.21,55.73-42.02,18.41-1.33,34.33,4.84,45.96,18.54l1.13-15.37,37.63-.22.36,137.56h.01ZM451.41,143.22c0-19.82-16.06-35.88-35.88-35.88s-35.88,16.06-35.88,35.88,16.06,35.88,35.88,35.88,35.88-16.06,35.88-35.88Z"/>
|
||||
<path class="cls-1" d="M294.69,165.71c11.08,4.99,20.6,11.87,31.28,18.31-14.54,22.26-39.3,32.78-65.14,31.91-42.29-1.42-73.12-35.5-70.7-77.31,2.1-36.29,28.66-64.56,65.17-67.92,26.31-2.42,52.01,8.19,66.02,30.99,9.52,15.5,11.55,33.39,9.76,50.92l-101,.13c1.4,14.77,13.51,25.01,25.92,27.74,14.72,3.24,29.02-.82,38.69-14.77h0ZM294.02,127.16c-4.12-16.52-17.68-24.4-32.5-24.13-13.74.25-27.5,9.07-30.5,24.11l63,.02h0Z"/>
|
||||
<path class="cls-1" d="M780.3,143.31c0,40.74-33.02,73.76-73.76,73.76s-73.76-33.02-73.76-73.76,33.02-73.76,73.76-73.76,73.76,33.02,73.76,73.76ZM742.63,143.26c0-19.97-16.19-36.16-36.16-36.16s-36.16,16.19-36.16,36.16,16.19,36.16,36.16,36.16,36.16-16.19,36.16-36.16Z"/>
|
||||
<path class="cls-1" d="M865.82,106.67c-16.4-.48-30.2,10.42-30.32,26.41l-.59,78.88-38.91-.18.06-137.83,38.15.35c.41,5.66-1.03,9.97,1.45,15.75,19.54-26.51,63.34-25.07,82.95-1.16,9.2,11.23,13.46,25.62,13.45,40.38l-.06,82.62-38.94.14-.69-79.28c-.13-14.92-10.82-25.64-26.54-26.1v.02h-.01Z"/>
|
||||
<path class="cls-1" d="M1151.55,211.51l-44.84.51-29.9-41.82-30.52,41.81-43.29-.52,50.71-70.03-48.12-67.06c16.29-1.81,30.31-.14,45.42-.02l26.06,38.81,27.14-39.29c14.85-.38,28.79-.61,44.5.69l-47.96,66.65,50.79,70.28h0Z"/>
|
||||
<path class="cls-1" d="M595.11,210.55c-30.01,11.52-78.69,6.96-94.37-27.63l29.88-14.4c4.49,1.96,5.5,6.8,9.44,9.49,16.43,11.17,43.99,9.75,44.36-4.61.12-4.72-3.46-8.89-8.58-10.29l-36.51-9.95c-11.77-3.21-22.59-9.81-28.13-21.2-10.25-21.06.12-45.53,21.63-54.96,29.01-12.72,77.53-8.27,89.7,25.13l-29.06,12.62c-10.47-12.8-27.66-16.84-42.64-10.42-3.51,1.5-5.54,6.03-5.28,8.81.35,3.7,3.39,7.71,7.59,8.86l40.46,11.09c9.77,2.68,19.02,7.82,24.29,16.77,13.52,22.96,2.31,51.05-22.78,60.69h0Z"/>
|
||||
<rect class="cls-1" x="902.92" y="123.55" width="138.32" height="38.82" transform="translate(828.4464 1114.9374) rotate(-89.96)"/>
|
||||
<path class="cls-1" d="M134.57,139.75l.02-.02c12.9-3.8,24.24-10.63,33.31-20.48v-.03c9.89-11.07,16.1-24.46,18.25-39.19,4.93-32.21-11.06-61.62-41.17-74.07C134.39,1.71,123.51-.11,111.97,0L0,.03v214.57l55.04-.04v-72.42c6,1.93,12.33,2.97,18.82,2.94l18.02,24.87,32.75,44.72,65.37-.09-55.43-74.84h0ZM132.36,129.86c-7.5,2.48-15.55,2.11-23.21-.12-13.17-9.95-20.3-20.57-31.51-30.7-14.37-12.97-33.39-21.96-52.86-21.08,1.55-4.25,3.78-8.31,6.68-12.03,10.05-12.55,23.77-16.55,39.52-14.34,5.82.82,12.03-6.02,23.86-4.7.79.09,1.6.68,1.67,1.16.27,1.61-4.3,2.49-4.3,6.9,0,1.86.83,4.01,2.59,5.24,6.03,4.23,11.26,8.99,16.72,13.9,2.66,2.39,11.66,9.1,13.95,3.64,1.34-3.19,2.22-6.53,3.11-9.93.44-1.68-.49-2.68-1.85-3.48-9.29-5.51-12.79-16.96-9.16-27.12.35-.98,1.26-1.46,2.03-1.49,3.12-.13,1.46,5.65,9.18,8.03,7.32,2.26,7.54,8.34,11.1,5.92,8.29-5.65,11.28-1.02,18.98-8.27.79-.74,2.15-.8,2.97-.22.51.35.95,1.26.91,2.33-.22,6.12-2.61,11.97-6.8,16.46-7.91,8.49-14.56,4.43-14.99,10.38-1.26,17.46-6.97,34.9-19.37,47.58-.4.41-.63.99-.57,1.38.07.41.56.82,1.1.99l10.44,3.55c1.47.5,2.55,1.83,2.35,3.24-.17,1.23-1.15,2.32-2.55,2.79h.01Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 77 KiB |
@@ -1,4 +0,0 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://reasonix.io/sitemap-index.xml
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,63 +0,0 @@
|
||||
// Prebuild: fetch live community stats + bake contributor avatars into public/av/.
|
||||
// Never fails the build — falls back to the committed snapshot on any error.
|
||||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const repo = 'esengine/DeepSeek-Reasonix';
|
||||
const api = `https://api.github.com/repos/${repo}`;
|
||||
const headers = { 'User-Agent': 'reasonix-site', Accept: 'application/vnd.github+json' };
|
||||
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
|
||||
const fallback = JSON.parse(await readFile('src/data/contributors.json', 'utf8'));
|
||||
let { stars, mergedPrs, contributors, list } = fallback;
|
||||
|
||||
try {
|
||||
const r = await fetch(`${api}/contributors?per_page=100`, { headers });
|
||||
if (r.ok) {
|
||||
const humans = (await r.json())
|
||||
.filter((c) => c.type === 'User')
|
||||
.map((c) => ({ login: c.login, avatar: String(c.avatar_url).split('?')[0], url: c.html_url, c: c.contributions }));
|
||||
if (humans.length) { list = humans; contributors = humans.length; }
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
const r = await fetch(api, { headers });
|
||||
if (r.ok) { const d = await r.json(); if (typeof d.stargazers_count === 'number') stars = d.stargazers_count; }
|
||||
} catch {}
|
||||
try {
|
||||
const r = await fetch(`https://api.github.com/search/issues?q=repo:${repo}+is:pr+is:merged&per_page=1`, { headers });
|
||||
if (r.ok) { const d = await r.json(); if (typeof d.total_count === 'number') mergedPrs = d.total_count; }
|
||||
} catch {}
|
||||
|
||||
// 64px is the largest rendered avatar; bake at 2x. GitHub honours ?s= for its
|
||||
// own CDN but serves some custom avatars unscaled and in whatever format was
|
||||
// uploaded, so re-encode rather than trusting the response.
|
||||
const AVATAR_PX = 128;
|
||||
|
||||
await rm('public/av', { recursive: true, force: true });
|
||||
await mkdir('public/av', { recursive: true });
|
||||
let baked = 0;
|
||||
let bakedBytes = 0;
|
||||
const out = await Promise.all(list.map(async (c) => {
|
||||
const remote = `${c.avatar}?s=${AVATAR_PX}`;
|
||||
try {
|
||||
const r = await fetch(remote);
|
||||
if (!r.ok) throw new Error(String(r.status));
|
||||
const webp = await sharp(Buffer.from(await r.arrayBuffer()))
|
||||
.resize(AVATAR_PX, AVATAR_PX, { fit: 'cover' })
|
||||
.webp({ quality: 80 })
|
||||
.toBuffer();
|
||||
await writeFile(`public/av/${c.login}.webp`, webp);
|
||||
baked++;
|
||||
bakedBytes += webp.byteLength;
|
||||
return { ...c, avatar: `/av/${c.login}.webp` };
|
||||
} catch {
|
||||
return { ...c, avatar: remote };
|
||||
}
|
||||
}));
|
||||
|
||||
await writeFile('src/data/community.json', JSON.stringify({ stars, mergedPrs, contributors, list: out }, null, 2) + '\n');
|
||||
console.log(
|
||||
`community: ${contributors} contributors · ${stars} stars · ${mergedPrs} merged PRs · ` +
|
||||
`${baked}/${out.length} avatars baked (${(bakedBytes / 1024).toFixed(0)} KB)`,
|
||||
);
|
||||
@@ -1,49 +0,0 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { validateReleaseEvent } from "./release-event.mjs";
|
||||
|
||||
const siteRoot = resolve(import.meta.dirname, "..");
|
||||
const catalog = JSON.parse(await readFile(resolve(siteRoot, "src/data/releases.json"), "utf8"));
|
||||
const output = resolve(siteRoot, ".generated/publications.json");
|
||||
const headers = {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": "reasonix-site-publications",
|
||||
};
|
||||
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
|
||||
const publications = [];
|
||||
for (const release of catalog.releases) {
|
||||
// Legacy entries predate publication markers and remain public. Every new
|
||||
// reviewed entry fails closed until its exact CLI release carries the marker
|
||||
// uploaded by the all-surface postflight.
|
||||
if (!release.status || release.status === "published") {
|
||||
publications.push(release.version);
|
||||
continue;
|
||||
}
|
||||
const tag = `v${release.version}`;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/esengine/DeepSeek-Reasonix/releases/tags/${encodeURIComponent(tag)}`,
|
||||
{ headers, signal: AbortSignal.timeout(20_000) },
|
||||
);
|
||||
if (!response.ok) continue;
|
||||
const githubRelease = await response.json();
|
||||
const marker = githubRelease.assets?.find((asset) => asset.name === "release-event.json" && asset.size > 0);
|
||||
if (!marker?.browser_download_url) continue;
|
||||
const markerResponse = await fetch(marker.browser_download_url, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(20_000),
|
||||
});
|
||||
if (!markerResponse.ok) continue;
|
||||
const event = await markerResponse.json();
|
||||
validateReleaseEvent(event, release);
|
||||
publications.push(release.version);
|
||||
} catch {
|
||||
// A reviewed release must never become public because an upstream lookup
|
||||
// failed. The next Pages build retries after the release postflight.
|
||||
}
|
||||
}
|
||||
|
||||
await mkdir(resolve(siteRoot, ".generated"), { recursive: true });
|
||||
await writeFile(output, `${JSON.stringify({ schemaVersion: 1, publications }, null, 2)}\n`);
|
||||
console.log(`Resolved ${publications.length} public release note(s).`);
|
||||
@@ -1,27 +0,0 @@
|
||||
function invariant(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
export function validateReleaseEvent(event, release) {
|
||||
invariant(event && typeof event === "object" && !Array.isArray(event), "release event must be an object");
|
||||
invariant(event.schemaVersion === 1, "release event schemaVersion must be 1");
|
||||
invariant(event.releaseId === release.version, "release event releaseId does not match reviewed notes");
|
||||
invariant(
|
||||
event.channel === (release.channel === "prerelease" ? "preview" : "stable"),
|
||||
"release event channel does not match reviewed notes",
|
||||
);
|
||||
invariant(/^[0-9a-f]{40}$/.test(event.candidateSha), "release event candidateSha must be a full commit SHA");
|
||||
if (release.candidateSha) {
|
||||
invariant(event.candidateSha === release.candidateSha, "release event candidateSha does not match reviewed notes");
|
||||
}
|
||||
invariant(/^\d{4}-\d{2}-\d{2}T/.test(event.publishedAt), "release event publishedAt must be an ISO timestamp");
|
||||
invariant(event.releaseNotesUrl === `https://reasonix.io/changelog/v${release.version}/`, "release event URL is invalid");
|
||||
invariant(event.builds && typeof event.builds === "object", "release event builds are required");
|
||||
for (const surface of ["cli", "desktop", "npm"]) {
|
||||
invariant(typeof event.builds[surface] === "string" && event.builds[surface], `release event builds.${surface} is required`);
|
||||
if (release.builds?.[surface]) {
|
||||
invariant(event.builds[surface] === release.builds[surface], `release event builds.${surface} does not match reviewed notes`);
|
||||
}
|
||||
}
|
||||
return event;
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
import { withoutOwner } from "../../src/lib/repository-owner.mjs";
|
||||
import { compareVersionsDesc, loadCatalog, upsertRelease, validateCatalog } from "./release-notes.mjs";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const apiBase = process.env.DEEPSEEK_API_BASE || "https://api.deepseek.com";
|
||||
const model = process.env.DEEPSEEK_MODEL || "deepseek-v4-pro";
|
||||
const releaseTargetOrder = ["desktop", "cli", "site", "service"];
|
||||
|
||||
export const releaseOutputBudgets = Object.freeze({
|
||||
standard: Object.freeze({ highlights: 6, changes: 15, guides: 6, upgrade: 4, risks: 4, bodyChars: 280 }),
|
||||
compact: Object.freeze({ highlights: 4, changes: 9, guides: 4, upgrade: 2, risks: 2, bodyChars: 220 }),
|
||||
});
|
||||
|
||||
export function editorialLimitInstruction(compact = false) {
|
||||
const budget = compact ? releaseOutputBudgets.compact : releaseOutputBudgets.standard;
|
||||
return `Editorial budget: at most ${budget.highlights} highlights, ${budget.changes} total change items across new/improved/fixed, ${budget.guides} guides, ${budget.upgrade} upgrade notes, and ${budget.risks} risks. Keep every English and Chinese body within ${budget.bodyChars} characters. Select the most important user outcomes across the supplied PRs, combine related work, and avoid repeating the same outcome in highlights and changes. Always return a complete JSON object within this budget.`;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const values = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (!arg.startsWith("--")) throw new Error(`unexpected argument ${arg}`);
|
||||
values[arg.slice(2)] = argv[++index];
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function runGit(args) {
|
||||
return execFileSync("git", args, { cwd: repoRoot, encoding: "utf8" }).trim();
|
||||
}
|
||||
|
||||
function normalizeVersion(version) {
|
||||
return String(version || "").replace(/^(?:desktop-|npm-)?v/, "");
|
||||
}
|
||||
|
||||
function repositoryName() {
|
||||
if (process.env.GITHUB_REPOSITORY) return process.env.GITHUB_REPOSITORY;
|
||||
const remote = runGit(["remote", "get-url", "origin"]);
|
||||
const match = remote.match(/github\.com[/:]([^/]+\/[^/.]+)(?:\.git)?$/);
|
||||
if (!match) throw new Error("cannot determine GitHub repository; set GITHUB_REPOSITORY");
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function commitRange(from, to) {
|
||||
return runGit(["log", "--first-parent", "--format=%H%x09%s%x09%b%x00", `${from}..${to}`])
|
||||
.split("\0")
|
||||
.map((record) => record.trim())
|
||||
.filter(Boolean)
|
||||
.map((record) => {
|
||||
const [sha, subject, ...body] = record.split("\t");
|
||||
return { sha, subject, body: body.join("\t").trim() };
|
||||
});
|
||||
}
|
||||
|
||||
function prNumbersFromCommits(commits) {
|
||||
const refs = new Set();
|
||||
for (const commit of commits) {
|
||||
for (const match of `${commit.subject}\n${commit.body}`.matchAll(/#(\d+)/g)) refs.add(Number(match[1]));
|
||||
}
|
||||
return [...refs];
|
||||
}
|
||||
|
||||
async function githubJson(path, { allowMissing = false } = {}) {
|
||||
const headers = { Accept: "application/vnd.github+json", "User-Agent": "reasonix-release-notes" };
|
||||
if (process.env.GITHUB_TOKEN || process.env.GH_TOKEN) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN || process.env.GH_TOKEN}`;
|
||||
}
|
||||
const response = await fetch(`https://api.github.com${path}`, { headers, signal: AbortSignal.timeout(30_000) });
|
||||
if (allowMissing && response.status === 404) return null;
|
||||
if (!response.ok) throw new Error(`GitHub API ${path} failed: ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function githubList(path) {
|
||||
const items = [];
|
||||
for (let page = 1; ; page += 1) {
|
||||
const separator = path.includes("?") ? "&" : "?";
|
||||
const batch = await githubJson(`${path}${separator}per_page=100&page=${page}`);
|
||||
items.push(...batch);
|
||||
if (batch.length < 100) return items;
|
||||
}
|
||||
}
|
||||
|
||||
export function inferPullTargetHints(labels, files) {
|
||||
const labelSet = new Set(labels);
|
||||
const targets = new Set();
|
||||
const add = (target) => targets.add(target);
|
||||
const hasPath = (pattern) => files.some((path) => pattern.test(path));
|
||||
|
||||
const explicitlyDesktop = labelSet.has("desktop");
|
||||
const explicitlyCLI = labelSet.has("tui") || labelSet.has("cli");
|
||||
if (explicitlyDesktop) add("desktop");
|
||||
if (explicitlyCLI) add("cli");
|
||||
if (hasPath(/^desktop\//)) add("desktop");
|
||||
if (hasPath(/^(?:internal\/cli\/|cmd\/)/)) add("cli");
|
||||
if (hasPath(/^site\//)) add("site");
|
||||
if (hasPath(/^(?:workers\/|\.github\/|scripts\/|release-notes\/)/)) add("service");
|
||||
|
||||
const hasSharedProductCode = files.some((path) =>
|
||||
/^(?:internal\/|sdk\/|npm\/)/.test(path) &&
|
||||
!/^internal\/cli\//.test(path) &&
|
||||
!/^internal\/telemetry\//.test(path),
|
||||
);
|
||||
if (!explicitlyDesktop && !explicitlyCLI && hasSharedProductCode) {
|
||||
add("desktop");
|
||||
add("cli");
|
||||
}
|
||||
if (!targets.size) {
|
||||
add("desktop");
|
||||
add("cli");
|
||||
}
|
||||
return releaseTargetOrder.filter((target) => targets.has(target));
|
||||
}
|
||||
|
||||
async function collectPullRequests(repository, commits) {
|
||||
const numbers = new Set(prNumbersFromCommits(commits));
|
||||
const associated = await Promise.all(
|
||||
commits.map((commit) => githubJson(`/repos/${repository}/commits/${commit.sha}/pulls`, { allowMissing: true })),
|
||||
);
|
||||
for (const pulls of associated) for (const pull of pulls || []) numbers.add(pull.number);
|
||||
const pulls = await Promise.all([...numbers].map((number) => githubJson(`/repos/${repository}/pulls/${number}`, { allowMissing: true })));
|
||||
return Promise.all(pulls.filter(Boolean).map(async (pull) => {
|
||||
const labels = (pull.labels || []).map((label) => label.name);
|
||||
const files = (await githubList(`/repos/${repository}/pulls/${pull.number}/files`)).map((file) => file.filename);
|
||||
return {
|
||||
number: pull.number,
|
||||
title: pull.title,
|
||||
body: String(pull.body || "").slice(0, 2000),
|
||||
author: pull.user?.login || "",
|
||||
labels,
|
||||
changedFileCount: files.length,
|
||||
files: files.slice(0, 200),
|
||||
targetHints: inferPullTargetHints(labels, files),
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
function releaseItems(release) {
|
||||
return [
|
||||
...(release.highlights || []),
|
||||
...["new", "improved", "fixed"].flatMap((kind) => release.changes?.[kind] || []),
|
||||
...(release.upgrade || []),
|
||||
...(release.risks || []),
|
||||
];
|
||||
}
|
||||
|
||||
function normalizeReleaseTargets(release) {
|
||||
for (const item of releaseItems(release)) {
|
||||
if (!Array.isArray(item.targets)) continue;
|
||||
item.targets.sort((a, b) => releaseTargetOrder.indexOf(a) - releaseTargetOrder.indexOf(b));
|
||||
}
|
||||
const targets = new Set(releaseItems(release).flatMap((item) => item.targets || []));
|
||||
release.surfaces = [
|
||||
...releaseTargetOrder.filter((target) => targets.has(target)),
|
||||
...[...targets].filter((target) => !releaseTargetOrder.includes(target)).sort(),
|
||||
];
|
||||
}
|
||||
|
||||
function collectDocLinks(from, repository, to) {
|
||||
const linkRef = runGit(["rev-parse", to]);
|
||||
const paths = runGit(["diff", "--name-only", `${from}..${to}`])
|
||||
.split("\n")
|
||||
.filter((path) => /^(?:docs|README)[/\w.-]*\.(?:md|mdx)$/i.test(path));
|
||||
return paths.map((path) => `https://github.com/${repository}/blob/${linkRef}/${path}`);
|
||||
}
|
||||
|
||||
function assertGroundedRefs(value, allowedRefs, path = "release") {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => assertGroundedRefs(item, allowedRefs, `${path}[${index}]`));
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== "object") return;
|
||||
if (Array.isArray(value.refs)) {
|
||||
for (const ref of value.refs) {
|
||||
if (!allowedRefs.has(ref)) throw new Error(`${path}.refs contains PR #${ref}, which is outside the release range`);
|
||||
}
|
||||
}
|
||||
for (const [key, child] of Object.entries(value)) assertGroundedRefs(child, allowedRefs, `${path}.${key}`);
|
||||
}
|
||||
|
||||
function extractJson(content) {
|
||||
if (!content?.trim()) throw new Error("DeepSeek returned empty content");
|
||||
const parsed = JSON.parse(content);
|
||||
return parsed.release || parsed;
|
||||
}
|
||||
|
||||
async function askDeepSeek(payload, attempt = 0) {
|
||||
const key = process.env.DEEPSEEK_API_KEY;
|
||||
if (!key) throw new Error("DEEPSEEK_API_KEY is required");
|
||||
const compact = attempt > 0;
|
||||
const response = await fetch(`${apiBase.replace(/\/$/, "")}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
// Release notes need deterministic structured output, not hidden chain of
|
||||
// thought. Thinking tokens share the generation budget with content and
|
||||
// can otherwise leave response_format JSON empty or truncated.
|
||||
thinking: { type: "disabled" },
|
||||
temperature: 0,
|
||||
max_tokens: 8000,
|
||||
response_format: { type: "json_object" },
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `You are Reasonix's release editor. Return one JSON object with a \"release\" property. Write factual, user-facing product release notes in equivalent English and Simplified Chinese. Group changes by user outcome, not by commit. Never invent capabilities, migrations, risks, PR numbers, contributors, URLs, or metrics. Every highlight and change must cite one or more supplied PR numbers. Use this exact release shape:
|
||||
{
|
||||
\"version\": \"semver\", \"date\": \"YYYY-MM-DD\", \"channel\": \"stable|prerelease\", \"targetingVersion\": 1,
|
||||
\"title\": {\"en\":\"\",\"zh\":\"\"}, \"summary\": {\"en\":\"\",\"zh\":\"\"},
|
||||
\"surfaces\": [\"desktop\",\"cli\"],
|
||||
\"guides\": [{\"title\":{\"en\":\"\",\"zh\":\"\"},\"body\":{\"en\":\"\",\"zh\":\"\"},\"href\":\"https://...\"}],
|
||||
\"highlights\": [{\"kind\":\"new|improved|fixed|security\",\"targets\":[\"desktop\",\"cli\"],\"title\":{\"en\":\"\",\"zh\":\"\"},\"body\":{\"en\":\"\",\"zh\":\"\"},\"refs\":[123]}],
|
||||
\"changes\": {\"new\":[],\"improved\":[],\"fixed\":[]},
|
||||
\"upgrade\": [{\"level\":\"info|warning\",\"targets\":[\"desktop\"],\"title\":{\"en\":\"\",\"zh\":\"\"},\"body\":{\"en\":\"\",\"zh\":\"\"},\"refs\":[123]}],
|
||||
\"risks\": [{\"targets\":[\"cli\"],\"title\":{\"en\":\"\",\"zh\":\"\"},\"body\":{\"en\":\"\",\"zh\":\"\"},\"refs\":[123]}],
|
||||
\"contributors\": [], \"links\": {\"github\":\"https://...\",\"compare\":\"https://...\",\"download\":\"https://...\"}
|
||||
}
|
||||
Every highlight, change, upgrade note, and risk must have a non-empty \"targets\" array using only this canonical order: desktop, cli, site, service. Use each PR's targetHints as deterministic candidates, then choose the user-visible delivery target supported by its labels, changed files, title, and body. Shared product-core behavior belongs to both desktop and cli. Website-only work belongs to site; hosted workers and release infrastructure belong to service and must not be marked as a client update merely because a client file accompanies the server change. Set top-level surfaces to the canonical union of all item targets. Return guides only for supplied documentation URLs. Mention upgrade action or risk only when explicitly supported; otherwise use empty arrays. ${editorialLimitInstruction(compact)} Output JSON only.`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `${compact ? "The previous response was invalid or truncated. Produce a smaller complete record and obey the compact editorial budget.\n" : ""}Create the release record from these public GitHub sources:\n${JSON.stringify(payload)}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (!response.ok) throw new Error(`DeepSeek API failed: ${response.status} ${await response.text()}`);
|
||||
const data = await response.json();
|
||||
const choice = data.choices?.[0];
|
||||
if (choice?.finish_reason === "length") {
|
||||
if (compact) throw new Error("DeepSeek response exceeded the compact editorial budget (finish_reason=length)");
|
||||
return askDeepSeek(payload, attempt + 1);
|
||||
}
|
||||
try {
|
||||
return extractJson(choice?.message?.content);
|
||||
} catch (error) {
|
||||
if (compact) {
|
||||
throw new Error(`${error.message} (finish_reason=${choice?.finish_reason || "unknown"})`);
|
||||
}
|
||||
return askDeepSeek(payload, attempt + 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function contributorsOf(pulls) {
|
||||
return withoutOwner([...new Set(pulls.map((pull) => pull.author).filter(Boolean))]);
|
||||
}
|
||||
|
||||
export function previousCLIRelease(catalog, version) {
|
||||
return catalog.releases.find((release) =>
|
||||
release.product !== "studio" && release.channel === "stable" &&
|
||||
compareVersionsDesc(release.version, version) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (!args.version) throw new Error("--version is required");
|
||||
const version = normalizeVersion(args.version);
|
||||
const catalog = await loadCatalog();
|
||||
const channel = version.includes("-") ? "prerelease" : "stable";
|
||||
const baseVersion = version.split("-")[0];
|
||||
const previousRecord = channel === "stable"
|
||||
? previousCLIRelease(catalog, version)
|
||||
: catalog.releases.find(
|
||||
(release) =>
|
||||
release.version !== version &&
|
||||
release.channel === "prerelease" &&
|
||||
release.baseVersion === baseVersion,
|
||||
) || catalog.releases.find((release) => release.product !== "studio" && release.channel === "stable");
|
||||
const previous = args.from || previousRecord?.version;
|
||||
if (!previous) throw new Error("--from is required when no previous release exists");
|
||||
const previousVersion = normalizeVersion(previous);
|
||||
const previousIsPreview = previousVersion.includes("-");
|
||||
const from = previous.match(/^(?:desktop-|npm-)?v/)
|
||||
? previous
|
||||
: previousIsPreview
|
||||
? `v${previousVersion}`
|
||||
: `desktop-v${previousVersion}`;
|
||||
const to = args.to || "HEAD";
|
||||
const repository = repositoryName();
|
||||
const commits = commitRange(from, to);
|
||||
if (!commits.length) throw new Error(`no commits found in ${from}..${to}`);
|
||||
const pulls = await collectPullRequests(repository, commits);
|
||||
if (!pulls.length) throw new Error(`no pull requests found in ${from}..${to}`);
|
||||
const docLinks = collectDocLinks(from, repository, to);
|
||||
const date = args.date || new Date().toISOString().slice(0, 10);
|
||||
const tag = args.tag || (channel === "prerelease" ? `v${version}` : `desktop-v${version}`);
|
||||
const source = {
|
||||
version,
|
||||
date,
|
||||
channel,
|
||||
range: `${from}..${to}`,
|
||||
pullRequests: pulls,
|
||||
documentationUrls: docLinks,
|
||||
};
|
||||
const release = await askDeepSeek(source);
|
||||
release.product = "cli";
|
||||
release.targetingVersion = 1;
|
||||
release.version = version;
|
||||
release.releaseId = version;
|
||||
release.baseVersion = baseVersion;
|
||||
release.date = date;
|
||||
release.channel = source.channel;
|
||||
release.status = "reviewed";
|
||||
release.previousRelease = previousVersion;
|
||||
const previewOrdinal = version.match(/-preview\.([1-9][0-9]*)$/)?.[1];
|
||||
if (channel === "prerelease") {
|
||||
if (!previewOrdinal) throw new Error("Preview release version must use MAJOR.MINOR.PATCH-preview.N");
|
||||
release.builds = {
|
||||
cli: `v${version}`,
|
||||
desktop: `v${baseVersion}-preview.${previewOrdinal}`,
|
||||
npm: `${baseVersion}-canary.${previewOrdinal}`,
|
||||
};
|
||||
} else {
|
||||
release.builds = {
|
||||
cli: `v${version}`,
|
||||
desktop: `v${version}`,
|
||||
npm: version,
|
||||
};
|
||||
}
|
||||
release.contributors = contributorsOf(pulls);
|
||||
release.links = {
|
||||
github: `https://github.com/${repository}/releases/tag/${tag}`,
|
||||
compare: `https://github.com/${repository}/compare/${from}...${tag}`,
|
||||
download: channel === "prerelease"
|
||||
? "https://reasonix.io/?download=desktop&channel=preview#start"
|
||||
: "https://reasonix.io/?download=desktop&channel=stable#start",
|
||||
};
|
||||
release.guides = (release.guides || []).filter((guide) => docLinks.includes(guide.href));
|
||||
normalizeReleaseTargets(release);
|
||||
assertGroundedRefs(release, new Set(pulls.map((pull) => pull.number)));
|
||||
validateCatalog({ schemaVersion: 1, releases: [release] });
|
||||
await upsertRelease(release);
|
||||
console.log(`Generated bilingual release notes for v${version} from ${pulls.length} pull request(s).`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start CI on a Notes PR that the Actions bot opened. Events raised with
|
||||
# GITHUB_TOKEN never start workflows, so such a PR shows action_required with
|
||||
# zero jobs until a human-authenticated close/reopen re-emits pull_request.
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 1 ] || [[ ! "$1" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
||||
echo "usage: site/scripts/releases/release-notes-pr-kick.sh MAJOR.MINOR.PATCH" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
version="$1"
|
||||
repository="${RELEASE_REPOSITORY:-esengine/DeepSeek-Reasonix}"
|
||||
branch="website-release-notes/v$version"
|
||||
wait_seconds="${RELEASE_NOTES_KICK_WAIT_SECONDS:-120}"
|
||||
|
||||
for command in gh jq; do
|
||||
command -v "$command" >/dev/null || {
|
||||
echo "required command is unavailable: $command" >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
|
||||
prs="$(gh pr list --repo "$repository" --head "$branch" --state open --json number,headRefOid --limit 2)"
|
||||
count="$(jq 'length' <<<"$prs")"
|
||||
if [ "$count" -ne 1 ]; then
|
||||
echo "expected exactly one open PR for $branch, found $count" >&2
|
||||
exit 1
|
||||
fi
|
||||
number="$(jq -r '.[0].number' <<<"$prs")"
|
||||
head="$(jq -r '.[0].headRefOid' <<<"$prs")"
|
||||
|
||||
check_runs() {
|
||||
gh api "repos/$repository/commits/$head/check-runs" --jq '.total_count'
|
||||
}
|
||||
|
||||
if [ "$(check_runs)" -gt 0 ]; then
|
||||
echo "PR #$number already has check runs on $head; nothing to kick"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Only the recorded-but-never-started shape is safe to kick. Anything else
|
||||
# with zero check runs needs a look, not a reopen.
|
||||
stalled="$(gh api "repos/$repository/actions/runs?event=pull_request&head_sha=$head&per_page=20" \
|
||||
--jq "[.workflow_runs[] | select(.head_sha == \"$head\" and .conclusion == \"action_required\")] | length")"
|
||||
if [ "$stalled" -eq 0 ]; then
|
||||
echo "PR #$number has no check runs and no action_required run on $head; inspect before kicking" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PR #$number: $stalled recorded-but-unstarted run(s) on $head; closing and reopening"
|
||||
gh pr close "$number" --repo "$repository"
|
||||
gh pr reopen "$number" --repo "$repository"
|
||||
|
||||
deadline=$((SECONDS + wait_seconds))
|
||||
while [ "$(check_runs)" -eq 0 ]; do
|
||||
if [ "$SECONDS" -ge "$deadline" ]; then
|
||||
echo "no check runs appeared on $head within ${wait_seconds}s" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
echo "CI started on PR #$number ($head)"
|
||||
@@ -1,459 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { withoutOwner } from "../../src/lib/repository-owner.mjs";
|
||||
|
||||
const siteRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
export const defaultCatalogPath = resolve(siteRoot, "src/data/releases.json");
|
||||
|
||||
const localizedFields = ["title", "body"];
|
||||
const changeKinds = ["new", "improved", "fixed"];
|
||||
const itemKinds = new Set(["new", "improved", "fixed", "security"]);
|
||||
const releaseChannels = new Set(["stable", "prerelease"]);
|
||||
const releaseStatuses = new Set(["reviewed", "published"]);
|
||||
const releaseTargetOrder = ["desktop", "cli", "site", "service"];
|
||||
const releaseTargets = new Set(releaseTargetOrder);
|
||||
const releaseTargetingRequiredFrom = "1.31.4";
|
||||
|
||||
function invariant(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function validateLocalized(value, path) {
|
||||
invariant(isObject(value), `${path} must be an object`);
|
||||
for (const lang of ["en", "zh"]) {
|
||||
invariant(typeof value[lang] === "string" && value[lang].trim(), `${path}.${lang} must be a non-empty string`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateRefs(refs, path) {
|
||||
if (refs === undefined) return;
|
||||
invariant(Array.isArray(refs), `${path} must be an array`);
|
||||
for (const ref of refs) invariant(Number.isInteger(ref) && ref > 0, `${path} contains invalid PR number ${ref}`);
|
||||
}
|
||||
|
||||
function validateItem(item, path, { kind = false, href = false, level = false, targets = false } = {}) {
|
||||
invariant(isObject(item), `${path} must be an object`);
|
||||
for (const field of localizedFields) validateLocalized(item[field], `${path}.${field}`);
|
||||
if (kind) invariant(itemKinds.has(item.kind), `${path}.kind is invalid`);
|
||||
if (href) invariant(typeof item.href === "string" && /^https:\/\//.test(item.href), `${path}.href must be HTTPS`);
|
||||
if (level) invariant(item.level === "info" || item.level === "warning", `${path}.level is invalid`);
|
||||
if (targets || item.targets !== undefined) {
|
||||
invariant(Array.isArray(item.targets) && item.targets.length > 0, `${path}.targets must be a non-empty array`);
|
||||
invariant(new Set(item.targets).size === item.targets.length, `${path}.targets contains duplicates`);
|
||||
for (const target of item.targets) invariant(releaseTargets.has(target), `${path}.targets contains invalid target ${target}`);
|
||||
const sorted = releaseTargetOrder.filter((target) => item.targets.includes(target));
|
||||
invariant(sorted.every((target, index) => target === item.targets[index]), `${path}.targets must use canonical order`);
|
||||
}
|
||||
validateRefs(item.refs, `${path}.refs`);
|
||||
}
|
||||
|
||||
function releaseItems(release) {
|
||||
return [
|
||||
...release.highlights,
|
||||
...changeKinds.flatMap((kind) => release.changes[kind]),
|
||||
...release.upgrade,
|
||||
...release.risks,
|
||||
];
|
||||
}
|
||||
|
||||
function itemTargetsUnion(release) {
|
||||
const targets = new Set(releaseItems(release).flatMap((item) => item.targets || []));
|
||||
return releaseTargetOrder.filter((target) => targets.has(target));
|
||||
}
|
||||
|
||||
function semverParts(version) {
|
||||
const match = String(version).match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
||||
invariant(match, `invalid version ${version}`);
|
||||
return [Number(match[1]), Number(match[2]), Number(match[3]), match[4] || ""];
|
||||
}
|
||||
|
||||
function isCoreVersionAtLeast(version, minimum) {
|
||||
const current = semverParts(version);
|
||||
const floor = semverParts(minimum);
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
if (current[index] !== floor[index]) return current[index] > floor[index];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function compareVersionsDesc(a, b) {
|
||||
const aa = semverParts(a);
|
||||
const bb = semverParts(b);
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
if (aa[i] !== bb[i]) return bb[i] - aa[i];
|
||||
}
|
||||
if (aa[3] === bb[3]) return 0;
|
||||
if (!aa[3]) return -1;
|
||||
if (!bb[3]) return 1;
|
||||
return String(bb[3]).localeCompare(String(aa[3]), "en", { numeric: true });
|
||||
}
|
||||
|
||||
export function validateCatalog(catalog) {
|
||||
invariant(isObject(catalog), "catalog must be an object");
|
||||
invariant(catalog.schemaVersion === 1, "catalog.schemaVersion must be 1");
|
||||
invariant(Array.isArray(catalog.releases) && catalog.releases.length > 0, "catalog.releases must not be empty");
|
||||
|
||||
const versions = new Set();
|
||||
for (const [index, release] of catalog.releases.entries()) {
|
||||
const path = `releases[${index}]`;
|
||||
invariant(isObject(release), `${path} must be an object`);
|
||||
semverParts(release.version);
|
||||
invariant(release.product === undefined || ["cli", "studio"].includes(release.product), `${path}.product is invalid`);
|
||||
if (release.product === "studio") {
|
||||
invariant(/^2\.\d+\.\d+$/.test(release.version), `${path}.Studio version must be 2.x`);
|
||||
invariant(release.sourceNotes?.language === "zh", `${path}.sourceNotes.language must be zh`);
|
||||
invariant(typeof release.sourceNotes.markdown === "string" && release.sourceNotes.markdown.trim(), `${path}.sourceNotes.markdown must not be empty`);
|
||||
invariant(/^[0-9a-f]{40}$/.test(release.sourceNotes.sha), `${path}.sourceNotes.sha must be a full commit SHA`);
|
||||
invariant(release.sourceNotes.path === `release-notes/studio/${release.version}.md`, `${path}.sourceNotes.path is invalid`);
|
||||
invariant(release.status === undefined, `${path}.Studio does not use CLI publication markers`);
|
||||
}
|
||||
invariant(!versions.has(release.version), `duplicate version ${release.version}`);
|
||||
versions.add(release.version);
|
||||
invariant(/^\d{4}-\d{2}-\d{2}$/.test(release.date), `${path}.date must use YYYY-MM-DD`);
|
||||
invariant(releaseChannels.has(release.channel), `${path}.channel is invalid`);
|
||||
if (release.releaseId !== undefined) {
|
||||
invariant(release.releaseId === release.version, `${path}.releaseId must equal version`);
|
||||
}
|
||||
if (release.baseVersion !== undefined) {
|
||||
semverParts(release.baseVersion);
|
||||
invariant(!release.baseVersion.includes("-"), `${path}.baseVersion must be stable semver`);
|
||||
invariant(
|
||||
release.version === release.baseVersion || release.version.startsWith(`${release.baseVersion}-`),
|
||||
`${path}.baseVersion does not match version`,
|
||||
);
|
||||
}
|
||||
if (release.status !== undefined) {
|
||||
invariant(releaseStatuses.has(release.status), `${path}.status is invalid`);
|
||||
}
|
||||
if (release.candidateSha !== undefined) {
|
||||
invariant(/^[0-9a-f]{40}$/.test(release.candidateSha), `${path}.candidateSha must be a full commit SHA`);
|
||||
}
|
||||
if (release.previousRelease !== undefined) {
|
||||
semverParts(release.previousRelease);
|
||||
invariant(release.previousRelease !== release.version, `${path}.previousRelease must differ from version`);
|
||||
}
|
||||
if (release.builds !== undefined) {
|
||||
invariant(isObject(release.builds), `${path}.builds must be an object`);
|
||||
for (const surface of ["cli", "desktop", "npm"]) {
|
||||
invariant(
|
||||
typeof release.builds[surface] === "string" && release.builds[surface].trim(),
|
||||
`${path}.builds.${surface} must be a non-empty string`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (release.status !== undefined) {
|
||||
for (const field of ["releaseId", "baseVersion", "previousRelease", "builds"]) {
|
||||
invariant(release[field] !== undefined, `${path}.${field} is required for managed release records`);
|
||||
}
|
||||
if (release.channel === "stable") {
|
||||
invariant(release.version === release.baseVersion, `${path}.stable version must equal baseVersion`);
|
||||
invariant(release.builds.cli === `v${release.version}`, `${path}.builds.cli does not match Stable version`);
|
||||
invariant(release.builds.desktop === `v${release.version}`, `${path}.builds.desktop does not match Stable version`);
|
||||
invariant(release.builds.npm === release.version, `${path}.builds.npm does not match Stable version`);
|
||||
} else {
|
||||
const preview = release.version.match(/^(\d+\.\d+\.\d+)-preview\.([1-9][0-9]*)$/);
|
||||
invariant(preview, `${path}.managed prerelease must use MAJOR.MINOR.PATCH-preview.N`);
|
||||
invariant(preview[1] === release.baseVersion, `${path}.Preview baseVersion does not match version`);
|
||||
invariant(release.builds.cli === `v${release.version}`, `${path}.builds.cli does not match Preview version`);
|
||||
invariant(release.builds.desktop === `v${release.version}`, `${path}.builds.desktop does not match Preview version`);
|
||||
invariant(
|
||||
release.builds.npm === `${release.baseVersion}-canary.${preview[2]}`,
|
||||
`${path}.builds.npm does not match Preview ordinal`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const targetingRequiredByVersion = release.product !== "studio" && isCoreVersionAtLeast(release.version, releaseTargetingRequiredFrom);
|
||||
if (targetingRequiredByVersion) {
|
||||
invariant(
|
||||
release.targetingVersion === 1,
|
||||
`${path}.targetingVersion must be 1 for v${releaseTargetingRequiredFrom} and newer`,
|
||||
);
|
||||
} else if (release.targetingVersion !== undefined) {
|
||||
invariant(release.targetingVersion === 1, `${path}.targetingVersion is invalid`);
|
||||
}
|
||||
const targetsRequired = release.targetingVersion === 1;
|
||||
validateLocalized(release.title, `${path}.title`);
|
||||
validateLocalized(release.summary, `${path}.summary`);
|
||||
invariant(Array.isArray(release.surfaces) && release.surfaces.length > 0, `${path}.surfaces must not be empty`);
|
||||
invariant(new Set(release.surfaces).size === release.surfaces.length, `${path}.surfaces contains duplicates`);
|
||||
invariant(Array.isArray(release.guides), `${path}.guides must be an array`);
|
||||
release.guides.forEach((item, itemIndex) => validateItem(item, `${path}.guides[${itemIndex}]`, { href: true }));
|
||||
invariant(Array.isArray(release.highlights) && release.highlights.length > 0, `${path}.highlights must not be empty`);
|
||||
release.highlights.forEach((item, itemIndex) =>
|
||||
validateItem(item, `${path}.highlights[${itemIndex}]`, { kind: true, targets: targetsRequired }),
|
||||
);
|
||||
invariant(isObject(release.changes), `${path}.changes must be an object`);
|
||||
for (const changeKind of changeKinds) {
|
||||
invariant(Array.isArray(release.changes[changeKind]), `${path}.changes.${changeKind} must be an array`);
|
||||
release.changes[changeKind].forEach((item, itemIndex) =>
|
||||
validateItem(item, `${path}.changes.${changeKind}[${itemIndex}]`, { targets: targetsRequired }),
|
||||
);
|
||||
}
|
||||
invariant(Array.isArray(release.upgrade), `${path}.upgrade must be an array`);
|
||||
release.upgrade.forEach((item, itemIndex) =>
|
||||
validateItem(item, `${path}.upgrade[${itemIndex}]`, { level: true, targets: targetsRequired }),
|
||||
);
|
||||
invariant(Array.isArray(release.risks), `${path}.risks must be an array`);
|
||||
release.risks.forEach((item, itemIndex) =>
|
||||
validateItem(item, `${path}.risks[${itemIndex}]`, { targets: targetsRequired }),
|
||||
);
|
||||
if (targetsRequired) {
|
||||
const derivedTargets = itemTargetsUnion(release);
|
||||
invariant(
|
||||
derivedTargets.includes("desktop") || derivedTargets.includes("cli"),
|
||||
`${path} must contain at least one Desktop or CLI update`,
|
||||
);
|
||||
invariant(
|
||||
derivedTargets.length === release.surfaces.length &&
|
||||
derivedTargets.every((target, index) => target === release.surfaces[index]),
|
||||
`${path}.surfaces must equal the canonical union of item targets`,
|
||||
);
|
||||
}
|
||||
invariant(Array.isArray(release.contributors), `${path}.contributors must be an array`);
|
||||
invariant(isObject(release.links), `${path}.links must be an object`);
|
||||
for (const link of ["github", "compare", "download"]) {
|
||||
invariant(typeof release.links[link] === "string" && /^https:\/\//.test(release.links[link]), `${path}.links.${link} must be HTTPS`);
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = [...catalog.releases].sort((a, b) => compareVersionsDesc(a.version, b.version));
|
||||
invariant(
|
||||
sorted.every((release, index) => release.version === catalog.releases[index].version),
|
||||
"catalog.releases must be sorted newest first",
|
||||
);
|
||||
return catalog;
|
||||
}
|
||||
|
||||
export async function loadCatalog(path = defaultCatalogPath) {
|
||||
const catalog = JSON.parse(await readFile(path, "utf8"));
|
||||
return validateCatalog(catalog);
|
||||
}
|
||||
|
||||
export function releaseForVersion(catalog, version) {
|
||||
const normalized = String(version).replace(/^(?:studio-|desktop-|npm-)?v/, "");
|
||||
const release = catalog.releases.find((entry) => entry.version === normalized);
|
||||
invariant(release, `release notes for v${normalized} are missing`);
|
||||
return release;
|
||||
}
|
||||
|
||||
function localized(value, lang) {
|
||||
return value[lang] || value.en;
|
||||
}
|
||||
|
||||
function refsSuffix(refs = []) {
|
||||
if (!refs.length) return "";
|
||||
return ` (${refs.map((ref) => `[#${ref}](https://github.com/esengine/DeepSeek-Reasonix/pull/${ref})`).join(", ")})`;
|
||||
}
|
||||
|
||||
function targetLabel(target) {
|
||||
return { desktop: "Desktop", cli: "CLI", site: "Site", service: "Service" }[target] || target;
|
||||
}
|
||||
|
||||
function targetsPrefix(item) {
|
||||
if (!item.targets?.length) return "";
|
||||
return `**[${item.targets.map(targetLabel).join(" · ")}]** `;
|
||||
}
|
||||
|
||||
function renderItems(items, lang) {
|
||||
return items
|
||||
.map((item) => `- ${targetsPrefix(item)}**${localized(item.title, lang)}** — ${localized(item.body, lang)}${refsSuffix(item.refs)}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function hasTarget(item, target) {
|
||||
return item.targets?.includes(target) || false;
|
||||
}
|
||||
|
||||
function targetItemCount(release, target) {
|
||||
return releaseItems(release).filter((item) => hasTarget(item, target)).length;
|
||||
}
|
||||
|
||||
function appendTargetSection(lines, release, target, lang) {
|
||||
const isZh = lang === "zh";
|
||||
const title = target === "desktop"
|
||||
? (isZh ? "桌面端更新" : "Desktop updates")
|
||||
: (isZh ? "CLI 端更新" : "CLI updates");
|
||||
lines.push(`## ${title}`, "");
|
||||
|
||||
const highlights = release.highlights.filter((item) => hasTarget(item, target));
|
||||
if (highlights.length) {
|
||||
lines.push(`### ${isZh ? "重点内容" : "Highlights"}`, "", renderItems(highlights, lang), "");
|
||||
}
|
||||
const headings = {
|
||||
new: isZh ? "新增" : "New",
|
||||
improved: isZh ? "改进" : "Improved",
|
||||
fixed: isZh ? "修复" : "Fixed",
|
||||
};
|
||||
for (const kind of changeKinds) {
|
||||
const items = release.changes[kind].filter((item) => hasTarget(item, target));
|
||||
if (items.length) lines.push(`### ${headings[kind]}`, "", renderItems(items, lang), "");
|
||||
}
|
||||
|
||||
if (!highlights.length && !changeKinds.some((kind) => release.changes[kind].some((item) => hasTarget(item, target)))) {
|
||||
const noTargetedItems = targetItemCount(release, target) === 0;
|
||||
lines.push(
|
||||
target === "cli" && noTargetedItems
|
||||
? (isZh ? "本版本没有 CLI 相关功能、改进或修复,CLI 用户可按需跳过。" : "This release has no CLI features, improvements, or fixes; CLI users may skip it if preferred.")
|
||||
: (isZh ? "此板块没有功能或修复条目;相关说明请查看下方升级提醒和风险提示。" : "No feature or fix entries are listed here; see the upgrade and risk notes below."),
|
||||
"",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function appendOtherTargetSection(lines, release, lang) {
|
||||
const isZh = lang === "zh";
|
||||
const isOtherOnly = (item) => item.targets?.length && !hasTarget(item, "desktop") && !hasTarget(item, "cli");
|
||||
const highlights = release.highlights.filter(isOtherOnly);
|
||||
const changes = Object.fromEntries(changeKinds.map((kind) => [kind, release.changes[kind].filter(isOtherOnly)]));
|
||||
if (!highlights.length && !changeKinds.some((kind) => changes[kind].length)) return;
|
||||
|
||||
lines.push(`## ${isZh ? "其他项目更新" : "Other project updates"}`, "");
|
||||
if (highlights.length) lines.push(renderItems(highlights, lang), "");
|
||||
for (const kind of changeKinds) {
|
||||
if (changes[kind].length) lines.push(renderItems(changes[kind], lang), "");
|
||||
}
|
||||
}
|
||||
|
||||
export function renderGitHubRelease(release, lang = "zh") {
|
||||
if (release.product === "studio") return release.sourceNotes.markdown;
|
||||
const isZh = lang === "zh";
|
||||
const isPreview = release.channel === "prerelease";
|
||||
const channelLabel = isPreview ? (isZh ? "预览版" : "Preview") : (isZh ? "稳定版" : "Stable");
|
||||
const lines = [
|
||||
`> ${localized(release.summary, lang)}`,
|
||||
"",
|
||||
`**${isZh ? "发布渠道" : "Release channel"}:${channelLabel} · v${release.version}**`,
|
||||
"",
|
||||
isZh
|
||||
? `[English →](https://reasonix.io/changelog/v${release.version}/?lang=en) · [网页版完整更新日志 →](https://reasonix.io/changelog/v${release.version}/)`
|
||||
: `[中文 →](https://reasonix.io/changelog/v${release.version}/?lang=zh) · [Full release notes →](https://reasonix.io/changelog/v${release.version}/?lang=en)`,
|
||||
"",
|
||||
];
|
||||
|
||||
if (release.guides.length) {
|
||||
lines.push(`## ${isZh ? "使用攻略" : "Guides"}`, "");
|
||||
for (const guide of release.guides) {
|
||||
lines.push(`- [**${localized(guide.title, lang)}**](${guide.href}) — ${localized(guide.body, lang)}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push(
|
||||
`## ${isZh ? "概览" : "Overview"}`,
|
||||
"",
|
||||
`**Reasonix v${release.version} — ${localized(release.title, lang)}**`,
|
||||
"",
|
||||
localized(release.summary, lang),
|
||||
"",
|
||||
`${isZh ? "发布日期" : "Released"}:${release.date}`,
|
||||
"",
|
||||
);
|
||||
|
||||
if (release.targetingVersion === 1) {
|
||||
lines.push(
|
||||
`**Desktop ${targetItemCount(release, "desktop")} · CLI ${targetItemCount(release, "cli")}**`,
|
||||
"",
|
||||
);
|
||||
appendTargetSection(lines, release, "desktop", lang);
|
||||
appendTargetSection(lines, release, "cli", lang);
|
||||
appendOtherTargetSection(lines, release, lang);
|
||||
} else {
|
||||
lines.push(`## ${isZh ? "重点内容" : "Highlights"}`, "", renderItems(release.highlights, lang), "");
|
||||
const headings = {
|
||||
new: isZh ? "新功能" : "New",
|
||||
improved: isZh ? "改进" : "Improved",
|
||||
fixed: isZh ? "修复" : "Fixed",
|
||||
};
|
||||
for (const kind of changeKinds) {
|
||||
const items = release.changes[kind];
|
||||
if (!items.length) continue;
|
||||
lines.push(`## ${headings[kind]}`, "", renderItems(items, lang), "");
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(`## ${isZh ? "升级提醒" : "Upgrade notes"}`, "");
|
||||
if (release.upgrade.length) lines.push(renderItems(release.upgrade, lang));
|
||||
else lines.push(isZh ? "本版本无需手动迁移。" : "No manual migration is required.");
|
||||
lines.push("");
|
||||
|
||||
lines.push(`## ${isZh ? "风险提示" : "Risk notes"}`, "");
|
||||
if (release.risks.length) lines.push(renderItems(release.risks, lang));
|
||||
else lines.push(isZh ? "当前没有需要额外操作的已知风险。" : "There are no known risks requiring extra action.");
|
||||
lines.push("");
|
||||
|
||||
const thanked = withoutOwner(release.contributors);
|
||||
if (thanked.length) {
|
||||
lines.push(
|
||||
`## ${isZh ? "致谢" : "Thanks"}`,
|
||||
"",
|
||||
`${isZh ? "感谢本版本的贡献者" : "Thanks to the contributors in this release"}:${thanked
|
||||
.map((name) => `[@${name}](https://github.com/${name})`)
|
||||
.join("、")}`,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
`## ${isZh ? "下载与安装" : "Download and install"}`,
|
||||
"",
|
||||
`- [${isZh ? "官网按平台下载" : "Platform downloads"}](${release.links.download})`,
|
||||
`- [${isZh ? "查看完整差异" : "Full comparison"}](${release.links.compare})`,
|
||||
"",
|
||||
);
|
||||
return `${lines.join("\n").trim()}\n`;
|
||||
}
|
||||
|
||||
export async function upsertRelease(release, path = defaultCatalogPath) {
|
||||
const catalog = await loadCatalog(path);
|
||||
const next = {
|
||||
...catalog,
|
||||
releases: [release, ...catalog.releases.filter((entry) => entry.version !== release.version)].sort((a, b) =>
|
||||
compareVersionsDesc(a.version, b.version),
|
||||
),
|
||||
};
|
||||
validateCatalog(next);
|
||||
await writeFile(path, `${JSON.stringify(next, null, 2)}\n`);
|
||||
return next;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const [command = "validate", ...rest] = argv;
|
||||
const values = { command };
|
||||
for (let i = 0; i < rest.length; i += 1) {
|
||||
const arg = rest[i];
|
||||
if (!arg.startsWith("--")) throw new Error(`unexpected argument ${arg}`);
|
||||
values[arg.slice(2)] = rest[++i];
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const catalogPath = args.catalog ? resolve(args.catalog) : defaultCatalogPath;
|
||||
const catalog = await loadCatalog(catalogPath);
|
||||
if (args.command === "validate") {
|
||||
console.log(`Validated ${catalog.releases.length} release note(s).`);
|
||||
return;
|
||||
}
|
||||
if (args.command !== "render") throw new Error(`unknown command ${args.command}`);
|
||||
invariant(args.version, "render requires --version");
|
||||
invariant(args.output, "render requires --output");
|
||||
const release = releaseForVersion(catalog, args.version);
|
||||
const output = resolve(args.output);
|
||||
await writeFile(output, renderGitHubRelease(release, args.lang === "en" ? "en" : "zh"));
|
||||
console.log(`Rendered v${release.version} release notes to ${output}`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import {
|
||||
editorialLimitInstruction,
|
||||
inferPullTargetHints,
|
||||
releaseOutputBudgets,
|
||||
} from "./generate-release-notes.mjs";
|
||||
import { loadCatalog, releaseForVersion, renderGitHubRelease, validateCatalog } from "./release-notes.mjs";
|
||||
import { validateReleaseEvent } from "../release-event.mjs";
|
||||
|
||||
test("the committed release catalog is valid and newest first", async () => {
|
||||
const catalog = await loadCatalog();
|
||||
assert.equal(catalog.schemaVersion, 1);
|
||||
assert.equal(new Set(catalog.releases.map((release) => release.version)).size, catalog.releases.length);
|
||||
});
|
||||
|
||||
test("tag namespaces resolve to the same product release", async () => {
|
||||
const catalog = await loadCatalog();
|
||||
assert.equal(releaseForVersion(catalog, "v1.17.13").version, "1.17.13");
|
||||
assert.equal(releaseForVersion(catalog, "desktop-v1.17.13").version, "1.17.13");
|
||||
assert.equal(releaseForVersion(catalog, "npm-v1.17.13").version, "1.17.13");
|
||||
});
|
||||
|
||||
test("GitHub rendering keeps product sections and source PR links", async () => {
|
||||
const catalog = await loadCatalog();
|
||||
const markdown = renderGitHubRelease(releaseForVersion(catalog, "1.17.13"), "zh");
|
||||
assert.match(markdown, /## 使用攻略/);
|
||||
assert.match(markdown, /## 重点内容/);
|
||||
assert.match(markdown, /## 升级提醒/);
|
||||
assert.match(markdown, /## 风险提示/);
|
||||
assert.match(markdown, /## 致谢/);
|
||||
assert.match(markdown, /\/pull\/6460/);
|
||||
assert.match(markdown, /reasonix\.io\/changelog\/v1\.17\.13/);
|
||||
});
|
||||
|
||||
test("targeted releases render Desktop and CLI sections from one shared item", async () => {
|
||||
const catalog = await loadCatalog();
|
||||
const markdown = renderGitHubRelease(releaseForVersion(catalog, "1.31.4"), "zh");
|
||||
assert.match(markdown, /## 桌面端更新/);
|
||||
assert.match(markdown, /## CLI 端更新/);
|
||||
assert.match(markdown, /## 其他项目更新/);
|
||||
assert.match(markdown, /\*\*\[Service\]\*\* \*\*降低数据库读取成本\*\*/);
|
||||
assert.equal(markdown.match(/更好的数学渲染/g)?.length, 2);
|
||||
});
|
||||
|
||||
test("targeted release validation requires canonical item targets and matching surfaces", () => {
|
||||
const release = {
|
||||
version: "2.0.0",
|
||||
targetingVersion: 1,
|
||||
date: "2026-01-01",
|
||||
channel: "stable",
|
||||
title: { en: "Targeted", zh: "分类版本" },
|
||||
summary: { en: "Summary", zh: "摘要" },
|
||||
surfaces: ["desktop", "cli"],
|
||||
guides: [],
|
||||
highlights: [{
|
||||
kind: "fixed",
|
||||
targets: ["desktop", "cli"],
|
||||
title: { en: "Shared fix", zh: "共享修复" },
|
||||
body: { en: "A shared fix.", zh: "一项共享修复。" },
|
||||
refs: [1],
|
||||
}],
|
||||
changes: { new: [], improved: [], fixed: [] },
|
||||
upgrade: [],
|
||||
risks: [],
|
||||
contributors: [],
|
||||
links: {
|
||||
github: "https://example.com/release",
|
||||
compare: "https://example.com/compare",
|
||||
download: "https://example.com/download",
|
||||
},
|
||||
};
|
||||
assert.doesNotThrow(() => validateCatalog({ schemaVersion: 1, releases: [release] }));
|
||||
assert.throws(
|
||||
() => validateCatalog({
|
||||
schemaVersion: 1,
|
||||
releases: [{ ...release, highlights: [{ ...release.highlights[0], targets: undefined }] }],
|
||||
}),
|
||||
/targets must be a non-empty array/,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateCatalog({ schemaVersion: 1, releases: [{ ...release, surfaces: ["desktop"] }] }),
|
||||
/surfaces must equal the canonical union/,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateCatalog({
|
||||
schemaVersion: 1,
|
||||
releases: [{ ...release, highlights: [{ ...release.highlights[0], targets: ["cli", "desktop"] }] }],
|
||||
}),
|
||||
/targets must use canonical order/,
|
||||
);
|
||||
});
|
||||
|
||||
test("targeting adoption boundary preserves historical releases and rejects future legacy records", () => {
|
||||
const release = {
|
||||
version: "1.31.3",
|
||||
date: "2026-01-01",
|
||||
channel: "stable",
|
||||
title: { en: "Legacy", zh: "旧版" },
|
||||
summary: { en: "Summary", zh: "摘要" },
|
||||
surfaces: ["desktop"],
|
||||
guides: [],
|
||||
highlights: [{
|
||||
kind: "fixed",
|
||||
title: { en: "Legacy fix", zh: "旧版修复" },
|
||||
body: { en: "A historical fix.", zh: "一项历史修复。" },
|
||||
refs: [1],
|
||||
}],
|
||||
changes: { new: [], improved: [], fixed: [] },
|
||||
upgrade: [],
|
||||
risks: [],
|
||||
contributors: [],
|
||||
links: {
|
||||
github: "https://example.com/release",
|
||||
compare: "https://example.com/compare",
|
||||
download: "https://example.com/download",
|
||||
},
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => validateCatalog({ schemaVersion: 1, releases: [release] }));
|
||||
for (const version of ["1.31.4", "1.32.0", "2.0.0-preview.1"]) {
|
||||
assert.throws(
|
||||
() => validateCatalog({ schemaVersion: 1, releases: [{ ...release, version }] }),
|
||||
/targetingVersion must be 1 for v1\.31\.4 and newer/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("release target hints prefer explicit product labels and identify shared or service paths", () => {
|
||||
assert.deepEqual(inferPullTargetHints(["desktop"], ["internal/sessioncatalog/catalog.go"]), ["desktop"]);
|
||||
assert.deepEqual(inferPullTargetHints(["desktop", "tui"], ["desktop/app.go", "internal/cli/cli.go"]), ["desktop", "cli"]);
|
||||
assert.deepEqual(inferPullTargetHints([], ["internal/agent/agent.go"]), ["desktop", "cli"]);
|
||||
assert.deepEqual(inferPullTargetHints([], ["workers/crash-report/src/index.ts"]), ["service"]);
|
||||
});
|
||||
|
||||
test("release generation uses bounded output and a tighter truncation retry", () => {
|
||||
assert.ok(releaseOutputBudgets.standard.highlights > releaseOutputBudgets.compact.highlights);
|
||||
assert.ok(releaseOutputBudgets.standard.changes > releaseOutputBudgets.compact.changes);
|
||||
assert.ok(releaseOutputBudgets.standard.bodyChars > releaseOutputBudgets.compact.bodyChars);
|
||||
assert.match(editorialLimitInstruction(), /Always return a complete JSON object/);
|
||||
assert.match(editorialLimitInstruction(true), /at most 4 highlights/);
|
||||
assert.match(editorialLimitInstruction(true), /9 total change items/);
|
||||
});
|
||||
|
||||
test("validation rejects bilingual drift", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
validateCatalog({
|
||||
schemaVersion: 1,
|
||||
releases: [
|
||||
{
|
||||
version: "1.0.0",
|
||||
date: "2026-01-01",
|
||||
channel: "stable",
|
||||
title: { en: "Title", zh: "" },
|
||||
summary: { en: "Summary", zh: "摘要" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
/title\.zh/,
|
||||
);
|
||||
});
|
||||
|
||||
test("managed Preview records bind every surface to one exact ordinal", () => {
|
||||
const release = {
|
||||
version: "1.19.0-preview.3",
|
||||
releaseId: "1.19.0-preview.3",
|
||||
baseVersion: "1.19.0",
|
||||
date: "2026-07-31",
|
||||
channel: "prerelease",
|
||||
status: "reviewed",
|
||||
previousRelease: "1.19.0-preview.2",
|
||||
builds: {
|
||||
cli: "v1.19.0-preview.3",
|
||||
desktop: "v1.19.0-preview.3",
|
||||
npm: "1.19.0-canary.3",
|
||||
},
|
||||
title: { en: "Preview", zh: "预览版" },
|
||||
summary: { en: "Preview summary", zh: "预览版摘要" },
|
||||
surfaces: ["cli"],
|
||||
guides: [],
|
||||
highlights: [{
|
||||
kind: "fixed",
|
||||
title: { en: "Fix", zh: "修复" },
|
||||
body: { en: "A fix.", zh: "一项修复。" },
|
||||
refs: [1],
|
||||
}],
|
||||
changes: { new: [], improved: [], fixed: [] },
|
||||
upgrade: [],
|
||||
risks: [],
|
||||
contributors: [],
|
||||
links: {
|
||||
github: "https://github.com/esengine/DeepSeek-Reasonix/releases/tag/v1.19.0-preview.3",
|
||||
compare: "https://github.com/esengine/DeepSeek-Reasonix/compare/v1.19.0-preview.2...v1.19.0-preview.3",
|
||||
download: "https://reasonix.io/?download=desktop&channel=preview#start",
|
||||
},
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => validateCatalog({ schemaVersion: 1, releases: [release] }));
|
||||
assert.throws(
|
||||
() => validateCatalog({
|
||||
schemaVersion: 1,
|
||||
releases: [{ ...release, builds: { ...release.builds, npm: "1.19.0-canary.4" } }],
|
||||
}),
|
||||
/builds\.npm/,
|
||||
);
|
||||
});
|
||||
|
||||
test("publication marker is bound to reviewed version, channel, SHA, and builds", () => {
|
||||
const release = {
|
||||
version: "1.19.0-preview.3",
|
||||
channel: "prerelease",
|
||||
candidateSha: "a".repeat(40),
|
||||
builds: {
|
||||
cli: "v1.19.0-preview.3",
|
||||
desktop: "v1.19.0-preview.3",
|
||||
npm: "1.19.0-canary.3",
|
||||
},
|
||||
};
|
||||
const event = {
|
||||
schemaVersion: 1,
|
||||
releaseId: release.version,
|
||||
channel: "preview",
|
||||
candidateSha: release.candidateSha,
|
||||
publishedAt: "2026-07-31T00:00:00.000Z",
|
||||
releaseNotesUrl: "https://reasonix.io/changelog/v1.19.0-preview.3/",
|
||||
builds: release.builds,
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => validateReleaseEvent(event, release));
|
||||
assert.throws(
|
||||
() => validateReleaseEvent({ ...event, candidateSha: "b".repeat(40) }, release),
|
||||
/candidateSha/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Studio tag lookup and verbatim rendering stay separate from CLI records", async () => {
|
||||
const { studioRecord } = await import('./sync-releases.mjs');
|
||||
const source = '中立发布说明。\n\n## 改进\n\n- 窗口布局\n';
|
||||
const studio = studioRecord({ tag_name: 'studio-v2.24.0', published_at: '2026-09-30T00:00:00Z' }, source, 'a'.repeat(40));
|
||||
const catalog = validateCatalog({ schemaVersion: 1, releases: [studio] });
|
||||
assert.equal(releaseForVersion(catalog, 'studio-v2.24.0').product, 'studio');
|
||||
assert.equal(renderGitHubRelease(studio, 'en'), source);
|
||||
});
|
||||
|
||||
test("the repository owner is never thanked: not collected, not rendered, case-insensitively", async () => {
|
||||
const { contributorsOf } = await import("./generate-release-notes.mjs");
|
||||
const { withoutOwner, repositoryOwner } = await import("../../src/lib/repository-owner.mjs");
|
||||
assert.equal(repositoryOwner, "esengine");
|
||||
assert.deepEqual(withoutOwner(["alice", "EsEngine", "bob"]), ["alice", "bob"]);
|
||||
assert.deepEqual(contributorsOf([{ author: "esengine" }, { author: "alice" }, { author: "alice" }, { author: null }, { author: "ESENGINE" }]), ["alice"]);
|
||||
assert.deepEqual(contributorsOf([{ author: "esengine" }]), []);
|
||||
|
||||
const catalog = await loadCatalog();
|
||||
const base = catalog.releases.find((release) => release.product !== "studio" && release.contributors.length);
|
||||
const markdown = renderGitHubRelease({ ...base, contributors: ["esengine", "alice"] }, "en");
|
||||
assert.doesNotMatch(markdown, /@esengine/);
|
||||
assert.match(markdown, /\[@alice\]/);
|
||||
const alone = renderGitHubRelease({ ...base, contributors: ["esengine"] }, "en");
|
||||
assert.doesNotMatch(alone, /@esengine|## Thanks/);
|
||||
});
|
||||
|
||||
test("the changelog page filters contributors through the owner helper", async () => {
|
||||
const { readFile } = await import("node:fs/promises");
|
||||
const page = await readFile(new URL("../../src/components/ChangelogPage.astro", import.meta.url), "utf8");
|
||||
assert.match(page, /withoutOwner\(release\.contributors\)/);
|
||||
assert.doesNotMatch(page, /release\.contributors\.(length|map)/);
|
||||
});
|
||||
@@ -1,139 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { appendFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseStudioNotes } from '../../src/lib/studio-notes.mjs';
|
||||
import { compareVersionsDesc, loadCatalog, upsertRelease, validateCatalog } from './release-notes.mjs';
|
||||
|
||||
const repository = 'esengine/DeepSeek-Reasonix';
|
||||
|
||||
export function missingReleases(published, catalog, { studioOnly = false } = {}) {
|
||||
const existing = new Set(catalog.releases.map((r) => `${r.product || 'cli'}:${r.version}`));
|
||||
// The archive starts after the earliest GitHub tags. Do not require source
|
||||
// catalogs from releases that predate the website's supported history.
|
||||
const oldestCLI = catalog.releases.filter((r) => r.product !== 'studio' && r.channel === 'stable')
|
||||
.map((r) => r.version).sort(compareVersionsDesc).at(-1);
|
||||
const missing = new Map();
|
||||
for (const release of published) {
|
||||
if (release.draft || !release.published_at) continue;
|
||||
const studio = release.tag_name.match(/^studio-v(2\.\d+\.\d+)$/);
|
||||
const cli = release.tag_name.match(/^(?:desktop-|npm-)?v(1\.\d+\.\d+)$/);
|
||||
if (!studio && (!cli || release.prerelease)) continue;
|
||||
const product = studio ? 'studio' : 'cli';
|
||||
if (studioOnly && product !== 'studio') continue;
|
||||
const version = (studio || cli)[1];
|
||||
if (product === 'cli' && oldestCLI && compareVersionsDesc(version, oldestCLI) > 0) continue;
|
||||
const key = `${product}:${version}`;
|
||||
if (existing.has(key)) continue;
|
||||
// Prefer the canonical CLI release over duplicate desktop/package tags.
|
||||
if (!missing.has(key) || release.tag_name === `v${version}`) {
|
||||
missing.set(key, { ...release, product, version });
|
||||
}
|
||||
}
|
||||
return [...missing.values()].sort((a, b) => -compareVersionsDesc(a.version, b.version));
|
||||
}
|
||||
|
||||
export function studioRecord(release, markdown, sha) {
|
||||
if (!markdown?.trim()) throw new Error(`Missing Studio source for ${release.tag_name}`);
|
||||
const version = release.version || release.tag_name.replace(/^studio-v/, '');
|
||||
const title = { en: `Reasonix Studio v${version}`, zh: `Reasonix Studio v${version}` };
|
||||
const line = parseStudioNotes(markdown).summary || `Reasonix Studio v${version}`;
|
||||
const summary = { en: line, zh: line };
|
||||
return {
|
||||
product: 'studio', version, date: release.published_at.slice(0, 10),
|
||||
channel: 'stable',
|
||||
sourceNotes: { language: 'zh', markdown, sha, path: `release-notes/studio/${version}.md` },
|
||||
title, summary, surfaces: ['desktop'], guides: [],
|
||||
highlights: [{ kind: 'improved', title, body: summary }],
|
||||
changes: { new: [], improved: [], fixed: [] }, upgrade: [], risks: [], contributors: [],
|
||||
links: {
|
||||
github: `https://github.com/${repository}/releases/tag/studio-v${version}`,
|
||||
compare: `https://github.com/${repository}/blob/${sha}/release-notes/studio/${version}.md`,
|
||||
download: 'https://reasonix.io/#start',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function describeFailure(error, secrets = [process.env.DEEPSEEK_API_KEY, process.env.GH_TOKEN, process.env.GITHUB_TOKEN]) {
|
||||
let text = String(error?.message ?? error).split('\n')[0];
|
||||
for (const secret of secrets) if (secret && secret.length > 3) text = text.split(secret).join('[redacted]');
|
||||
text = text.replace(/(Bearer\s+|sk-|ghp_|gho_|ghs_|github_pat_)\S+/g, '$1[redacted]').slice(0, 200);
|
||||
return `${error?.name || 'Error'}: ${text}`;
|
||||
}
|
||||
|
||||
export async function readStudioSource(release, api) {
|
||||
const { sha } = JSON.parse(await api(`commits/${release.tag_name}`));
|
||||
if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`Invalid source commit for ${release.tag_name}`);
|
||||
const version = release.version || release.tag_name.replace(/^studio-v/, '');
|
||||
const markdown = await api(`contents/release-notes/studio/${version}.md?ref=${sha}`, true);
|
||||
return { sha, markdown };
|
||||
}
|
||||
|
||||
export async function readCLISource(release, api) {
|
||||
const { sha } = JSON.parse(await api(`commits/${release.tag_name}`));
|
||||
if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`Invalid source commit for ${release.tag_name}`);
|
||||
const catalog = JSON.parse(await api(`contents/release-notes/releases.json?ref=${sha}`, true));
|
||||
const record = catalog.releases?.find((r) => r.version === release.version && r.product !== 'studio');
|
||||
if (!record) throw new Error(`Missing CLI source for ${release.tag_name}`);
|
||||
validateCatalog({ schemaVersion: catalog.schemaVersion, releases: [record] });
|
||||
// Keep reviewed records gated by fetch-publications.mjs and its all-surface
|
||||
// marker. A GitHub release alone must not promote them to published.
|
||||
return record;
|
||||
}
|
||||
|
||||
export function requireSync(result) {
|
||||
if (result.pending.length) throw new Error(`Changelog sync incomplete: ${result.pending.join(', ')}`);
|
||||
}
|
||||
|
||||
export function requireStudioSync(result, catalog, expectedVersion) {
|
||||
if (result.pending.length) throw new Error(`Studio changelog sync incomplete: ${result.pending.join(', ')}`);
|
||||
const version = expectedVersion?.replace(/^v/, '');
|
||||
if (version && !catalog.releases.some((r) => r.product === 'studio' && r.version === version)) {
|
||||
throw new Error(`Studio changelog is missing requested release ${expectedVersion}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncCatalog({ catalog, published, studioOnly = false, readStudio, readCLI, save }) {
|
||||
const added = [], pending = [], failures = [];
|
||||
for (const release of missingReleases(published, catalog, { studioOnly })) {
|
||||
const key = `${release.product}:${release.version}`;
|
||||
try {
|
||||
if (release.product === 'studio') {
|
||||
const { markdown, sha } = await readStudio(release);
|
||||
await save(studioRecord(release, markdown, sha));
|
||||
} else {
|
||||
await save(await readCLI(release));
|
||||
}
|
||||
added.push(key);
|
||||
} catch (error) {
|
||||
pending.push(key);
|
||||
failures.push({ key, reason: describeFailure(error) });
|
||||
}
|
||||
}
|
||||
return { added, pending, failures };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const studioOnly = process.argv.includes('--studio-only');
|
||||
const api = (endpoint, raw = false, paginate = false) => execFileSync('gh', [
|
||||
'api', '--hostname', 'github.com', ...(paginate ? ['--paginate', '--slurp'] : []),
|
||||
...(raw ? ['-H', 'Accept: application/vnd.github.raw+json'] : []), `repos/${repository}/${endpoint}`,
|
||||
], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
const published = JSON.parse(api('releases?per_page=100', false, true)).flat();
|
||||
const result = await syncCatalog({
|
||||
catalog: await loadCatalog(), published, studioOnly,
|
||||
readStudio: (release) => readStudioSource(release, api),
|
||||
save: (release) => upsertRelease(release),
|
||||
readCLI: (release) => readCLISource(release, api),
|
||||
});
|
||||
const summary = `## Website changelog sync\n\nAdded: ${result.added.join(', ') || 'none'}\n\nPending (retried next run): ${result.pending.join(', ') || 'none'}\n${result.failures.map((f) => `\n- ${f.key}: ${f.reason}`).join('')}\n`;
|
||||
console.log(summary);
|
||||
if (process.env.GITHUB_STEP_SUMMARY) await appendFile(process.env.GITHUB_STEP_SUMMARY, summary);
|
||||
requireSync(result);
|
||||
requireStudioSync(result, await loadCatalog(), process.env.STUDIO_RELEASE_VERSION);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => { console.error(error.message); process.exitCode = 1; });
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { test } from 'node:test';
|
||||
import { describeFailure, missingReleases, readCLISource, readStudioSource, requireSync, requireStudioSync, studioRecord, syncCatalog } from './sync-releases.mjs';
|
||||
import { loadCatalog, validateCatalog, renderGitHubRelease } from './release-notes.mjs';
|
||||
|
||||
const published = (tag, extra = {}) => ({ tag_name: tag, draft: false, prerelease: false, published_at: '2026-09-30T12:00:00Z', ...extra });
|
||||
|
||||
test('discovery deduplicates CLI namespaces, includes published Studio previews and ignores drafts and unrelated tags', () => {
|
||||
const found = missingReleases([
|
||||
published('v1.39.3'), published('desktop-v1.39.3'), published('npm-v1.39.3'),
|
||||
published('studio-v2.24.0', { prerelease: true }), published('studio-v2.25.0', { draft: true }),
|
||||
published('v1.39.2'), published('v1.40.0-preview.1', { prerelease: true }), published('other-v3.0.0'),
|
||||
], { releases: [{ version: '1.39.2', product: 'cli' }] });
|
||||
assert.deepEqual(found.map(({ product, version }) => [product, version]), [['cli', '1.39.3'], ['studio', '2.24.0']]);
|
||||
});
|
||||
|
||||
const cliOnlyCatalog = async () => {
|
||||
const catalog = await loadCatalog();
|
||||
return { ...catalog, releases: catalog.releases.filter((r) => r.product !== 'studio') };
|
||||
};
|
||||
|
||||
test('Studio preserves Chinese source verbatim and validates without CLI targeting', async () => {
|
||||
const notes = '中立版本说明。\n\n## 修复\n\n- 改善窗口布局\n';
|
||||
const release = studioRecord(published('studio-v2.24.0', { prerelease: true }), notes, 'a'.repeat(40));
|
||||
assert.equal(release.product, 'studio');
|
||||
assert.equal(release.sourceNotes.language, 'zh');
|
||||
assert.equal(release.sourceNotes.markdown, notes);
|
||||
assert.equal(release.channel, 'stable');
|
||||
assert.doesNotThrow(() => validateCatalog({ schemaVersion: 1, releases: [release] }));
|
||||
assert.equal(renderGitHubRelease(release), notes);
|
||||
const catalog = await loadCatalog();
|
||||
assert.throws(() => validateCatalog({ ...catalog, releases: [{ ...catalog.releases[0], product: 'invalid' }] }));
|
||||
assert.throws(() => validateCatalog({ schemaVersion: 1, releases: [{ ...release, sourceNotes: undefined }] }));
|
||||
});
|
||||
|
||||
test('batch sync imports reviewed CLI and Studio source without model generation', async () => {
|
||||
const catalog = await cliOnlyCatalog();
|
||||
const saved = [];
|
||||
const result = await syncCatalog({
|
||||
catalog, published: [published('v1.99.0'), published('studio-v2.24.0')],
|
||||
readStudio: async () => ({ sha: 'a'.repeat(40), markdown: '中立更新。\n' }),
|
||||
save: async (release) => saved.push(release),
|
||||
readCLI: async () => ({ version: '1.99.0', status: 'reviewed' }),
|
||||
});
|
||||
assert.deepEqual(saved.map((r) => r.version), ['1.99.0', '2.24.0']);
|
||||
assert.equal(saved[0].status, 'reviewed');
|
||||
assert.deepEqual(result.pending, []);
|
||||
assert.deepEqual(result.added, ['cli:1.99.0', 'studio:2.24.0']);
|
||||
});
|
||||
|
||||
test('existing catalog entries are never regenerated', async () => {
|
||||
const catalog = await loadCatalog();
|
||||
const result = await syncCatalog({ catalog, published: [published('v1.39.2')], save: () => { throw new Error('unexpected write'); } });
|
||||
assert.deepEqual(result, { added: [], pending: [], failures: [] });
|
||||
});
|
||||
|
||||
test('batch failures retry independently and never manufacture missing Studio notes', async () => {
|
||||
const result = await syncCatalog({
|
||||
catalog: await cliOnlyCatalog(), published: [published('studio-v2.23.0'), published('studio-v2.24.0')],
|
||||
readStudio: async (release) => ({ sha: 'a'.repeat(40), markdown: release.version === '2.23.0' ? '' : '中立说明。' }),
|
||||
save: async () => {},
|
||||
});
|
||||
assert.deepEqual(result.added, ['studio:2.24.0']);
|
||||
assert.deepEqual(result.pending, ['studio:2.23.0']);
|
||||
assert.match(result.failures[0].reason, /^Error: Missing Studio source/);
|
||||
});
|
||||
|
||||
test('CLI range selection uses the earlier CLI release even with newer Studio and CLI records', async () => {
|
||||
const { previousCLIRelease } = await import('./generate-release-notes.mjs');
|
||||
assert.equal(previousCLIRelease({ releases: [
|
||||
{ version: '2.24.0', product: 'studio', channel: 'stable' },
|
||||
{ version: '1.39.6', product: 'cli', channel: 'stable' },
|
||||
{ version: '1.39.2', channel: 'stable' },
|
||||
] }, '1.39.3').version, '1.39.2');
|
||||
});
|
||||
|
||||
test('an upstream published CLI release preserves its publication-marker gate', async () => {
|
||||
const saved = [];
|
||||
const result = await syncCatalog({
|
||||
catalog: await loadCatalog(), published: [published('v1.99.0')],
|
||||
readCLI: async () => ({ version: '1.99.0', product: 'cli', status: 'reviewed' }),
|
||||
save: async (release) => saved.push(release),
|
||||
});
|
||||
assert.deepEqual(result.added, ['cli:1.99.0']);
|
||||
assert.equal(saved[0].status, 'reviewed');
|
||||
});
|
||||
|
||||
test('failure reasons carry the error class and a message but never secrets', async () => {
|
||||
const result = await syncCatalog({
|
||||
catalog: await loadCatalog(), published: [published('v1.99.0')],
|
||||
readCLI: async () => { throw new TypeError('request failed with Bearer abc123token and key sk-live-999\nstack line'); },
|
||||
save: async () => {},
|
||||
});
|
||||
assert.deepEqual(result.pending, ['cli:1.99.0']);
|
||||
assert.equal(result.failures[0].reason, 'TypeError: request failed with Bearer [redacted] and key sk-[redacted]');
|
||||
assert.equal(describeFailure(new Error('boom hunter2'), ['hunter2']), 'Error: boom [redacted]');
|
||||
});
|
||||
|
||||
test('Studio entries take their summary from the first line of the notes', () => {
|
||||
const release = studioRecord(published('studio-v2.24.0'), '本版加入反馈。\n\n## 新增\n\n- 项 (#1)\n', 'a'.repeat(40));
|
||||
assert.equal(release.summary.zh, '本版加入反馈。');
|
||||
assert.equal(release.summary.en, '本版加入反馈。');
|
||||
});
|
||||
|
||||
test('Studio source is read from its published tag commit, never the moving product branch', async () => {
|
||||
const sha = 'b'.repeat(40);
|
||||
const source = await readStudioSource(published('studio-v2.45.0'), async (endpoint, raw) => {
|
||||
if (endpoint === 'commits/studio-v2.45.0') return JSON.stringify({ sha });
|
||||
assert.equal(endpoint, `contents/release-notes/studio/2.45.0.md?ref=${sha}`);
|
||||
assert.equal(raw, true);
|
||||
return '该版本原始说明。\n';
|
||||
});
|
||||
assert.deepEqual(source, { sha, markdown: '该版本原始说明。\n' });
|
||||
});
|
||||
|
||||
test('build sync imports all missing Studio releases, leaves CLI alone and is idempotent', async () => {
|
||||
const catalog = { schemaVersion: 1, releases: [] };
|
||||
const releases = [published('v1.99.0'), published('studio-v2.44.0'), published('studio-v2.45.0'), published('studio-v2.46.0', { draft: true })];
|
||||
const run = () => syncCatalog({
|
||||
catalog, published: releases, studioOnly: true,
|
||||
readStudio: async (release) => ({ sha: release.version === '2.44.0' ? 'a'.repeat(40) : 'b'.repeat(40), markdown: `${release.version} 原文。\n` }),
|
||||
save: async (release) => catalog.releases.unshift(release),
|
||||
});
|
||||
const first = await run();
|
||||
assert.deepEqual(first.added, ['studio:2.44.0', 'studio:2.45.0']);
|
||||
assert.deepEqual(first.pending, []);
|
||||
assert.equal(catalog.releases[0].version, '2.45.0');
|
||||
assert.equal(catalog.releases[0].sourceNotes.sha, 'b'.repeat(40));
|
||||
assert.deepEqual((await run()).added, []);
|
||||
});
|
||||
|
||||
test('a failed or incomplete Studio sync blocks publication of a stale catalog', () => {
|
||||
const catalog = { releases: [{ product: 'studio', version: '2.44.0' }, { version: '2.45.0', product: 'cli' }] };
|
||||
const complete = { pending: [], failures: [] };
|
||||
assert.doesNotThrow(() => requireStudioSync(complete, catalog));
|
||||
assert.doesNotThrow(() => requireStudioSync(complete, catalog, '2.44.0'));
|
||||
assert.doesNotThrow(() => requireStudioSync(complete, catalog, 'v2.44.0'));
|
||||
assert.throws(() => requireStudioSync(complete, catalog, '2.45.0'), /2\.45\.0/);
|
||||
assert.throws(() => requireStudioSync({ pending: ['studio:2.45.0'] }, catalog), /studio:2\.45\.0/);
|
||||
});
|
||||
|
||||
test('production builds synchronize both products before rendering and deployments require the website branch', async () => {
|
||||
const pkg = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
|
||||
assert.ok(pkg.scripts.prebuild.startsWith('node scripts/releases/sync-releases.mjs &&'));
|
||||
const workflow = await readFile(new URL('../../../.github/workflows/pages.yml', import.meta.url), 'utf8');
|
||||
assert.match(workflow, /workflow_dispatch:\n inputs:/);
|
||||
assert.match(workflow, /if: github\.ref == 'refs\/heads\/website'/);
|
||||
assert.match(workflow, /STUDIO_RELEASE_VERSION: \$\{\{ inputs\.studio_version \}\}/);
|
||||
});
|
||||
|
||||
test('CLI sources use exact published tag commits and preserve reviewed bilingual content', async () => {
|
||||
const catalog = await loadCatalog();
|
||||
const record = catalog.releases.find((r) => r.version === '1.39.7');
|
||||
const sha = 'c'.repeat(40);
|
||||
for (const tag of ['v1.39.7', 'desktop-v1.39.7']) {
|
||||
const result = await readCLISource({ ...published(tag), version: '1.39.7' }, async (endpoint, raw) => {
|
||||
if (endpoint === `commits/${tag}`) return JSON.stringify({ sha });
|
||||
assert.equal(endpoint, `contents/release-notes/releases.json?ref=${sha}`);
|
||||
assert.equal(raw, true);
|
||||
return JSON.stringify({ schemaVersion: 1, releases: [record] });
|
||||
});
|
||||
assert.deepEqual(result, record);
|
||||
}
|
||||
});
|
||||
|
||||
test('missing or invalid CLI source fails closed while independent Studio sync continues', async () => {
|
||||
const release = { ...published('v1.99.0'), version: '1.99.0' };
|
||||
const api = async (endpoint) => endpoint.startsWith('commits/')
|
||||
? JSON.stringify({ sha: 'd'.repeat(40) })
|
||||
: JSON.stringify({ schemaVersion: 1, releases: [] });
|
||||
const result = await syncCatalog({
|
||||
catalog: await cliOnlyCatalog(), published: [release, published('studio-v2.99.0')],
|
||||
readCLI: (r) => readCLISource(r, api),
|
||||
readStudio: async () => ({ sha: 'e'.repeat(40), markdown: '更新说明。' }),
|
||||
save: async () => {},
|
||||
});
|
||||
assert.deepEqual(result.pending, ['cli:1.99.0']);
|
||||
assert.deepEqual(result.added, ['studio:2.99.0']);
|
||||
assert.throws(() => requireSync(result), /cli:1\.99\.0/);
|
||||
await assert.rejects(readCLISource(release, async () => JSON.stringify({ sha: 'invalid' })), /Invalid source commit/);
|
||||
await assert.rejects(readCLISource(release, async (endpoint) => endpoint.startsWith('commits/')
|
||||
? JSON.stringify({ sha: 'd'.repeat(40) })
|
||||
: JSON.stringify({ schemaVersion: 1, releases: [{ version: '1.99.0' }] })), /date/);
|
||||
});
|
||||
|
||||
test('build sync respects the archive history boundary and never imports unpublished CLI drafts', async () => {
|
||||
const catalog = { schemaVersion: 1, releases: [{ version: '1.39.7', channel: 'stable' }] };
|
||||
const releases = [published('v1.0.0'), published('v1.39.8'), published('desktop-v1.39.8'),
|
||||
published('v1.39.9'), published('v1.39.10', { draft: true }), published('v1.39.11', { prerelease: true })];
|
||||
const run = () => syncCatalog({
|
||||
catalog, published: releases,
|
||||
readCLI: async (r) => ({ version: r.version, channel: 'stable', status: 'reviewed' }),
|
||||
save: async (r) => catalog.releases.unshift(r),
|
||||
});
|
||||
assert.deepEqual((await run()).added, ['cli:1.39.8', 'cli:1.39.9']);
|
||||
assert.deepEqual(catalog.releases.map((r) => r.version), ['1.39.9', '1.39.8', '1.39.7']);
|
||||
const repeated = await run();
|
||||
assert.deepEqual(repeated, { added: [], pending: [], failures: [] });
|
||||
assert.doesNotThrow(() => requireSync(repeated));
|
||||
});
|
||||
@@ -1,445 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import CommunityJoin from './CommunityJoin.astro';
|
||||
import SiteHeader from './SiteHeader.astro';
|
||||
import type { ReleaseItem, ReleaseRecord, ReleaseTarget } from '../data/releases';
|
||||
import { releasePath, releaseTargetCount } from '../data/releases';
|
||||
import { issueHref, parseStudioNotes } from '../lib/studio-notes.mjs';
|
||||
import { withoutOwner } from '../lib/repository-owner.mjs';
|
||||
import '../styles/changelog.css';
|
||||
|
||||
const { release, releases, canonical } = Astro.props as {
|
||||
release: ReleaseRecord;
|
||||
releases: ReleaseRecord[];
|
||||
canonical?: string;
|
||||
};
|
||||
const thanked = withoutOwner(release.contributors);
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const repo = 'https://github.com/esengine/DeepSeek-Reasonix';
|
||||
const prHref = (ref: number) => `${repo}/pull/${ref}`;
|
||||
|
||||
const isStudio = release.product === 'studio';
|
||||
const isPreview = !isStudio && release.channel === 'prerelease';
|
||||
const latestForChannel = releases[0];
|
||||
const studioDoc = isStudio && release.sourceNotes ? parseStudioNotes(release.sourceNotes.markdown) : null;
|
||||
const studioBlocks = studioDoc ? studioDoc.blocks.filter((block, index) => !(index === 0 && block.type === 'paragraph' && studioDoc.summary === release.summary.zh)) : [];
|
||||
const downloadHref = isPreview ? 'https://reasonix.io/?download=desktop#start' : release.links.download;
|
||||
|
||||
const kindLabel = (kind: string) => ({
|
||||
new: { en: 'New', zh: '新增' },
|
||||
improved: { en: 'Improved', zh: '改进' },
|
||||
fixed: { en: 'Fixed', zh: '修复' },
|
||||
security: { en: 'Security', zh: '安全' },
|
||||
}[kind] ?? { en: kind, zh: kind });
|
||||
|
||||
const surfaceLabel = (surface: string) => ({
|
||||
agent: { en: 'Agent', zh: 'Agent' },
|
||||
desktop: { en: 'Desktop', zh: '桌面端' },
|
||||
cli: { en: 'CLI', zh: 'CLI' },
|
||||
mcp: { en: 'MCP', zh: 'MCP' },
|
||||
security: { en: 'Security', zh: '安全' },
|
||||
provider: { en: 'Provider', zh: '模型服务' },
|
||||
plugin: { en: 'Plugins', zh: '插件' },
|
||||
site: { en: 'Site', zh: '网站' },
|
||||
service: { en: 'Service', zh: '服务端' },
|
||||
}[surface] ?? { en: surface, zh: surface });
|
||||
|
||||
const targetingEnabled = release.targetingVersion === 1;
|
||||
const changeKinds = ['new', 'improved', 'fixed'] as const;
|
||||
const clientTargets: { id: ReleaseTarget; en: string; zh: string }[] = [
|
||||
{ id: 'desktop', en: 'Desktop updates', zh: '桌面端更新' },
|
||||
{ id: 'cli', en: 'CLI updates', zh: 'CLI 端更新' },
|
||||
];
|
||||
const hasTarget = (item: ReleaseItem, target: ReleaseTarget) => item.targets?.includes(target) ?? false;
|
||||
const clientHighlights = (target: ReleaseTarget) => release.highlights.filter((item) => hasTarget(item, target));
|
||||
const clientChanges = (target: ReleaseTarget, kind: typeof changeKinds[number]) =>
|
||||
release.changes[kind].filter((item) => hasTarget(item, target));
|
||||
const hasClientChangeContent = (target: ReleaseTarget) =>
|
||||
clientHighlights(target).length > 0 || changeKinds.some((kind) => clientChanges(target, kind).length > 0);
|
||||
const isOtherOnly = (item: ReleaseItem) =>
|
||||
Boolean(item.targets?.length) && !hasTarget(item, 'desktop') && !hasTarget(item, 'cli');
|
||||
const otherHighlights = release.highlights.filter(isOtherOnly);
|
||||
const otherChanges = Object.fromEntries(
|
||||
changeKinds.map((kind) => [kind, release.changes[kind].filter(isOtherOnly)]),
|
||||
) as Record<typeof changeKinds[number], ReleaseItem[]>;
|
||||
const hasOtherChanges = otherHighlights.length > 0 || changeKinds.some((kind) => otherChanges[kind].length > 0);
|
||||
|
||||
/* Sections render only when they have content; numbering is sequential. */
|
||||
const sectionDefs = [
|
||||
...(release.guides.length ? [{ id: 'guides', en: 'Guides', zh: '使用攻略' }] : []),
|
||||
...(targetingEnabled
|
||||
? [
|
||||
...clientTargets,
|
||||
...(hasOtherChanges ? [{ id: 'other', en: 'Other project updates', zh: '其他项目更新' }] : []),
|
||||
]
|
||||
: [
|
||||
{ id: 'highlights', en: 'Highlights', zh: '重点内容' },
|
||||
...changeKinds
|
||||
.filter((kind) => release.changes[kind].length)
|
||||
.map((kind) => ({ id: kind, ...kindLabel(kind) })),
|
||||
]),
|
||||
...(release.upgrade.length ? [{ id: 'upgrade', en: 'Upgrade notes', zh: '升级提醒' }] : []),
|
||||
...(release.risks.length ? [{ id: 'risks', en: 'Risk notes', zh: '风险提示' }] : []),
|
||||
...(thanked.length ? [{ id: 'thanks', en: 'Thanks', zh: '致谢' }] : []),
|
||||
];
|
||||
const sectionNum = new Map(sectionDefs.map((s, i) => [s.id, String(i + 1).padStart(2, '0')]));
|
||||
const tocItems = [...(isStudio ? [{ id: 'source-notes', en: 'Chinese source notes', zh: '中文原始说明' }] : sectionDefs), { id: 'download', en: 'Download', zh: '下载与安装' }];
|
||||
|
||||
/* Version rail: group by minor, show the latest few patches per group,
|
||||
older ones behind a native <details> expander. */
|
||||
const minorKey = (v: string) => v.split('.').slice(0, 2).join('.');
|
||||
const railGroups: { key: string; items: ReleaseRecord[] }[] = [];
|
||||
for (const item of releases) {
|
||||
const key = minorKey(item.version);
|
||||
const last = railGroups[railGroups.length - 1];
|
||||
if (last && last.key === key) last.items.push(item);
|
||||
else railGroups.push({ key, items: [item] });
|
||||
}
|
||||
const RAIL_VISIBLE = 3;
|
||||
|
||||
const titleEn = `Reasonix${isStudio ? ' Studio' : ''} v${release.version} release notes`;
|
||||
const titleZh = `Reasonix${isStudio ? ' Studio' : ''} v${release.version} 更新日志`;
|
||||
const description = release.summary.en;
|
||||
---
|
||||
|
||||
<Base title={titleEn} titleEn={titleEn} titleZh={titleZh} description={description} canonical={canonical} noindex={isPreview}>
|
||||
<SiteHeader active="changelog" scrolled />
|
||||
|
||||
<main class="release-stage">
|
||||
<nav class="release-product-tabs" aria-label="Products">
|
||||
<a href={`${base}/changelog/cli/`} aria-current={!isStudio ? 'page' : undefined}><span class="l-en">CLI · 1.x (previous)</span><span class="l-zh">CLI · 1.x(上一代)</span></a>
|
||||
<a href={`${base}/changelog/`} aria-current={isStudio ? 'page' : undefined}>Studio · 2.x</a>
|
||||
</nav>
|
||||
<div class="release-mobile-switcher">
|
||||
<label for="release-version"><span class="l-en">Version</span><span class="l-zh">版本</span></label>
|
||||
<select id="release-version" data-release-version>
|
||||
{releases.map((item) => <option value={item.version} selected={item.version === release.version}>v{item.version}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="release-grid">
|
||||
<aside class="release-rail" aria-label="Release versions">
|
||||
<div class="release-rail__eyebrow"><span class="release-rail__mark"></span><span class="l-en">Release ledger</span><span class="l-zh">版本档案</span></div>
|
||||
<nav>
|
||||
{railGroups.map((group) => {
|
||||
const channelItems = group.items;
|
||||
if (!channelItems.length) return null;
|
||||
const visible = channelItems.slice(0, RAIL_VISIBLE);
|
||||
const overflow = channelItems.slice(RAIL_VISIBLE);
|
||||
return (
|
||||
<div class="release-rail__group">
|
||||
<p class="release-rail__minor">{group.key}.x</p>
|
||||
{visible.map((item) => (
|
||||
<a class:list={["release-version-link", { active: item.version === release.version }]} href={`${base}${releasePath(item.version)}`}>
|
||||
<span class="release-version-link__line"><strong>v{item.version}</strong>{item === latestForChannel && <em><span class="l-en">Latest</span><span class="l-zh">最新</span></em>}</span>
|
||||
<small>{item.date}</small>
|
||||
</a>
|
||||
))}
|
||||
{overflow.length > 0 && (
|
||||
<details class="release-rail__more" open={overflow.some((item) => item.version === release.version)}>
|
||||
<summary>
|
||||
<span class="when-closed"><span class="l-en">+{overflow.length} more</span><span class="l-zh">还有 {overflow.length} 个</span></span>
|
||||
<span class="when-open"><span class="l-en">Show less</span><span class="l-zh">收起</span></span>
|
||||
</summary>
|
||||
{overflow.map((item) => (
|
||||
<a class:list={["release-version-link", { active: item.version === release.version }]} href={`${base}${releasePath(item.version)}`}>
|
||||
<span class="release-version-link__line"><strong>v{item.version}</strong>{item === latestForChannel && <em><span class="l-en">Latest</span><span class="l-zh">最新</span></em>}</span>
|
||||
<small>{item.date}</small>
|
||||
</a>
|
||||
))}
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<a class="release-rail__all" href={`${repo}/releases`}><span class="l-en">All GitHub releases ↗</span><span class="l-zh">全部 GitHub Releases ↗</span></a>
|
||||
</aside>
|
||||
|
||||
<article class="release-paper">
|
||||
<header class="release-hero">
|
||||
<div class="release-hero__meta">
|
||||
{isPreview && <span class="release-channel release-channel--preview"><span class="release-channel__dot"></span><span class="l-en">Historical archive</span><span class="l-zh">历史归档</span></span>}
|
||||
<time datetime={release.date}>{release.date}</time>
|
||||
</div>
|
||||
<h1>Reasonix{isStudio ? ' Studio' : ''} v{release.version}</h1>
|
||||
<div class="release-surfaces">
|
||||
{release.surfaces.map((surface) => {
|
||||
const label = surfaceLabel(surface);
|
||||
return <span><span class="l-en">{label.en}</span><span class="l-zh">{label.zh}</span></span>;
|
||||
})}
|
||||
</div>
|
||||
{release.builds && (
|
||||
<div class="release-builds" aria-label="Published builds">
|
||||
<span><strong>CLI</strong> {release.builds.cli}</span>
|
||||
<span><strong>Desktop</strong> {release.builds.desktop}</span>
|
||||
<span><strong>npm</strong> {release.builds.npm}</span>
|
||||
</div>
|
||||
)}
|
||||
{targetingEnabled && (
|
||||
<div class="release-target-summary" aria-label="Updates by product">
|
||||
<span><strong>Desktop</strong> {releaseTargetCount(release, 'desktop')}</span>
|
||||
<span><strong>CLI</strong> {releaseTargetCount(release, 'cli')}</span>
|
||||
</div>
|
||||
)}
|
||||
<p class="release-lede"><span class="l-en">{release.summary.en}</span><span class="l-zh">{release.summary.zh}</span></p>
|
||||
</header>
|
||||
|
||||
{isStudio && release.sourceNotes && (
|
||||
<section id="source-notes" class="release-section">
|
||||
<div class="release-section__head"><span>01</span><h2><span class="l-en">Chinese source notes</span><span class="l-zh">中文原始说明</span></h2></div>
|
||||
<p class="release-source-link"><a href={release.links.compare}><span class="l-en">View source ↗</span><span class="l-zh">查看原文 ↗</span></a></p>
|
||||
<div class="release-source-notes" lang="zh">
|
||||
{studioBlocks.map((block) => block.type === 'heading' ? (
|
||||
<h3>{block.inline.map((token) => token.type === 'code' ? <code>{token.value}</code> : token.value)}</h3>
|
||||
) : block.type === 'list' ? (
|
||||
<ul>
|
||||
{block.items.map((item) => (
|
||||
<li>{item.map((token) => token.type === 'code' ? <code>{token.value}</code> : token.type === 'ref' ? <a href={issueHref(token.number)} rel="noopener">{token.value}</a> : token.value)}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>{block.inline.map((token) => token.type === 'code' ? <code>{token.value}</code> : token.type === 'ref' ? <a href={issueHref(token.number)} rel="noopener">{token.value}</a> : token.value)}</p>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{!isStudio && <>
|
||||
{release.guides.length > 0 && (
|
||||
<section id="guides" class="release-section release-guides">
|
||||
<div class="release-section__head"><span>{sectionNum.get('guides')}</span><h2><span class="l-en">Start here</span><span class="l-zh">使用攻略</span></h2></div>
|
||||
<div class="release-guide-grid">
|
||||
{release.guides.map((guide) => (
|
||||
<a class="release-guide" href={guide.href}>
|
||||
<span class="release-guide__arrow">↗</span>
|
||||
<h3><span class="l-en">{guide.title.en}</span><span class="l-zh">{guide.title.zh}</span></h3>
|
||||
<p><span class="l-en">{guide.body.en}</span><span class="l-zh">{guide.body.zh}</span></p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{targetingEnabled ? (
|
||||
<>
|
||||
{clientTargets.map((target) => {
|
||||
const highlights = clientHighlights(target.id);
|
||||
const total = releaseTargetCount(release, target.id);
|
||||
return (
|
||||
<section id={target.id} class="release-section release-target-section" data-target={target.id}>
|
||||
<div class="release-section__head"><span>{sectionNum.get(target.id)}</span><h2><span class="l-en">{target.en}</span><span class="l-zh">{target.zh}</span></h2></div>
|
||||
<p class="release-target-section__summary">
|
||||
<span class="l-en">{total} update{total === 1 ? '' : 's'} relevant to {target.id === 'desktop' ? 'Desktop' : 'CLI'}.</span>
|
||||
<span class="l-zh">与{target.id === 'desktop' ? '桌面端' : ' CLI'}相关的更新共 {total} 项。</span>
|
||||
</p>
|
||||
|
||||
{highlights.length > 0 && (
|
||||
<div class="release-target-group">
|
||||
<h3 class="release-target-group__title"><span class="l-en">Highlights</span><span class="l-zh">重点内容</span></h3>
|
||||
<div class="release-highlight-list">
|
||||
{highlights.map((item, index) => {
|
||||
const kind = kindLabel(item.kind);
|
||||
return (
|
||||
<article class="release-highlight" data-kind={item.kind}>
|
||||
<div class="release-highlight__index">{String(index + 1).padStart(2, '0')}</div>
|
||||
<div>
|
||||
<div class="release-item-meta">
|
||||
<div class="release-highlight__kind"><span class="l-en">{kind.en}</span><span class="l-zh">{kind.zh}</span></div>
|
||||
<div class="release-item-targets">{item.targets?.map((itemTarget) => {
|
||||
const label = surfaceLabel(itemTarget);
|
||||
return <span><span class="l-en">{label.en}</span><span class="l-zh">{label.zh}</span></span>;
|
||||
})}</div>
|
||||
</div>
|
||||
<h3><span class="l-en">{item.title.en}</span><span class="l-zh">{item.title.zh}</span></h3>
|
||||
<p><span class="l-en">{item.body.en}</span><span class="l-zh">{item.body.zh}</span></p>
|
||||
{item.refs?.length && <div class="release-refs">{item.refs.map((ref) => <a href={prHref(ref)}>#{ref}</a>)}</div>}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{changeKinds.map((kind) => {
|
||||
const items = clientChanges(target.id, kind);
|
||||
if (!items.length) return null;
|
||||
const label = kindLabel(kind);
|
||||
return (
|
||||
<div class="release-target-group">
|
||||
<h3 class="release-target-group__title"><span class="l-en">{label.en}</span><span class="l-zh">{label.zh}</span></h3>
|
||||
<div class="release-change-list">
|
||||
{items.map((item: ReleaseItem) => (
|
||||
<article>
|
||||
<div class="release-item-targets">{item.targets?.map((itemTarget) => {
|
||||
const targetLabel = surfaceLabel(itemTarget);
|
||||
return <span><span class="l-en">{targetLabel.en}</span><span class="l-zh">{targetLabel.zh}</span></span>;
|
||||
})}</div>
|
||||
<h3><span class="l-en">{item.title.en}</span><span class="l-zh">{item.title.zh}</span></h3>
|
||||
<p><span class="l-en">{item.body.en}</span><span class="l-zh">{item.body.zh}</span></p>
|
||||
{item.refs?.length && <div class="release-refs">{item.refs.map((ref) => <a href={prHref(ref)}>#{ref}</a>)}</div>}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{!hasClientChangeContent(target.id) && (
|
||||
<div class="release-target-empty">
|
||||
{target.id === 'cli' && total === 0 ? (
|
||||
<><strong><span class="l-en">No CLI changes in this release.</span><span class="l-zh">本版本没有 CLI 相关改动。</span></strong><p><span class="l-en">CLI users may skip this version if preferred.</span><span class="l-zh">CLI 用户可按需跳过这个版本。</span></p></>
|
||||
) : (
|
||||
<><strong><span class="l-en">No feature or fix entries in this section.</span><span class="l-zh">此板块没有功能或修复条目。</span></strong><p><span class="l-en">See the upgrade and risk notes below for relevant guidance.</span><span class="l-zh">相关说明请查看下方升级提醒和风险提示。</span></p></>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
|
||||
{hasOtherChanges && (
|
||||
<section id="other" class="release-section release-target-section" data-target="other">
|
||||
<div class="release-section__head"><span>{sectionNum.get('other')}</span><h2><span class="l-en">Other project updates</span><span class="l-zh">其他项目更新</span></h2></div>
|
||||
<p class="release-target-section__summary"><span class="l-en">Website and service changes that do not require a Desktop or CLI update.</span><span class="l-zh">不要求更新桌面端或 CLI 的网站与服务端变更。</span></p>
|
||||
<div class="release-change-list">
|
||||
{[...otherHighlights, ...changeKinds.flatMap((kind) => otherChanges[kind])].map((item) => (
|
||||
<article>
|
||||
<div class="release-item-targets">{item.targets?.map((itemTarget) => {
|
||||
const label = surfaceLabel(itemTarget);
|
||||
return <span><span class="l-en">{label.en}</span><span class="l-zh">{label.zh}</span></span>;
|
||||
})}</div>
|
||||
<h3><span class="l-en">{item.title.en}</span><span class="l-zh">{item.title.zh}</span></h3>
|
||||
<p><span class="l-en">{item.body.en}</span><span class="l-zh">{item.body.zh}</span></p>
|
||||
{item.refs?.length && <div class="release-refs">{item.refs.map((ref) => <a href={prHref(ref)}>#{ref}</a>)}</div>}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<section id="highlights" class="release-section">
|
||||
<div class="release-section__head"><span>{sectionNum.get('highlights')}</span><h2><span class="l-en">Highlights</span><span class="l-zh">重点内容</span></h2></div>
|
||||
<div class="release-highlight-list">
|
||||
{release.highlights.map((item, index) => {
|
||||
const kind = kindLabel(item.kind);
|
||||
return (
|
||||
<article class="release-highlight" data-kind={item.kind}>
|
||||
<div class="release-highlight__index">{String(index + 1).padStart(2, '0')}</div>
|
||||
<div>
|
||||
<div class="release-highlight__kind"><span class="l-en">{kind.en}</span><span class="l-zh">{kind.zh}</span></div>
|
||||
<h3><span class="l-en">{item.title.en}</span><span class="l-zh">{item.title.zh}</span></h3>
|
||||
<p><span class="l-en">{item.body.en}</span><span class="l-zh">{item.body.zh}</span></p>
|
||||
{item.refs?.length && <div class="release-refs">{item.refs.map((ref) => <a href={prHref(ref)}>#{ref}</a>)}</div>}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{changeKinds.map((kind) => {
|
||||
const items = release.changes[kind];
|
||||
if (!items.length) return null;
|
||||
const label = kindLabel(kind);
|
||||
return (
|
||||
<section id={kind} class="release-section release-change-section">
|
||||
<div class="release-section__head"><span>{sectionNum.get(kind)}</span><h2><span class="l-en">{label.en}</span><span class="l-zh">{label.zh}</span></h2></div>
|
||||
<div class="release-change-list">
|
||||
{items.map((item: ReleaseItem) => (
|
||||
<article>
|
||||
<h3><span class="l-en">{item.title.en}</span><span class="l-zh">{item.title.zh}</span></h3>
|
||||
<p><span class="l-en">{item.body.en}</span><span class="l-zh">{item.body.zh}</span></p>
|
||||
{item.refs?.length && <div class="release-refs">{item.refs.map((ref) => <a href={prHref(ref)}>#{ref}</a>)}</div>}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{release.upgrade.length > 0 && (
|
||||
<section id="upgrade" class="release-section">
|
||||
<div class="release-section__head"><span>{sectionNum.get('upgrade')}</span><h2><span class="l-en">Upgrade notes</span><span class="l-zh">升级提醒</span></h2></div>
|
||||
<div class="release-notice-list">
|
||||
{release.upgrade.map((item) => (
|
||||
<article class:list={["release-notice", `release-notice--${item.level}`]}>
|
||||
<div class="release-notice__icon">{item.level === 'warning' ? '!' : 'i'}</div>
|
||||
<div>
|
||||
{item.targets?.length && <div class="release-item-targets">{item.targets.map((itemTarget) => {
|
||||
const label = surfaceLabel(itemTarget);
|
||||
return <span><span class="l-en">{label.en}</span><span class="l-zh">{label.zh}</span></span>;
|
||||
})}</div>}
|
||||
<h3><span class="l-en">{item.title.en}</span><span class="l-zh">{item.title.zh}</span></h3>
|
||||
<p><span class="l-en">{item.body.en}</span><span class="l-zh">{item.body.zh}</span></p>
|
||||
{item.refs?.length && <div class="release-refs">{item.refs.map((ref) => <a href={prHref(ref)}>#{ref}</a>)}</div>}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{release.risks.length > 0 && (
|
||||
<section id="risks" class="release-section">
|
||||
<div class="release-section__head"><span>{sectionNum.get('risks')}</span><h2><span class="l-en">Risk notes</span><span class="l-zh">风险提示</span></h2></div>
|
||||
<div class="release-change-list">
|
||||
{release.risks.map((item) => (
|
||||
<article>
|
||||
{item.targets?.length && <div class="release-item-targets">{item.targets.map((itemTarget) => {
|
||||
const label = surfaceLabel(itemTarget);
|
||||
return <span><span class="l-en">{label.en}</span><span class="l-zh">{label.zh}</span></span>;
|
||||
})}</div>}
|
||||
<h3><span class="l-en">{item.title.en}</span><span class="l-zh">{item.title.zh}</span></h3>
|
||||
<p><span class="l-en">{item.body.en}</span><span class="l-zh">{item.body.zh}</span></p>
|
||||
{item.refs?.length && <div class="release-refs">{item.refs.map((ref) => <a href={prHref(ref)}>#{ref}</a>)}</div>}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{thanked.length > 0 && (
|
||||
<section id="thanks" class="release-section release-thanks">
|
||||
<div class="release-section__head"><span>{sectionNum.get('thanks')}</span><h2><span class="l-en">Thanks</span><span class="l-zh">致谢</span></h2></div>
|
||||
<p><span class="l-en">This release was shaped by:</span><span class="l-zh">感谢本版本的贡献者:</span></p>
|
||||
<div>{thanked.map((name) => <a href={`https://github.com/${name}`}>@{name}</a>)}</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
</>}
|
||||
|
||||
<section id="download" class="release-section release-download">
|
||||
<div>
|
||||
<p class="release-download__kicker"><span class="l-en">Ready when you are</span><span class="l-zh">准备好即可升级</span></p>
|
||||
<h2><span class="l-en">Ship with a safer Reasonix.</span><span class="l-zh">升级到更可靠的 Reasonix。</span></h2>
|
||||
</div>
|
||||
<div class="release-download__actions">
|
||||
<a class="btn btn-dark" href={downloadHref}><span class="l-en">Download</span><span class="l-zh">下载</span></a>
|
||||
<a class="release-compare" href={release.links.compare}><span class="l-en">{isStudio ? 'Source notes ↗' : 'Full comparison ↗'}</span><span class="l-zh">{isStudio ? '原始说明 ↗' : '完整差异 ↗'}</span></a>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<aside class="release-toc">
|
||||
<p><span class="l-en">In v{release.version}</span><span class="l-zh">在 v{release.version} 中</span></p>
|
||||
<nav>{tocItems.map((item) => <a href={`#${item.id}`}><span class="l-en">{item.en}</span><span class="l-zh">{item.zh}</span></a>)}</nav>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<CommunityJoin />
|
||||
<footer class="release-footer">
|
||||
<a class="brand" href={`${base}/`}><img src={`${base}/logo.svg`} alt="" /><span>Reasonix</span></a>
|
||||
<span>© 2026 Reasonix · MIT</span>
|
||||
<a href={repo}>GitHub ↗</a>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
import '../scripts/changelog.js';
|
||||
</script>
|
||||
</Base>
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
import { QQ_GROUP, DISCORD_URL, DOUYIN } from '../lib/community-links.mjs';
|
||||
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
---
|
||||
<section class="join" aria-labelledby="join-title" data-community-join>
|
||||
<div class="join-card">
|
||||
<div class="join-head">
|
||||
<h2 id="join-title"><span class="l-en">Join the community</span><span class="l-zh">加入社区</span></h2>
|
||||
<p><span class="l-en">Get setup help, share workflows, and talk to other Reasonix users.</span><span class="l-zh">安装答疑、分享工作流,和其他 Reasonix 用户交流。</span></p>
|
||||
</div>
|
||||
<div class="join-body">
|
||||
<div class="join-qq">
|
||||
<a class="join-qr" href={QQ_GROUP.joinUrl} target="_blank" rel="noreferrer" data-qq-join>
|
||||
<img src={`${base}${QQ_GROUP.qrPath}`} width="132" height="132" alt="QQ group QR code" loading="lazy" decoding="async" />
|
||||
</a>
|
||||
<div class="join-info">
|
||||
<span class="join-label">QQ</span>
|
||||
<strong class="join-name">{QQ_GROUP.name}</strong>
|
||||
<span class="join-num"><span class="l-en">Group</span><span class="l-zh">群号</span> <code>{QQ_GROUP.number}</code></span>
|
||||
<a class="join-link" href={QQ_GROUP.joinUrl} target="_blank" rel="noreferrer"><span class="l-en">Join the QQ group</span><span class="l-zh">加入 QQ 群</span> ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="join-discord">
|
||||
<span class="join-label">Discord</span>
|
||||
<span class="join-note"><span class="l-en">Bilingual server with help channels and feature ideas.</span><span class="l-zh">双语服务器,有答疑频道和功能讨论。</span></span>
|
||||
<a class="join-link" href={DISCORD_URL} target="_blank" rel="noreferrer" data-discord-join><span class="l-en">Join Discord</span><span class="l-zh">加入 Discord</span> ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
<details class="join-minor" data-douyin>
|
||||
<summary><span class="l-en">Douyin</span><span class="l-zh">抖音</span> · <span class="l-en">ID</span><span class="l-zh">抖音号</span> <code>{DOUYIN.id}</code> <span class="join-minor-name">{DOUYIN.name}</span></summary>
|
||||
<img src={`${base}${DOUYIN.qrPath}`} width="120" height="179" alt="Douyin QR code" loading="lazy" decoding="async" />
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.join { max-width: 1180px; margin: 0 auto; padding: 36px 40px 8px; }
|
||||
.join-card {
|
||||
background: var(--bg-soft); border: 1px solid var(--line); border-radius: var(--radius);
|
||||
padding: 24px 28px; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 28px 40px; align-items: center;
|
||||
}
|
||||
.join-head h2 { font-size: 18px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink); margin: 0 0 6px; }
|
||||
.join-head p { font-size: 13.5px; line-height: 1.6; color: var(--ink-2); margin: 0; max-width: 34ch; }
|
||||
.join-body { display: flex; gap: 32px; align-items: center; flex-wrap: wrap; }
|
||||
.join-qq { display: flex; gap: 18px; align-items: center; }
|
||||
.join-qr { display: block; line-height: 0; border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; background: #fff; flex: none; }
|
||||
.join-qr img { width: 132px; height: 132px; display: block; }
|
||||
.join-minor { grid-column: 1 / -1; font-size: 12.5px; color: var(--ink-3); }
|
||||
.join-minor summary { cursor: pointer; width: fit-content; }
|
||||
.join-minor code { font-family: var(--font-mono); }
|
||||
.join-minor img { display: block; margin-top: 10px; border-radius: var(--radius-sm); border: 1px solid var(--line); }
|
||||
.join-info, .join-discord { display: flex; flex-direction: column; gap: 5px; align-items: flex-start; font-size: 13.5px; color: var(--ink-2); }
|
||||
.join-label { font-family: var(--font-mono); font-size: 11.5px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-3); }
|
||||
.join-name { font-size: 15px; font-weight: 600; color: var(--ink); }
|
||||
.join-num code { font-family: var(--font-mono); font-size: 14px; color: var(--ink); }
|
||||
.join-note { max-width: 24ch; line-height: 1.5; }
|
||||
.join-link { color: var(--accent-deep); font-weight: 600; text-decoration: none; margin-top: 4px; }
|
||||
.join-link:hover { text-decoration: underline; }
|
||||
@media (max-width: 900px) {
|
||||
.join { padding: 28px 24px 4px; }
|
||||
.join-card { grid-template-columns: 1fr; }
|
||||
.join-head p { max-width: none; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.join { padding: 24px 16px 4px; }
|
||||
.join-card { padding: 20px 18px; }
|
||||
.join-qq { flex-direction: column; align-items: flex-start; }
|
||||
.join-body { gap: 22px; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
interface Props {
|
||||
id: string;
|
||||
name?: string;
|
||||
autocomplete: string;
|
||||
minlength?: number;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const { id, name, autocomplete, minlength, required = false } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="password-input">
|
||||
<input
|
||||
id={id}
|
||||
name={name}
|
||||
type="password"
|
||||
autocomplete={autocomplete}
|
||||
minlength={minlength}
|
||||
required={required}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="password-toggle"
|
||||
data-password-toggle
|
||||
aria-controls={id}
|
||||
aria-pressed="false"
|
||||
aria-label="Show password / 显示密码">
|
||||
<svg class="password-eye-open" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" />
|
||||
<circle cx="12" cy="12" r="2.7" />
|
||||
</svg>
|
||||
<svg class="password-eye-closed" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M3 3l18 18M10.6 6.2A10.9 10.9 0 0 1 12 6c6 0 9.5 6 9.5 6a17 17 0 0 1-2.1 2.7M6.2 6.2C3.8 7.8 2.5 12 2.5 12s3.5 6 9.5 6a9.8 9.8 0 0 0 3.2-.5M9.9 9.9a3 3 0 0 0 4.2 4.2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,129 +0,0 @@
|
||||
---
|
||||
type ActiveItem = 'features' | 'surfaces' | 'studio' | 'skills' | 'community' | 'docs' | 'changelog';
|
||||
|
||||
const {
|
||||
active,
|
||||
scrolled = false,
|
||||
} = Astro.props as {
|
||||
active?: ActiveItem;
|
||||
scrolled?: boolean;
|
||||
};
|
||||
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const repo = 'https://github.com/esengine/DeepSeek-Reasonix';
|
||||
const items: Array<{
|
||||
id: ActiveItem;
|
||||
href: string;
|
||||
en: string;
|
||||
zh: string;
|
||||
secondary?: boolean;
|
||||
}> = [
|
||||
{ id: 'features', href: `${base}/#features`, en: 'Features', zh: '特性' },
|
||||
{ id: 'surfaces', href: `${base}/#surfaces`, en: 'Surfaces', zh: '使用形态', secondary: true },
|
||||
{ id: 'studio', href: `${base}/remote/`, en: 'Web Studio', zh: '网页版 Studio' },
|
||||
{ id: 'skills', href: `${base}/skills/`, en: 'Skills', zh: '技能市场' },
|
||||
{ id: 'community', href: `${base}/#community`, en: 'Community', zh: '社区', secondary: true },
|
||||
{ id: 'docs', href: `${base}/docs/`, en: 'Docs', zh: '文档' },
|
||||
{ id: 'changelog', href: `${base}/changelog/`, en: 'Changelog', zh: '更新日志' },
|
||||
];
|
||||
---
|
||||
|
||||
<header class:list={['nav', { scrolled }]}>
|
||||
<div class="nav-inner">
|
||||
<a class="brand" href={`${base}/`}>
|
||||
<picture class="brand-picture">
|
||||
<source media="(max-width: 400px)" srcset={`${base}/favicon.svg`} />
|
||||
<img src={`${base}/logo.svg`} alt="Reasonix logo" />
|
||||
</picture>
|
||||
</a>
|
||||
<nav class="nav-links" aria-label="Primary navigation">
|
||||
{items.map((item) => (
|
||||
<a
|
||||
class:list={[{ here: active === item.id, 'nav-link--secondary': item.secondary }]}
|
||||
href={item.href}
|
||||
aria-current={active === item.id ? 'page' : undefined}
|
||||
>
|
||||
<span class="l-en">{item.en}</span>
|
||||
<span class="l-zh">{item.zh}</span>
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
<a
|
||||
class="nav-github"
|
||||
href={repo}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="Reasonix GitHub repository"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12 .7a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2.3c-3.3.7-4-1.4-4-1.4-.5-1.4-1.3-1.8-1.3-1.8-1.1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.6-.3-5.4-1.3-5.4-5.9 0-1.3.5-2.4 1.2-3.2-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.3 1.2a11.5 11.5 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.6.3 2.8.1 3.1.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.4 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .7Z" />
|
||||
</svg>
|
||||
<span class="nav-github-label">GitHub</span>
|
||||
</a>
|
||||
<div class="lang-switch" role="group" aria-label="Language">
|
||||
<button type="button" data-lang="en" class="active">EN</button>
|
||||
<button type="button" data-lang="zh">中文</button>
|
||||
</div>
|
||||
<div class="theme-switch" role="group" aria-label="Theme">
|
||||
<button type="button" data-theme="system" class="active" aria-label="System theme"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></button>
|
||||
<button type="button" data-theme="light" aria-label="Light theme"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><line x1="12" y1="1" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="23"/><line x1="4.2" y1="4.2" x2="6.3" y2="6.3"/><line x1="17.7" y1="17.7" x2="19.8" y2="19.8"/><line x1="1" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="23" y2="12"/><line x1="4.2" y1="19.8" x2="6.3" y2="17.7"/><line x1="17.7" y1="6.3" x2="19.8" y2="4.2"/></svg></button>
|
||||
<button type="button" data-theme="dark" aria-label="Dark theme"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg></button>
|
||||
</div>
|
||||
<span class="nav-acct nav-sign-in" data-account-slot aria-busy="true">
|
||||
<a class="btn btn-ghost" href={`${base}/login/`}>
|
||||
<span class="l-en">Sign in</span>
|
||||
<span class="l-zh">登录</span>
|
||||
</a>
|
||||
</span>
|
||||
<a class="btn btn-dark nav-install" href={`${base}/#start`}>
|
||||
<span class="l-en">Install</span>
|
||||
<span class="l-zh">安装</span>
|
||||
</a>
|
||||
<button
|
||||
class="nav-menu-toggle"
|
||||
type="button"
|
||||
data-mobile-nav-toggle
|
||||
aria-controls="site-mobile-nav"
|
||||
aria-expanded="false"
|
||||
aria-label="Open navigation"
|
||||
title="Open navigation"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mobile-nav" id="site-mobile-nav" data-mobile-nav hidden>
|
||||
<button class="mobile-nav-backdrop" type="button" data-mobile-nav-close aria-label="Close navigation"></button>
|
||||
<aside class="mobile-nav-panel" role="dialog" aria-modal="true" aria-labelledby="site-mobile-nav-title">
|
||||
<div class="mobile-nav-head">
|
||||
<span id="site-mobile-nav-title"><span class="l-en">Navigation</span><span class="l-zh">导航</span></span>
|
||||
<button class="mobile-nav-close" type="button" data-mobile-nav-close aria-label="Close navigation" title="Close navigation">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="mobile-nav-links" aria-label="Mobile primary navigation">
|
||||
{items.map((item) => (
|
||||
<a class:list={[{ here: active === item.id }]} href={item.href} aria-current={active === item.id ? 'page' : undefined}>
|
||||
<span class="l-en">{item.en}</span>
|
||||
<span class="l-zh">{item.zh}</span>
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
<div class="mobile-nav-actions">
|
||||
<a class="mobile-nav-github" href={repo} target="_blank" rel="noreferrer">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12 .7a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2.3c-3.3.7-4-1.4-4-1.4-.5-1.4-1.3-1.8-1.3-1.8-1.1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.6-.3-5.4-1.3-5.4-5.9 0-1.3.5-2.4 1.2-3.2-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.3 1.2a11.5 11.5 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.6.3 2.8.1 3.1.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.4 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .7Z" /></svg>
|
||||
GitHub
|
||||
</a>
|
||||
<span class="nav-acct" data-account-slot aria-busy="true">
|
||||
<a class="btn btn-ghost" href={`${base}/login/`}><span class="l-en">Sign in</span><span class="l-zh">登录</span></a>
|
||||
</span>
|
||||
<a class="btn btn-dark" href={`${base}/#start`}><span class="l-en">Install</span><span class="l-zh">安装</span></a>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script>
|
||||
import '../scripts/account-nav.js';
|
||||
</script>
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
const href = "https://www.packyapi.ai/register?aff=rIbQ";
|
||||
const rightCodeHref = "https://www.rightapi.ai/register?aff=e083bf73";
|
||||
---
|
||||
<div class="foot-sponsor">
|
||||
<span class="sp-label"><span class="l-en">Sponsor</span><span class="l-zh">赞助商</span></span>
|
||||
<a class="sp-link" href={rightCodeHref} target="_blank" rel="sponsored noopener noreferrer" aria-label="Right Code">
|
||||
<img src="/sponsor/rightcode.png" alt="" width="18" height="18" />
|
||||
<span class="sp-name">Right Code</span>
|
||||
</a>
|
||||
<a class="sp-link" href={href} target="_blank" rel="sponsored noopener noreferrer" aria-label="PackyCode">
|
||||
<svg viewBox="0 0 145.55 113.29" aria-hidden="true" focusable="false">
|
||||
<path d="M144.68,38.49l-.06-.23c-.88-3.28-2.5-5.94-4.58-8.06.14,5.65-2.96,11.02-6.22,16.66l-.39.68c-2.26,3.91-4.66,7.94-6.57,11.1l-.86,1.38c-3.36,5.44-6.27,10.14-12.18,14.18-8.34,5.87-18.2,5.81-26.92,5.76-2.81-.02-5.48-.03-7.95.14l-.35.02c-3.22.06-5.96,1.57-8.17,4.49l-.14.18c-.86,1.06-1.7,2.26-2.58,3.54-3.43,4.92-7.69,11.04-16.17,12.42-4.37.86-9.98.84-14.94.83h-1.95c-.52,0-1.06,0-1.61,0-6.95,0-16.08-.89-21.94-6.55-.15-.15-.3-.3-.44-.45.5,4.73,2.33,8.64,5.44,11.65,5.86,5.66,14.98,6.55,21.94,6.55.55,0,1.09,0,1.61-.01h1.93c4.96.02,10.58.04,14.96-.83,8.48-1.37,12.74-7.5,16.17-12.42.88-1.27,1.72-2.48,2.58-3.54l.14-.18c2.21-2.92,4.95-4.43,8.17-4.49l.35-.02c2.47-.17,5.13-.16,7.95-.14,8.72.05,18.58.11,26.91-5.76,5.92-4.03,8.82-8.73,12.19-14.17l.86-1.39c1.91-3.15,4.3-7.18,6.57-11.09l.39-.69c3.81-6.6,7.41-12.84,5.86-19.57ZM120.9,23.02c-.28,0-.56,0-.83,0-9.68-.19-24.03-.09-35.57-.01l-2.04.02c-.93.01-1.82.02-2.67.03-8.36.08-14.4.13-23.33,3.82l-.27.12c-10.76,4.68-16.91,12.16-22.83,21.95-5.53,8.76-12.62,20.57-16.32,26.79-.41.69-.77,1.3-1.09,1.84l-.49.84c-1.49,2.53-3.23,5.49-4.21,8.95,3.98,1.9,8.52,2.65,12.71,2.91.27-.77.64-1.56,1.07-2.38.48-.94,1.05-1.9,1.63-2.89l.49-.84c.96-1.62,2.38-3.99,4.05-6.78,3.82-6.36,8.98-14.88,13.19-21.55l.07-.11c5.02-8.31,9.2-13.45,16.91-16.81l.11-.05c6.57-2.7,10.54-2.74,18.43-2.81.87-.01,1.78-.02,2.69-.03l1.99-.02c9.17-.06,20.13-.14,29.01-.07,2.26.01,4.39.04,6.32.08h.12s.11,0,.11,0c1.25-.01,2.91.11,4.61.46,1.03.22,2.08.52,3.05.93.21-.35.41-.71.62-1.06l.39-.68c1.92-3.33,3.79-6.57,4.97-9.82-4.08-1.92-8.7-2.76-12.89-2.82Z"/>
|
||||
<path d="M115.07,11.81c-9.7-.19-24.07-.09-35.62-.01h-1.99c-.93.03-1.82.04-2.67.04-8.36.08-14.4.14-23.33,3.83l-.27.12c-10.76,4.67-16.91,12.16-22.83,21.95-6.13,9.71-14.19,23.21-17.41,28.63l-.5.84c-2.08,3.55-4.68,7.97-4.94,13.39v.2s0,.21,0,.21c.01.81.06,1.61.15,2.38.14.15.29.3.44.45,1.53,1.47,3.28,2.63,5.15,3.52,3.98,1.9,8.52,2.65,12.71,2.91,1.41.09,2.78.12,4.08.12.55,0,1.09-.01,1.61-.01h1.95c4.95.01,10.57.03,14.94-.83,8.48-1.38,12.74-7.5,16.17-12.42.88-1.28,1.72-2.48,2.58-3.54l.14-.18c2.21-2.92,4.95-4.44,8.17-4.49l.35-.02c2.47-.17,5.14-.16,7.95-.14,8.71.05,18.58.1,26.92-5.76,5.91-4.04,8.82-8.74,12.18-14.18l.86-1.39c1.74-2.86,3.87-6.45,5.95-10.03.21-.35.41-.71.62-1.06l.39-.68c1.92-3.33,3.79-6.57,4.97-9.82.82-2.26,1.31-4.53,1.25-6.84-5.18-5.29-13.22-7.25-19.97-7.19ZM122.56,40.37l-.39.67c-2.21,3.81-4.55,7.77-6.39,10.79l-.84,1.36c-3.01,4.87-4.83,7.81-8.48,10.29l-.1.07c-4.94,3.49-11.95,3.45-19.38,3.41-2.88-.02-5.87-.04-8.79.16-7.1.19-13.51,3.58-18.07,9.57-1.13,1.4-2.12,2.83-3.08,4.21-2.92,4.18-4.71,6.57-7.64,7.02l-.31.06c-3.09.63-8.28.61-12.45.6h-2.16c-3.85.07-7.01-.16-9.45-.69-2.24-.48-3.87-1.22-4.89-2.2-.66-.64-1.54-1.81-1.63-4.65.11-1.35.71-2.82,1.52-4.35.48-.94,1.05-1.9,1.63-2.89l.49-.84c3.17-5.33,11.19-18.75,17.24-28.33l.07-.11c5.02-8.32,9.2-13.45,16.92-16.81l.1-.05c6.57-2.7,10.54-2.74,18.43-2.82.87,0,1.78,0,2.69-.03h1.94c11.52-.09,25.86-.19,35.38,0h.23c1.25-.01,2.92.11,4.61.46,3.15.66,6.4,2.12,7.27,5.02.2,1.2-.89,3.62-2.27,6.18-.7,1.31-1.48,2.65-2.2,3.9ZM120.07,23.01c-9.68-.19-24.03-.09-35.57-.01l-2.04.02c-.93.01-1.82.02-2.67.03-8.36.08-14.4.13-23.33,3.82l-.27.12c-10.76,4.68-16.91,12.16-22.83,21.95-5.53,8.76-12.62,20.57-16.32,26.79-.78-.35-1.41-.77-1.9-1.24-.66-.64-1.54-1.81-1.63-4.65.18-2.18,1.62-4.64,3.15-7.25l.49-.83c3.17-5.32,11.18-18.73,17.24-28.33l.07-.12c5.02-8.31,9.2-13.45,16.92-16.81l.1-.04c6.57-2.7,10.54-2.74,18.43-2.82.87-.01,1.78-.01,2.69-.03h1.99c11.5-.09,25.82-.19,35.33,0h.23c3.57-.04,10.55,1.02,11.88,5.48.14.84-.35,2.27-1.13,3.93-.28,0-.56,0-.83,0Z"/>
|
||||
<path d="M134.68,16.09l-.06-.23c-3.07-11.45-15.08-15.33-24.55-15.25-9.68-.19-24.03-.09-35.57-.01h-2.04c-.93.03-1.82.04-2.67.04-8.36.08-14.4.14-23.33,3.83l-.27.12c-10.76,4.67-16.91,12.16-22.83,21.95-6.14,9.73-14.2,23.22-17.41,28.62l-.49.85c-2.09,3.55-4.69,7.96-4.95,13.39v.2s0,.21,0,.21c.09,5.57,1.82,10.13,5.15,13.58.14.15.29.3.44.45,1.53,1.47,3.28,2.63,5.15,3.52,3.98,1.9,8.52,2.65,12.71,2.91,1.41.09,2.78.12,4.08.12.55,0,1.09-.01,1.61-.01h1.95c4.95.01,10.57.03,14.94-.83,8.48-1.38,12.74-7.5,16.17-12.42.88-1.28,1.72-2.48,2.58-3.54l.14-.18c2.21-2.92,4.95-4.44,8.17-4.49l.35-.02c2.47-.17,5.14-.16,7.95-.14,8.71.05,18.58.1,26.92-5.76,5.91-4.04,8.82-8.74,12.18-14.18l.86-1.39c1.74-2.86,3.87-6.45,5.95-10.03.21-.35.41-.71.62-1.06l.39-.68c1.92-3.33,3.79-6.57,4.97-9.82.82-2.26,1.31-4.53,1.25-6.84-.02-.96-.14-1.93-.36-2.91ZM119.76,25.27c-.7,1.31-1.48,2.65-2.2,3.9l-.39.67c-1.19,2.04-2.41,4.13-3.57,6.09-1.01,1.7-1.97,3.31-2.82,4.7l-.84,1.36c-3.01,4.87-4.83,7.81-8.48,10.29l-.1.07c-4.94,3.49-11.96,3.45-19.38,3.41-2.89-.02-5.87-.04-8.79.16-7.1.18-13.51,3.58-18.07,9.57-1.13,1.4-2.12,2.83-3.08,4.21-2.92,4.18-4.71,6.57-7.64,7.02l-.31.06c-3.09.63-8.27.61-12.45.6h-2.16c-3.85.07-7.01-.16-9.45-.69-1.16-.25-2.16-.57-2.99-.96-.78-.35-1.41-.77-1.9-1.24-.66-.64-1.54-1.81-1.63-4.65.18-2.18,1.62-4.64,3.15-7.25l.49-.83c3.17-5.32,11.18-18.73,17.24-28.33l.07-.12c5.02-8.31,9.2-13.45,16.92-16.81l.1-.04c6.57-2.7,10.54-2.74,18.43-2.82.87-.01,1.78-.01,2.69-.03h1.99c11.5-.09,25.82-.19,35.33,0h.23c3.57-.04,10.55,1.02,11.88,5.48.14.84-.35,2.27-1.13,3.93-.34.72-.73,1.48-1.14,2.25Z"/>
|
||||
</svg>
|
||||
<span class="sp-name">PackyCode</span>
|
||||
</a>
|
||||
</div>
|
||||
<style>
|
||||
.foot-sponsor {
|
||||
max-width: 1180px; margin: 0 auto; padding: 14px 40px 22px;
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px;
|
||||
border-top: 1px solid var(--line);
|
||||
font-size: 13px; color: var(--ink-3);
|
||||
}
|
||||
.sp-link { display: inline-flex; align-items: center; gap: 8px; color: var(--ink-2); text-decoration: none; font-weight: 600; transition: color 0.2s; }
|
||||
.sp-link:hover { color: var(--ink); }
|
||||
.sp-link img { height: 18px; width: 18px; display: block; }
|
||||
.sp-link svg { height: 18px; width: auto; fill: currentColor; stroke: currentColor; stroke-miterlimit: 10; }
|
||||
</style>
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
import ChangelogPage from './ChangelogPage.astro';
|
||||
import Base from '../layouts/Base.astro';
|
||||
import SiteHeader from './SiteHeader.astro';
|
||||
import { releasePath, studioReleases } from '../data/releases';
|
||||
import '../styles/changelog.css';
|
||||
const latest = studioReleases[0];
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
---
|
||||
{latest ? <ChangelogPage release={latest} releases={studioReleases} canonical={new URL(`${base}${releasePath(latest.version)}`, Astro.site).href} /> : (
|
||||
<Base title="Reasonix Studio release notes" titleEn="Reasonix Studio release notes" titleZh="Reasonix Studio 更新日志" description="Reasonix Studio release notes">
|
||||
<SiteHeader active="changelog" scrolled />
|
||||
<main class="release-stage">
|
||||
<nav class="release-product-tabs" aria-label="Products">
|
||||
<a href={`${base}/changelog/cli/`}><span class="l-en">CLI · 1.x (previous)</span><span class="l-zh">CLI · 1.x(上一代)</span></a>
|
||||
<a href={`${base}/changelog/`} aria-current="page">Studio · 2.x</a>
|
||||
</nav>
|
||||
<article class="release-paper release-studio-empty">
|
||||
<h1>Reasonix Studio</h1>
|
||||
<p><span class="l-en">Website notes are awaiting review. Published releases remain available on GitHub.</span><span class="l-zh">网站发布说明正在等待审核。已发布版本可在 GitHub 查看。</span></p>
|
||||
<a href="https://github.com/esengine/DeepSeek-Reasonix/releases">GitHub ↗</a>
|
||||
</article>
|
||||
</main>
|
||||
</Base>
|
||||
)}
|
||||
@@ -1,595 +0,0 @@
|
||||
{
|
||||
"stars": 29222,
|
||||
"mergedPrs": 2816,
|
||||
"contributors": 98,
|
||||
"list": [
|
||||
{
|
||||
"login": "SivanCola",
|
||||
"avatar": "/av/SivanCola.webp",
|
||||
"url": "https://github.com/SivanCola",
|
||||
"c": 2311
|
||||
},
|
||||
{
|
||||
"login": "esengine",
|
||||
"avatar": "/av/esengine.webp",
|
||||
"url": "https://github.com/esengine",
|
||||
"c": 518
|
||||
},
|
||||
{
|
||||
"login": "ttmouse",
|
||||
"avatar": "/av/ttmouse.webp",
|
||||
"url": "https://github.com/ttmouse",
|
||||
"c": 121
|
||||
},
|
||||
{
|
||||
"login": "lifu963",
|
||||
"avatar": "/av/lifu963.webp",
|
||||
"url": "https://github.com/lifu963",
|
||||
"c": 105
|
||||
},
|
||||
{
|
||||
"login": "HUQIANTAO",
|
||||
"avatar": "/av/HUQIANTAO.webp",
|
||||
"url": "https://github.com/HUQIANTAO",
|
||||
"c": 74
|
||||
},
|
||||
{
|
||||
"login": "GTC2080",
|
||||
"avatar": "/av/GTC2080.webp",
|
||||
"url": "https://github.com/GTC2080",
|
||||
"c": 61
|
||||
},
|
||||
{
|
||||
"login": "light-front-theory",
|
||||
"avatar": "/av/light-front-theory.webp",
|
||||
"url": "https://github.com/light-front-theory",
|
||||
"c": 56
|
||||
},
|
||||
{
|
||||
"login": "Li-Charles-One",
|
||||
"avatar": "/av/Li-Charles-One.webp",
|
||||
"url": "https://github.com/Li-Charles-One",
|
||||
"c": 49
|
||||
},
|
||||
{
|
||||
"login": "eghrhegpe",
|
||||
"avatar": "/av/eghrhegpe.webp",
|
||||
"url": "https://github.com/eghrhegpe",
|
||||
"c": 28
|
||||
},
|
||||
{
|
||||
"login": "CVEngineer66",
|
||||
"avatar": "/av/CVEngineer66.webp",
|
||||
"url": "https://github.com/CVEngineer66",
|
||||
"c": 16
|
||||
},
|
||||
{
|
||||
"login": "JesonChou",
|
||||
"avatar": "/av/JesonChou.webp",
|
||||
"url": "https://github.com/JesonChou",
|
||||
"c": 15
|
||||
},
|
||||
{
|
||||
"login": "HaoyueQin",
|
||||
"avatar": "/av/HaoyueQin.webp",
|
||||
"url": "https://github.com/HaoyueQin",
|
||||
"c": 14
|
||||
},
|
||||
{
|
||||
"login": "SuMuxi66",
|
||||
"avatar": "/av/SuMuxi66.webp",
|
||||
"url": "https://github.com/SuMuxi66",
|
||||
"c": 11
|
||||
},
|
||||
{
|
||||
"login": "lanshi17",
|
||||
"avatar": "/av/lanshi17.webp",
|
||||
"url": "https://github.com/lanshi17",
|
||||
"c": 11
|
||||
},
|
||||
{
|
||||
"login": "CnsMaple",
|
||||
"avatar": "/av/CnsMaple.webp",
|
||||
"url": "https://github.com/CnsMaple",
|
||||
"c": 9
|
||||
},
|
||||
{
|
||||
"login": "cyq1017",
|
||||
"avatar": "/av/cyq1017.webp",
|
||||
"url": "https://github.com/cyq1017",
|
||||
"c": 9
|
||||
},
|
||||
{
|
||||
"login": "ipanta",
|
||||
"avatar": "/av/ipanta.webp",
|
||||
"url": "https://github.com/ipanta",
|
||||
"c": 8
|
||||
},
|
||||
{
|
||||
"login": "XTLine",
|
||||
"avatar": "/av/XTLine.webp",
|
||||
"url": "https://github.com/XTLine",
|
||||
"c": 8
|
||||
},
|
||||
{
|
||||
"login": "YoungDan-hero",
|
||||
"avatar": "/av/YoungDan-hero.webp",
|
||||
"url": "https://github.com/YoungDan-hero",
|
||||
"c": 8
|
||||
},
|
||||
{
|
||||
"login": "paradoxSCH",
|
||||
"avatar": "/av/paradoxSCH.webp",
|
||||
"url": "https://github.com/paradoxSCH",
|
||||
"c": 8
|
||||
},
|
||||
{
|
||||
"login": "wade19990814-hue",
|
||||
"avatar": "/av/wade19990814-hue.webp",
|
||||
"url": "https://github.com/wade19990814-hue",
|
||||
"c": 7
|
||||
},
|
||||
{
|
||||
"login": "SauronSkywalker",
|
||||
"avatar": "/av/SauronSkywalker.webp",
|
||||
"url": "https://github.com/SauronSkywalker",
|
||||
"c": 7
|
||||
},
|
||||
{
|
||||
"login": "yizeyi18",
|
||||
"avatar": "/av/yizeyi18.webp",
|
||||
"url": "https://github.com/yizeyi18",
|
||||
"c": 6
|
||||
},
|
||||
{
|
||||
"login": "ashishexee",
|
||||
"avatar": "/av/ashishexee.webp",
|
||||
"url": "https://github.com/ashishexee",
|
||||
"c": 6
|
||||
},
|
||||
{
|
||||
"login": "erphenheimer",
|
||||
"avatar": "/av/erphenheimer.webp",
|
||||
"url": "https://github.com/erphenheimer",
|
||||
"c": 6
|
||||
},
|
||||
{
|
||||
"login": "Bernardxu123",
|
||||
"avatar": "/av/Bernardxu123.webp",
|
||||
"url": "https://github.com/Bernardxu123",
|
||||
"c": 6
|
||||
},
|
||||
{
|
||||
"login": "lizhengwu",
|
||||
"avatar": "/av/lizhengwu.webp",
|
||||
"url": "https://github.com/lizhengwu",
|
||||
"c": 6
|
||||
},
|
||||
{
|
||||
"login": "qiaone",
|
||||
"avatar": "/av/qiaone.webp",
|
||||
"url": "https://github.com/qiaone",
|
||||
"c": 5
|
||||
},
|
||||
{
|
||||
"login": "mchenziyi",
|
||||
"avatar": "/av/mchenziyi.webp",
|
||||
"url": "https://github.com/mchenziyi",
|
||||
"c": 4
|
||||
},
|
||||
{
|
||||
"login": "ferstar",
|
||||
"avatar": "/av/ferstar.webp",
|
||||
"url": "https://github.com/ferstar",
|
||||
"c": 4
|
||||
},
|
||||
{
|
||||
"login": "zhangsanouo",
|
||||
"avatar": "/av/zhangsanouo.webp",
|
||||
"url": "https://github.com/zhangsanouo",
|
||||
"c": 4
|
||||
},
|
||||
{
|
||||
"login": "par73e",
|
||||
"avatar": "/av/par73e.webp",
|
||||
"url": "https://github.com/par73e",
|
||||
"c": 4
|
||||
},
|
||||
{
|
||||
"login": "liaoyl830",
|
||||
"avatar": "/av/liaoyl830.webp",
|
||||
"url": "https://github.com/liaoyl830",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "jiaxindeyang-a11y",
|
||||
"avatar": "/av/jiaxindeyang-a11y.webp",
|
||||
"url": "https://github.com/jiaxindeyang-a11y",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "nostalgia296",
|
||||
"avatar": "/av/nostalgia296.webp",
|
||||
"url": "https://github.com/nostalgia296",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "Diarica",
|
||||
"avatar": "/av/Diarica.webp",
|
||||
"url": "https://github.com/Diarica",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "yekern",
|
||||
"avatar": "/av/yekern.webp",
|
||||
"url": "https://github.com/yekern",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "maoxin1234",
|
||||
"avatar": "/av/maoxin1234.webp",
|
||||
"url": "https://github.com/maoxin1234",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "lightfront",
|
||||
"avatar": "/av/lightfront.webp",
|
||||
"url": "https://github.com/lightfront",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "ikelvingo",
|
||||
"avatar": "/av/ikelvingo.webp",
|
||||
"url": "https://github.com/ikelvingo",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "ffff2004",
|
||||
"avatar": "/av/ffff2004.webp",
|
||||
"url": "https://github.com/ffff2004",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "kekexunxun",
|
||||
"avatar": "/av/kekexunxun.webp",
|
||||
"url": "https://github.com/kekexunxun",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "Tatlatat",
|
||||
"avatar": "/av/Tatlatat.webp",
|
||||
"url": "https://github.com/Tatlatat",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "PaulDing98",
|
||||
"avatar": "/av/PaulDing98.webp",
|
||||
"url": "https://github.com/PaulDing98",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "AtmoOmen",
|
||||
"avatar": "/av/AtmoOmen.webp",
|
||||
"url": "https://github.com/AtmoOmen",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"login": "billycat",
|
||||
"avatar": "/av/billycat.webp",
|
||||
"url": "https://github.com/billycat",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "xianyu-sheng",
|
||||
"avatar": "/av/xianyu-sheng.webp",
|
||||
"url": "https://github.com/xianyu-sheng",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "heshuimu",
|
||||
"avatar": "/av/heshuimu.webp",
|
||||
"url": "https://github.com/heshuimu",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "drafish",
|
||||
"avatar": "/av/drafish.webp",
|
||||
"url": "https://github.com/drafish",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "yhemeo613",
|
||||
"avatar": "/av/yhemeo613.webp",
|
||||
"url": "https://github.com/yhemeo613",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "wqshan-cn",
|
||||
"avatar": "/av/wqshan-cn.webp",
|
||||
"url": "https://github.com/wqshan-cn",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "warvyvr",
|
||||
"avatar": "/av/warvyvr.webp",
|
||||
"url": "https://github.com/warvyvr",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "Xio-Shark",
|
||||
"avatar": "/av/Xio-Shark.webp",
|
||||
"url": "https://github.com/Xio-Shark",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "LVGG",
|
||||
"avatar": "/av/LVGG.webp",
|
||||
"url": "https://github.com/LVGG",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "assoyuan",
|
||||
"avatar": "/av/assoyuan.webp",
|
||||
"url": "https://github.com/assoyuan",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "closss",
|
||||
"avatar": "/av/closss.webp",
|
||||
"url": "https://github.com/closss",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "iysun",
|
||||
"avatar": "/av/iysun.webp",
|
||||
"url": "https://github.com/iysun",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "whale-fall-ouo",
|
||||
"avatar": "/av/whale-fall-ouo.webp",
|
||||
"url": "https://github.com/whale-fall-ouo",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "sudoviber",
|
||||
"avatar": "/av/sudoviber.webp",
|
||||
"url": "https://github.com/sudoviber",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "qiukeren",
|
||||
"avatar": "/av/qiukeren.webp",
|
||||
"url": "https://github.com/qiukeren",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "nanami7777777",
|
||||
"avatar": "/av/nanami7777777.webp",
|
||||
"url": "https://github.com/nanami7777777",
|
||||
"c": 2
|
||||
},
|
||||
{
|
||||
"login": "alxbnct",
|
||||
"avatar": "/av/alxbnct.webp",
|
||||
"url": "https://github.com/alxbnct",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "Zhuocheng-Lang",
|
||||
"avatar": "/av/Zhuocheng-Lang.webp",
|
||||
"url": "https://github.com/Zhuocheng-Lang",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "dcs02280653-collab",
|
||||
"avatar": "/av/dcs02280653-collab.webp",
|
||||
"url": "https://github.com/dcs02280653-collab",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "dengbushi",
|
||||
"avatar": "/av/dengbushi.webp",
|
||||
"url": "https://github.com/dengbushi",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "h4rk8s",
|
||||
"avatar": "/av/h4rk8s.webp",
|
||||
"url": "https://github.com/h4rk8s",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "myipanta",
|
||||
"avatar": "/av/myipanta.webp",
|
||||
"url": "https://github.com/myipanta",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "jackijianxa",
|
||||
"avatar": "/av/jackijianxa.webp",
|
||||
"url": "https://github.com/jackijianxa",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "jbts6",
|
||||
"avatar": "/av/jbts6.webp",
|
||||
"url": "https://github.com/jbts6",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "ji-tz",
|
||||
"avatar": "/av/ji-tz.webp",
|
||||
"url": "https://github.com/ji-tz",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "kabaka9527",
|
||||
"avatar": "/av/kabaka9527.webp",
|
||||
"url": "https://github.com/kabaka9527",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "9710willy",
|
||||
"avatar": "/av/9710willy.webp",
|
||||
"url": "https://github.com/9710willy",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "onenewcode",
|
||||
"avatar": "/av/onenewcode.webp",
|
||||
"url": "https://github.com/onenewcode",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "Ninot1Quyi",
|
||||
"avatar": "/av/Ninot1Quyi.webp",
|
||||
"url": "https://github.com/Ninot1Quyi",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "xiaoshuai1024",
|
||||
"avatar": "/av/xiaoshuai1024.webp",
|
||||
"url": "https://github.com/xiaoshuai1024",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "rixzkiye",
|
||||
"avatar": "/av/rixzkiye.webp",
|
||||
"url": "https://github.com/rixzkiye",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "taibai233",
|
||||
"avatar": "/av/taibai233.webp",
|
||||
"url": "https://github.com/taibai233",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "ying-hua",
|
||||
"avatar": "/av/ying-hua.webp",
|
||||
"url": "https://github.com/ying-hua",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "yuanchenglu",
|
||||
"avatar": "/av/yuanchenglu.webp",
|
||||
"url": "https://github.com/yuanchenglu",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "zhuguang-ZFG",
|
||||
"avatar": "/av/zhuguang-ZFG.webp",
|
||||
"url": "https://github.com/zhuguang-ZFG",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "zlwu",
|
||||
"avatar": "/av/zlwu.webp",
|
||||
"url": "https://github.com/zlwu",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "xinrenH",
|
||||
"avatar": "/av/xinrenH.webp",
|
||||
"url": "https://github.com/xinrenH",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "zdjmrq",
|
||||
"avatar": "/av/zdjmrq.webp",
|
||||
"url": "https://github.com/zdjmrq",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "i1j",
|
||||
"avatar": "/av/i1j.webp",
|
||||
"url": "https://github.com/i1j",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "Abysstsuki",
|
||||
"avatar": "/av/Abysstsuki.webp",
|
||||
"url": "https://github.com/Abysstsuki",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "themarchrain",
|
||||
"avatar": "/av/themarchrain.webp",
|
||||
"url": "https://github.com/themarchrain",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "wudiandy",
|
||||
"avatar": "/av/wudiandy.webp",
|
||||
"url": "https://github.com/wudiandy",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "EMEEEEMMMM",
|
||||
"avatar": "/av/EMEEEEMMMM.webp",
|
||||
"url": "https://github.com/EMEEEEMMMM",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "HBLADEH",
|
||||
"avatar": "/av/HBLADEH.webp",
|
||||
"url": "https://github.com/HBLADEH",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "HHHEEEWWW",
|
||||
"avatar": "/av/HHHEEEWWW.webp",
|
||||
"url": "https://github.com/HHHEEEWWW",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "HorusJiang",
|
||||
"avatar": "/av/HorusJiang.webp",
|
||||
"url": "https://github.com/HorusJiang",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "J3y0r",
|
||||
"avatar": "/av/J3y0r.webp",
|
||||
"url": "https://github.com/J3y0r",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "JimmyDaddy",
|
||||
"avatar": "/av/JimmyDaddy.webp",
|
||||
"url": "https://github.com/JimmyDaddy",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "LegGasai",
|
||||
"avatar": "/av/LegGasai.webp",
|
||||
"url": "https://github.com/LegGasai",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "gcoder1991",
|
||||
"avatar": "/av/gcoder1991.webp",
|
||||
"url": "https://github.com/gcoder1991",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "LouF666",
|
||||
"avatar": "/av/LouF666.webp",
|
||||
"url": "https://github.com/LouF666",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "MaitreyeeDeshmukh",
|
||||
"avatar": "/av/MaitreyeeDeshmukh.webp",
|
||||
"url": "https://github.com/MaitreyeeDeshmukh",
|
||||
"c": 1
|
||||
},
|
||||
{
|
||||
"login": "ERroooooR",
|
||||
"avatar": "/av/ERroooooR.webp",
|
||||
"url": "https://github.com/ERroooooR",
|
||||
"c": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"stars": 20843,
|
||||
"mergedPrs": 1779,
|
||||
"contributors": 41,
|
||||
"list": [
|
||||
{ "login": "esengine", "avatar": "https://avatars.githubusercontent.com/u/18465053", "url": "https://github.com/esengine", "c": 310 },
|
||||
{ "login": "SivanCola", "avatar": "https://avatars.githubusercontent.com/u/32437197", "url": "https://github.com/SivanCola", "c": 158 },
|
||||
{ "login": "HUQIANTAO", "avatar": "https://avatars.githubusercontent.com/u/58421104", "url": "https://github.com/HUQIANTAO", "c": 73 },
|
||||
{ "login": "GTC2080", "avatar": "https://avatars.githubusercontent.com/u/140309575", "url": "https://github.com/GTC2080", "c": 16 },
|
||||
{ "login": "CVEngineer66", "avatar": "https://avatars.githubusercontent.com/u/129239603", "url": "https://github.com/CVEngineer66", "c": 10 },
|
||||
{ "login": "Li-Charles-One", "avatar": "https://avatars.githubusercontent.com/u/258708209", "url": "https://github.com/Li-Charles-One", "c": 10 },
|
||||
{ "login": "SuMuxi66", "avatar": "https://avatars.githubusercontent.com/u/96619914", "url": "https://github.com/SuMuxi66", "c": 9 },
|
||||
{ "login": "lanshi17", "avatar": "https://avatars.githubusercontent.com/u/94416777", "url": "https://github.com/lanshi17", "c": 9 },
|
||||
{ "login": "paradoxSCH", "avatar": "https://avatars.githubusercontent.com/u/64133628", "url": "https://github.com/paradoxSCH", "c": 7 },
|
||||
{ "login": "wade19990814-hue", "avatar": "https://avatars.githubusercontent.com/u/280641290", "url": "https://github.com/wade19990814-hue", "c": 7 },
|
||||
{ "login": "YoungDan-hero", "avatar": "https://avatars.githubusercontent.com/u/66891653", "url": "https://github.com/YoungDan-hero", "c": 6 },
|
||||
{ "login": "lizhengwu", "avatar": "https://avatars.githubusercontent.com/u/38285521", "url": "https://github.com/lizhengwu", "c": 5 },
|
||||
{ "login": "Bernardxu123", "avatar": "https://avatars.githubusercontent.com/u/85089406", "url": "https://github.com/Bernardxu123", "c": 5 },
|
||||
{ "login": "CnsMaple", "avatar": "https://avatars.githubusercontent.com/u/92523839", "url": "https://github.com/CnsMaple", "c": 5 },
|
||||
{ "login": "AtmoOmen", "avatar": "https://avatars.githubusercontent.com/u/110901588", "url": "https://github.com/AtmoOmen", "c": 3 },
|
||||
{ "login": "Tatlatat", "avatar": "https://avatars.githubusercontent.com/u/269959336", "url": "https://github.com/Tatlatat", "c": 3 },
|
||||
{ "login": "kekexunxun", "avatar": "https://avatars.githubusercontent.com/u/44473551", "url": "https://github.com/kekexunxun", "c": 3 },
|
||||
{ "login": "jiaxindeyang-a11y", "avatar": "https://avatars.githubusercontent.com/u/278341870", "url": "https://github.com/jiaxindeyang-a11y", "c": 3 },
|
||||
{ "login": "nostalgia296", "avatar": "https://avatars.githubusercontent.com/u/207565364", "url": "https://github.com/nostalgia296", "c": 2 },
|
||||
{ "login": "yhemeo613", "avatar": "https://avatars.githubusercontent.com/u/138780695", "url": "https://github.com/yhemeo613", "c": 2 },
|
||||
{ "login": "ttmouse", "avatar": "https://avatars.githubusercontent.com/u/8982044", "url": "https://github.com/ttmouse", "c": 2 },
|
||||
{ "login": "iysun", "avatar": "https://avatars.githubusercontent.com/u/102790274", "url": "https://github.com/iysun", "c": 2 },
|
||||
{ "login": "closss", "avatar": "https://avatars.githubusercontent.com/u/106432698", "url": "https://github.com/closss", "c": 2 },
|
||||
{ "login": "XTLine", "avatar": "https://avatars.githubusercontent.com/u/140948646", "url": "https://github.com/XTLine", "c": 2 },
|
||||
{ "login": "LVGG", "avatar": "https://avatars.githubusercontent.com/u/20256205", "url": "https://github.com/LVGG", "c": 2 },
|
||||
{ "login": "xinrenH", "avatar": "https://avatars.githubusercontent.com/u/121069519", "url": "https://github.com/xinrenH", "c": 1 },
|
||||
{ "login": "lightfront", "avatar": "https://avatars.githubusercontent.com/u/13162893", "url": "https://github.com/lightfront", "c": 1 },
|
||||
{ "login": "lifu963", "avatar": "https://avatars.githubusercontent.com/u/56394323", "url": "https://github.com/lifu963", "c": 1 },
|
||||
{ "login": "kabaka9527", "avatar": "https://avatars.githubusercontent.com/u/142149737", "url": "https://github.com/kabaka9527", "c": 1 },
|
||||
{ "login": "dcs02280653-collab", "avatar": "https://avatars.githubusercontent.com/u/272150807", "url": "https://github.com/dcs02280653-collab", "c": 1 },
|
||||
{ "login": "ashishexee", "avatar": "https://avatars.githubusercontent.com/u/144021866", "url": "https://github.com/ashishexee", "c": 1 },
|
||||
{ "login": "alxbnct", "avatar": "https://avatars.githubusercontent.com/u/52067352", "url": "https://github.com/alxbnct", "c": 1 },
|
||||
{ "login": "PorunC", "avatar": "https://avatars.githubusercontent.com/u/63705582", "url": "https://github.com/PorunC", "c": 1 },
|
||||
{ "login": "ERroooooR", "avatar": "https://avatars.githubusercontent.com/u/121479279", "url": "https://github.com/ERroooooR", "c": 1 },
|
||||
{ "login": "LegGasai", "avatar": "https://avatars.githubusercontent.com/u/75920107", "url": "https://github.com/LegGasai", "c": 1 },
|
||||
{ "login": "JimmyDaddy", "avatar": "https://avatars.githubusercontent.com/u/16460813", "url": "https://github.com/JimmyDaddy", "c": 1 },
|
||||
{ "login": "drafish", "avatar": "https://avatars.githubusercontent.com/u/18543527", "url": "https://github.com/drafish", "c": 1 },
|
||||
{ "login": "HorusJiang", "avatar": "https://avatars.githubusercontent.com/u/29286555", "url": "https://github.com/HorusJiang", "c": 1 },
|
||||
{ "login": "HBLADEH", "avatar": "https://avatars.githubusercontent.com/u/27012363", "url": "https://github.com/HBLADEH", "c": 1 },
|
||||
{ "login": "Nahiyi", "avatar": "https://avatars.githubusercontent.com/u/163129774", "url": "https://github.com/Nahiyi", "c": 1 },
|
||||
{ "login": "i1j", "avatar": "https://avatars.githubusercontent.com/u/35729767", "url": "https://github.com/i1j", "c": 1 }
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Set this temporarily to an exact vX.Y.Z only during a documented download
|
||||
// incident. The production default follows the current Stable manifest.
|
||||
export const desktopDownloadVersion = "";
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,91 +0,0 @@
|
||||
import source from './releases.json';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export type LocalizedText = { en: string; zh: string };
|
||||
export type ReleaseTarget = 'desktop' | 'cli' | 'site' | 'service';
|
||||
|
||||
export type ReleaseItem = {
|
||||
title: LocalizedText;
|
||||
body: LocalizedText;
|
||||
refs?: number[];
|
||||
targets?: ReleaseTarget[];
|
||||
};
|
||||
|
||||
export type ReleaseHighlight = ReleaseItem & {
|
||||
kind: 'new' | 'improved' | 'fixed' | 'security';
|
||||
};
|
||||
|
||||
export type ReleaseGuide = ReleaseItem & { href: string };
|
||||
export type ReleaseUpgrade = ReleaseItem & { level: 'info' | 'warning' };
|
||||
|
||||
export type ReleaseRecord = {
|
||||
version: string;
|
||||
product?: 'cli' | 'studio';
|
||||
sourceNotes?: { language: 'zh'; markdown: string; sha: string; path: string };
|
||||
targetingVersion?: 1;
|
||||
releaseId?: string;
|
||||
baseVersion?: string;
|
||||
date: string;
|
||||
channel: 'stable' | 'prerelease';
|
||||
status?: 'reviewed' | 'published';
|
||||
candidateSha?: string;
|
||||
previousRelease?: string;
|
||||
builds?: { cli: string; desktop: string; npm: string };
|
||||
title: LocalizedText;
|
||||
summary: LocalizedText;
|
||||
surfaces: string[];
|
||||
guides: ReleaseGuide[];
|
||||
highlights: ReleaseHighlight[];
|
||||
changes: { new: ReleaseItem[]; improved: ReleaseItem[]; fixed: ReleaseItem[] };
|
||||
upgrade: ReleaseUpgrade[];
|
||||
risks: ReleaseItem[];
|
||||
contributors: string[];
|
||||
links: { github: string; compare: string; download: string };
|
||||
};
|
||||
|
||||
export const releaseCatalog = source as { schemaVersion: number; releases: ReleaseRecord[] };
|
||||
export const allReleases = releaseCatalog.releases;
|
||||
|
||||
let publishedIds = new Set<string>();
|
||||
try {
|
||||
const generated = JSON.parse(
|
||||
readFileSync(resolve(process.cwd(), '.generated/publications.json'), 'utf8'),
|
||||
) as { publications?: string[] };
|
||||
publishedIds = new Set(generated.publications ?? []);
|
||||
} catch {
|
||||
// Before prebuild, preserve legacy entries and fail closed for reviewed
|
||||
// releases that require a successful all-surface publication marker.
|
||||
}
|
||||
|
||||
export const publishedReleases = allReleases.filter(
|
||||
(release) => !release.status || release.status === 'published' || publishedIds.has(release.version),
|
||||
);
|
||||
// Public navigation contains official releases only. Historical prereleases
|
||||
// remain addressable by their exact version URL for compatibility.
|
||||
export const studioReleases = publishedReleases.filter((release) => release.product === 'studio');
|
||||
export const releases = publishedReleases.filter((release) => release.product !== 'studio' && release.channel === 'stable');
|
||||
export const stableReleases = releases;
|
||||
export const previewReleases = publishedReleases.filter((release) => release.channel === 'prerelease');
|
||||
export const latestStableRelease = stableReleases[0];
|
||||
export const latestPreviewRelease = previewReleases[0];
|
||||
export const latestRelease = latestStableRelease;
|
||||
|
||||
export function releasePath(version: string): string {
|
||||
return `/changelog/v${version}/`;
|
||||
}
|
||||
|
||||
export function releaseContentItems(release: ReleaseRecord): ReleaseItem[] {
|
||||
return [
|
||||
...release.highlights,
|
||||
...release.changes.new,
|
||||
...release.changes.improved,
|
||||
...release.changes.fixed,
|
||||
...release.upgrade,
|
||||
...release.risks,
|
||||
];
|
||||
}
|
||||
|
||||
export function releaseTargetCount(release: ReleaseRecord, target: ReleaseTarget): number {
|
||||
return releaseContentItems(release).filter((item) => item.targets?.includes(target)).length;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
import Base from './Base.astro';
|
||||
import CommunityJoin from '../components/CommunityJoin.astro';
|
||||
import SponsorSlot from '../components/SponsorSlot.astro';
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const repo = 'https://github.com/esengine/DeepSeek-Reasonix';
|
||||
const { title, description, titleEn, titleZh, wide, dash } = Astro.props;
|
||||
---
|
||||
<Base title={title} description={description} titleEn={titleEn} titleZh={titleZh}>
|
||||
<Fragment slot="head">
|
||||
<meta name="robots" content="noindex" />
|
||||
<link rel="preconnect" href="https://id.reasonix.io" crossorigin="anonymous" />
|
||||
<link rel="preconnect" href="https://studio.reasonix.io" crossorigin="anonymous" />
|
||||
<link rel="dns-prefetch" href="//id.reasonix.io" />
|
||||
<link rel="dns-prefetch" href="//studio.reasonix.io" />
|
||||
</Fragment>
|
||||
<header class="nav auth-nav scrolled">
|
||||
<div class="nav-inner">
|
||||
<a class="brand" href={`${base}/`}><img src={`${base}/logo.svg`} alt="Reasonix logo" /><span>Reasonix</span></a>
|
||||
<div class="lang-switch" role="group" aria-label="Language" style="margin-left:auto">
|
||||
<button type="button" data-lang="en" class="active">EN</button>
|
||||
<button type="button" data-lang="zh">中文</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="auth-main">
|
||||
<div class={dash ? 'auth-card wide dash' : wide ? 'auth-card wide' : 'auth-card'}>
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<CommunityJoin />
|
||||
<div class="foot-sub" style="border-top:none">
|
||||
<a class="brand" href={`${base}/`}><img src={`${base}/logo.svg`} alt="Reasonix logo" /><span>Reasonix</span></a>
|
||||
<nav class="foot-links">
|
||||
<a href={`${base}/`}><span class="l-en">Home</span><span class="l-zh">首页</span></a>
|
||||
<a href={`${base}/docs/`}><span class="l-en">Docs</span><span class="l-zh">文档</span></a>
|
||||
<a href={repo}>GitHub</a>
|
||||
</nav>
|
||||
</div>
|
||||
<SponsorSlot />
|
||||
</footer>
|
||||
|
||||
<script>import '../scripts/auth.js';</script>
|
||||
</Base>
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const { title, description, titleEn, titleZh, canonical: canonicalOverride, noindex = false } = Astro.props;
|
||||
const canonical = canonicalOverride ?? new URL(Astro.url.pathname, Astro.site).href;
|
||||
const ogImage = new URL(`${base}/og.png`, Astro.site).href;
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
{noindex && <meta name="robots" content="noindex, nofollow" />}
|
||||
<link rel="canonical" href={canonical} />
|
||||
<link rel="icon" href={`${base}/favicon.svg`} type="image/svg+xml" />
|
||||
<link rel="sitemap" href={`${base}/sitemap-index.xml`} />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<script is:inline>
|
||||
(function(){try{var p=localStorage.getItem("reasonix-theme")||"system";var d=p==="dark"||(p==="system"&&window.matchMedia("(prefers-color-scheme: dark)").matches);document.documentElement.dataset.theme=d?"dark":"light";var m=document.querySelector('meta[name="theme-color"]');if(m)m.content=d?"#1f232b":"#ffffff";}catch(e){}})();
|
||||
</script>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Reasonix" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="Reasonix — an open-source coding agent for your terminal, desktop, browser and editor" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:locale:alternate" content="zh_CN" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
<slot name="head" />
|
||||
</head>
|
||||
<body data-motion="rich" data-lang="en" data-title-en={titleEn ?? title} data-title-zh={titleZh ?? title}>
|
||||
<slot />
|
||||
<script>
|
||||
import '../scripts/site.js';
|
||||
import '../scripts/fx.js';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
import "../styles/community.css";
|
||||
import CommunityJoin from "../components/CommunityJoin.astro";
|
||||
const { title, description, active } = Astro.props;
|
||||
const canonical = new URL(Astro.url.pathname, Astro.site).href;
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<script is:inline>
|
||||
(function(){try{var p=localStorage.getItem("reasonix-theme")||"system";var d=p==="dark"||(p==="system"&&window.matchMedia("(prefers-color-scheme: dark)").matches);document.documentElement.dataset.theme=d?"dark":"light";var m=document.querySelector('meta[name="theme-color"]');if(m)m.content=d?"#1f232b":"#ffffff";}catch(e){}})();
|
||||
</script>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Reasonix Community" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
</head>
|
||||
<body data-lang="en">
|
||||
<header class="nav">
|
||||
<div class="nav-inner">
|
||||
<a class="brand" href="/community/" aria-label="Reasonix Community"><span class="mark" aria-hidden="true">R</span><span class="brand-name">Reasonix</span><span class="ctag"><span class="l-en">Community</span><span class="l-zh">社区</span></span></a>
|
||||
<nav class="nav-links">
|
||||
<a class={active === "discussions" ? "here" : ""} href="/community/"><span class="l-en">Discussions</span><span class="l-zh">讨论</span></a>
|
||||
<a href="/community/?category=skills"><span class="l-en">Skills</span><span class="l-zh">技能</span></a>
|
||||
<a href="/community/?category=show"><span class="l-en">Showcase</span><span class="l-zh">展示</span></a>
|
||||
<a href="/docs/"><span class="l-en">Docs ↗</span><span class="l-zh">文档 ↗</span></a>
|
||||
</nav>
|
||||
<div class="nav-right">
|
||||
<div class="lang-switch" role="group" aria-label="Language">
|
||||
<button type="button" data-lang="en" class="active">EN</button>
|
||||
<button type="button" data-lang="zh">中文</button>
|
||||
</div>
|
||||
<div class="theme-switch" role="group" aria-label="Theme">
|
||||
<button type="button" data-theme="system" class="active" aria-label="System theme"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></button>
|
||||
<button type="button" data-theme="light" aria-label="Light theme"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><line x1="12" y1="1" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="23"/><line x1="4.2" y1="4.2" x2="6.3" y2="6.3"/><line x1="17.7" y1="17.7" x2="19.8" y2="19.8"/><line x1="1" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="23" y2="12"/><line x1="4.2" y1="19.8" x2="6.3" y2="17.7"/><line x1="17.7" y1="6.3" x2="19.8" y2="4.2"/></svg></button>
|
||||
<button type="button" data-theme="dark" aria-label="Dark theme"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg></button>
|
||||
</div>
|
||||
<span id="nav-account"></span>
|
||||
</div>
|
||||
<button
|
||||
class="nav-menu-toggle"
|
||||
type="button"
|
||||
data-mobile-nav-toggle
|
||||
aria-controls="community-mobile-nav"
|
||||
aria-expanded="false"
|
||||
aria-label="Open navigation"
|
||||
title="Open navigation"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mobile-nav" id="community-mobile-nav" data-mobile-nav hidden>
|
||||
<button class="mobile-nav-backdrop" type="button" data-mobile-nav-close aria-label="Close navigation"></button>
|
||||
<aside class="mobile-nav-panel" role="dialog" aria-modal="true" aria-labelledby="community-mobile-nav-title">
|
||||
<div class="mobile-nav-head">
|
||||
<span id="community-mobile-nav-title"><span class="l-en">Navigation</span><span class="l-zh">导航</span></span>
|
||||
<button class="mobile-nav-close" type="button" data-mobile-nav-close aria-label="Close navigation" title="Close navigation">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="mobile-nav-links" aria-label="Mobile community navigation">
|
||||
<a class:list={[{ here: active === "discussions" }]} href="/community/"><span class="l-en">Discussions</span><span class="l-zh">讨论</span></a>
|
||||
<a href="/community/?category=skills"><span class="l-en">Skills</span><span class="l-zh">技能</span></a>
|
||||
<a href="/community/?category=show"><span class="l-en">Showcase</span><span class="l-zh">展示</span></a>
|
||||
<a href="/docs/"><span class="l-en">Docs ↗</span><span class="l-zh">文档 ↗</span></a>
|
||||
</nav>
|
||||
<div class="mobile-nav-actions">
|
||||
<a class="mobile-nav-github" href="https://github.com/esengine/DeepSeek-Reasonix" target="_blank" rel="noreferrer">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12 .7a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2.3c-3.3.7-4-1.4-4-1.4-.5-1.4-1.3-1.8-1.3-1.8-1.1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.6-.3-5.4-1.3-5.4-5.9 0-1.3.5-2.4 1.2-3.2-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.3 1.2a11.5 11.5 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.6.3 2.8.1 3.1.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.4 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .7Z" /></svg>
|
||||
GitHub
|
||||
</a>
|
||||
<span id="mobile-nav-account"></span>
|
||||
<a class="btn btn-primary" href="/community/new/"><span class="l-en">New topic</span><span class="l-zh">发起话题</span></a>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<CommunityJoin />
|
||||
<footer class="foot">
|
||||
<div class="foot-inner">
|
||||
<a class="brand" href="/"><span class="mark">R</span>Reasonix</a>
|
||||
<span><span class="l-en">© 2026 · MIT · built to be left running.</span><span class="l-zh">© 2026 · MIT 许可 · 为常驻运行而生。</span></span>
|
||||
<nav class="links">
|
||||
<a href="/community/guidelines/"><span class="l-en">Guidelines</span><span class="l-zh">规范</span></a>
|
||||
<a href="/docs/"><span class="l-en">Docs</span><span class="l-zh">文档</span></a>
|
||||
<a href="https://github.com/esengine/DeepSeek-Reasonix">GitHub</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>import "../scripts/community.js";</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,115 +0,0 @@
|
||||
const TIERS = [
|
||||
["New", "新成员"],
|
||||
["Basic", "基础"],
|
||||
["Member", "成员"],
|
||||
["Regular", "常客"],
|
||||
["Leader", "领袖"],
|
||||
];
|
||||
|
||||
// [en, zh] for a forum trust level; a value outside the known range reads as unknown.
|
||||
export function trustName(level) {
|
||||
return Number.isInteger(level) && level >= 0 && level < TIERS.length ? TIERS[level] : null;
|
||||
}
|
||||
|
||||
export function deviceSummary(payload) {
|
||||
const list = Array.isArray(payload?.devices) ? payload.devices.filter((d) => d && !d.revokedAt) : [];
|
||||
return { total: list.length, online: payload?.presenceAvailable === false ? null : list.filter((d) => d.online === true).length };
|
||||
}
|
||||
|
||||
export function backupCount(payload) {
|
||||
return Array.isArray(payload?.backups) ? payload.backups.length : null;
|
||||
}
|
||||
|
||||
const whole = (n) => (Number.isFinite(n) && n >= 0 ? Math.floor(n) : null);
|
||||
|
||||
// Only well-formed numbers reach the page; anything else renders as "—".
|
||||
export function forumSummary(payload) {
|
||||
const m = payload?.member;
|
||||
const s = payload?.stats;
|
||||
if (!m || !s) return null;
|
||||
return {
|
||||
trust: Number.isInteger(m.trust) ? m.trust : null,
|
||||
role: m.role === "admin" || m.role === "moderator" ? m.role : "member",
|
||||
dailyPostCap: whole(m.dailyPostCap),
|
||||
canPostLinks: m.canPostLinks === true,
|
||||
silencedUntil: typeof m.silencedUntil === "string" ? m.silencedUntil : null,
|
||||
topics: whole(s.topics),
|
||||
posts: whole(s.posts),
|
||||
likesReceived: whole(s.likesReceived),
|
||||
solved: whole(s.solved),
|
||||
recent: Array.isArray(payload.recent)
|
||||
? payload.recent.filter((r) => Number.isInteger(r?.id) && typeof r.title === "string").slice(0, 5)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function initials(handle) {
|
||||
return String(handle || "?").replace(/[^a-zA-Z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
|
||||
}
|
||||
|
||||
// "empty" is only for an answered request whose counts are all zero; a failed or
|
||||
// malformed answer is "unavailable", never a claim that the member has not taken part.
|
||||
export function forumState(summary) {
|
||||
if (!summary) return "unavailable";
|
||||
return summary.topics === 0 && summary.posts === 0 ? "empty" : "active";
|
||||
}
|
||||
|
||||
const LEVELS = [
|
||||
["New seed", "新种"],
|
||||
["Sprout", "萌芽"],
|
||||
["Seedling", "幼苗"],
|
||||
["Sapling", "小树"],
|
||||
["Bloom", "繁花"],
|
||||
["Canopy", "成荫"],
|
||||
["Grove", "共林"],
|
||||
];
|
||||
|
||||
// [en, zh] for a feedback level, null outside the known ladder.
|
||||
export function levelName(level) {
|
||||
return Number.isInteger(level) && level >= 0 && level < LEVELS.length ? LEVELS[level] : null;
|
||||
}
|
||||
|
||||
const TRUST = new Set(["active", "legacy_active", "lapsed", "revoked", "none"]);
|
||||
const count = (n) => (Number.isInteger(n) && n >= 0 ? n : null);
|
||||
|
||||
function standing(p) {
|
||||
if (!p || count(p.level) === null || count(p.adoptedCount) === null || !TRUST.has(p.trustState)) return null;
|
||||
const limits = p.effectiveLimits ?? {};
|
||||
return {
|
||||
level: p.level,
|
||||
adopted: p.adoptedCount,
|
||||
base: count(p.currentThreshold),
|
||||
nextLevel: count(p.nextLevel),
|
||||
nextThreshold: count(p.nextThreshold),
|
||||
remaining: count(p.remaining),
|
||||
trustState: p.trustState,
|
||||
trustExpiresAt: typeof p.trustExpiresAt === "string" ? p.trustExpiresAt : null,
|
||||
perHour: count(limits.reportsPerHour),
|
||||
perDay: count(limits.reportsPerDay),
|
||||
repliesPerHour: count(limits.repliesPerHour),
|
||||
};
|
||||
}
|
||||
|
||||
// Share of the way to the next level, or null at the top or when the thresholds do not hold together.
|
||||
export function levelProgress(s) {
|
||||
if (!s || s.nextThreshold === null || s.base === null || s.nextThreshold <= s.base) return null;
|
||||
return Math.min(1, Math.max(0, (s.adopted - s.base) / (s.nextThreshold - s.base)));
|
||||
}
|
||||
|
||||
// "unavailable" is a failed or malformed answer; "none" is an answered request with no linked install.
|
||||
export function feedbackSummary(payload) {
|
||||
if (!payload || !Array.isArray(payload.installs) || !Array.isArray(payload.items)) return { state: "unavailable" };
|
||||
const installs = payload.installs.map((i) => ({ id: String(i?.id ?? ""), linkedAt: i?.linkedAt, standing: standing(i?.profile) })).filter((i) => /^[0-9a-f]{12}$/.test(i.id));
|
||||
if (installs.length === 0) return { state: "none" };
|
||||
const items = payload.items
|
||||
.filter((it) => typeof it?.receipt === "string" && typeof it.titleSnippet === "string")
|
||||
.slice(0, 5)
|
||||
.map((it) => ({
|
||||
receipt: it.receipt,
|
||||
title: it.titleSnippet,
|
||||
status: typeof it.status === "string" ? it.status : "",
|
||||
issueNumber: count(it.issueNumber),
|
||||
version: typeof it.resolvedVersion === "string" && /^[0-9A-Za-z][0-9A-Za-z.+-]{0,31}$/.test(it.resolvedVersion) ? it.resolvedVersion : null,
|
||||
}));
|
||||
return { state: "linked", headline: standing(payload.profile), installs, items };
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// The contributor board's pure parts: what a name may look like on the page, and the
|
||||
// rows built from the public answer. Names come from users, so every one is cleaned
|
||||
// and set as text; nothing here makes markup.
|
||||
|
||||
export const NAME_LIMIT = 60;
|
||||
|
||||
// Bidirectional controls and other invisible formatting characters can make a name
|
||||
// read as something else (a right-to-left override flips what follows it), so none
|
||||
// reaches the page.
|
||||
const INVISIBLE = /[\u0000-\u001f\u007f-\u009f----]/gu;
|
||||
|
||||
export function cleanName(raw) {
|
||||
const text = String(raw ?? "").normalize("NFKC").replace(/[\t\n\r]+/g, " ").replace(INVISIBLE, "").replace(/\s+/g, " ").trim();
|
||||
const chars = [...text];
|
||||
return chars.length > NAME_LIMIT ? chars.slice(0, NAME_LIMIT - 1).join("") + "…" : text;
|
||||
}
|
||||
|
||||
export function newestFirst(a, b) {
|
||||
const pa = a.replace(/^v/, "").split(".").map(Number);
|
||||
const pb = b.replace(/^v/, "").split(".").map(Number);
|
||||
for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pb[i] ?? 0) - (pa[i] ?? 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function versionsOf(entries) {
|
||||
return [...new Set(entries.flatMap((e) => e.releases.map((r) => r.version)))].sort(newestFirst);
|
||||
}
|
||||
|
||||
// One row as plain parts, whatever the document is: the caller sets each as text.
|
||||
export function rowOf(entry, ranked) {
|
||||
return {
|
||||
rank: ranked ? String(entry.rank ?? "") : null,
|
||||
name: cleanName(entry.displayName),
|
||||
credits: Number.isFinite(entry.credits) ? entry.credits : 0,
|
||||
chips: ranked ? [] : (entry.releases ?? []).slice(0, 3).map((r) => (r.count > 1 ? `${r.version} ×${r.count}` : r.version)),
|
||||
};
|
||||
}
|
||||
|
||||
// buildRow draws a row with the given document. It writes only through
|
||||
// textContent, so a name can never become markup.
|
||||
export function buildRow(entry, ranked, doc) {
|
||||
const row = rowOf(entry, ranked);
|
||||
const li = doc.createElement("li");
|
||||
const part = (cls, text) => {
|
||||
const el = doc.createElement("span");
|
||||
el.className = cls;
|
||||
el.textContent = text;
|
||||
return el;
|
||||
};
|
||||
if (row.rank !== null) li.append(part("board-rank", row.rank));
|
||||
li.append(part("board-name", row.name));
|
||||
const credits = doc.createElement("span");
|
||||
credits.className = "board-credits";
|
||||
credits.append(part("l-en", `${row.credits} adopted`), part("l-zh", `${row.credits} 条被采纳`));
|
||||
li.append(credits);
|
||||
if (!ranked) {
|
||||
const where = doc.createElement("span");
|
||||
where.className = "board-where";
|
||||
for (const chip of row.chips) where.append(part("board-chip", chip));
|
||||
li.append(where);
|
||||
}
|
||||
return li;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export const QQ_GROUP = {
|
||||
name: 'DeepSeek-Reasonix官方群',
|
||||
number: '1093562660',
|
||||
joinUrl: 'https://qm.qq.com/q/i59b0z2R8s',
|
||||
qrPath: '/community/qq-group.svg',
|
||||
};
|
||||
|
||||
export const DISCORD_URL = 'https://discord.gg/XF78rEME2D';
|
||||
|
||||
export const DOUYIN = {
|
||||
name: '做游戏的小鱼',
|
||||
id: '22703872788',
|
||||
qrPath: '/community/douyin.png',
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
// A forum thread with source "github" mirrors a GitHub Discussion, and one with source "feedback" mirrors an in-app report's issue: its text
|
||||
// is GitHub's, so the page shows where it lives instead of a reply box.
|
||||
export const DISCUSSIONS_URL = "https://github.com/esengine/DeepSeek-Reasonix/discussions";
|
||||
export const ISSUES_URL = "https://github.com/esengine/DeepSeek-Reasonix/issues";
|
||||
|
||||
export function mirrorOf(topic) {
|
||||
if (!topic || (topic.source !== "github" && topic.source !== "feedback")) return null;
|
||||
const feedback = topic.source === "feedback";
|
||||
const base = feedback ? ISSUES_URL : DISCUSSIONS_URL;
|
||||
const url = Number.isInteger(topic.ghNumber) && topic.ghNumber > 0 ? `${base}/${topic.ghNumber}` : base;
|
||||
const status = ["recorded", "in_progress", "fixed"].includes(topic.ghStatus) ? topic.ghStatus : null;
|
||||
const version = status === "fixed" && typeof topic.fixedIn === "string" && /^[0-9A-Za-z][0-9A-Za-z.+-]{0,31}$/.test(topic.fixedIn) ? topic.fixedIn : null;
|
||||
return { url, status, version, feedback };
|
||||
}
|
||||
|
||||
const LABELS = {
|
||||
recorded: ["Recorded", "已记录"],
|
||||
in_progress: ["In progress", "处理中"],
|
||||
fixed: ["Fixed", "已修复"],
|
||||
};
|
||||
|
||||
// [en, zh] for the status badge, with the shipped version when there is one.
|
||||
export function statusText(mirror) {
|
||||
if (!mirror?.status) return null;
|
||||
const [en, zh] = LABELS[mirror.status];
|
||||
return mirror.version ? [`${en} in v${mirror.version.replace(/^v/, "")}`, `${zh}于 v${mirror.version.replace(/^v/, "")}`] : [en, zh];
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// The repository owner is never thanked on release pages; change the login here only.
|
||||
export const repositoryOwner = "esengine";
|
||||
|
||||
export function withoutOwner(logins) {
|
||||
const owner = repositoryOwner.toLowerCase();
|
||||
return logins.filter((login) => String(login).toLowerCase() !== owner);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
const REPO_ISSUES = 'https://github.com/esengine/DeepSeek-Reasonix/issues/';
|
||||
const TRAILING_REFS = /(\(?)(#\d{1,9}(?:\s*,\s*#\d{1,9})*)(\)?)\s*$/;
|
||||
|
||||
export function issueHref(digits) {
|
||||
if (!/^\d{1,9}$/.test(digits)) throw new Error('issue number must be digits only');
|
||||
return `${REPO_ISSUES}${digits}`;
|
||||
}
|
||||
|
||||
function codeSpans(text) {
|
||||
const out = [];
|
||||
const pattern = /`([^`\n]+)`/g;
|
||||
let last = 0;
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
if (match.index > last) out.push({ type: 'text', value: text.slice(last, match.index) });
|
||||
out.push({ type: 'code', value: match[1] });
|
||||
last = match.index + match[0].length;
|
||||
}
|
||||
if (last < text.length) out.push({ type: 'text', value: text.slice(last) });
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseInline(line) {
|
||||
const trailing = line.match(TRAILING_REFS);
|
||||
const hasBoundary = trailing && (trailing.index === 0 || /\s/.test(line[trailing.index - 1]) || trailing[1] === '(');
|
||||
const insideCode = trailing && (line.slice(0, trailing.index).split('`').length - 1) % 2 === 1;
|
||||
if (!trailing || !hasBoundary || insideCode || (trailing[1] === '(') !== (trailing[3] === ')')) return codeSpans(line);
|
||||
const body = line.slice(0, trailing.index);
|
||||
const tokens = codeSpans(body);
|
||||
tokens.push({ type: 'text', value: trailing[1] });
|
||||
trailing[2].split(/(\s*,\s*)/).forEach((part) => {
|
||||
const ref = part.match(/^#(\d+)$/);
|
||||
tokens.push(ref ? { type: 'ref', number: ref[1], value: part } : { type: 'text', value: part });
|
||||
});
|
||||
if (trailing[3]) tokens.push({ type: 'text', value: trailing[3] });
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export function parseStudioNotes(markdown) {
|
||||
const blocks = [];
|
||||
let list = null;
|
||||
let summary = '';
|
||||
for (const raw of String(markdown ?? '').split(/\r?\n/)) {
|
||||
const line = raw.trimEnd();
|
||||
if (!line.trim()) { list = null; continue; }
|
||||
const heading = line.match(/^##\s+(\S.*)$/);
|
||||
const bullet = line.match(/^[-*]\s+(\S.*)$/);
|
||||
if (heading) {
|
||||
list = null;
|
||||
blocks.push({ type: 'heading', inline: parseInline(heading[1].trim()) });
|
||||
} else if (bullet) {
|
||||
if (!list) { list = { type: 'list', items: [] }; blocks.push(list); }
|
||||
list.items.push(parseInline(bullet[1]));
|
||||
} else {
|
||||
list = null;
|
||||
if (!summary && blocks.length === 0) summary = line.trim();
|
||||
blocks.push({ type: 'paragraph', inline: parseInline(line.trim()) });
|
||||
}
|
||||
}
|
||||
return { summary, blocks };
|
||||
}
|
||||
|
||||
export function plainText(tokens) {
|
||||
return tokens.map((t) => t.value).join('');
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import CommunityJoin from '../components/CommunityJoin.astro';
|
||||
import SponsorSlot from '../components/SponsorSlot.astro';
|
||||
import SiteHeader from '../components/SiteHeader.astro';
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
---
|
||||
<Base
|
||||
title="Page not found — Reasonix"
|
||||
titleEn="Page not found — Reasonix"
|
||||
titleZh="页面不存在 — Reasonix"
|
||||
description="The page you are looking for does not exist.">
|
||||
|
||||
<Fragment slot="head"><meta name="robots" content="noindex" /></Fragment>
|
||||
|
||||
<script is:inline>
|
||||
// Preserve the documented /u/<handle> shape on static hosting by moving it
|
||||
// to the client-rendered public profile page.
|
||||
try {
|
||||
const match = location.pathname.match(/^\/u\/([^/]+)\/?$/);
|
||||
if (match) location.replace(`/u/?handle=${encodeURIComponent(decodeURIComponent(match[1]))}`);
|
||||
} catch {}
|
||||
</script>
|
||||
|
||||
<SiteHeader scrolled />
|
||||
|
||||
<main class="hero" style="padding-bottom:150px">
|
||||
<div class="wrap">
|
||||
<div class="eyebrow">404</div>
|
||||
<h1><span class="l-en">This page went <span class="grad">cold.</span></span><span class="l-zh">这一页<span class="grad">冷掉了</span>。</span></h1>
|
||||
<p class="sub"><span class="l-en">The page you're looking for doesn't exist — but the session is still warm.</span><span class="l-zh">你找的页面不存在——但会话还热着。</span></p>
|
||||
<div class="cold-blocks" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i class="new"></i></div>
|
||||
<div class="hero-cta">
|
||||
<a class="btn btn-dark" href={`${base}/`}><span class="l-en">Back to home</span><span class="l-zh">回到首页</span></a>
|
||||
<a class="btn btn-ghost" href={`${base}/docs/`}><span class="l-en">Read the docs</span><span class="l-zh">查看文档</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<CommunityJoin />
|
||||
<SponsorSlot />
|
||||
</footer>
|
||||
</Base>
|
||||
@@ -1,125 +0,0 @@
|
||||
---
|
||||
import Account from '../layouts/Account.astro';
|
||||
import PasswordInput from '../components/PasswordInput.astro';
|
||||
---
|
||||
<Account
|
||||
wide
|
||||
dash
|
||||
title="Your account — Reasonix"
|
||||
titleEn="Your account — Reasonix"
|
||||
titleZh="我的账号 — Reasonix"
|
||||
description="Manage your Reasonix account.">
|
||||
<div id="account-gate" class="auth-head">
|
||||
<p><span class="l-en">Loading your account…</span><span class="l-zh">正在加载账号…</span></p>
|
||||
</div>
|
||||
|
||||
<div id="account-view" hidden>
|
||||
<div class="acct-id">
|
||||
<span id="acct-avatar" class="acct-avatar" aria-hidden="true"></span>
|
||||
<div class="acct-who">
|
||||
<div id="acct-name" class="acct-name-big"></div>
|
||||
<div id="acct-handle" class="acct-handle"></div>
|
||||
<div id="acct-email" class="acct-email"></div>
|
||||
<div class="acct-chips">
|
||||
<span id="acct-role" class="acct-role"></span>
|
||||
<span id="acct-verified" class="acct-chip"></span>
|
||||
<span id="acct-joined" class="acct-chip"></span>
|
||||
</div>
|
||||
</div>
|
||||
<a id="acct-profile-link" class="btn btn-ghost sm" href="/u/"><span class="l-en">Public profile</span><span class="l-zh">公开主页</span></a>
|
||||
</div>
|
||||
|
||||
<div id="acct-overview" class="acct-grid" role="group" aria-label="Overview">
|
||||
<article class="acct-card" id="card-forum">
|
||||
<h3><span class="l-en">Community</span><span class="l-zh">社区</span></h3>
|
||||
<div class="acct-level"><span id="forum-level" class="acct-lv">—</span><span id="forum-level-note" class="acct-note"></span></div>
|
||||
<dl class="acct-stats">
|
||||
<div><dt><span class="l-en">Topics</span><span class="l-zh">话题</span></dt><dd id="st-topics">—</dd></div>
|
||||
<div><dt><span class="l-en">Replies</span><span class="l-zh">回复</span></dt><dd id="st-posts">—</dd></div>
|
||||
<div><dt><span class="l-en">Likes</span><span class="l-zh">获赞</span></dt><dd id="st-likes">—</dd></div>
|
||||
<div><dt><span class="l-en">Solved</span><span class="l-zh">已解决</span></dt><dd id="st-solved">—</dd></div>
|
||||
</dl>
|
||||
<p id="forum-perms" class="acct-note"></p>
|
||||
<ul id="forum-recent" class="acct-recent"></ul>
|
||||
<a class="acct-more" href="/community/"><span class="l-en">Open the forum →</span><span class="l-zh">进入论坛 →</span></a>
|
||||
</article>
|
||||
|
||||
<article class="acct-card" id="card-feedback">
|
||||
<h3><span class="l-en">Feedback level</span><span class="l-zh">反馈等级</span></h3>
|
||||
<p class="acct-big" id="fb-level">—</p>
|
||||
<div id="fb-track" class="acct-track" hidden><i id="fb-fill"></i></div>
|
||||
<p class="acct-note" id="fb-note"></p>
|
||||
<p class="acct-note" id="fb-limits"></p>
|
||||
<ul id="fb-installs" class="acct-recent"></ul>
|
||||
<ul id="fb-items" class="acct-recent"></ul>
|
||||
</article>
|
||||
|
||||
<article class="acct-card" id="card-devices">
|
||||
<h3><span class="l-en">Devices</span><span class="l-zh">设备</span></h3>
|
||||
<p class="acct-big"><span id="dev-total">—</span> <span class="acct-unit"><span class="l-en">registered</span><span class="l-zh">台已注册</span></span></p>
|
||||
<p class="acct-note" id="dev-online"></p>
|
||||
<a class="acct-more" href="/remote/"><span class="l-en">Open Web Studio →</span><span class="l-zh">进入网页版 Studio →</span></a>
|
||||
</article>
|
||||
|
||||
<article class="acct-card" id="card-backups">
|
||||
<h3><span class="l-en">Config backups</span><span class="l-zh">配置备份</span></h3>
|
||||
<p class="acct-big"><span id="bk-total">—</span> <span class="acct-unit"><span class="l-en">saved</span><span class="l-zh">份</span></span></p>
|
||||
<p class="acct-note"><span class="l-en">Back up and restore your Studio settings from Studio.</span><span class="l-zh">在 Studio 里备份和恢复你的设置。</span></p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div id="verification-wrap" hidden>
|
||||
<h2 class="acct-h2"><span class="l-en">Email verification</span><span class="l-zh">邮箱验证</span></h2>
|
||||
<div id="verification-msg" class="auth-msg" hidden></div>
|
||||
<p class="hint"><span class="l-en">Your email is not confirmed. Some community actions remain locked.</span><span class="l-zh">你的邮箱尚未验证,部分社区操作仍不可用。</span></p>
|
||||
<button id="resend-verification-btn" type="button" class="btn btn-ghost"><span class="l-en">Resend verification email</span><span class="l-zh">重新发送验证邮件</span></button>
|
||||
</div>
|
||||
|
||||
<h2 class="acct-h2"><span class="l-en">Profile</span><span class="l-zh">资料</span></h2>
|
||||
<div id="profile-msg" class="auth-msg" hidden></div>
|
||||
<form id="profile-form" class="auth-form" novalidate>
|
||||
<div class="field">
|
||||
<label for="f-displayName"><span class="l-en">Display name</span><span class="l-zh">显示名</span></label>
|
||||
<input id="f-displayName" type="text" maxlength="80" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="f-handle"><span class="l-en">Handle</span><span class="l-zh">用户名</span></label>
|
||||
<input id="f-handle" type="text" minlength="3" maxlength="30" />
|
||||
<span class="hint"><span class="l-en">Your public profile is linked above.</span><span class="l-zh">你的公开主页链接显示在上方。</span></span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="f-bio"><span class="l-en">Bio</span><span class="l-zh">简介</span></label>
|
||||
<textarea id="f-bio" rows="3" maxlength="500"></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="f-avatarUrl"><span class="l-en">Avatar URL</span><span class="l-zh">头像链接</span></label>
|
||||
<input id="f-avatarUrl" type="url" maxlength="500" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-dark"><span class="l-en">Save profile</span><span class="l-zh">保存资料</span></button>
|
||||
</form>
|
||||
|
||||
<h2 class="acct-h2"><span class="l-en">Password</span><span class="l-zh">密码</span></h2>
|
||||
<div id="password-msg" class="auth-msg" hidden></div>
|
||||
<form id="password-form" class="auth-form" novalidate>
|
||||
<div class="field">
|
||||
<label for="currentPassword"><span class="l-en">Current password</span><span class="l-zh">当前密码</span></label>
|
||||
<PasswordInput id="currentPassword" autocomplete="current-password" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="newPassword"><span class="l-en">New password</span><span class="l-zh">新密码</span></label>
|
||||
<PasswordInput id="newPassword" autocomplete="new-password" minlength={8} required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-ghost"><span class="l-en">Change password</span><span class="l-zh">修改密码</span></button>
|
||||
</form>
|
||||
|
||||
<h2 class="acct-h2"><span class="l-en">Web Studio</span><span class="l-zh">网页版 Studio</span></h2>
|
||||
<p class="hint"><span class="l-en">Use the full Studio interface from this browser and control a computer signed in to the same account.</span><span class="l-zh">在浏览器中使用完整的 Studio 界面,并控制已登录同一账号的电脑。</span></p>
|
||||
<a class="btn btn-ghost acct-remote" href="/remote/"><span class="l-en">Enter Web Studio</span><span class="l-zh">进入网页版 Studio</span></a>
|
||||
|
||||
<div class="acct-foot">
|
||||
<button id="logout-btn" type="button" class="btn btn-ghost"><span class="l-en">Sign out</span><span class="l-zh">退出登录</span></button>
|
||||
<button id="delete-btn" type="button" class="acct-danger"><span class="l-en">Delete account</span><span class="l-zh">注销账号</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<script>import "../scripts/account-overview.js";</script>
|
||||
</Account>
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
import ChangelogPage from '../../components/ChangelogPage.astro';
|
||||
import { allReleases, publishedReleases, releases, studioReleases } from '../../data/releases';
|
||||
|
||||
export function getStaticPaths() {
|
||||
const publishedVersions = new Set(publishedReleases.map((release) => release.version));
|
||||
return allReleases.map((release) => ({
|
||||
params: { version: `v${release.version}` },
|
||||
props: { release, published: publishedVersions.has(release.version) },
|
||||
}));
|
||||
}
|
||||
|
||||
const { release, published } = Astro.props;
|
||||
if (!published) {
|
||||
return Astro.redirect(release.product === 'studio' ? '/changelog/' : '/changelog/cli/');
|
||||
}
|
||||
---
|
||||
<ChangelogPage release={release} releases={release.product === 'studio' ? studioReleases : releases} />
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
import ChangelogPage from '../../components/ChangelogPage.astro';
|
||||
import { latestStableRelease, releases, releasePath } from '../../data/releases';
|
||||
|
||||
/* /changelog/cli/ renders the latest 1.x release but canonicalizes to its version URL
|
||||
to avoid duplicate content with /changelog/vX.Y.Z/. */
|
||||
const canonical = new URL(releasePath(latestStableRelease.version), Astro.site).href;
|
||||
---
|
||||
|
||||
<ChangelogPage release={latestStableRelease} releases={releases} canonical={canonical} />
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import StudioChangelog from '../../components/StudioChangelog.astro';
|
||||
---
|
||||
|
||||
<StudioChangelog />
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
return Astro.redirect('/changelog/cli/', 301);
|
||||
---
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
return Astro.redirect('/changelog/cli/', 301);
|
||||
---
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import StudioChangelog from '../../components/StudioChangelog.astro';
|
||||
---
|
||||
|
||||
<StudioChangelog />
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
import Community from "../../layouts/Community.astro";
|
||||
---
|
||||
<Community title="Contributors · Reasonix Community" description="The people whose suggestions shipped in Reasonix, by release." active="board">
|
||||
<div class="wrap board">
|
||||
<header class="board-head">
|
||||
<h1><span class="l-en">Contributors</span><span class="l-zh">贡献榜</span></h1>
|
||||
<p class="board-lead">
|
||||
<span class="l-en">People whose feedback was adopted and shipped in a release. A release has a ranking; all releases together are listed by name, not ranked. Only people who chose to appear are here.</span>
|
||||
<span class="l-zh">反馈被采纳并发布的人。每个版本有名次;全部版本只按名字排序,不比高低。只有自己选择加入的人才会出现。</span>
|
||||
</p>
|
||||
<label class="board-pick">
|
||||
<span class="l-en">Release</span><span class="l-zh">版本</span>
|
||||
<select id="board-release"><option value="" data-l-en="All releases" data-l-zh="全部版本">All releases</option></select>
|
||||
</label>
|
||||
</header>
|
||||
<ol id="board-list" class="board-list" aria-live="polite"></ol>
|
||||
<p id="board-empty" class="board-note" hidden>
|
||||
<span class="l-en">Nobody is listed for this release yet.</span><span class="l-zh">这个版本还没有人上榜。</span>
|
||||
</p>
|
||||
<p id="board-off" class="board-note" hidden>
|
||||
<span class="l-en">The contributor board is not open yet.</span><span class="l-zh">贡献榜还没有开放。</span>
|
||||
</p>
|
||||
<p id="board-error" class="board-note" hidden>
|
||||
<span class="l-en">The board could not be loaded. Try again in a moment.</span><span class="l-zh">榜单暂时读不出来,稍后再试。</span>
|
||||
</p>
|
||||
<p class="board-note">
|
||||
<span class="l-en">A ranking depends on what the maintainers chose to ship: a good report that was declined earns nothing. To appear, link your install to your account and turn the board on in Studio's feedback panel; you can leave at any time.</span>
|
||||
<span class="l-zh">名次取决于维护者选择发布的内容:被婉拒的好反馈不计入。想上榜,在 Studio 的反馈面板里把这台电脑关联到账号并打开贡献榜;随时可以退出。</span>
|
||||
</p>
|
||||
</div>
|
||||
<script>import "../../scripts/board.js";</script>
|
||||
</Community>
|
||||
|
||||
<style is:global>
|
||||
.board { max-width: 760px; margin: 0 auto; padding: 32px 16px 56px; }
|
||||
.board-head { display: grid; gap: 10px; margin-bottom: 18px; }
|
||||
.board-head h1 { margin: 0; font-size: 28px; letter-spacing: -0.01em; }
|
||||
.board-lead { margin: 0; color: var(--ink-3); line-height: 1.6; }
|
||||
.board-pick { display: inline-flex; align-items: center; gap: 8px; color: var(--ink-3); font-size: 14px; }
|
||||
.board-pick select { min-width: 160px; padding: 5px 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: inherit; font: inherit; }
|
||||
.board-list { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
|
||||
.board-list li { display: flex; align-items: center; gap: 12px; padding: 11px 16px; border-top: 1px solid var(--line); }
|
||||
.board-list li:first-child { border-top: 0; }
|
||||
.board-rank { flex: none; width: 28px; text-align: right; font: 600 14px ui-monospace, "JetBrains Mono Variable", monospace; color: var(--ink-3); }
|
||||
.board-list[data-ranked] li:nth-child(-n+3) .board-rank { color: var(--accent); }
|
||||
.board-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
|
||||
.board-credits { flex: none; color: var(--ink-3); font-size: 14px; }
|
||||
.board-where { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; }
|
||||
.board-chip { padding: 1px 8px; border: 1px solid var(--line); border-radius: 999px; font-size: 12px; white-space: nowrap; }
|
||||
.board-note { margin: 14px 2px 0; color: var(--ink-3); font-size: 14px; line-height: 1.6; }
|
||||
@media (max-width: 560px) { .board-where { display: none; } .board-list li { padding: 10px 12px; } }
|
||||
</style>
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
import Community from "../../layouts/Community.astro";
|
||||
---
|
||||
<Community title="Guidelines — Reasonix Community" description="How the Reasonix community forum works.">
|
||||
<div class="wrap" style="padding:36px 32px 80px;max-width:760px">
|
||||
<div class="crumb"><a href="/community/"><span class="l-en">Community</span><span class="l-zh">社区</span></a><span class="sep">/</span><span><span class="l-en">Guidelines</span><span class="l-zh">规范</span></span></div>
|
||||
<h1 style="font-size:32px;font-weight:600;letter-spacing:-0.02em"><span class="l-en">Community guidelines</span><span class="l-zh">社区规范</span></h1>
|
||||
<p style="margin:14px 0 28px;color:var(--ink-2);font-size:16px;line-height:1.7"><span class="l-en">A small forum works when everyone keeps it useful and kind. A few things worth knowing.</span><span class="l-zh">一个小论坛,靠大家保持有用与友善才能运转。几点值得先了解。</span></p>
|
||||
|
||||
<div class="side-card" style="margin-bottom:16px"><h4><span class="l-en">Be kind & on-topic</span><span class="l-zh">友善、切题</span></h4><p style="color:var(--ink-2);line-height:1.7"><span class="l-en">Assume good faith, search before asking, and keep threads focused. Format code in fenced <code>```</code> blocks.</span><span class="l-zh">与人为善、提问前先搜索、保持话题聚焦。代码请放进 <code>```</code> 围栏块。</span></p></div>
|
||||
<div class="side-card" style="margin-bottom:16px"><h4><span class="l-en">Trust levels</span><span class="l-zh">信任等级</span></h4><p style="color:var(--ink-2);line-height:1.7"><span class="l-en">Everyone signs in with their Reasonix account. New accounts start limited — no links, a small daily post cap — and unlock more as they read and contribute. It keeps the forum spam-free without getting in real members' way.</span><span class="l-zh">所有人用 Reasonix 账号登录。新账号有限制——不能发链接、每日发帖上限较低——随着阅读与贡献逐步解锁。既挡住广告,又不妨碍真实成员。</span></p></div>
|
||||
<div class="side-card"><h4><span class="l-en">Moderation</span><span class="l-zh">审核</span></h4><p style="color:var(--ink-2);line-height:1.7"><span class="l-en">Flag anything off — a few flags auto-hide a post for review. Moderators can restrict accounts that abuse the forum. All moderator actions are logged.</span><span class="l-zh">发现不妥就举报——几次举报会自动隐藏帖子待审。版主可限制滥用论坛的账号。所有版主操作都有记录。</span></p></div>
|
||||
</div>
|
||||
</Community>
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
import Community from "../../layouts/Community.astro";
|
||||
import { QQ_GROUP, DISCORD_URL, DOUYIN } from "../../lib/community-links.mjs";
|
||||
---
|
||||
<Community title="Reasonix Community" description="Ask questions, share skills, and shape a DeepSeek-native agent." active="discussions">
|
||||
<div class="wrap forum">
|
||||
<aside class="fnav" aria-label="Forum navigation">
|
||||
<a class="btn btn-primary fnew" href="/community/new/"><span class="l-en">✎ New topic</span><span class="l-zh">✎ 发起话题</span></a>
|
||||
<nav class="fmenu" id="sort-tabs" aria-label="Topic lists">
|
||||
<button class="on" data-sort="latest"><span class="l-en">Latest</span><span class="l-zh">最新</span></button>
|
||||
<button data-sort="top"><span class="l-en">Top</span><span class="l-zh">热门</span></button>
|
||||
<button data-sort="unanswered"><span class="l-en">Unanswered</span><span class="l-zh">未回复</span></button>
|
||||
</nav>
|
||||
<h5 class="fh"><span class="l-en">Categories</span><span class="l-zh">分区</span></h5>
|
||||
<nav class="fcats" id="cat-list" aria-label="Categories">
|
||||
<a class="fcat" href="/community/"><i class="dot" data-cat=""></i><span class="nm"><span class="l-en">Loading…</span><span class="l-zh">加载中…</span></span></a>
|
||||
</nav>
|
||||
<h5 class="fh"><span class="l-en">Community</span><span class="l-zh">社区</span></h5>
|
||||
<nav class="flinks">
|
||||
<a href="/community/guidelines/"><span class="l-en">Guidelines</span><span class="l-zh">社区规范</span></a>
|
||||
<a href={QQ_GROUP.joinUrl} target="_blank" rel="noreferrer"><span class="l-en">QQ group</span><span class="l-zh">QQ 群</span> <code>{QQ_GROUP.number}</code></a>
|
||||
<a href={DISCORD_URL} target="_blank" rel="noreferrer">Discord</a>
|
||||
<a href="/docs/"><span class="l-en">Documentation ↗</span><span class="l-zh">文档 ↗</span></a>
|
||||
</nav>
|
||||
<p class="fstat"><span id="s-topics">—</span> <span class="l-en">topics</span><span class="l-zh">话题</span> · <span id="s-cats">—</span> <span class="l-en">categories</span><span class="l-zh">分区</span></p>
|
||||
</aside>
|
||||
|
||||
<main class="fmain">
|
||||
<div class="fbar">
|
||||
<div class="ftitle">
|
||||
<h1 id="f-title"><span class="l-en">All topics</span><span class="l-zh">全部话题</span></h1>
|
||||
<p id="f-desc"></p>
|
||||
</div>
|
||||
<label class="fsearch"><span class="l-en sr">Filter topics</span><span class="l-zh sr">筛选话题</span>
|
||||
<input id="t-search" type="search" autocomplete="off" placeholder="Filter topics…" data-ph-zh="筛选话题…" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="ftable" role="table" aria-label="Topics">
|
||||
<div class="fhead" role="row">
|
||||
<span role="columnheader"><span class="l-en">Topic</span><span class="l-zh">话题</span></span>
|
||||
<span role="columnheader" class="num"><span class="l-en">Replies</span><span class="l-zh">回复</span></span>
|
||||
<span role="columnheader" class="num"><span class="l-en">Views</span><span class="l-zh">浏览</span></span>
|
||||
<span role="columnheader" class="act"><span class="l-en">Activity</span><span class="l-zh">活动</span></span>
|
||||
</div>
|
||||
<div id="topic-list" class="topics" role="rowgroup">
|
||||
<div class="skeleton"><div class="bar"></div><div class="bar short"></div></div>
|
||||
<div class="skeleton"><div class="bar"></div><div class="bar short"></div></div>
|
||||
<div class="skeleton"><div class="bar"></div><div class="bar short"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</Community>
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
import Community from "../../layouts/Community.astro";
|
||||
---
|
||||
<Community title="New topic — Reasonix Community" description="Start a discussion on the Reasonix community forum.">
|
||||
<div class="wrap" style="padding-top:32px;max-width:820px">
|
||||
<div class="crumb"><a href="/community/">Community</a><span class="sep">/</span><span><span class="l-en">New topic</span><span class="l-zh">发起话题</span></span></div>
|
||||
<h1 style="font-size:28px;font-weight:600;letter-spacing:-0.02em;margin-bottom:6px"><span class="l-en">Start a discussion</span><span class="l-zh">发起讨论</span></h1>
|
||||
|
||||
<div id="new-gate" hidden class="card cta" style="margin-top:18px">
|
||||
<h4><span class="l-en">Sign in to post</span><span class="l-zh">登录后发帖</span></h4>
|
||||
<p class="lead"><span class="l-en">You'll post with your Reasonix account. New here? Signing in creates one.</span><span class="l-zh">用你的 Reasonix 账号发帖。还没有账号?登录即创建。</span></p>
|
||||
<a class="btn btn-primary" id="gate-login" href="#"><span class="l-en">Sign in</span><span class="l-zh">登录</span></a>
|
||||
</div>
|
||||
|
||||
<div id="new-form" hidden>
|
||||
<div class="msg error" id="new-msg" hidden></div>
|
||||
<div class="composer">
|
||||
<input class="title" id="f-title" maxlength="160" data-ph-en="Topic title — be specific" data-ph-zh="话题标题 —— 尽量具体" />
|
||||
<textarea id="f-body" data-ph-en="Write your post… Markdown and ``` code blocks supported." data-ph-zh="写下内容… 支持 Markdown 和 ``` 代码块。"></textarea>
|
||||
<div class="foot">
|
||||
<select id="f-category"><option value="" data-l-en="Choose a category…" data-l-zh="选择分区…">Choose a category…</option></select>
|
||||
<button class="btn btn-primary" id="f-submit"><span class="l-en">Post topic</span><span class="l-zh">发布话题</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="lead" style="margin-top:14px"><span class="l-en">Please search before posting, keep it on-topic, and format code in fenced blocks. New accounts can't post links until they've participated a little.</span><span class="l-zh">发帖前请先搜索、保持切题、代码放进围栏块。新账号需参与一段时间后才能发链接。</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</Community>
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
import Community from "../../layouts/Community.astro";
|
||||
---
|
||||
<Community title="Discussion — Reasonix Community" description="A discussion on the Reasonix community forum.">
|
||||
<div class="wrap" style="padding-top:28px">
|
||||
<div class="crumb">
|
||||
<a href="/community/"><span class="l-en">Community</span><span class="l-zh">社区</span></a><span class="sep">/</span>
|
||||
<span id="crumb-cat">…</span><span class="sep">/</span>
|
||||
<span id="crumb-title"><span class="l-en">Loading…</span><span class="l-zh">加载中…</span></span>
|
||||
</div>
|
||||
<div class="thread-head">
|
||||
<h1 id="t-title">
|
||||
<div class="skeleton" style="border:none;padding:0"><div class="bar" style="width:70%;height:22px"></div></div>
|
||||
</h1>
|
||||
<div class="meta" id="t-meta"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrap thread-layout">
|
||||
<div>
|
||||
<div id="posts">
|
||||
<div class="skeleton"><div class="bar"></div><div class="bar short"></div></div>
|
||||
<div class="skeleton"><div class="bar"></div><div class="bar short"></div></div>
|
||||
</div>
|
||||
|
||||
<div id="reply-zone"></div>
|
||||
</div>
|
||||
|
||||
<aside class="side">
|
||||
<div class="side-card">
|
||||
<h4><span class="l-en">Participants</span><span class="l-zh">参与者</span></h4>
|
||||
<div class="parti" id="parti"></div>
|
||||
</div>
|
||||
<div class="side-card">
|
||||
<h4><span class="l-en">Actions</span><span class="l-zh">操作</span></h4>
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
<a class="link-act" href="/community/"><span class="l-en">← Back to discussions</span><span class="l-zh">← 返回讨论区</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</Community>
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
import Account from '../layouts/Account.astro';
|
||||
---
|
||||
<Account
|
||||
title="Authorize a device — Reasonix"
|
||||
titleEn="Authorize a device — Reasonix"
|
||||
titleZh="授权设备 — Reasonix"
|
||||
description="Approve a sign-in request from the Reasonix CLI or desktop app.">
|
||||
<div id="device-gate" class="auth-head">
|
||||
<p><span class="l-en">Checking your session…</span><span class="l-zh">正在检查登录状态…</span></p>
|
||||
</div>
|
||||
|
||||
<div id="device-view" hidden>
|
||||
<div class="auth-head">
|
||||
<h1><span class="l-en">Approve Reasonix sign-in</span><span class="l-zh">批准 Reasonix 登录</span></h1>
|
||||
<p><span class="l-en">Only approve if you started this request.</span><span class="l-zh">仅在这是你本人发起的请求时批准。</span></p>
|
||||
</div>
|
||||
|
||||
<div id="device-msg" class="auth-msg" hidden></div>
|
||||
|
||||
<div id="device-code-step">
|
||||
<div class="field">
|
||||
<label for="device-code"><span class="l-en">Device code</span><span class="l-zh">设备码</span></label>
|
||||
<input id="device-code" class="device-code" type="text" autocomplete="one-time-code" spellcheck="false" placeholder="XXXX-XXXX" />
|
||||
</div>
|
||||
<button id="device-check" type="button" class="btn btn-ghost device-check"><span class="l-en">Continue</span><span class="l-zh">继续</span></button>
|
||||
</div>
|
||||
|
||||
<p id="device-meta" class="device-meta" hidden></p>
|
||||
<p id="device-expiry" class="device-expiry" hidden></p>
|
||||
|
||||
<div id="device-actions" class="device-actions" hidden>
|
||||
<button id="device-approve" type="button" class="btn btn-dark"><span class="l-en">Approve sign-in</span><span class="l-zh">批准登录</span></button>
|
||||
<button id="device-deny" type="button" class="btn btn-ghost"><span class="l-en">Reject</span><span class="l-zh">拒绝</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</Account>
|
||||
@@ -1,484 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import CommunityJoin from '../components/CommunityJoin.astro';
|
||||
import SponsorSlot from '../components/SponsorSlot.astro';
|
||||
import SiteHeader from '../components/SiteHeader.astro';
|
||||
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const repo = 'https://github.com/esengine/DeepSeek-Reasonix';
|
||||
const desktopStableFallback = `${repo}/releases/latest/download`;
|
||||
const studioReleases = `${repo}/releases?q=studio-v`;
|
||||
const goVer = 'latest';
|
||||
---
|
||||
<Base
|
||||
title="Docs — Reasonix"
|
||||
titleEn="Docs — Reasonix"
|
||||
titleZh="文档 — Reasonix"
|
||||
description="Install Reasonix, start your first session, set up your model provider and key, choose how much it asks before acting, rewind file edits, connect editors that support ACP, add MCP plugins, and look up settings, commands and release notes.">
|
||||
|
||||
<SiteHeader active="docs" scrolled />
|
||||
|
||||
<main class="docs-layout">
|
||||
<aside class="docs-side" aria-label="Documentation navigation">
|
||||
<nav class="side-nav" aria-label="Docs sections">
|
||||
<div class="side-group">
|
||||
<span class="gh"><span class="l-en">Start</span><span class="l-zh">开始使用</span></span>
|
||||
<a href="#studio"><strong><span class="l-en">Install</span><span class="l-zh">安装入口</span></strong><small><span class="l-en">Studio desktop app, CLI/TUI</span><span class="l-zh">Studio 桌面端、CLI/TUI</span></small></a>
|
||||
<a href="#config"><strong><span class="l-en">Model keys</span><span class="l-zh">模型密钥</span></strong><small><span class="l-en">Where API keys are saved</span><span class="l-zh">API Key 存在哪里</span></small></a>
|
||||
<a href="#quickstart"><strong><span class="l-en">First session</span><span class="l-zh">第一次会话</span></strong><small><span class="l-en">Open a repo and ask</span><span class="l-zh">打开仓库并提问</span></small></a>
|
||||
<a href="#serve"><strong><span class="l-en">Browser UI</span><span class="l-zh">浏览器界面</span></strong><small><span class="l-en">Run <code>reasonix serve</code></span><span class="l-zh">运行 <code>reasonix serve</code></span></small></a>
|
||||
</div>
|
||||
<div class="side-group">
|
||||
<span class="gh"><span class="l-en">Daily use</span><span class="l-zh">日常使用</span></span>
|
||||
<a href="#cli"><strong><span class="l-en">Commands</span><span class="l-zh">命令与快捷键</span></strong><small><span class="l-en">Slash commands, mouse, Goal</span><span class="l-zh">斜杠命令、鼠标、Goal</span></small></a>
|
||||
<a href="#embedded-docs"><strong><span class="l-en">Built-in docs</span><span class="l-zh">内置文档检索</span></strong><small><span class="l-en">Offline guides and release notes</span><span class="l-zh">离线指南与更新日志</span></small></a>
|
||||
<a href="#permissions"><strong><span class="l-en">Permissions</span><span class="l-zh">权限与沙箱</span></strong><small><span class="l-en">Read only, Workspace write, Full access</span><span class="l-zh">仅可查看、工作区内修改、完全权限</span></small></a>
|
||||
<a href="#extensions"><strong><span class="l-en">Extensions</span><span class="l-zh">扩展与 Sidecar</span></strong><small><span class="l-en">Providers, UI, runtime hooks</span><span class="l-zh">Provider、UI、运行时拦截</span></small></a>
|
||||
<a href="#mcp"><strong><span class="l-en">Plugins</span><span class="l-zh">插件 MCP</span></strong><small><span class="l-en">Tools, prompts, resources</span><span class="l-zh">工具、提示词、资源</span></small></a>
|
||||
<a href="#acp"><strong><span class="l-en">Editor integration</span><span class="l-zh">编辑器接入</span></strong><small><span class="l-en">ACP sessions and steering</span><span class="l-zh">ACP 会话与回合中引导</span></small></a>
|
||||
<a href="#dsh"><strong><span class="l-en">DeepSeek Harness</span><span class="l-zh">DeepSeek Harness</span></strong><small><span class="l-en">Use Reasonix as a subagent</span><span class="l-zh">把 Reasonix 用作子智能体</span></small></a>
|
||||
<a href="#surfaces"><strong><span class="l-en">Desktop & bots</span><span class="l-zh">桌面端与 Bot</span></strong><small><span class="l-en">Use Reasonix from IM</span><span class="l-zh">从 IM 使用 Reasonix</span></small></a>
|
||||
</div>
|
||||
<div class="side-group">
|
||||
<span class="gh"><span class="l-en">Understand</span><span class="l-zh">理解机制</span></span>
|
||||
<a href="#cache"><strong><span class="l-en">Prefix cache</span><span class="l-zh">前缀缓存</span></strong><small><span class="l-en">Why long sessions stay cheap</span><span class="l-zh">长会话为什么更省</span></small></a>
|
||||
<a href="#memory"><strong><span class="l-en">Memory & rewind</span><span class="l-zh">记忆与回退</span></strong><small><span class="l-en">Recall facts, restore snapshots</span><span class="l-zh">记住事实、恢复快照</span></small></a>
|
||||
<a href="#configfile"><strong><span class="l-en">Config file</span><span class="l-zh">配置文件</span></strong><small><span class="l-en">TOML, paths, priority</span><span class="l-zh">TOML、路径、优先级</span></small></a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="docs-side-links">
|
||||
<span class="side-card-kicker"><span class="l-en">Source docs</span><span class="l-zh">完整文档</span></span>
|
||||
<a href={`${repo}/blob/main-v2/docs/GUIDE.md`}><span class="l-en">Guide</span><span class="l-zh">使用指南</span></a>
|
||||
<a class="l-en" href={`${repo}/blob/main-v2/docs/ACP.md`}>ACP editor integration</a>
|
||||
<a class="l-zh" href={`${repo}/blob/main-v2/docs/ACP.zh-CN.md`}>ACP 编辑器接入</a>
|
||||
<a href={`${repo}/blob/studio/docs/DSH.md`}><span class="l-en">Use from DeepSeek Harness</span><span class="l-zh">在 DeepSeek Harness 中使用</span></a>
|
||||
<a class="l-en" href={`${repo}/blob/main-v2/docs/EXTENSIONS.md`}>Extensions & sidecars</a>
|
||||
<a class="l-zh" href={`${repo}/blob/main-v2/docs/EXTENSIONS.zh-CN.md`}>扩展与 Sidecar</a>
|
||||
<a href={`${repo}/blob/main-v2/docs/CAPABILITY_DIAGNOSTICS.md`}><span class="l-en">Capability diagnostics</span><span class="l-zh">能力诊断</span></a>
|
||||
<a href={`${repo}/blob/main-v2/docs/SPEC.md`}><span class="l-en">Engineering spec</span><span class="l-zh">工程规范</span></a>
|
||||
<a href={`${repo}/blob/main-v2/docs/CONFIG_PATHS.md`}><span class="l-en">Config paths</span><span class="l-zh">配置路径</span></a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<article class="docs-main">
|
||||
<section class="docs-hero" id="overview">
|
||||
<span class="docs-kicker"><span class="l-en">Reasonix docs</span><span class="l-zh">Reasonix 文档</span></span>
|
||||
<h1><span class="l-en">Install, configure, and run coding work locally.</span><span class="l-zh">安装、配置,并在本地运行编码任务。</span></h1>
|
||||
<p class="lede"><span class="l-en">This page is the concise website guide. It covers the normal path first, then links out to the full repo docs when you need every field or internal contract.</span><span class="l-zh">这是官网上的精简使用说明:先讲最常用路径,再把完整字段和工程契约链接到仓库文档。</span></p>
|
||||
<div class="docs-path-grid" aria-label="Documentation overview">
|
||||
<a href="#studio">
|
||||
<strong><span class="l-en">New user path</span><span class="l-zh">新手路径</span></strong>
|
||||
<span><span class="l-en">Install, add an API key, open a repo, and start chatting.</span><span class="l-zh">安装、配置密钥、打开仓库,然后开始对话。</span></span>
|
||||
</a>
|
||||
<a href="#permissions">
|
||||
<strong><span class="l-en">Control work</span><span class="l-zh">控制执行</span></strong>
|
||||
<span><span class="l-en">Use approvals, sandboxing, Goal, and rewind to keep changes manageable.</span><span class="l-zh">用审批、沙箱、Goal 和回退管理改动。</span></span>
|
||||
</a>
|
||||
<a href="#extensions">
|
||||
<strong><span class="l-en">Extend Reasonix</span><span class="l-zh">扩展 Reasonix</span></strong>
|
||||
<span><span class="l-en">Build sidecars, Providers, UI, prompts, themes, and MCP tools.</span><span class="l-zh">开发 Sidecar、Provider、UI、提示词、主题与 MCP 工具。</span></span>
|
||||
</a>
|
||||
<a href="#configfile">
|
||||
<strong><span class="l-en">Look up details</span><span class="l-zh">查阅细节</span></strong>
|
||||
<span><span class="l-en">Find config paths, TOML examples, and full reference docs.</span><span class="l-zh">查看配置路径、TOML 示例和完整参考。</span></span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h2 id="studio"><span class="l-en">Reasonix Studio (recommended)</span><span class="l-zh">Reasonix Studio(推荐)</span></h2>
|
||||
<p><span class="l-en">Studio is the desktop app and the default way to use Reasonix: visual sessions, settings, approvals and automatic updates on the same local engine. Download the build for your system:</span><span class="l-zh">Studio 是桌面应用,也是使用 Reasonix 的默认方式:可视化会话、设置、审批与自动更新,运行在同一套本地引擎上。请下载对应系统的安装包:</span></p>
|
||||
<div class="desktop-downloads desktop-downloads--two" aria-label="Studio downloads">
|
||||
<a class="desktop-download" href={studioReleases} data-studio-asset="ReasonixStudio-darwin-arm64.dmg">
|
||||
<strong>macOS · Apple Silicon</strong>
|
||||
<span><span class="l-en">DMG</span><span class="l-zh">DMG</span> · <span data-release-version="studio">latest</span></span>
|
||||
</a>
|
||||
<a class="desktop-download" href={studioReleases} data-studio-asset="ReasonixStudio-darwin-amd64.dmg">
|
||||
<strong>macOS · Intel</strong>
|
||||
<span><span class="l-en">DMG</span><span class="l-zh">DMG</span> · <span data-release-version="studio">latest</span></span>
|
||||
</a>
|
||||
<a class="desktop-download" href={studioReleases} data-studio-asset="ReasonixStudio-windows-amd64-installer.exe">
|
||||
<strong>Windows</strong>
|
||||
<span><span class="l-en">x64 installer</span><span class="l-zh">x64 安装器</span> · <span data-release-version="studio">latest</span></span>
|
||||
</a>
|
||||
<a class="desktop-download" href={studioReleases} data-studio-asset="ReasonixStudio-windows-arm64-installer.exe" data-studio-optional hidden>
|
||||
<strong>Windows · ARM64</strong>
|
||||
<span><span class="l-en">ARM64 installer</span><span class="l-zh">ARM64 安装器</span> · <span data-release-version="studio">latest</span></span>
|
||||
</a>
|
||||
<a class="desktop-download" href={studioReleases} data-studio-asset="ReasonixStudio-linux-amd64.deb">
|
||||
<strong>Linux</strong>
|
||||
<span><span class="l-en">.deb · amd64</span><span class="l-zh">.deb · amd64</span> · <span data-release-version="studio">latest</span></span>
|
||||
</a>
|
||||
<a class="desktop-download" href={studioReleases} data-studio-asset="ReasonixStudio-linux-amd64.rpm" data-studio-optional hidden>
|
||||
<strong>Linux</strong>
|
||||
<span><span class="l-en">.rpm · amd64</span><span class="l-zh">.rpm · amd64</span> · <span data-release-version="studio">latest</span></span>
|
||||
</a>
|
||||
<a class="desktop-download" href={studioReleases} data-studio-asset="ReasonixStudio-linux-arm64.deb" data-studio-optional hidden>
|
||||
<strong>Linux</strong>
|
||||
<span><span class="l-en">.deb · arm64</span><span class="l-zh">.deb · arm64</span> · <span data-release-version="studio">latest</span></span>
|
||||
</a>
|
||||
<a class="desktop-download" href={studioReleases} data-studio-asset="ReasonixStudio-linux-arm64.rpm" data-studio-optional hidden>
|
||||
<strong>Linux</strong>
|
||||
<span><span class="l-en">.rpm · arm64</span><span class="l-zh">.rpm · arm64</span> · <span data-release-version="studio">latest</span></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 id="install"><span class="l-en">Command line (2.x)</span><span class="l-zh">命令行(2.x)</span></h2>
|
||||
<p><span class="l-en">Prefer the terminal, or need scripting? Install the CLI / TUI app with one command on macOS, Linux, Windows, or WSL. <code>reasonix</code> opens the interactive TUI; <code>reasonix run</code> is the headless automation entry.</span><span class="l-zh">更喜欢终端,或需要脚本化?一条命令安装终端应用,支持 macOS、Linux、Windows 或 WSL。<code>reasonix</code> 打开交互式 TUI;<code>reasonix run</code> 用于无界面自动执行。</span></p>
|
||||
<div class="codeblock codeblock--copy"><button data-copy="npm i -g reasonix">Copy</button><span class="c"># npm (recommended · 推荐)</span>
|
||||
npm i -g reasonix
|
||||
|
||||
<span class="c"># Homebrew</span>
|
||||
brew install esengine/reasonix/reasonix</div>
|
||||
<div class="doc-choice-grid">
|
||||
<div>
|
||||
<strong><span class="l-en">Use the CLI when</span><span class="l-zh">适合用 CLI</span></strong>
|
||||
<p><span class="l-en">You live in the terminal, want fast repo work, or need scriptable <code>reasonix run</code> automation.</span><span class="l-zh">你主要在终端工作、需要快速处理仓库任务,或要用 <code>reasonix run</code> 做自动化。</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<strong><span class="l-en">Use the 1.x desktop app when</span><span class="l-zh">适合用 1.x 桌面端</span></strong>
|
||||
<p><span class="l-en">You want visual sessions, settings, MCP status, checkpoints, approvals, and bot connections in one place.</span><span class="l-zh">你希望在一个界面里管理会话、设置、MCP 状态、checkpoint、审批和 Bot 连接。</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tui-panel">
|
||||
<span class="desktop-kicker"><span class="l-en">Terminal-first workflow</span><span class="l-zh">终端优先工作流</span></span>
|
||||
<ul class="desktop-feature-list">
|
||||
<li><span class="l-en">Interactive TUI with Plan, selectable permission modes, transcript scrolling, and compact tool approval prompts.</span><span class="l-zh">交互式 TUI 支持计划模式、可切换的权限模式、对话滚动与紧凑工具审批提示。</span></li>
|
||||
<li><span class="l-en">Drag-select transcript text to copy through the verified local system clipboard; SSH uses a clearly labelled OSC 52 fallback. Run <code>/mouse</code> to use your terminal's native selection instead.</span><span class="l-zh">拖选对话文本会通过可验证的本地系统剪贴板复制;SSH 使用明确标记的 OSC 52 回退。也可用 <code>/mouse</code> 切回终端原生选择。</span></li>
|
||||
<li><span class="l-en">Text paste stays terminal-native. Image paste is explicit: <code>Ctrl+V</code> on macOS/Linux, <code>Alt+V</code> on Windows, or <code>/paste-image</code>, with a live <code>Pasting image…</code> footer state.</span><span class="l-zh">文本粘贴保持终端原生;图片粘贴使用独立入口:macOS/Linux 按 <code>Ctrl+V</code>,Windows 按 <code>Alt+V</code>,或运行 <code>/paste-image</code>,读取时底栏显示“正在粘贴图片…”。</span></li>
|
||||
<li><span class="l-en">Scriptable runs through <code>reasonix run</code>, including piped input and explicit model selection.</span><span class="l-zh">可脚本化的 <code>reasonix run</code>,支持管道输入和显式模型选择。</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="callout"><span class="ico">!</span><span><span class="l-en">To install the legacy <strong>0.x</strong> version, pin it explicitly with <code>npm i -g [email protected]</code>. The commands above and Homebrew install the Reasonix 2.x command line. Coming from 1.x? See <a href={`${repo}/blob/studio/docs/MIGRATING.md`}>Moving from 1.x to 2.x</a>.</span><span class="l-zh">如需安装 legacy <strong>0.x</strong> 版本,请显式 pin: <code>npm i -g [email protected]</code>。上方命令与 Homebrew 安装的是 Reasonix 2.x 命令行。从 1.x 迁移请见 <a href={`${repo}/blob/studio/docs/MIGRATING.md`}>Moving from 1.x to 2.x</a>。</span></span></div>
|
||||
|
||||
<details class="legacy-line" id="desktop">
|
||||
<summary><span class="l-en">Previous line: desktop app 1.x — maintenance only (bug fixes, no new features)</span><span class="l-zh">上一代:桌面端 1.x —— 仅维护(只修缺陷,不加新功能)</span></summary>
|
||||
<div class="desktop-panel">
|
||||
<div class="desktop-panel-head">
|
||||
<span class="desktop-kicker"><span class="l-en">Native app · <span data-release-version="desktop">{goVer}</span></span><span class="l-zh">原生桌面端 · <span data-release-version="desktop">{goVer}</span></span></span>
|
||||
<h3><span class="l-en">Use Reasonix with a full desktop workspace</span><span class="l-zh">用完整桌面工作区运行 Reasonix</span></h3>
|
||||
<p><span class="l-en">The desktop app runs the same local Reasonix engine as the CLI, with visual sessions, settings, MCP status, memory, checkpoints, tool approvals, and IM bot connections in one place.</span><span class="l-zh">桌面端运行与 CLI 相同的本地 Reasonix 引擎,并把可视化会话、设置、MCP 状态、记忆、检查点、工具审批和 IM Bot 连接集中到一个工作区。</span></p>
|
||||
</div>
|
||||
<div class="desktop-downloads" aria-label="Desktop downloads">
|
||||
<a class="desktop-download" href={`${desktopStableFallback}/Reasonix-darwin-universal.dmg`} data-desktop-asset="Reasonix-darwin-universal.dmg" download>
|
||||
<strong>macOS</strong>
|
||||
<span><span class="l-en">Universal DMG · Apple Silicon + Intel</span><span class="l-zh">通用 DMG · Apple Silicon + Intel</span></span>
|
||||
</a>
|
||||
<a class="desktop-download" href={`${desktopStableFallback}/Reasonix-windows-amd64-installer.exe`} data-desktop-asset="Reasonix-windows-amd64-installer.exe" download>
|
||||
<strong>Windows</strong>
|
||||
<span><span class="l-en">x64 installer</span><span class="l-zh">x64 安装器</span></span>
|
||||
</a>
|
||||
<a class="desktop-download" href={`${desktopStableFallback}/Reasonix-linux-amd64.deb`} data-desktop-asset="Reasonix-linux-amd64.deb" download>
|
||||
<strong>Linux</strong>
|
||||
<span><span class="l-en">Debian / Ubuntu .deb</span><span class="l-zh">Debian / Ubuntu .deb</span></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="desktop-downloads desktop-downloads--secondary" aria-label="Alternate desktop downloads">
|
||||
<a class="desktop-download" href={`${desktopStableFallback}/Reasonix-darwin-arm64.dmg`} data-desktop-asset="Reasonix-darwin-arm64.dmg" download>
|
||||
<strong>macOS</strong>
|
||||
<span>Apple Silicon DMG</span>
|
||||
</a>
|
||||
<a class="desktop-download" href={`${desktopStableFallback}/Reasonix-darwin-amd64.dmg`} data-desktop-asset="Reasonix-darwin-amd64.dmg" download>
|
||||
<strong>macOS</strong>
|
||||
<span>Intel DMG</span>
|
||||
</a>
|
||||
<a class="desktop-download" href={`${desktopStableFallback}/Reasonix-windows-arm64-installer.exe`} data-desktop-asset="Reasonix-windows-arm64-installer.exe" download>
|
||||
<strong>Windows</strong>
|
||||
<span>ARM64 installer</span>
|
||||
</a>
|
||||
<a class="desktop-download" href={`${desktopStableFallback}/Reasonix-linux-amd64.tar.gz`} data-desktop-asset="Reasonix-linux-amd64.tar.gz" download>
|
||||
<strong>Linux</strong>
|
||||
<span><span class="l-en">Generic amd64 tarball</span><span class="l-zh">通用 amd64 压缩包</span></span>
|
||||
</a>
|
||||
</div>
|
||||
<ul class="desktop-feature-list">
|
||||
<li><span class="l-en">Open and resume project sessions from a visual sidebar.</span><span class="l-zh">从可视化侧边栏打开和恢复项目会话。</span></li>
|
||||
<li><span class="l-en">Approve tools, review checkpoints, and rewind code or conversation without leaving the app.</span><span class="l-zh">在应用内审批工具、查看 checkpoint,并回退代码或对话。</span></li>
|
||||
<li><span class="l-en">Connect Feishu, Lark, or WeChat bots from Settings and handle remote approvals locally.</span><span class="l-zh">在设置中连接飞书、Lark 或微信 Bot,并在本地处理远程审批。</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="callout"><span class="ico">!</span><span><span class="l-en"><strong>Recovery from desktop 1.38.7:</strong> if update reports <code>current executable is not inside a macOS .app bundle</code>, quit Reasonix, download the Apple Silicon, Intel, or Universal DMG above, and replace <code>/Applications/Reasonix.app</code>. This one-time full reinstall keeps Reasonix settings and sessions. Automatic updates resume from the repaired version.</span><span class="l-zh"><strong>桌面端 1.38.7 恢复:</strong>若更新提示 <code>current executable is not inside a macOS .app bundle</code>,请退出 Reasonix,下载上方 Apple Silicon、Intel 或 Universal DMG,并覆盖 <code>/Applications/Reasonix.app</code>。这次完整覆盖安装不会删除 Reasonix 的配置和会话;安装修复版后,后续版本可继续自动更新。</span></span></div>
|
||||
</details>
|
||||
<h3 id="macos-quarantine"><span class="l-en">macOS quarantine warning</span><span class="l-zh">macOS 隔离属性提示</span></h3>
|
||||
<p><span class="l-en">Use this only when Reasonix was downloaded from the official site or GitHub release, moved to <code>/Applications</code>, and macOS still says the app cannot be opened, cannot be verified, or is damaged. This removes the quarantine flag that macOS attaches to downloaded apps.</span><span class="l-zh">仅在 Reasonix 来自官网或 GitHub Release、已经放入 <code>/Applications</code>,但 macOS 仍提示“无法打开”“无法验证开发者”或“应用已损坏”时使用。它会移除 macOS 给下载应用附加的隔离属性。</span></p>
|
||||
<div class="codeblock codeblock--copy"><button data-copy="sudo xattr -rd com.apple.quarantine /Applications/Reasonix.app">Copy</button><span class="c"># Quit Reasonix first, then run in Terminal.</span>
|
||||
sudo xattr -rd com.apple.quarantine /Applications/Reasonix.app</div>
|
||||
<div class="callout"><span class="ico">!</span><span><span class="l-en">Only run this for an official Reasonix app you trust. If the app is installed somewhere else, replace <code>/Applications/Reasonix.app</code> with that exact app path, then reopen Reasonix.</span><span class="l-zh">只对可信的官方 Reasonix 应用执行这条命令。如果安装在其他位置,请把 <code>/Applications/Reasonix.app</code> 替换为实际 app 路径,然后重新打开 Reasonix。</span></span></div>
|
||||
|
||||
<h2 id="quickstart"><span class="l-en">Quick start</span><span class="l-zh">快速上手</span></h2>
|
||||
<p><span class="l-en">First run is minimal — <code>reasonix setup</code> walks you through picking a provider and key, then saves it under Reasonix home. Then point Reasonix at a repo and start a session:</span><span class="l-zh">首次运行很简单——<code>reasonix setup</code> 引导你选择 provider 并填写密钥,然后保存到 Reasonix home。接着指向仓库、开始会话:</span></p>
|
||||
<div class="codeblock codeblock--copy"><button data-copy="cd your-project && reasonix">Copy</button>cd your-project
|
||||
reasonix</div>
|
||||
<p><span class="l-en">Then just describe the task:</span><span class="l-zh">然后直接描述任务:</span></p>
|
||||
<div class="codeblock"><span class="a">›</span> add retry with backoff to the http client
|
||||
<span class="a">›</span> refactor the auth flow, keep the public API stable
|
||||
<span class="a">›</span> why is the worker leaking memory?</div>
|
||||
<p><span class="l-en">Run <code>/init</code> when you want project memory, then leave the session running — context is append-only, so every new turn starts from a cache hit instead of a cold start.</span><span class="l-zh">需要项目记忆时运行 <code>/init</code>,然后让会话持续运行——上下文只追加,每一轮都从缓存命中开始,而不是冷启动。</span></p>
|
||||
|
||||
<h2 id="serve"><span class="l-en">Web frontend</span><span class="l-zh">Web 前端</span></h2>
|
||||
<p><span class="l-en"><code>reasonix web</code> starts the same local Reasonix engine and opens its browser UI. Inside the CLI, <code>/web</code> hands the current session to that UI. Keep <code>reasonix serve</code> for supervisors, tunnels, reverse proxies, and remote development boxes.</span><span class="l-zh"><code>reasonix web</code> 会启动同一个本地 Reasonix 引擎并打开浏览器 UI;在 CLI 中执行 <code>/web</code> 可把当前会话交接到该界面。进程托管、tunnel、反向代理和远程开发机继续使用 <code>reasonix serve</code>。</span></p>
|
||||
<div class="codeblock codeblock--copy"><button data-copy="cd your-project && reasonix web">Copy</button>cd your-project
|
||||
reasonix web</div>
|
||||
<div class="web-panel">
|
||||
<span class="desktop-kicker"><span class="l-en">Browser UI</span><span class="l-zh">浏览器界面</span></span>
|
||||
<h3><span class="l-en">Chat, approvals, goals, todos, and balance</span><span class="l-zh">聊天、审批、Goal、Todo 与余额</span></h3>
|
||||
<ul class="desktop-feature-list">
|
||||
<li><span class="l-en">Use chat, tool approvals, session history, rewind, fork, summarize, model switching, and reasoning-effort controls from the browser.</span><span class="l-zh">在浏览器里使用聊天、工具审批、会话历史、rewind、fork、summarize、模型切换与 reasoning effort 控件。</span></li>
|
||||
<li><span class="l-en">Start or inspect Goal work, and follow the live todo panel fed by the <code>todo_write</code> tool.</span><span class="l-zh">启动或查看 Goal,并跟随由 <code>todo_write</code> 工具驱动的实时 Todo 面板。</span></li>
|
||||
<li><span class="l-en">Show provider balance when configured; pass <code>--model</code>, <code>--max-steps</code>, or <code>--resume</code> for one-off launches.</span><span class="l-zh">已配置 provider 时显示余额;临时启动可传 <code>--model</code>、<code>--max-steps</code> 或 <code>--resume</code>。</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><span class="l-en"><code>reasonix web</code> starts at <code>127.0.0.1:8787</code>, walks upward when the port is busy, and defaults to an automatically generated token. <code>reasonix serve</code> keeps its config-driven <code>auth_mode = "none"</code> default; enable auth before making Serve reachable from another machine.</span><span class="l-zh"><code>reasonix web</code> 从 <code>127.0.0.1:8787</code> 开始监听,端口占用时自动递增,并默认生成 Token。<code>reasonix serve</code> 保持由配置决定的 <code>auth_mode = "none"</code> 默认值;如果其他机器可以访问 Serve,请先开启认证。</span></p>
|
||||
<div class="codeblock codeblock--copy"><button data-copy="reasonix serve --auth token">Copy</button><span class="c"># token auth prints a share URL with #token=...</span>
|
||||
reasonix serve --auth token
|
||||
reasonix serve --addr 0.0.0.0:8787 --auth token
|
||||
|
||||
<span class="c"># password auth can use a startup password or a stored bcrypt hash</span>
|
||||
reasonix serve --auth password --password 'temporary-password'
|
||||
reasonix serve --hash-password --password 'strong-password'</div>
|
||||
<div class="codeblock"><span class="c"># ~/.reasonix/config.toml</span>
|
||||
[serve]
|
||||
auth_mode = "password" <span class="c"># none|token|password</span>
|
||||
password_hash = "$2a$12$..."
|
||||
behind_proxy = true <span class="c"># only behind a trusted reverse proxy</span></div>
|
||||
|
||||
<h2 id="config"><span class="l-en">Configuration</span><span class="l-zh">配置</span></h2>
|
||||
<p><span class="l-en">Reasonix talks directly to configured OpenAI-compatible providers such as DeepSeek or MiMo with your own API keys. Provider config stores only the key name in <code>api_key_env</code>; the actual secret saved by <code>reasonix setup</code> or desktop Settings lives in the global <code><Reasonix home>/.env</code>.</span><span class="l-zh">Reasonix 使用你自己的 API Key 直连配置好的 OpenAI-compatible provider,例如 DeepSeek 或 MiMo。Provider 配置只在 <code>api_key_env</code> 里记录密钥名称;通过 <code>reasonix setup</code> 或桌面端设置保存的真实密钥会写入全局 <code><Reasonix home>/.env</code>。</span></p>
|
||||
<div class="codeblock codeblock--copy"><button data-copy="reasonix setup">Copy</button>reasonix setup
|
||||
<span class="c"># saves provider keys to <Reasonix home>/.env, for example:</span>
|
||||
DEEPSEEK_API_KEY=sk-...</div>
|
||||
<p><span class="l-en">Project <code>.env</code> files are not provider-key fallbacks. They are only used for workspace-scoped variable expansion in MCP/plugin settings. Persistent options live in the <a href="#configfile">config file</a>.</span><span class="l-zh">项目 <code>.env</code> 不是 provider key 的 fallback。它只用于 MCP/plugin 配置里的工作区级变量展开。持久化选项见<a href="#configfile">配置文件</a>。</span></p>
|
||||
<div class="doc-note-grid">
|
||||
<div><strong><span class="l-en">Global secret file</span><span class="l-zh">全局密钥文件</span></strong><span><code><Reasonix home>/.env</code></span></div>
|
||||
<div><strong><span class="l-en">Project config</span><span class="l-zh">项目配置</span></strong><span><code>./reasonix.toml</code></span></div>
|
||||
<div><strong><span class="l-en">User config</span><span class="l-zh">用户配置</span></strong><span><code>~/.reasonix/config.toml</code></span></div>
|
||||
</div>
|
||||
|
||||
<h2 id="cache"><span class="l-en">Prefix cache</span><span class="l-zh">前缀缓存</span></h2>
|
||||
<p><span class="l-en">DeepSeek bills cached prefix tokens at a fraction of fresh computation. Most agents waste this: they reorder messages, rewrite summaries mid-session, or inject volatile timestamps — every change invalidates the cache from that point on.</span><span class="l-zh">DeepSeek 对缓存前缀 token 的计费远低于新计算。多数智能体浪费了这一点:重排消息、会话中改写摘要、注入易变的时间戳——任何改动都会让其后的缓存全部失效。</span></p>
|
||||
<p><span class="l-en">Reasonix serializes context deterministically and only ever appends. The practical effect: hours-long sessions where 90%+ of every request replays from cache, and input-token cost collapses to ~1/5.</span><span class="l-zh">Reasonix 以确定性方式序列化上下文,并且永远只追加。实际效果:数小时的会话中每次请求 90% 以上从缓存重放,输入 token 成本降到约 1/5。</span></p>
|
||||
<p><span class="l-en">Automatic compaction starts at 80% of the model context by default. Desktop Settings and <code>reasonix config compact-ratio</code> share the user-level value; choose 30–85%, or use <code>--local</code> for a project override. Lower values compact earlier, may increase summary calls and cost, and can reduce prefix-cache reuse.</span><span class="l-zh">自动压缩默认在模型上下文达到 80% 时启动。桌面端设置与 <code>reasonix config compact-ratio</code> 共用用户级设置;可在 30–85% 之间选择,也可用 <code>--local</code> 添加项目覆盖。阈值越低越早压缩,可能增加摘要调用和成本,也可能降低前缀缓存复用率。</span></p>
|
||||
|
||||
<h2 id="permissions"><span class="l-en">Permissions & sandbox</span><span class="l-zh">权限与沙箱</span></h2>
|
||||
<p><span class="l-en">Every surface uses one permission preset. <strong>Read only</strong> mounts session files read only; <strong>Workspace write</strong> allows the workspace and session-private temporary directory and is the default; <strong>Full access</strong> runs as the current OS user without Reasonix filesystem or network sandboxing, while explicit host denies still run before launch. Pipes, command substitution, and inline Python or Node do not trigger approval merely because of their syntax.</span><span class="l-zh">所有入口共用同一权限模式:<strong>仅可查看</strong>强制会话文件只读;默认的<strong>工作区内修改</strong>允许写入工作区和会话私有临时目录;<strong>完全权限</strong>以当前系统账户运行,不使用 Reasonix 文件和网络沙箱,宿主仍会在启动前执行显式禁止规则。管道、命令替换及 Python/Node 内联脚本不会仅因语法触发审批。</span></p>
|
||||
<p><span class="l-en">Restricted presets use Seatbelt on macOS, bubblewrap on Linux, and a restricted token plus ACLs and Job Objects on Windows. If enforcement cannot start, the operation fails closed. Approvals are limited to Allow once, Allow for this session, and Deny; permanent grants are not offered.</span><span class="l-zh">受限模式在 macOS 使用 Seatbelt,在 Linux 使用 bubblewrap,在 Windows 使用受限令牌、ACL 和 Job Object。强制边界无法启动时操作会失败关闭。审批只提供“允许一次/本会话允许/拒绝”,不再提供永久授权。</span></p>
|
||||
|
||||
<h2 id="extensions"><span class="l-en">Extensions & sidecars</span><span class="l-zh">扩展与 Sidecar</span></h2>
|
||||
<p><span class="l-en">Plugin packages have two layers. Declarative contributions ship skills, agents, commands, prompts, hooks, MCP servers, and themes. A Manifest v2 <code>runtime</code> adds an out-of-process Sidecar speaking the stable Extension Protocol v1; it can intercept runtime events, own replacement strategies, contribute streaming model Providers, and publish structured UI. Native Manifest v2 is an explicit capability boundary: Reasonix loads only resources declared in <code>reasonix-plugin.json</code> and does not implicitly import host-specific companion files such as a root <code>CLAUDE.md</code> or <code>.mcp.json</code>. Codex and Claude compatibility manifests keep their own mappings; Claude manifests do not load a plugin-root <code>CLAUDE.md</code> as project context.</span><span class="l-zh">插件包分为两层:声明式贡献可以分发 skills、agents、commands、prompts、hooks、MCP server 与主题;Manifest v2 的 <code>runtime</code> 则启动独立 Sidecar,通过稳定的 Extension Protocol v1 拦截运行时事件、持有替换策略、提供流式模型 Provider,并发布结构化 UI。原生 Manifest v2 是显式能力边界:Reasonix 只加载 <code>reasonix-plugin.json</code> 中声明的资源,不会隐式导入根 <code>CLAUDE.md</code>、<code>.mcp.json</code> 等宿主专用伴随文件。Codex 与 Claude 兼容 manifest 继续采用各自映射;Claude manifest 不会把插件根 <code>CLAUDE.md</code> 当作项目上下文。</span></p>
|
||||
<div class="doc-choice-grid">
|
||||
<div>
|
||||
<strong><span class="l-en">MCP or Extension Protocol?</span><span class="l-zh">MCP 还是 Extension Protocol?</span></strong>
|
||||
<p><span class="l-en">Use MCP for portable tools, prompts, and resources. Use a code extension when the feature must participate in the Agent lifecycle, replace host behavior, provide models, or render native surfaces.</span><span class="l-zh">可移植的工具、提示词与资源优先使用 MCP;需要进入 Agent 生命周期、替换宿主行为、提供模型或原生界面时,才使用代码型扩展。</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<strong><span class="l-en">Stable and cache-aware</span><span class="l-zh">稳定且缓存友好</span></strong>
|
||||
<p><span class="l-en">Each turn pins one runtime generation. Sidecars initialize with four-way concurrency inside one shared 30-second startup budget. No-op reloads keep the prompt prefix byte-identical; stable replacements remain cacheable, while per-turn timestamps or random values in prompts and schemas destroy reuse.</span><span class="l-zh">每个回合固定使用一个 runtime generation;Sidecar 最多 4 路并行初始化,并共享 30 秒总启动预算。空操作重载保持提示词前缀字节不变;稳定替换仍可缓存,把时间戳或随机值逐回合写入提示词与 Schema 则会破坏复用。</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<p><span class="l-en">The repository includes a complete starter with its manifest, Go Sidecar, build commands, linked installation, reload loop, and visible input rewrite:</span><span class="l-zh">仓库提供了完整 starter,包含 Manifest、Go Sidecar、构建命令、链接安装、重载循环与可观察的输入改写:</span></p>
|
||||
<div class="codeblock">git clone https://github.com/esengine/DeepSeek-Reasonix.git
|
||||
cd DeepSeek-Reasonix/sdk/go/examples/starterextension
|
||||
go build -o bin/starter-extension.exe .
|
||||
plugin_root="$(pwd -P)"
|
||||
reasonix plugin install "$plugin_root" --dry-run
|
||||
reasonix plugin install "$plugin_root" --link --replace --yes
|
||||
<span class="c"># Start a session, or reload while idle</span>
|
||||
/reload</div>
|
||||
<p><span class="l-en">On Windows PowerShell, set <code>$pluginRoot = (Resolve-Path .).Path</code> and pass <code>$pluginRoot</code> to both install commands. Then send <code>starter: explain sidecars</code> to verify the intercept. The first public Go SDK release is <code>sdk/go/v1.0.0</code>; before that immutable tag exists, use the starter from a source checkout.</span><span class="l-zh">Windows PowerShell 请先设置 <code>$pluginRoot = (Resolve-Path .).Path</code>,再把 <code>$pluginRoot</code> 传给两条安装命令。随后发送 <code>starter: 解释 Sidecar</code> 验证拦截。Go SDK 的首个公开版本是 <code>sdk/go/v1.0.0</code>;该不可变标签发布前,请直接从源码 checkout 使用 starter。</span></p>
|
||||
<div class="callout"><span class="ico">!</span><span><span class="l-en"><strong>Full trust:</strong> a code Sidecar runs outside the Reasonix sandbox with the inherited environment and can override permission decisions. Dry-run the install, review the runtime command and declared capabilities, and install only code you trust completely.</span><span class="l-zh"><strong>完全信任:</strong>代码型 Sidecar 运行在 Reasonix Sandbox 之外,继承宿主环境并可覆盖权限决定。请先 dry-run,检查 Runtime 命令与声明能力,只安装完全信任的代码。</span></span></div>
|
||||
<p><span class="l-en">Continue with the <a href={`${repo}/blob/main-v2/docs/EXTENSIONS.md`}>extension guide</a>, <a href={`${repo}/blob/main-v2/docs/PLUGIN_PACKAGES.md#manifest-v2-extensions`}>Manifest v2 reference</a>, <a href={`${repo}/blob/main-v2/sdk/go/README.md`}>Go SDK</a>, and <a href={`${repo}/blob/main-v2/docs/EXTENSION_PROTOCOL.md`}>wire protocol</a>.</span><span class="l-zh">后续请阅读<a href={`${repo}/blob/main-v2/docs/EXTENSIONS.zh-CN.md`}>扩展指南</a>、<a href={`${repo}/blob/main-v2/docs/PLUGIN_PACKAGES.zh-CN.md#manifest-v2扩展`}>Manifest v2 参考</a>、<a href={`${repo}/blob/main-v2/sdk/go/README.md`}>Go SDK</a>和<a href={`${repo}/blob/main-v2/docs/EXTENSION_PROTOCOL.zh-CN.md`}>线协议</a>。</span></p>
|
||||
|
||||
<h2 id="mcp"><span class="l-en">Plugins (MCP)</span><span class="l-zh">插件(MCP)</span></h2>
|
||||
<p><span class="l-en">Reasonix is an MCP client. A <code>[[plugins]]</code> entry's <code>type</code> selects the transport: <code>stdio</code> launches a local subprocess, while <code>http</code> connects to a Streamable HTTP server with optional headers expanded from the environment. Tools surface to the model as <code>mcp__<server>__<tool></code>.</span><span class="l-zh">Reasonix 是一个 MCP 客户端。<code>[[plugins]]</code> 条目的 <code>type</code> 选择传输方式:<code>stdio</code> 启动本地子进程;<code>http</code> 连接 Streamable HTTP server,可从环境变量展开静态 headers。工具以 <code>mcp__<server>__<tool></code> 呈现给模型。</span></p>
|
||||
<div class="codeblock"><span class="c"># reasonix.toml</span>
|
||||
[[plugins]] <span class="c"># local stdio server</span>
|
||||
name = "example"
|
||||
command = "reasonix-plugin-example"
|
||||
|
||||
[[plugins]] <span class="c"># remote server over Streamable HTTP</span>
|
||||
name = "stripe"
|
||||
type = "http"
|
||||
url = "https://mcp.stripe.com"
|
||||
headers = { Authorization = "Bearer ${STRIPE_KEY}" }</div>
|
||||
<p><span class="l-en">Installing an MCP server is the authorization decision, so its tools are directly usable without server-level, per-tool, writer, or destructive approval settings. Repository-declared servers require one exact identity confirmation before startup and ask again only when that identity changes. Explicit global deny rules still win. <code>readOnlyHint</code> and <code>destructiveHint</code> remain internal facts for parallel dispatch, Plan restrictions, strict read-only children, and cached-to-live safety reclassification; schema-only changes refresh the next-session cache without interrupting an authorized call. Plan hard-blocks MCP writer/destructive targets and unauthorized readers; <code>read_only_task</code> / <code>read_only_skill</code> never expose writers.</span><span class="l-zh">安装 MCP server 本身就是授权决定,安装后工具直接可用,不再需要 server、逐工具、writer 或 destructive 审批设置。仓库声明的 server 只在启动前确认一次精确身份,身份变化时才重新确认;显式全局 deny 仍然优先。<code>readOnlyHint</code> 与 <code>destructiveHint</code> 只作为并行调度、Plan 限制、严格只读子会话和缓存到实时安全分类复核的内部事实;仅 schema 变化会刷新下一会话缓存,不再中断已授权调用。Plan 会硬阻断 MCP writer/destructive 目标和未授权 reader;<code>read_only_task</code> / <code>read_only_skill</code> 完全不暴露 writer。</span></p>
|
||||
<p><span class="l-en">MCP prompts also become slash commands such as <code>/mcp__server__prompt</code>, and MCP resources can be referenced with <code>@server:uri</code>. Enabled servers connect in the background after a session starts; use <code>/mcp</code> or the desktop MCP panel to inspect, refresh, reconnect, or disable servers. Already have an <code>.mcp.json</code>? Put it in the project root; Reasonix reads the Claude Code <code>mcpServers</code> schema as-is and lets <code>reasonix.toml</code> win on name collisions.</span><span class="l-zh">MCP prompts 也会变成 <code>/mcp__server__prompt</code> 这样的斜杠命令,MCP resources 可通过 <code>@server:uri</code> 引用。启用的 server 会在会话开始后后台连接;用 <code>/mcp</code> 或桌面端 MCP 面板查看、刷新、重连或临时禁用。已有 <code>.mcp.json</code>?放到项目根目录即可;Reasonix 会原样读取 Claude Code 的 <code>mcpServers</code> schema,同名时 <code>reasonix.toml</code> 优先。</span></p>
|
||||
|
||||
<h2 id="acp"><span class="l-en">Editor integration (ACP)</span><span class="l-zh">编辑器接入(ACP)</span></h2>
|
||||
<p><span class="l-en">Reasonix implements Agent Client Protocol (ACP) v1 as an NDJSON JSON-RPC 2.0 agent over stdio. An ACP-compatible editor or host starts <code>reasonix acp</code>, opens workspace-scoped sessions, and receives streamed messages, tool activity, plans, permission requests, and configuration updates.</span><span class="l-zh">Reasonix 实现了 Agent Client Protocol(ACP)v1,通过 stdio 提供 NDJSON JSON-RPC 2.0 agent。兼容 ACP 的编辑器或 host 启动 <code>reasonix acp</code>、打开工作区会话,并接收流式消息、工具活动、计划、权限请求和配置更新。</span></p>
|
||||
<div class="codeblock codeblock--copy"><button data-copy="reasonix acp">Copy</button>reasonix acp <span class="c"># use the configured default model</span>
|
||||
reasonix acp --model <name> <span class="c"># choose the startup model</span></div>
|
||||
<div class="doc-choice-grid">
|
||||
<div>
|
||||
<strong><span class="l-en">Independent sessions</span><span class="l-zh">独立会话</span></strong>
|
||||
<p><span class="l-en">Each session owns its workspace, history, model, reasoning effort, collaboration mode, approval posture, and host-supplied MCP servers.</span><span class="l-zh">每个会话独立持有工作区、历史、模型、推理强度、协作模式、审批策略和 host 提供的 MCP server。</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<strong><span class="l-en">Editor-owned capabilities</span><span class="l-zh">编辑器持有的能力</span></strong>
|
||||
<p><span class="l-en">When advertised by the client, file operations use unsaved editor buffers and foreground commands run in a client-owned terminal.</span><span class="l-zh">客户端声明相关能力后,文件操作会经过编辑器未保存的 buffer,前台命令则在客户端持有的 terminal 中运行。</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<p><span class="l-en">Reasonix also advertises the vendor method <code>_reasonix.io/session/steer</code> through <code>agentCapabilities._meta</code>. A host may call it while <code>session/prompt</code> is active to queue user guidance for the next safe model boundary without cancelling the turn. Clients must discover the method first and treat <code>InvalidRequest</code> as not queued.</span><span class="l-zh">Reasonix 还会通过 <code>agentCapabilities._meta</code> 声明厂商方法 <code>_reasonix.io/session/steer</code>。Host 可在 <code>session/prompt</code> 活动期间调用它,把用户引导排入下一个安全模型边界,而无需取消当前回合。客户端必须先发现该方法,并把 <code>InvalidRequest</code> 视为“未入队”。</span></p>
|
||||
<div class="callout"><span class="ico">i</span><span><span class="l-en">ACP steering is a namespaced ACP v1 extension, not a core <code>session/steer</code> method and not the unreleased ACP v2 <code>session/inject</code> proposal. See the <a href={`${repo}/blob/main-v2/docs/ACP.md`}>complete ACP integration guide</a> for lifecycle methods, capability negotiation, MCP transport shapes, compatibility, errors, and a client checklist.</span><span class="l-zh">ACP 回合中引导是带命名空间的 ACP v1 扩展,不是核心 <code>session/steer</code> 方法,也不是尚未发布的 ACP v2 <code>session/inject</code> 提案。生命周期方法、能力协商、MCP transport 结构、兼容性、错误处理和客户端检查清单见<a href={`${repo}/blob/main-v2/docs/ACP.zh-CN.md`}>完整 ACP 接入文档</a>。</span></span></div>
|
||||
|
||||
<h2 id="dsh"><span class="l-en">DeepSeek Harness</span><span class="l-zh">DeepSeek Harness</span></h2>
|
||||
<p><span class="l-en">DeepSeek Harness (<code>dsh</code>) can hand a task to Reasonix as a subagent over ACP, with configuration only. This repository publishes a plugin index for it. Run <code>reasonix setup</code> once on the machine that runs <code>dsh</code> first.</span><span class="l-zh">DeepSeek Harness(<code>dsh</code>)可以通过 ACP 把任务交给 Reasonix 作为子智能体去做,只需要配置。本仓库为此发布了一个插件索引。先在运行 <code>dsh</code> 的机器上运行一次 <code>reasonix setup</code>。</span></p>
|
||||
<div class="doc-choice-grid">
|
||||
<div>
|
||||
<strong><span class="l-en">1. Add the index</span><span class="l-zh">1. 添加索引</span></strong>
|
||||
<p><span class="l-en">In the DSH Web UI turn on Custom plugin markets (Plugins, More), then add the index below in the Discover tab.</span><span class="l-zh">在 DSH 网页界面打开“自定义插件市场”(插件,更多),再到 Discover 页添加下面的索引。</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<strong><span class="l-en">2. Install</span><span class="l-zh">2. 安装</span></strong>
|
||||
<p><span class="l-en">Select "Reasonix subagent" from the index and install it.</span><span class="l-zh">在索引中选择“Reasonix subagent”并安装。</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<strong><span class="l-en">3. Switch it on</span><span class="l-zh">3. 打开开关</span></strong>
|
||||
<p><span class="l-en">Open the plugin's page and turn its switch on; the switch and both components must read Running.</span><span class="l-zh">打开这个插件的页面并把它的开关打开;开关和两个组件都要显示 Running。</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="codeblock codeblock--copy" style="white-space: pre-wrap; overflow-wrap: anywhere"><button data-copy="https://raw.githubusercontent.com/esengine/DeepSeek-Reasonix/studio/integrations/dsh-plugin/market-index.json">Copy</button>https://raw.githubusercontent.com/esengine/DeepSeek-Reasonix/studio/integrations/dsh-plugin/market-index.json</div>
|
||||
<div class="callout"><span class="ico">i</span><span><span class="l-en">A plugin that is installed but left off gives the model no <code>reasonix</code> tool, and the model may run <code>reasonix -p</code> in its own shell instead. That looks like a delegation and is not one. The permission policy, limits and manual setup are in <a href={`${repo}/blob/studio/docs/DSH.md`}>Using Reasonix from DeepSeek Harness</a>.</span><span class="l-zh">装好但没打开的插件不会给模型 <code>reasonix</code> 工具,模型可能改在自己的 shell 里直接运行 <code>reasonix -p</code>,看起来像委派,其实不是。权限策略、限制和手工配置见<a href={`${repo}/blob/studio/docs/DSH.md`}>在 DeepSeek Harness 中使用 Reasonix</a>。</span></span></div>
|
||||
|
||||
<h2 id="memory"><span class="l-en">Memory & rewind</span><span class="l-zh">记忆与回退</span></h2>
|
||||
<p><span class="l-en">Reasonix keeps project memory in <code>REASONIX.md</code> or <code>AGENTS.md</code>, and stores approved auto-memory facts under Reasonix home. During turns, read-only <code>history</code> and <code>memory</code> tools retrieve prior sessions, compacted archives, and saved facts on demand instead of injecting noisy dynamic state into the stable prompt prefix.</span><span class="l-zh">Reasonix 将项目记忆放在 <code>REASONIX.md</code> 或 <code>AGENTS.md</code>,并把经过批准的 auto-memory fact 存在 Reasonix home 下。运行时,只读 <code>history</code> 与 <code>memory</code> 工具按需检索历史会话、压缩归档和已保存事实,而不是把易变状态塞进稳定 prompt 前缀。</span></p>
|
||||
<p><span class="l-en">Agent-initiated <code>remember</code> and <code>forget</code> remain explicit user decisions. <code>/memory</code> shows active and archived facts; <code>/forget</code> archives rather than permanently erasing a fact from traceability.</span><span class="l-zh">模型主动调用 <code>remember</code> 与 <code>forget</code> 时仍需用户明确决定。<code>/memory</code> 可查看 active 与 archived facts;<code>/forget</code> 会归档而不是永久抹掉可追溯记录。</span></p>
|
||||
<p><span class="l-en">Rewind is snapshot-based, not git-based. Press double <code>Esc</code> in the CLI, use <code>/rewind</code>, or use the desktop hover control to restore code, conversation, or both from an earlier turn without touching <code>.git</code>.</span><span class="l-zh">回退基于文件快照,不是 git。CLI 中双击 <code>Esc</code>、使用 <code>/rewind</code>,或在桌面端用户消息上使用 hover 控件,即可从较早 turn 恢复代码、对话或两者,不会触碰 <code>.git</code>。</span></p>
|
||||
|
||||
<h2 id="cli"><span class="l-en">CLI & slash commands</span><span class="l-zh">命令行与斜杠命令</span></h2>
|
||||
<div class="callout"><span class="ico">i</span><span><span class="l-en">Parts of this reference still describe the 1.x line, for example <code>reasonix bot</code>, which 2.x does not have, and the permission preset names. For the 2.x command line see the <a href={`${repo}/blob/studio/docs/CLI.md`}>CLI reference</a> and <a href={`${repo}/blob/studio/docs/MIGRATING.md`}>Moving from 1.x to 2.x</a>.</span><span class="l-zh">本节部分内容仍按 1.x 版本线撰写,例如 2.x 没有的 <code>reasonix bot</code>,以及权限预设的名称。2.x 命令行请见 <a href={`${repo}/blob/studio/docs/CLI.md`}>CLI reference</a> 与 <a href={`${repo}/blob/studio/docs/MIGRATING.md`}>Moving from 1.x to 2.x</a>。</span></span></div>
|
||||
<div class="codeblock">reasonix setup <span class="c"># first-run: pick provider + keys</span>
|
||||
reasonix <span class="c"># interactive session; workspace write by default</span>
|
||||
reasonix run "<task>" <span class="c"># headless workspace-write session</span>
|
||||
reasonix run --permission-mode read-only "<task>"
|
||||
reasonix web <span class="c"># open the local browser UI</span>
|
||||
reasonix serve <span class="c"># supervised or remote HTTP+SSE frontend</span>
|
||||
reasonix bot doctor <span class="c"># check saved IM bot connections</span></div>
|
||||
<div class="codeblock">reasonix config compact-ratio <span class="c"># effective value + source</span>
|
||||
reasonix config compact-ratio 75 <span class="c"># user-global default</span>
|
||||
reasonix config compact-ratio --local 75 <span class="c"># project override</span></div>
|
||||
<p><span class="l-en">Run <code>reasonix upgrade</code> to install the latest official release. Legacy channel arguments and saved channel settings remain accepted during the 1.x compatibility period, but they resolve to the same official release and are removed when configuration is saved.</span><span class="l-zh">运行 <code>reasonix upgrade</code> 安装最新正式版。1.x 兼容期内仍接受旧渠道参数与已保存设置,但它们都会解析为同一正式版,并在下次保存配置时移除。</span></p>
|
||||
<div class="codeblock">reasonix upgrade
|
||||
reasonix upgrade</div>
|
||||
<p><span class="l-en">Inside a session, slash commands run locally — <code>/help</code> lists them all:</span><span class="l-zh">会话中,斜杠命令在本地运行——<code>/help</code> 列出全部:</span></p>
|
||||
<div class="codeblock">/compact /new /clear /rewind /tree
|
||||
/branch /switch /todo /model /mouse /mcp
|
||||
/skills /hooks /memory /sandbox /docs
|
||||
/language /theme /paste-image /goal
|
||||
/reasoning-language /output-style /help</div>
|
||||
<p><span class="l-en"><code>Shift+Tab</code> cycles Read only → Workspace write → Plan. Full access is selected explicitly with <code>--permission-mode danger-full-access</code>. Use <code>/theme auto|light|dark</code> for the terminal background mode or choose a named accent from bare <code>/theme</code>.</span><span class="l-zh"><code>Shift+Tab</code> 按“仅可查看 → 工作区内修改 → 计划”循环。完全权限必须用 <code>--permission-mode danger-full-access</code> 显式选择。可用 <code>/theme auto|light|dark</code> 选择终端背景模式。</span></p>
|
||||
<p><span class="l-en">The responsive footer shows the active permission posture and interaction state first. Wide terminals keep model and effort together on the right, with available Git, cache, context, compaction, job, and balance telemetry below. <code>ready</code> means the composer is idle; pickers, approvals, image paste, shell mode, and other interactions replace it. Narrow terminals move or compact complete groups, and visible labels follow <code>/language</code>.</span><span class="l-zh">响应式底栏优先显示当前权限姿态和交互状态;终端较宽时,模型和推理强度在右侧成组显示,下方按可用性显示 Git、缓存、上下文、压缩余量、任务和余额。“就绪”表示输入框空闲;选择器、审批、图片粘贴、shell 模式等活动会替换它。窄终端会移动或压缩完整信息组,可见标签跟随 <code>/language</code>。</span></p>
|
||||
<p><span class="l-en">Mouse capture is on by default so Reasonix can handle transcript selection, wheel scroll, and the scrollbar. Turn it off with <code>/mouse</code>, or start with <code>REASONIX_DISABLE_MOUSE=1</code>, when you prefer the terminal's own selection behavior.</span><span class="l-zh">默认会开启鼠标接管,用于对话选中、滚轮滚动和滚动条。需要终端自己的选中行为时,用 <code>/mouse</code> 关闭;也可以用 <code>REASONIX_DISABLE_MOUSE=1</code> 默认关闭。</span></p>
|
||||
<p><span class="l-en">In a local session, releasing an in-app text selection copies through the native system clipboard and shows success only after the write completes. SSH falls back to a clearly labelled OSC 52 request. Text paste remains your terminal's bracketed-paste shortcut, such as <code>Cmd+V</code> on macOS. Image paste is separate: use <code>Ctrl+V</code> on macOS/Linux, <code>Alt+V</code> on Windows, or <code>/paste-image</code>; the footer shows <code>Pasting image…</code> while the attachment is prepared.</span><span class="l-zh">本地会话中,应用内文本选区会写入系统剪贴板,只有写入完成后才提示成功;SSH 会回退到明确标记的 OSC 52 请求。文本继续使用终端原生 bracketed-paste 快捷键,例如 macOS 的 <code>Cmd+V</code>。图片粘贴使用独立入口:macOS/Linux 按 <code>Ctrl+V</code>,Windows 按 <code>Alt+V</code>,或运行 <code>/paste-image</code>;附件准备期间底栏显示“正在粘贴图片…”。</span></p>
|
||||
<p><span class="l-en"><code>/branch [name]</code> forks the current conversation tip, <code>/switch <id|name></code> loads another branch, and <code>/clear</code> confirms before discarding unsaved context. Custom commands are Markdown files under <code>.reasonix/commands/</code> or <code>~/.reasonix/commands/</code>.</span><span class="l-zh"><code>/branch [name]</code> 从当前会话尖端分叉,<code>/switch <id|name></code> 加载另一条分支,<code>/clear</code> 会确认后丢弃未保存上下文。自定义命令是 <code>.reasonix/commands/</code> 或 <code>~/.reasonix/commands/</code> 下的 Markdown 文件。</span></p>
|
||||
<p><span class="l-en"><code>/goal</code> is for long-running objectives. Ordinary chat never changes mode automatically. Goal selects a simple (10), write (20), or research (40) turn budget and pauses after four turns without host-verifiable progress. <code>/goal status</code> shows the runtime, <code>/goal pause</code> suspends, and <code>/goal resume</code> continues. Every class uses the same Goal state machine, structured <code>update_goal</code> reports, host receipts, Delivery readiness, and bounded evaluator. Legacy research archives are read-only and new Goals never create them.</span><span class="l-zh"><code>/goal</code> 用于长目标。普通聊天不会自动切换模式。Goal 自动选择简单(10)、写入(20)或研究(40)轮预算,连续 4 轮无宿主可验证进展会暂停。<code>/goal status</code> 显示运行摘要,<code>/goal pause</code> 暂停,<code>/goal resume</code> 继续。所有预算类别共用同一个 Goal 状态机、结构化 <code>update_goal</code>、宿主 receipt、Delivery readiness 与有界 evaluator。旧研究归档保持只读,新 Goal 不再创建这些目录。</span></p>
|
||||
<p><span class="l-en">Use <code>@path</code> to inject files or directories, and <code>@server:uri</code> for MCP resources. Plan Mode is an explicit user choice: select it in the desktop collaboration control or cycle to it with <code>Shift+Tab</code> in the CLI. <code>reasonix config reasoning-language auto|zh|en</code> updates the user default from scripts; <code>--local</code> remains available for settings that support project-local overrides.</span><span class="l-zh">用 <code>@path</code> 注入文件或目录,用 <code>@server:uri</code> 引入 MCP resource。计划模式始终由用户显式选择:桌面端在协作方式中选择,CLI 用 <code>Shift+Tab</code> 切换。脚本中可用 <code>reasonix config reasoning-language auto|zh|en</code> 更新用户级默认值;<code>--local</code> 仍可用于支持项目级覆盖的设置。</span></p>
|
||||
|
||||
<h2 id="embedded-docs"><span class="l-en">Built-in documentation search</span><span class="l-zh">内置文档检索</span></h2>
|
||||
<p><span class="l-en">Every CLI and Desktop release bundles the reviewed Markdown guides and release-note catalog from that exact source revision. Search is local and offline, and every result identifies the product version, source revision, and corpus digest. Release CI rejects a build when its embedded corpus does not match the release candidate, so an installed version keeps version-matched guidance instead of silently switching to newer website content.</span><span class="l-zh">每个 CLI 与桌面端版本都会内置该源码 revision 对应的、经过审查的 Markdown 指南和更新日志目录。检索完全在本地离线完成,每条结果都会标明产品版本、源码 revision 与语料 digest。发布 CI 会拒绝内置语料与候选版本不匹配的构建,因此已安装版本始终使用与自身版本匹配的说明,不会静默切换到更新的官网内容。</span></p>
|
||||
<div class="codeblock">/docs <span class="c"># corpus version, revision, digest, and examples</span>
|
||||
/docs 1.19.5 changelog <span class="c"># search locally, then answer with the configured AI</span>
|
||||
/docs 1.19.5 更新日志 <span class="c"># 中文问题会优先返回中文证据</span></div>
|
||||
<p><span class="l-en">Bare <code>/docs</code> only displays local corpus information and does not call a model. With a question, Reasonix retrieves version-matched evidence first, then asks the AI configured for the current session to produce a sourced answer. Ordinary natural-language questions can also trigger the read-only <code>docs</code> tool automatically, but the slash command guarantees retrieval before the answer instead of relying on the model to choose the tool.</span><span class="l-zh">直接输入 <code>/docs</code> 只显示本地语料信息,不调用模型。带问题时,Reasonix 会先检索与版本匹配的证据,再交给当前会话配置的 AI 生成带来源的回答。普通自然语言问题也可能自动触发只读 <code>docs</code> 工具;斜杠命令则保证先检索再回答,不依赖模型自行决定是否调用工具。</span></p>
|
||||
<div class="callout"><span class="ico">i</span><span><span class="l-en">Custom commands and compatible plugin or skill aliases keep ownership of <code>/docs</code>. If it is already taken, choose the built-in entry shown in the slash menu—normally <code>/reasonix:docs</code>, or the next free <code>reasonix:</code>-qualified name. Reasonix never overwrites the existing command. CLI and Desktop use the same rule; a remote Desktop displays the command catalog resolved by its host.</span><span class="l-zh">自定义命令以及兼容插件或 Skill 别名会继续拥有 <code>/docs</code>。如果该名称已被占用,请使用斜杠菜单中显示的内置入口——通常是 <code>/reasonix:docs</code>,或下一个空闲的 <code>reasonix:</code> 限定名。Reasonix 不会覆盖已有命令。CLI 与桌面端遵循同一规则;远程桌面端显示由主机解析后的命令目录。</span></span></div>
|
||||
<p><span class="l-en">For the complete retrieval contract, corpus filters, tool schema, and maintainer release checks, see the <a href={`${repo}/blob/main-v2/docs/GUIDE.md#embedded-documentation-retrieval`}>full guide</a> and <a href={`${repo}/blob/main-v2/docs/TOOL_CONTRACT.md`}>tool contract</a>.</span><span class="l-zh">完整检索约定、语料筛选、工具 schema 与维护者发版检查见<a href={`${repo}/blob/main-v2/docs/GUIDE.zh-CN.md#内置文档检索`}>完整指南</a>和<a href={`${repo}/blob/main-v2/docs/TOOL_CONTRACT.zh-CN.md`}>工具契约</a>。</span></p>
|
||||
|
||||
<h2 id="configfile"><span class="l-en">Config file</span><span class="l-zh">配置文件</span></h2>
|
||||
<p><span class="l-en">Resolution order: flags > <code>./reasonix.toml</code> > global <code>config.toml</code> under Reasonix home > compatible legacy config > built-in defaults. Starting with v1.8.1, Reasonix home is <code>~/.reasonix</code> on macOS/Linux and <code>%APPDATA%\reasonix</code> on Windows; set <code>REASONIX_HOME</code> only for tests, CI, or portable installs.</span><span class="l-zh">解析顺序:flags > <code>./reasonix.toml</code> > Reasonix home 下的全局 <code>config.toml</code> > 兼容 legacy config > 内置默认值。从 v1.8.1 起,Reasonix home 在 macOS/Linux 为 <code>~/.reasonix</code>,Windows 为 <code>%APPDATA%\reasonix</code>;只有测试、CI 或便携安装才需要设置 <code>REASONIX_HOME</code>。</span></p>
|
||||
<div class="codeblock"><span class="c"># ~/.reasonix/config.toml (macOS/Linux)</span>
|
||||
default_model = "deepseek-flash"
|
||||
|
||||
[ui]
|
||||
shortcut_layout = "desktop" <span class="c"># optional compatibility setting</span>
|
||||
|
||||
[agent]
|
||||
reasoning_language = "auto" <span class="c"># auto|zh|en</span>
|
||||
compact_ratio = 0.8 <span class="c"># editable as 30–85% in Settings or CLI</span>
|
||||
planner_model = "deepseek-pro" <span class="c"># optional read-only planner</span>
|
||||
|
||||
[[providers]]
|
||||
name = "deepseek-flash"
|
||||
kind = "openai"
|
||||
base_url = "https://api.deepseek.com"
|
||||
model = "deepseek-v4-flash"
|
||||
api_key_env = "DEEPSEEK_API_KEY"
|
||||
|
||||
[tools]
|
||||
enabled = [] <span class="c"># empty = all built-ins</span>
|
||||
bash_timeout_seconds = 120
|
||||
mcp_call_timeout_seconds = 300
|
||||
|
||||
[permissions]
|
||||
mode = "ask" <span class="c"># ask|allow|deny</span>
|
||||
deny = ["Bash(rm -rf*)", "Bash(git push*)"]
|
||||
allow = ["Bash(go test:*)"]
|
||||
|
||||
[sandbox]
|
||||
workspace_root = "" <span class="c"># empty = current dir</span>
|
||||
allow_write = ["/tmp"]
|
||||
forbid_read = ["${HOME}/.ssh"]
|
||||
|
||||
[serve]
|
||||
auth_mode = "none" <span class="c"># none|token|password</span>
|
||||
behind_proxy = false <span class="c"># true only behind trusted proxy</span>
|
||||
|
||||
[[plugins]]
|
||||
name = "example"
|
||||
command = "reasonix-plugin-example"
|
||||
call_timeout_seconds = 600</div>
|
||||
<p><span class="l-en">Legacy config, credentials, memory, and sessions are migrated non-destructively when v1.8.1+ starts. If Reasonix was opened before old paths were available, run <code>/migrate</code> from the CLI TUI or desktop composer. For the full schema and every field's contract, see <a href={`${repo}/blob/main-v2/docs/SPEC.md`}>SPEC.md §5</a>.</span><span class="l-zh">v1.8.1+ 启动时会非破坏性迁移 legacy config、credentials、memory 与 sessions。如果旧路径尚不可用时已经打开过 Reasonix,可在 CLI TUI 或桌面端 composer 中运行 <code>/migrate</code>。完整 schema 与每个字段的契约见 <a href={`${repo}/blob/main-v2/docs/SPEC.md`}>SPEC.md §5</a>。</span></p>
|
||||
|
||||
<h2 id="surfaces"><span class="l-en">Desktop & bots</span><span class="l-zh">桌面端与 Bot</span></h2>
|
||||
<p><span class="l-en">The desktop app shares the same config, global provider-key file, controller, permissions, sandbox, MCP lifecycle, memory, and checkpoint model as the CLI. Desktop-only settings such as shortcuts and bot connections are stored under Reasonix home.</span><span class="l-zh">桌面端与 CLI 共用同一套 config、全局 provider 密钥文件、controller、权限、沙箱、MCP 生命周期、记忆与 checkpoint 模型。快捷键、Bot 连接等桌面端设置存储在 Reasonix home 下。</span></p>
|
||||
<p><span class="l-en">From <strong>Settings -> Bots</strong>, connect Feishu, Lark, or WeChat, then send Reasonix messages from IM. The local desktop runtime handles model calls, tools, approvals, and sandboxing, while IM receives progress, approval cards or text commands, and final results. Headless gateways can be started with <code>reasonix bot start --channels feishu,lark,weixin --dir /path/to/project</code>.</span><span class="l-zh">在 <strong>Settings -> Bots</strong> 中连接飞书、Lark 或微信后,即可从 IM 给 Reasonix 发消息。本地桌面运行时负责模型调用、工具、审批和沙箱,IM 侧接收进度、审批卡片或文本命令以及最终结果。也可以用 <code>reasonix bot start --channels feishu,lark,weixin --dir /path/to/project</code> 启动 headless gateway。</span></p>
|
||||
|
||||
<div class="doc-cards">
|
||||
<a class="doc-card" href={`${base}/#surfaces`}>
|
||||
<h3><span class="l-en">One engine, four surfaces →</span><span class="l-zh">一个引擎,四个入口 →</span></h3>
|
||||
<p><span class="l-en">Terminal, desktop app, browser, and ACP editors share this config.</span><span class="l-zh">终端、桌面端、浏览器与 ACP 编辑器共用这套配置。</span></p>
|
||||
</a>
|
||||
<a class="doc-card" href={`${repo}/blob/main-v2/docs/CONFIG_PATHS.md`}>
|
||||
<h3><span class="l-en">Configuration paths →</span><span class="l-zh">配置路径 →</span></h3>
|
||||
<p><span class="l-en">Reasonix home, credentials, sessions, archives, memory, and migration rescue.</span><span class="l-zh">Reasonix home、凭据、会话、归档、记忆与迁移救援。</span></p>
|
||||
</a>
|
||||
<a class="doc-card" href={`${repo}/blob/main-v2/docs/REASONING_LANGUAGE.md`}>
|
||||
<h3><span class="l-en">Reasoning language →</span><span class="l-zh">思考语言 →</span></h3>
|
||||
<p><span class="l-en">Visible thinking language preference without changing the stable prompt prefix.</span><span class="l-zh">可见思考语言偏好,不改变稳定 prompt 前缀。</span></p>
|
||||
</a>
|
||||
<a class="doc-card" href={`${repo}/blob/main-v2/docs/CHECKPOINTS.md`}>
|
||||
<h3><span class="l-en">Checkpoints & rewind →</span><span class="l-zh">检查点与回退 →</span></h3>
|
||||
<p><span class="l-en">Restore code, conversation, or both from snapshot-based checkpoints.</span><span class="l-zh">通过快照式 checkpoint 恢复代码、对话或两者。</span></p>
|
||||
</a>
|
||||
<a class="doc-card" href={`${repo}/blob/main-v2/docs/CAPABILITY_DIAGNOSTICS.md`}>
|
||||
<h3><span class="l-en">Capability diagnostics →</span><span class="l-zh">能力诊断 →</span></h3>
|
||||
<p><span class="l-en"><code>reasonix doctor capabilities</code>, desktop Diagnostics, and <code>/reasonix-guide</code>.</span><span class="l-zh"><code>reasonix doctor capabilities</code>、桌面端诊断与 <code>/reasonix-guide</code>。</span></p>
|
||||
</a>
|
||||
<a class="doc-card" href={`${repo}/blob/main-v2/docs/EXTENSIONS.md`}>
|
||||
<h3><span class="l-en">Build an extension →</span><span class="l-zh">开发扩展 →</span></h3>
|
||||
<p><span class="l-en">Start with a complete Sidecar, Manifest v2, Go SDK, reload loop, and security model.</span><span class="l-zh">从完整 Sidecar、Manifest v2、Go SDK、重载循环与安全模型开始。</span></p>
|
||||
</a>
|
||||
<a class="doc-card" href={`${repo}/blob/main-v2/docs/BOT_GUIDE.md`}>
|
||||
<h3><span class="l-en">Bot guide →</span><span class="l-zh">Bot 指南 →</span></h3>
|
||||
<p><span class="l-en">Connect Feishu, Lark, and WeChat bots, then approve work from IM.</span><span class="l-zh">连接飞书、Lark 与微信 Bot,并从 IM 中审批任务。</span></p>
|
||||
</a>
|
||||
<a class="doc-card" href={`${repo}/graphs/contributors`}>
|
||||
<h3><span class="l-en">Contribute →</span><span class="l-zh">参与贡献 →</span></h3>
|
||||
<p><span class="l-en">Reasonix is MIT-licensed and open source — good first issues are waiting.</span><span class="l-zh">Reasonix 采用 MIT 许可、开源——新手友好任务等着你。</span></p>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<CommunityJoin />
|
||||
<div class="foot-inner">
|
||||
<a class="brand" href={`${base}/`}><img src={`${base}/logo.svg`} alt="Reasonix logo" /><span>Reasonix</span></a>
|
||||
<span><span class="l-en">© 2026 Reasonix · MIT licensed · built to be left running.</span><span class="l-zh">© 2026 Reasonix · MIT 许可 · 为常驻运行而生。</span></span>
|
||||
<nav class="foot-links">
|
||||
<a href={repo}>GitHub</a>
|
||||
<a href={`${base}/docs/`}><span class="l-en">Docs</span><span class="l-zh">文档</span></a>
|
||||
<a href={`${base}/#start`}><span class="l-en">Install</span><span class="l-zh">安装</span></a>
|
||||
<a href="https://crash.reasonix.io/stats" rel="nofollow"><span class="l-en">Stats</span><span class="l-zh">统计</span></a>
|
||||
</nav>
|
||||
</div>
|
||||
<SponsorSlot />
|
||||
</footer>
|
||||
</Base>
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
import Account from '../layouts/Account.astro';
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
---
|
||||
<Account
|
||||
title="Reset password — Reasonix"
|
||||
titleEn="Reset password — Reasonix"
|
||||
titleZh="找回密码 — Reasonix"
|
||||
description="Reset your Reasonix account password.">
|
||||
<div class="auth-head">
|
||||
<h1><span class="l-en">Reset your password</span><span class="l-zh">找回密码</span></h1>
|
||||
<p><span class="l-en">We'll email you a link to set a new one.</span><span class="l-zh">我们会给你发一封重置链接邮件。</span></p>
|
||||
</div>
|
||||
|
||||
<div id="forgot-msg" class="auth-msg" hidden></div>
|
||||
|
||||
<form id="forgot-form" class="auth-form" novalidate>
|
||||
<div class="field">
|
||||
<label for="email"><span class="l-en">Email</span><span class="l-zh">邮箱</span></label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-dark"><span class="l-en">Send reset link</span><span class="l-zh">发送重置链接</span></button>
|
||||
</form>
|
||||
|
||||
<p class="auth-alt">
|
||||
<a href={`${base}/login/`}><span class="l-en">Back to sign in</span><span class="l-zh">返回登录</span></a>
|
||||
</p>
|
||||
</Account>
|
||||
@@ -1,562 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import CommunityJoin from '../components/CommunityJoin.astro';
|
||||
import SponsorSlot from '../components/SponsorSlot.astro';
|
||||
import SiteHeader from '../components/SiteHeader.astro';
|
||||
import community from '../data/community.json';
|
||||
import { desktopDownloadVersion } from '../data/desktop-download.js';
|
||||
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const repo = 'https://github.com/esengine/DeepSeek-Reasonix';
|
||||
const repoReleases = `${repo}/releases`;
|
||||
const studioReleases = `${repoReleases}?q=studio-v`;
|
||||
const desktopStableFallback = desktopDownloadVersion
|
||||
? `${repoReleases}/tag/desktop-${desktopDownloadVersion}` : repoReleases;
|
||||
const desktopVersionLabel = desktopDownloadVersion || 'latest';
|
||||
const desktopNotes = desktopDownloadVersion
|
||||
? `${base}/changelog/${desktopDownloadVersion}/` : `${base}/changelog/`;
|
||||
const vscodeMarketplace = 'https://marketplace.visualstudio.com/items?itemName=SivanLiu.reasonix-agent';
|
||||
const openVsx = 'https://open-vsx.org/extension/SivanLiu/reasonix-agent';
|
||||
const vscodeExtensionRepo = 'https://github.com/SivanCola/reasonix-vscode';
|
||||
|
||||
const goVer = 'latest';
|
||||
|
||||
const fmtN = (n) => Number(n).toLocaleString('en-US');
|
||||
const avClass = (i) => 'av' + (i % 5 === 2 ? ' sm' : i % 7 === 3 ? ' lg' : '');
|
||||
const half = Math.ceil(community.list.length / 2);
|
||||
const rowA = community.list.slice(0, half);
|
||||
const rowB = community.list.slice(half);
|
||||
|
||||
const desc = "Open-source coding agent for your terminal, desktop, browser and editor. The program runs on your computer and uses the model account you configure. Describe a task; it reads your project, edits files and runs commands. You choose how often it asks, and files its edit tools changed can be rewound. Use it as a desktop app, in the terminal, in the browser or in your editor. MIT.";
|
||||
const ld = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Reasonix',
|
||||
description: desc,
|
||||
url: 'https://reasonix.io/',
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
operatingSystem: 'macOS, Windows, Linux',
|
||||
license: `${repo}/blob/main-v2/LICENSE`,
|
||||
downloadUrl: `${repo}/releases`,
|
||||
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
||||
};
|
||||
---
|
||||
<Base
|
||||
title="Reasonix — an open-source coding agent for your terminal, desktop, browser and editor"
|
||||
titleEn="Reasonix — an open-source coding agent for your terminal, desktop, browser and editor"
|
||||
titleZh="Reasonix — 终端、桌面、浏览器、编辑器里都能用的 AI 编程助手"
|
||||
description={desc}>
|
||||
|
||||
<script type="application/ld+json" set:html={JSON.stringify(ld)} slot="head" is:inline />
|
||||
|
||||
<SiteHeader />
|
||||
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="wrap">
|
||||
<div class="eyebrow reveal"><span class="dot"></span><span class="l-en">Open source · MIT · runs on your computer, model of your choice</span><span class="l-zh">开源 · MIT · 程序跑在你电脑上,模型自选</span></div>
|
||||
<h1 class="reveal" style="--d:0.06s">
|
||||
<span class="l-en">An open-source <span class="tt">coding agent</span><br />for your terminal, desktop,<br />browser and editor.</span>
|
||||
<span class="l-zh" style="font-size:0.82em"><span style="white-space:nowrap">终端、</span><span style="white-space:nowrap">桌面、</span><span style="white-space:nowrap">浏览器、</span><span style="white-space:nowrap">编辑器里</span><span style="white-space:nowrap">都能用的</span> <span class="tt" style="white-space:nowrap">AI 编程助手</span>。</span>
|
||||
</h1>
|
||||
<p class="sub reveal" style="--d:0.12s">
|
||||
<span class="l-en">It reads your project, edits files and runs commands and tests. Have it ask before each step, or work on its own within limits you set. Files its edit tools changed can be rewound. New here? Start with the Studio desktop app.</span>
|
||||
<span class="l-zh">它会读你的项目、改文件、跑命令和测试。每一步先问你,或在你划定的范围内自己做,由你选;它用编辑工具改过的文件可以退回去。第一次用,从桌面应用 Studio 开始。</span>
|
||||
</p>
|
||||
<div class="hero-install reveal" style="--d:0.18s" aria-label="Reasonix install options">
|
||||
<div class="install-option install-option--desktop">
|
||||
<canvas class="particle-canvas"></canvas>
|
||||
<span class="install-kicker"><span class="l-en">Reasonix Studio · recommended</span><span class="l-zh">Reasonix Studio · 推荐</span></span>
|
||||
<strong><span class="l-en">The desktop app. The easiest way in.</span><span class="l-zh">桌面应用,有图形界面,第一次用就从这里开始。</span></strong>
|
||||
<a class="btn btn-dark" data-os-dl data-goto="studio" href="#start"><span class="l-en">Download Studio<span class="os-for" hidden> for <span class="os-name"></span></span></span><span class="l-zh">下载 Studio<span class="os-for" hidden>(<span class="os-name"></span>)</span></span></a>
|
||||
</div>
|
||||
<div class="install-option install-option--cli">
|
||||
<canvas class="particle-canvas"></canvas>
|
||||
<span class="install-kicker"><span class="l-en">Command line (<span class="keep-case">2.x</span>)</span><span class="l-zh">命令行(<span class="keep-case">2.x</span>)</span></span>
|
||||
<strong><span class="l-en">For the terminal, on the same 2.x line as Studio.</span><span class="l-zh">在终端里用,和 Studio 同属 2.x 版本线。</span></strong>
|
||||
<span class="install"><span class="ps1">$</span><span class="install-command">npm i -g reasonix</span><button data-copy="npm i -g reasonix">Copy</button></span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="also reveal" style="--d:0.22s" aria-label="More ways to use Reasonix / 更多使用方式">
|
||||
<span class="l-en also-row">
|
||||
<strong class="also-label">More ways to use Reasonix:</strong>
|
||||
<span class="also-links">
|
||||
<a href={`${base}/remote/`}>Web Studio</a>
|
||||
<span class="also-sep" aria-hidden="true">·</span>
|
||||
<a href="?download=vscode#start" data-goto="vscode">Editor extension</a>
|
||||
<span class="also-sep" aria-hidden="true">·</span>
|
||||
<a href="#start" data-goto="brew">Homebrew</a>
|
||||
<span class="also-sep" aria-hidden="true">·</span>
|
||||
<span>Local Web UI: <code>reasonix serve</code></span>
|
||||
<span class="also-sep" aria-hidden="true">·</span>
|
||||
<a href={`${base}/docs/#acp`}>ACP editor integration (other editors)</a>
|
||||
</span>
|
||||
</span>
|
||||
<span class="l-zh also-row">
|
||||
<strong class="also-label">更多使用方式:</strong>
|
||||
<span class="also-links">
|
||||
<a href={`${base}/remote/`}>网页版 Studio</a>
|
||||
<span class="also-sep" aria-hidden="true">·</span>
|
||||
<a href="?download=vscode#start" data-goto="vscode">编辑器扩展</a>
|
||||
<span class="also-sep" aria-hidden="true">·</span>
|
||||
<a href="#start" data-goto="brew">Homebrew</a>
|
||||
<span class="also-sep" aria-hidden="true">·</span>
|
||||
<span>本地 Web UI:<code>reasonix serve</code></span>
|
||||
<span class="also-sep" aria-hidden="true">·</span>
|
||||
<a href={`${base}/docs/#acp`}>ACP 编辑器接入(其他编辑器)</a>
|
||||
</span>
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
<div class="term-stage reveal" style="--d:0.26s">
|
||||
<div class="fig"><span class="l-en">fig. 01 — one task, start to finish</span><span class="l-zh">图 01 —— 一个任务,从开始到结束</span></div>
|
||||
<div class="term">
|
||||
<div class="term-bar"><i></i><i></i><i></i><span class="title">~/app — reasonix</span></div>
|
||||
<div class="term-body"><span class="tl"><span class="t-acc">◆</span> reasonix <span class="t-dim"><span class="rxv">{goVer}</span> · deepseek-v4-flash · ~/app</span></span><span class="tl"><span class="t-vio">›</span> add retry with backoff to the http client</span><span class="tl"><span class="t-acc">◇</span> plan <span class="t-dim">3 steps · 2 files · 1 test run</span></span><span class="tl"><span class="t-warm">●</span> allow <span class="t-dim">edit internal/net/client.go</span> <span class="t-ok">yes</span></span><span class="tl"><span class="t-ok">✓</span> edit <span class="t-dim">internal/net/client.go</span> <span class="t-add">+24 −3</span></span><span class="tl"><span class="t-ok">✓</span> edit <span class="t-dim">internal/net/client_test.go</span> <span class="t-add">+41 −0</span></span><span class="tl"><span class="t-ok">✓</span> run <span class="t-dim">go test ./internal/net/</span> <span class="t-ok">ok</span> <span class="t-dim">(0.21s)</span></span><span class="tl"><span class="t-acc">◆</span> checkpoint <span class="t-dim">#14 — revert this turn anytime</span></span><span class="tl"><span class="t-vio">›</span> <span class="caret"></span></span></div>
|
||||
<div class="term-stat"><span>surface <b>cli</b></span><span>tools <b data-cnt="9" data-suf=" calls">9 calls</b></span><span>session <b data-cnt="18" data-suf="m">18m</b></span><span>cost <b data-cnt="0.043" data-dec="3" data-pre="$">$0.043</b></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="features">
|
||||
<div class="wrap">
|
||||
<div class="sec-head split reveal">
|
||||
<span class="kicker"><span class="l-en">Features</span><span class="l-zh">特性</span></span>
|
||||
<h2><span class="l-en">You set the permissions. <span class="em">File edits can be rewound.</span></span><span class="l-zh">权限你来定,<span class="em" style="white-space:nowrap">改过的文件能退回。</span></span></h2>
|
||||
<p><span class="l-en">Read and edit files, run commands and tests, with you in control of each step.</span><span class="l-zh">读文件、改文件、跑命令和测试,每一步由你掌控。</span></p>
|
||||
</div>
|
||||
<div class="feat-grid">
|
||||
<div class="feat reveal">
|
||||
<span class="tag">01 / permissions</span>
|
||||
<h3><span class="l-en">Permission levels</span><span class="l-zh">权限分档</span></h3>
|
||||
<p><span class="l-en">Ask before every write and command, or let everyday actions run on their own. Your forbid rules always hold. On macOS and Linux, commands also run in a sandbox that limits where they can write.</span><span class="l-zh">每次写文件、跑命令都先问你,或让日常操作自动放行。你设的禁止规则始终有效。macOS 和 Linux 上,命令还跑在沙箱里,可写的位置受限。</span></p>
|
||||
<code class="feat-chip">$ allow edit internal/net/client.go? [y/n/always]</code>
|
||||
</div>
|
||||
<div class="feat reveal" style="--d:0.06s">
|
||||
<span class="tag">02 / checkpoints</span>
|
||||
<h3><span class="l-en">Rewind file edits</span><span class="l-zh">文件改动可回退</span></h3>
|
||||
<p><span class="l-en">A snapshot is saved before each file change, outside git. Rewind by conversation turn without touching your commit history. Only files changed by its edit tools are covered, not what a shell command did.</span><span class="l-zh">改文件前先存快照(不走 git),按对话轮次回退,不动提交历史。只覆盖编辑工具改的文件,命令行命令造成的改动不在内。</span></p>
|
||||
</div>
|
||||
<div class="feat reveal" style="--d:0.04s">
|
||||
<span class="tag">03 / plan mode</span>
|
||||
<h3><span class="l-en">Plan mode</span><span class="l-zh">计划模式</span></h3>
|
||||
<p><span class="l-en">It drafts a plan and waits. Work starts only after you approve.</span><span class="l-zh">先出计划并等你。批准之后才开始动手。</span></p>
|
||||
</div>
|
||||
<div class="feat reveal" style="--d:0.1s">
|
||||
<span class="tag">04 / mcp + skills</span>
|
||||
<h3><span class="l-en">More tools</span><span class="l-zh">接入更多工具</span></h3>
|
||||
<p><span class="l-en">Connect outside tools through MCP, a common plug-in standard. Write your own skills as Markdown files, and hand side tasks such as research or review to helper agents.</span><span class="l-zh">用 MCP(通用的工具接入标准)接外部工具,用 Markdown 文件写自己的技能,调研、审查这类支线任务交给辅助智能体。</span></p>
|
||||
<code class="feat-chip">$ reasonix mcp add · ~/.reasonix/skills/</code>
|
||||
</div>
|
||||
<div class="feat reveal" style="--d:0.06s">
|
||||
<span class="tag">05 / single binary</span>
|
||||
<h3><span class="l-en">Nothing else to install</span><span class="l-zh">不用另外装环境</span></h3>
|
||||
<p><span class="l-en">The command-line tool is a single self-contained program for macOS, Linux and Windows, on Intel/AMD and ARM chips.</span><span class="l-zh">命令行版就是一个独立的程序,macOS、Linux、Windows 都有,Intel/AMD 和 ARM 芯片都行。</span></p>
|
||||
</div>
|
||||
<div class="feat reveal" style="--d:0.1s">
|
||||
<span class="tag">06 / local-first</span>
|
||||
<h3><span class="l-en">Your key, your model account</span><span class="l-zh">你的密钥,你的模型账号</span></h3>
|
||||
<p><span class="l-en">Bring your own model key; DeepSeek is built in, and other OpenAI-compatible services work too. The program runs on your computer. MIT licensed, developed in public.</span><span class="l-zh">自带模型密钥:内置 DeepSeek,其他兼容 OpenAI 接口的服务也能用。程序跑在你的电脑上。MIT 许可,公开开发。</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider" aria-hidden="true"><span class="line"></span></div>
|
||||
|
||||
<section id="surfaces">
|
||||
<div class="wrap">
|
||||
<div class="sec-head split reveal">
|
||||
<span class="kicker"><span class="l-en">Surfaces</span><span class="l-zh">使用形态</span></span>
|
||||
<h2><span class="l-en">Four ways to use it. <span class="em">Same program.</span></span><span class="l-zh">四种用法,<span class="em">同一套程序。</span></span></h2>
|
||||
<p><span class="l-en">Four ways to use Reasonix, all running the same program. Studio and the command line are both on the 2.x line; the 1.x desktop app is the previous line, kept for bug fixes only.</span><span class="l-zh">四种用法,背后是同一套程序。Studio 与命令行同属 2.x 版本线;1.x 桌面端是上一代版本线,只做缺陷维护。</span></p>
|
||||
</div>
|
||||
<div class="surf-grid">
|
||||
<div class="surf reveal">
|
||||
<div class="surf-art" aria-hidden="true">
|
||||
<span class="surf-win app">
|
||||
<i class="bar"></i>
|
||||
<span class="body"><span class="side"><i></i><i></i><i></i></span><span class="main"><i class="l" style="--w:80%"></i><i class="l" style="--w:55%"></i><i class="l ok" style="--w:66%"></i></span></span>
|
||||
</span>
|
||||
</div>
|
||||
<h3><span class="l-en">Desktop app · Studio</span><span class="l-zh">桌面端 · Studio</span></h3>
|
||||
<p><span class="l-en">A graphical app: several sessions side by side, approvals, settings, auto-update.</span><span class="l-zh">图形界面:多个会话并排,确认操作、改设置、自动更新。</span></p>
|
||||
<code><span class="l-en">macOS (Apple Silicon, Intel) · Windows x64 · Linux amd64 and arm64 (.deb, .rpm)</span><span class="l-zh">macOS(Apple Silicon、Intel)· Windows x64 · Linux amd64 与 arm64(.deb、.rpm)</span></code>
|
||||
</div>
|
||||
<div class="surf reveal" style="--d:0.06s">
|
||||
<div class="surf-art" aria-hidden="true">
|
||||
<span class="surf-win cli">
|
||||
<i class="bar"></i>
|
||||
<span class="body"><i class="l" style="--w:62%"></i><i class="l" style="--w:44%"></i><i class="l ok" style="--w:70%"></i><i class="l" style="--w:38%"></i><i class="cur"></i></span>
|
||||
</span>
|
||||
</div>
|
||||
<h3><span class="l-en">Terminal · 2.x</span><span class="l-zh">终端 · 2.x</span></h3>
|
||||
<p><span class="l-en">A full-screen text interface: review changes, approve actions, use slash commands.</span><span class="l-zh">全屏文字界面:看改动、确认操作、用斜杠命令。</span></p>
|
||||
<code>reasonix</code>
|
||||
</div>
|
||||
<div class="surf reveal" style="--d:0.12s">
|
||||
<div class="surf-art" aria-hidden="true">
|
||||
<span class="surf-win web">
|
||||
<i class="bar url"></i>
|
||||
<span class="body"><i class="l" style="--w:72%"></i><i class="l" style="--w:48%"></i><i class="l ok" style="--w:60%"></i><i class="l" style="--w:36%"></i></span>
|
||||
</span>
|
||||
</div>
|
||||
<h3><span class="l-en">Browser</span><span class="l-zh">浏览器</span></h3>
|
||||
<p><span class="l-en">A web page served from your own machine. Turn on a password or token before sharing it.</span><span class="l-zh">在你自己的机器上开的网页界面。要分享给别人,先开密码或令牌。</span></p>
|
||||
<code>reasonix serve</code>
|
||||
</div>
|
||||
<div class="surf reveal" style="--d:0.18s">
|
||||
<div class="surf-art" aria-hidden="true">
|
||||
<span class="surf-win ide">
|
||||
<i class="bar"></i>
|
||||
<span class="body"><span class="tree"><i></i><i></i><i></i><i></i></span><span class="main"><i class="l" style="--w:70%"></i><i class="l add" style="--w:52%"></i><i class="l add" style="--w:44%"></i><i class="l" style="--w:64%"></i></span></span>
|
||||
</span>
|
||||
</div>
|
||||
<h3><span class="l-en">Your editor</span><span class="l-zh">你的编辑器</span></h3>
|
||||
<p><span class="l-en">VS Code and VSCodium via the extension. Other editors that support ACP (Agent Client Protocol, a common way for editors to connect to an AI assistant) can connect too.</span><span class="l-zh">VS Code 和 VSCodium 装个扩展就行。别的编辑器只要支持 ACP(编辑器接入 AI 助手的通用约定),也能连上。</span></p>
|
||||
<code>reasonix acp</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider" aria-hidden="true"><span class="line"></span></div>
|
||||
|
||||
<section>
|
||||
<div class="wrap">
|
||||
<div class="steps">
|
||||
<div class="step reveal">
|
||||
<span class="num">01</span>
|
||||
<h3><span class="l-en">Install</span><span class="l-zh">安装</span></h3>
|
||||
<p><span class="l-en">Download Reasonix Studio for macOS, Windows, or Linux — or use the 2.x command line:</span><span class="l-zh">下载 macOS、Windows 或 Linux 版 Reasonix Studio,或使用 2.x 命令行:</span></p>
|
||||
<code>npm i -g reasonix</code>
|
||||
<a class="step-link" href="#start" data-goto="studio"><span class="l-en">Get Studio →</span><span class="l-zh">获取 Studio →</span></a>
|
||||
</div>
|
||||
<div class="step reveal" style="--d:0.08s">
|
||||
<span class="num">02</span>
|
||||
<h3><span class="l-en">Open your project</span><span class="l-zh">打开你的项目</span></h3>
|
||||
<p><span class="l-en">Open a project folder in Studio, or run it from a terminal inside the folder. Then write what you want done.</span><span class="l-zh">在 Studio 里打开项目文件夹,或在终端里进到该文件夹再运行。然后写下想做的事。</span></p>
|
||||
<code>cd your-project && reasonix</code>
|
||||
</div>
|
||||
<div class="step reveal" style="--d:0.16s">
|
||||
<span class="num">03</span>
|
||||
<h3><span class="l-en">Or use the browser</span><span class="l-zh">或者用浏览器</span></h3>
|
||||
<p><span class="l-en">Keep it on your machine, or turn on a token or password before sharing it over a tunnel or remote port.</span><span class="l-zh">只在本机用,保持默认就行;要通过隧道或远程端口分享,先开令牌或密码。</span></p>
|
||||
<code>reasonix serve --auth token</code>
|
||||
</div>
|
||||
<div class="step reveal" style="--d:0.24s">
|
||||
<span class="num">04</span>
|
||||
<h3><span class="l-en">Review and continue</span><span class="l-zh">看结果,接着做</span></h3>
|
||||
<p><span class="l-en">Queue follow-up tasks, review the changes, and pick a session up again later.</span><span class="l-zh">接着排任务,看改动,改天也能回到这个会话继续。</span></p>
|
||||
<code>› refactor the auth flow</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider" aria-hidden="true"><span class="line"></span></div>
|
||||
|
||||
<section id="community">
|
||||
<div class="wrap">
|
||||
<div class="sec-head split reveal">
|
||||
<span class="kicker"><span class="l-en">Community</span><span class="l-zh">社区共建</span></span>
|
||||
<h2><span class="l-en">Built in the open, with <span class="em">{community.contributors} contributors.</span></span><span class="l-zh">开源共建,感谢 <span class="em">{community.contributors} 位贡献者。</span></span></h2>
|
||||
<p><span class="l-en">Reasonix is MIT-licensed and developed in public. Every issue, every PR, every late-night review made it what it is — thank you.</span><span class="l-zh">Reasonix 采用 MIT 许可,开放开发。每一个 issue、每一条 PR、每一次深夜 review,成就了今天的它——谢谢你们。</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="crew reveal" aria-hidden="false">
|
||||
<div class="crew-row">
|
||||
<span class="crew-set">
|
||||
{rowA.map((c, i) => (
|
||||
<a class={avClass(i)} data-h={`@${c.login}`} href={c.url} target="_blank" rel="noreferrer" aria-label={c.login}>
|
||||
<img src={c.avatar} alt={c.login} loading="lazy" width="60" height="60" />
|
||||
</a>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
<div class="crew-row rev">
|
||||
<span class="crew-set">
|
||||
{rowB.map((c, i) => (
|
||||
<a class={avClass(i + 3)} data-h={`@${c.login}`} href={c.url} target="_blank" rel="noreferrer" aria-label={c.login}>
|
||||
<img src={c.avatar} alt={c.login} loading="lazy" width="60" height="60" />
|
||||
</a>
|
||||
))}
|
||||
<a class="av you" data-h="this could be you · 虚位以待" href={`${repo}/issues`} target="_blank" rel="noreferrer">+</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrap">
|
||||
<div class="crew-cta reveal">
|
||||
<a class="btn btn-dark" href={repo}><span class="l-en">Contribute on GitHub</span><span class="l-zh">在 GitHub 上参与贡献</span></a>
|
||||
<a class="btn btn-ghost" href={`${repo}/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22`}><span class="l-en">Good first issues →</span><span class="l-zh">新手友好任务 →</span></a>
|
||||
</div>
|
||||
<div class="crew-note reveal"><span class="l-en">{fmtN(community.stars)} stars · {fmtN(community.mergedPrs)} merged PRs · every avatar above is a real contributor</span><span class="l-zh">{fmtN(community.stars)} stars · {fmtN(community.mergedPrs)} 个已合并 PR · 上面每个头像都是一位真实贡献者</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cta" id="start">
|
||||
<h2 class="reveal"><span class="l-en">Try it on a <span class="em">real project.</span></span><span class="l-zh">拿个<span class="em">真项目</span>试试。</span></h2>
|
||||
<p class="reveal" style="--d:0.08s"><span class="l-en">Open source under MIT.</span><span class="l-zh">MIT 开源。</span></p>
|
||||
|
||||
<div class="dl reveal" style="--d:0.14s">
|
||||
<div class="dl-tabs" role="tablist" aria-label="Download method / 下载方式">
|
||||
<button class="dl-tab active" id="download-tab-studio" type="button" data-pane="studio" role="tab" aria-controls="download-pane-studio" aria-selected="true" tabindex="0">Studio</button>
|
||||
<button class="dl-tab" id="download-tab-cli" type="button" data-pane="cli" role="tab" aria-controls="download-pane-cli" aria-selected="false" tabindex="-1"><span class="l-en">Command line 2.x</span><span class="l-zh">命令行 2.x</span></button>
|
||||
<button class="dl-tab" id="download-tab-npm" type="button" data-pane="npm" role="tab" aria-controls="download-pane-npm" aria-selected="false" tabindex="-1">npm</button>
|
||||
<button class="dl-tab" id="download-tab-brew" type="button" data-pane="brew" role="tab" aria-controls="download-pane-brew" aria-selected="false" tabindex="-1">Homebrew</button>
|
||||
<button class="dl-tab" id="download-tab-vscode" type="button" data-pane="vscode" role="tab" aria-controls="download-pane-vscode" aria-selected="false" tabindex="-1"><span class="l-en">Editor extension</span><span class="l-zh">编辑器扩展</span></button>
|
||||
<button class="dl-tab dl-tab--legacy" id="download-tab-desktop" type="button" data-pane="desktop" role="tab" aria-controls="download-pane-desktop" aria-selected="false" tabindex="-1"><span class="l-en">1.x · maintenance only</span><span class="l-zh">1.x · 仅维护</span></button>
|
||||
</div>
|
||||
|
||||
<div class="version-guide" aria-label="Reasonix version lines / Reasonix 版本选择">
|
||||
<div>
|
||||
<strong>Studio 2.x <span class="version-guide-badge"><span class="l-en">recommended</span><span class="l-zh">推荐</span></span></strong>
|
||||
<span><span class="l-en">The desktop app on the new architecture, with the newest features.</span><span class="l-zh">新架构桌面应用,包含最新功能。</span></span>
|
||||
</div>
|
||||
<div>
|
||||
<strong><span class="l-en">Command line 2.x</span><span class="l-zh">命令行 2.x</span></strong>
|
||||
<span><span class="l-en">Terminal, scripting and headless use — the same 2.x line as Studio.</span><span class="l-zh">终端、脚本与无界面场景,与 Studio 同属 2.x 版本线。</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dl-pane active" id="download-pane-studio" data-pane="studio" role="tabpanel" aria-labelledby="download-tab-studio">
|
||||
<div class="release-channel-summary" data-release-summary="studio" aria-live="polite">
|
||||
<span><span class="l-en">Reasonix Studio 2.x · <span data-release-version="studio">latest</span></span><span class="l-zh">Reasonix Studio 2.x · <span data-release-version="studio">latest</span></span></span>
|
||||
</div>
|
||||
<p class="dl-platform-note" data-platform-note hidden><span class="l-en">Studio is a desktop app with builds for macOS (Apple Silicon, Intel), Windows x64, and Linux amd64 and arm64 (.deb, .rpm). <span data-platform-reason="mobile">Open this page on a computer to download it.</span><span data-platform-reason="chromeos">None matches ChromeOS.</span><span data-platform-reason="arm-linux">None matches 32-bit ARM Linux.</span> The 2.x command line below has more platforms.</span><span class="l-zh">Studio 是桌面应用,提供 macOS(Apple Silicon、Intel)、Windows x64 与 Linux(amd64、arm64,.deb、.rpm)安装包。<span data-platform-reason="mobile">请在电脑上打开本页下载。</span><span data-platform-reason="chromeos">暂无适用于 ChromeOS 的安装包。</span><span data-platform-reason="arm-linux">暂无适用于 32 位 ARM Linux 的安装包。</span>下方的 2.x 命令行支持更多平台。</span></p>
|
||||
<div class="os-grid">
|
||||
<div class="os-card" data-os="mac">
|
||||
<h4>macOS</h4>
|
||||
<span class="meta"><span class="l-en">DMG · pick your chip</span><span class="l-zh">DMG · 请按芯片选择</span></span>
|
||||
<a class="btn btn-light" href={studioReleases} data-studio-asset="ReasonixStudio-darwin-arm64.dmg"><span class="l-en">Apple Silicon · <span data-release-version="studio">latest</span></span><span class="l-zh">Apple Silicon · <span data-release-version="studio">latest</span></span></a>
|
||||
<a class="btn btn-light" href={studioReleases} data-studio-asset="ReasonixStudio-darwin-amd64.dmg"><span class="l-en">Intel · <span data-release-version="studio">latest</span></span><span class="l-zh">Intel · <span data-release-version="studio">latest</span></span></a>
|
||||
</div>
|
||||
<div class="os-card" data-os="win">
|
||||
<h4>Windows</h4>
|
||||
<span class="meta"><span class="l-en">Installer · Windows 10+ · x64<span data-arm-offered hidden> or ARM64</span></span><span class="l-zh">安装器 · Windows 10+ · x64<span data-arm-offered hidden> 或 ARM64</span></span></span>
|
||||
<a class="btn btn-light" href={studioReleases} data-studio-asset="ReasonixStudio-windows-amd64-installer.exe"><span class="l-en"><span data-arm-offered hidden>x64 · </span>Download · <span data-release-version="studio">latest</span></span><span class="l-zh"><span data-arm-offered hidden>x64 · </span>下载 · <span data-release-version="studio">latest</span></span></a>
|
||||
<a class="btn btn-light" href={studioReleases} data-studio-asset="ReasonixStudio-windows-arm64-installer.exe" data-studio-optional hidden><span class="l-en">ARM64 · Download · <span data-release-version="studio">latest</span><span data-arm-rec hidden> · recommended</span></span><span class="l-zh">ARM64 · 下载 · <span data-release-version="studio">latest</span><span data-arm-rec hidden> · 推荐</span></span></a>
|
||||
<p class="alt" data-arm-offered hidden><span class="l-en">Already running the x64 build on an ARM64 PC? It does not switch to ARM64 by itself: install the ARM64 build once, and later updates stay on ARM64.</span><span class="l-zh">已经在 ARM64 电脑上运行 x64 版?它不会自动切换到 ARM64 版:请安装一次 ARM64 版,之后的更新会一直走 ARM64。</span></p>
|
||||
<p class="alt" data-arm-pending hidden><span class="l-en">Windows on ARM devices run the x64 installer through emulation. A native ARM64 build is coming soon.</span><span class="l-zh">ARM 设备上的 Windows 可通过模拟运行 x64 安装器,原生 ARM64 版即将提供。</span></p>
|
||||
</div>
|
||||
<div class="os-card" data-os="linux">
|
||||
<h4>Linux</h4>
|
||||
<span class="meta"><span class="l-en">.deb (Debian / Ubuntu) and .rpm (Fedora / openSUSE) · amd64 and arm64</span><span class="l-zh">.deb(Debian / Ubuntu)与 .rpm(Fedora / openSUSE)· amd64 与 arm64</span></span>
|
||||
<a class="btn btn-light" href={studioReleases} data-studio-asset="ReasonixStudio-linux-amd64.deb"><span class="l-en">.deb · amd64 · <span data-release-version="studio">latest</span></span><span class="l-zh">.deb · amd64 · <span data-release-version="studio">latest</span></span></a>
|
||||
<a class="btn btn-light" href={studioReleases} data-studio-asset="ReasonixStudio-linux-amd64.rpm" data-studio-optional hidden><span class="l-en">.rpm · amd64 · <span data-release-version="studio">latest</span></span><span class="l-zh">.rpm · amd64 · <span data-release-version="studio">latest</span></span></a>
|
||||
<a class="btn btn-light" href={studioReleases} data-studio-asset="ReasonixStudio-linux-arm64.deb" data-studio-optional hidden><span class="l-en">.deb · arm64 · <span data-release-version="studio">latest</span></span><span class="l-zh">.deb · arm64 · <span data-release-version="studio">latest</span></span></a>
|
||||
<a class="btn btn-light" href={studioReleases} data-studio-asset="ReasonixStudio-linux-arm64.rpm" data-studio-optional hidden><span class="l-en">.rpm · arm64 · <span data-release-version="studio">latest</span></span><span class="l-zh">.rpm · arm64 · <span data-release-version="studio">latest</span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dl-note"><span class="l-en"><a href={studioReleases} data-release-notes="studio">Release notes →</a> · <a href={studioReleases}>all Studio releases →</a></span><span class="l-zh"><a href={studioReleases} data-release-notes="studio">更新说明 →</a> · <a href={studioReleases}>全部 Studio 版本 →</a></span></div>
|
||||
<div class="dl-alt">
|
||||
<strong><span class="l-en">Prefer the terminal? Command line (2.x)</span><span class="l-zh">更喜欢终端?命令行(2.x)</span></strong>
|
||||
<span class="install"><span class="ps1">$</span>npm i -g reasonix<button data-copy="npm i -g reasonix">Copy</button></span>
|
||||
<a href="#start" data-goto="cli"><span class="l-en">Binaries, Homebrew and more →</span><span class="l-zh">二进制包、Homebrew 等 →</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dl-pane" id="download-pane-desktop" data-pane="desktop" role="tabpanel" aria-labelledby="download-tab-desktop" hidden>
|
||||
<div class="release-channel-summary" data-release-summary="desktop" aria-live="polite">
|
||||
<span><span class="l-en">Reasonix desktop 1.x (previous line, maintenance only: bug fixes, no new features) · <span data-release-version="desktop">{desktopVersionLabel}</span></span><span class="l-zh">Reasonix 桌面端 1.x(上一代,仅维护:只修缺陷,不加新功能)· <span data-release-version="desktop">{desktopVersionLabel}</span></span></span>
|
||||
</div>
|
||||
<div class="os-grid">
|
||||
<div class="os-card" data-os="mac">
|
||||
<h4>macOS</h4>
|
||||
<span class="meta"><span class="l-en">Universal DMG · Apple Silicon & Intel</span><span class="l-zh">通用 DMG · Apple Silicon 与 Intel</span></span>
|
||||
<a class="btn btn-light" href={desktopStableFallback} data-desktop-asset="Reasonix-darwin-universal.dmg"><span class="l-en">Download Universal · <span data-release-version="desktop">{desktopVersionLabel}</span></span><span class="l-zh">下载通用版 · <span data-release-version="desktop">{desktopVersionLabel}</span></span></a>
|
||||
{!desktopDownloadVersion && <div class="os-options" aria-label="macOS alternate downloads">
|
||||
<a href={desktopStableFallback} data-desktop-asset="Reasonix-darwin-arm64.dmg">Apple Silicon DMG</a>
|
||||
<a href={desktopStableFallback} data-desktop-asset="Reasonix-darwin-amd64.dmg">Intel DMG</a>
|
||||
</div>}
|
||||
</div>
|
||||
<div class="os-card" data-os="win">
|
||||
<h4>Windows</h4>
|
||||
<span class="meta"><span class="l-en">Installer · Windows 10+ · x64 / ARM64</span><span class="l-zh">安装器 · Windows 10+ · x64 / ARM64</span></span>
|
||||
<a class="btn btn-light" href={desktopStableFallback} data-desktop-asset="Reasonix-windows-amd64-installer.exe"><span class="l-en">Download · <span data-release-version="desktop">{desktopVersionLabel}</span></span><span class="l-zh">下载 · <span data-release-version="desktop">{desktopVersionLabel}</span></span></a>
|
||||
<div class="os-options" aria-label="Windows alternate downloads">
|
||||
<a href={desktopStableFallback} data-desktop-asset="Reasonix-windows-arm64-installer.exe">ARM64 installer</a>
|
||||
<a href={desktopStableFallback} data-desktop-asset="Reasonix-windows-amd64.zip">Portable zip</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="os-card" data-os="linux">
|
||||
<h4>Linux</h4>
|
||||
<span class="meta"><span class="l-en">Debian / Ubuntu package · amd64</span><span class="l-zh">Debian / Ubuntu 安装包 · amd64</span></span>
|
||||
<a class="btn btn-light" href={desktopStableFallback} data-desktop-asset="Reasonix-linux-amd64.deb"><span class="l-en">Download .deb · <span data-release-version="desktop">{desktopVersionLabel}</span></span><span class="l-zh">下载 .deb · <span data-release-version="desktop">{desktopVersionLabel}</span></span></a>
|
||||
<div class="os-options" aria-label="Linux alternate downloads">
|
||||
<a href={desktopStableFallback} data-desktop-asset="Reasonix-linux-amd64.tar.gz">Generic tar.gz</a>
|
||||
<a href={`${repo}/releases`}><span class="l-en">All releases</span><span class="l-zh">全部版本</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dl-note"><span class="l-en">Reasonix checks official releases automatically · <a href={desktopNotes} data-release-notes="desktop">release notes →</a> · <a href={`${repo}/releases`}>all releases →</a></span><span class="l-zh">Reasonix 会自动检查正式版本 · <a href={desktopNotes} data-release-notes="desktop">更新日志 →</a> · <a href={`${repo}/releases`}>全部版本 →</a></span></div>
|
||||
<div class="dl-help">
|
||||
<strong><span class="l-en">macOS opens with a quarantine warning?</span><span class="l-zh">macOS 提示无法打开或已损坏?</span></strong>
|
||||
<span><span class="l-en">If the app was installed from the official download and placed in <code>/Applications</code>, quit Reasonix, then run:</span><span class="l-zh">如果来自官网下载并已放入 <code>/Applications</code>,先退出 Reasonix,再运行:</span></span>
|
||||
<code>sudo xattr -rd com.apple.quarantine /Applications/Reasonix.app</code>
|
||||
<button type="button" data-copy="sudo xattr -rd com.apple.quarantine /Applications/Reasonix.app">Copy</button>
|
||||
<a href={`${base}/docs/#macos-quarantine`}><span class="l-en">When to use this →</span><span class="l-zh">查看适用场景 →</span></a>
|
||||
</div>
|
||||
{!desktopDownloadVersion && <div class="dl-help">
|
||||
<strong><span class="l-en">Updating from 1.38.3 or earlier?</span><span class="l-zh">从 1.38.3 或更早版本升级?</span></strong>
|
||||
<span><span class="l-en">The in-app updater cannot cross the Electron shell change and reports <code>unsupported install_layout "electron-v1"</code>. Quit Reasonix, then install the full package above (Windows installer, macOS DMG, Linux .deb or tar.gz) over the current version. Configuration, sessions and history are kept.</span><span class="l-zh">应用内更新无法跨越 Electron 壳切换,会提示 <code>unsupported install_layout "electron-v1"</code>。请退出 Reasonix,然后用上方的完整安装包(Windows 安装程序、macOS DMG、Linux .deb 或 tar.gz)覆盖安装。配置、会话与历史记录原样保留。</span></span>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<div class="dl-pane" id="download-pane-cli" data-pane="cli" role="tabpanel" aria-labelledby="download-tab-cli" hidden>
|
||||
<div class="release-channel-summary" data-release-summary="cli" aria-live="polite">
|
||||
<span><span class="l-en">Command line 2.x · current official version <span data-release-version="cli">latest</span></span><span class="l-zh">命令行 2.x · 当前正式版本 <span data-release-version="cli">latest</span></span></span>
|
||||
</div>
|
||||
<span class="install release-upgrade-command"><span class="ps1">$</span><span data-release-command="cli">reasonix upgrade</span><button data-copy="reasonix upgrade">Copy</button></span>
|
||||
<div class="os-grid cli-os-grid">
|
||||
<div class="os-card" data-os="mac">
|
||||
<h4>macOS</h4>
|
||||
<span class="meta">Apple Silicon / Intel · tar.gz</span>
|
||||
<a class="btn btn-light" href={repoReleases} data-cli-asset="reasonix-darwin-arm64.tar.gz"><span class="l-en">Apple Silicon · <span data-release-version="cli">latest</span></span><span class="l-zh">Apple Silicon · <span data-release-version="cli">latest</span></span></a>
|
||||
<div class="os-options"><a href={repoReleases} data-cli-asset="reasonix-darwin-amd64.tar.gz">Intel tar.gz</a></div>
|
||||
</div>
|
||||
<div class="os-card" data-os="win">
|
||||
<h4>Windows</h4>
|
||||
<span class="meta">Windows 10+ · x64 / ARM64 · zip</span>
|
||||
<a class="btn btn-light" href={repoReleases} data-cli-asset="reasonix-windows-amd64.zip"><span class="l-en">Download x64 · <span data-release-version="cli">latest</span></span><span class="l-zh">下载 x64 · <span data-release-version="cli">latest</span></span></a>
|
||||
<div class="os-options"><a href={repoReleases} data-cli-asset="reasonix-windows-arm64.zip">ARM64 zip</a></div>
|
||||
</div>
|
||||
<div class="os-card" data-os="linux">
|
||||
<h4>Linux</h4>
|
||||
<span class="meta">amd64 / ARM64 · tar.gz</span>
|
||||
<a class="btn btn-light" href={repoReleases} data-cli-asset="reasonix-linux-amd64.tar.gz"><span class="l-en">Download amd64 · <span data-release-version="cli">latest</span></span><span class="l-zh">下载 amd64 · <span data-release-version="cli">latest</span></span></a>
|
||||
<div class="os-options"><a href={repoReleases} data-cli-asset="reasonix-linux-arm64.tar.gz">ARM64 tar.gz</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dl-note"><span class="l-en"><a href={repoReleases} data-cli-asset="SHA256SUMS">SHA256SUMS →</a> · <a href={`${base}/changelog/`} data-release-notes="cli">release notes →</a> · npm and Homebrew track the same official version</span><span class="l-zh"><a href={repoReleases} data-cli-asset="SHA256SUMS">SHA256SUMS →</a> · <a href={`${base}/changelog/`} data-release-notes="cli">更新日志 →</a> · npm 与 Homebrew 跟随同一正式版本</span></div>
|
||||
</div>
|
||||
|
||||
<div class="dl-pane" id="download-pane-vscode" data-pane="vscode" role="tabpanel" aria-labelledby="download-tab-vscode" hidden>
|
||||
<div class="os-grid editor-grid">
|
||||
<div class="os-card editor-card editor-card--primary">
|
||||
<span class="os-chip"><span class="l-en">recommended</span><span class="l-zh">推荐</span></span>
|
||||
<h4>Visual Studio Marketplace</h4>
|
||||
<span class="meta"><span class="l-en">For Microsoft Visual Studio Code</span><span class="l-zh">适用于微软 Visual Studio Code</span></span>
|
||||
<a class="btn btn-dark" href={vscodeMarketplace} target="_blank" rel="noreferrer"><span class="l-en">Install extension</span><span class="l-zh">安装扩展</span></a>
|
||||
</div>
|
||||
<div class="os-card editor-card">
|
||||
<h4>Open VSX Registry</h4>
|
||||
<span class="meta"><span class="l-en">For VSCodium, Eclipse Theia, and compatible editors</span><span class="l-zh">适用于 VSCodium、Eclipse Theia 及兼容编辑器</span></span>
|
||||
<a class="btn btn-light" href={openVsx} target="_blank" rel="noreferrer"><span class="l-en">Install from Open VSX</span><span class="l-zh">从 Open VSX 安装</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dl-note editor-note"><span class="l-en">Extension ID <code>SivanLiu.reasonix-agent</code> · requires Reasonix 1.0+ installed and configured locally · the extension connects to <code>reasonix acp</code> and does not bundle the CLI · <a href={vscodeExtensionRepo}>source code →</a></span><span class="l-zh">扩展 ID <code>SivanLiu.reasonix-agent</code> · 需要先在本机安装并配置 Reasonix 1.0+ · 扩展连接 <code>reasonix acp</code>,不内置 CLI · <a href={vscodeExtensionRepo}>查看源码 →</a></span></div>
|
||||
</div>
|
||||
|
||||
<div class="dl-pane" id="download-pane-npm" data-pane="npm" role="tabpanel" aria-labelledby="download-tab-npm" hidden>
|
||||
<span class="install"><span class="ps1">$</span>npm i -g reasonix<button data-copy="npm i -g reasonix">Copy</button></span>
|
||||
<div class="chan">
|
||||
<div class="chan-row rec">
|
||||
<code>npm i -g reasonix</code><span class="v"><span class="rxv">{goVer}</span></span>
|
||||
<span class="s"><span class="l-en">Reasonix 2.x · native binary · launcher needs Node 18+</span><span class="l-zh">Reasonix 2.x · 原生二进制 · 启动器需 Node 18+</span></span>
|
||||
<span class="chan-badge"><span class="l-en">recommended</span><span class="l-zh">推荐</span></span>
|
||||
</div>
|
||||
<div class="chan-row">
|
||||
<code>npm i -g [email protected]</code><span class="v">v0.53.2</span>
|
||||
<span class="s"><span class="l-en">deprecated 0.x line · no longer maintained</span><span class="l-zh">已废弃的 0.x 线 · 不再维护</span></span>
|
||||
<span class="chan-badge pre"><span class="l-en">deprecated</span><span class="l-zh">已废弃</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dl-note"><span class="l-en">macOS / Linux / Windows / WSL · npm <span class="mono">latest</span> tracks the current official release · <a class="rxnotes" href={`${base}/changelog/`}><span class="rxv">{goVer}</span> release notes →</a></span><span class="l-zh">macOS / Linux / Windows / WSL · npm <span class="mono">latest</span> 指向当前正式版 · <a class="rxnotes" href={`${base}/changelog/`}><span class="rxv">{goVer}</span> 更新说明 →</a></span></div>
|
||||
</div>
|
||||
|
||||
<div class="dl-pane" id="download-pane-brew" data-pane="brew" role="tabpanel" aria-labelledby="download-tab-brew" hidden>
|
||||
<span class="install"><span class="ps1">$</span>brew install esengine/reasonix/reasonix<button data-copy="brew install esengine/reasonix/reasonix">Copy</button></span>
|
||||
<div class="dl-note"><span class="l-en"><span class="rxv">{goVer}</span> (Go rewrite) · macOS & Linux · update with <span class="mono">brew upgrade</span></span><span class="l-zh"><span class="rxv">{goVer}</span>(Go 重写)· macOS 与 Linux · 通过 <span class="mono">brew upgrade</span> 更新</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<CommunityJoin />
|
||||
<div class="foot-inner">
|
||||
<a class="brand" href={`${base}/`}><img src={`${base}/logo.svg`} alt="Reasonix logo" /><span>Reasonix</span></a>
|
||||
<span><span class="l-en">© 2026 Reasonix · MIT licensed.</span><span class="l-zh">© 2026 Reasonix · MIT 许可。</span></span>
|
||||
<nav class="foot-links">
|
||||
<a href={repo}>GitHub</a>
|
||||
<a href="https://www.npmjs.com/package/reasonix">npm</a>
|
||||
<a href="https://platform.deepseek.com" target="_blank" rel="noreferrer">DeepSeek</a>
|
||||
<a href={`${base}/docs/`}><span class="l-en">Docs</span><span class="l-zh">文档</span></a>
|
||||
<a href="#start"><span class="l-en">Install</span><span class="l-zh">安装</span></a>
|
||||
<a href="https://crash.reasonix.io/stats" rel="nofollow"><span class="l-en">Stats</span><span class="l-zh">统计</span></a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="foot-sub">
|
||||
<span><span class="l-en">An open-source coding agent for terminal, desktop, browser and editor.</span><span class="l-zh">终端、桌面、浏览器、编辑器里都能用的 AI 编程助手。</span></span>
|
||||
<span class="ver"><span class="rxv">{goVer}</span> · official</span>
|
||||
</div>
|
||||
<SponsorSlot />
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
|
||||
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
document.querySelectorAll('.install-option').forEach((col) => {
|
||||
const canvas = col.querySelector('.particle-canvas');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
let particles = [];
|
||||
let rafId = null;
|
||||
|
||||
canvas.style.willChange = 'transform';
|
||||
|
||||
function resize() {
|
||||
const r = col.getBoundingClientRect();
|
||||
canvas.width = Math.max(1, Math.round(r.width * devicePixelRatio));
|
||||
canvas.height = Math.max(1, Math.round(r.height * devicePixelRatio));
|
||||
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
|
||||
}
|
||||
resize();
|
||||
|
||||
function loop() {
|
||||
const r = col.getBoundingClientRect();
|
||||
const isDark = document.documentElement.dataset.theme === 'dark';
|
||||
const base = isDark ? '255,255,255' : '0,0,0';
|
||||
ctx.clearRect(0, 0, r.width, r.height);
|
||||
particles = particles.filter((p) => p.life > 0);
|
||||
particles.forEach((p) => {
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.vy += 0.01;
|
||||
p.life -= p.decay;
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(${base},${(p.life * 0.28).toFixed(3)})`;
|
||||
ctx.fill();
|
||||
});
|
||||
rafId = particles.length ? requestAnimationFrame(loop) : null;
|
||||
}
|
||||
|
||||
col.addEventListener('mousemove', (e) => {
|
||||
const r = col.getBoundingClientRect();
|
||||
const mx = e.clientX - r.left;
|
||||
const my = e.clientY - r.top;
|
||||
|
||||
if (particles.length > 80) return;
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
particles.push({
|
||||
x: mx + (Math.random() - 0.5) * 20,
|
||||
y: my + (Math.random() - 0.5) * 20,
|
||||
vx: (Math.random() - 0.5) * 1.2,
|
||||
vy: (Math.random() - 0.5) * 1.2 - 0.3,
|
||||
life: 1,
|
||||
decay: 0.015 + Math.random() * 0.015,
|
||||
size: 1.2 + Math.random() * 1.8,
|
||||
});
|
||||
}
|
||||
if (!rafId) loop();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
import Account from '../layouts/Account.astro';
|
||||
import PasswordInput from '../components/PasswordInput.astro';
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
---
|
||||
<Account
|
||||
title="Sign in — Reasonix"
|
||||
titleEn="Sign in — Reasonix"
|
||||
titleZh="登录 — Reasonix"
|
||||
description="Sign in to your Reasonix account.">
|
||||
<div class="auth-head">
|
||||
<h1><span class="l-en">Welcome back</span><span class="l-zh">欢迎回来</span></h1>
|
||||
<p><span class="l-en">Sign in to your Reasonix account.</span><span class="l-zh">登录你的 Reasonix 账号。</span></p>
|
||||
</div>
|
||||
|
||||
<div id="login-msg" class="auth-msg" hidden></div>
|
||||
|
||||
<form id="login-form" class="auth-form" novalidate>
|
||||
<div class="field">
|
||||
<label for="email"><span class="l-en">Email</span><span class="l-zh">邮箱</span></label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="auth-row">
|
||||
<label for="password"><span class="l-en">Password</span><span class="l-zh">密码</span></label>
|
||||
<a class="auth-small" href={`${base}/forgot/`}><span class="l-en">Forgot?</span><span class="l-zh">忘记密码?</span></a>
|
||||
</div>
|
||||
<PasswordInput id="password" name="password" autocomplete="current-password" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-dark"><span class="l-en">Sign in</span><span class="l-zh">登录</span></button>
|
||||
</form>
|
||||
|
||||
<p class="auth-alt">
|
||||
<span class="l-en">New to Reasonix? <a href={`${base}/register/`}>Create an account</a></span>
|
||||
<span class="l-zh">还没有账号?<a href={`${base}/register/`}>注册一个</a></span>
|
||||
</p>
|
||||
</Account>
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
import Account from '../layouts/Account.astro';
|
||||
import PasswordInput from '../components/PasswordInput.astro';
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
---
|
||||
<Account
|
||||
title="Create account — Reasonix"
|
||||
titleEn="Create account — Reasonix"
|
||||
titleZh="注册 — Reasonix"
|
||||
description="Create your Reasonix account.">
|
||||
<div class="auth-head">
|
||||
<h1><span class="l-en">Create your account</span><span class="l-zh">创建账号</span></h1>
|
||||
<p><span class="l-en">Join the Reasonix community.</span><span class="l-zh">加入 Reasonix 社区。</span></p>
|
||||
</div>
|
||||
|
||||
<div id="register-msg" class="auth-msg" hidden></div>
|
||||
|
||||
<form id="register-form" class="auth-form" novalidate>
|
||||
<div class="field">
|
||||
<label for="displayName"><span class="l-en">Display name</span><span class="l-zh">显示名</span> <span class="hint">(<span class="l-en">optional</span><span class="l-zh">可选</span>)</span></label>
|
||||
<input id="displayName" name="displayName" type="text" autocomplete="nickname" maxlength="80" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="email"><span class="l-en">Email</span><span class="l-zh">邮箱</span></label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password"><span class="l-en">Password</span><span class="l-zh">密码</span></label>
|
||||
<PasswordInput id="password" name="password" autocomplete="new-password" minlength={8} required />
|
||||
<span class="hint"><span class="l-en">At least 8 characters.</span><span class="l-zh">至少 8 个字符。</span></span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-dark"><span class="l-en">Create account</span><span class="l-zh">注册</span></button>
|
||||
</form>
|
||||
|
||||
<p class="auth-alt">
|
||||
<span class="l-en">Already have an account? <a href={`${base}/login/`}>Sign in</a></span>
|
||||
<span class="l-zh">已有账号?<a href={`${base}/login/`}>登录</a></span>
|
||||
</p>
|
||||
</Account>
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
import Account from '../layouts/Account.astro';
|
||||
---
|
||||
<Account
|
||||
wide
|
||||
title="Web Studio — Reasonix"
|
||||
titleEn="Web Studio — Reasonix"
|
||||
titleZh="网页版 Studio — Reasonix"
|
||||
description="Choose an online computer and open the full Reasonix Studio in your browser.">
|
||||
<div id="remote-gate" class="auth-head">
|
||||
<p><span class="l-en">Checking your account…</span><span class="l-zh">正在检查账号…</span></p>
|
||||
</div>
|
||||
|
||||
<div id="remote-view" hidden>
|
||||
<div class="auth-head">
|
||||
<span class="remote-eyebrow"><span class="l-en">Web Studio</span><span class="l-zh">网页版 Studio</span></span>
|
||||
<h1><span class="l-en">Choose an online computer</span><span class="l-zh">选择一台在线电脑</span></h1>
|
||||
<p><span class="l-en">The complete Studio interface will open in your browser. Conversations, tools, files, approvals, and terminal access stay in one place.</span><span class="l-zh">选择电脑后将在浏览器中打开完整 Studio。会话、工具、文件、审批和终端能力都在同一个界面中。</span></p>
|
||||
</div>
|
||||
|
||||
<div class="remote-security">
|
||||
<span aria-hidden="true">⌁</span>
|
||||
<p><span class="l-en"><strong>Before connecting:</strong> keep desktop Studio open and signed in to this same account. Remote traffic is end-to-end encrypted; the relay only routes ciphertext.</span><span class="l-zh"><strong>连接前:</strong>请保持电脑端 Studio 已打开,并登录当前同一账号。远程通信采用端到端加密,中转服务只转发密文。</span></p>
|
||||
</div>
|
||||
|
||||
<div class="remote-list-head">
|
||||
<h2><span class="l-en">Online computers</span><span class="l-zh">在线电脑</span></h2>
|
||||
<button id="remote-refresh" type="button" class="btn btn-ghost"><span class="l-en">Refresh</span><span class="l-zh">刷新</span></button>
|
||||
</div>
|
||||
<div id="remote-msg" class="auth-msg" hidden></div>
|
||||
<div id="remote-devices" class="remote-devices"></div>
|
||||
<div class="remote-footer">
|
||||
<a href="/?download=studio#start"><span class="l-en">Download desktop Studio</span><span class="l-zh">下载电脑端 Studio</span></a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<a href="/account/"><span class="l-en">Account settings</span><span class="l-zh">账号设置</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>import '../scripts/remote.js';</script>
|
||||
</Account>
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
import Account from '../layouts/Account.astro';
|
||||
import PasswordInput from '../components/PasswordInput.astro';
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
---
|
||||
<Account
|
||||
title="Set a new password — Reasonix"
|
||||
titleEn="Set a new password — Reasonix"
|
||||
titleZh="设置新密码 — Reasonix"
|
||||
description="Choose a new password for your Reasonix account.">
|
||||
<div class="auth-head">
|
||||
<h1><span class="l-en">Set a new password</span><span class="l-zh">设置新密码</span></h1>
|
||||
<p><span class="l-en">Choose a password you haven't used before.</span><span class="l-zh">请设置一个新密码。</span></p>
|
||||
</div>
|
||||
|
||||
<div id="reset-msg" class="auth-msg" hidden></div>
|
||||
|
||||
<form id="reset-form" class="auth-form" novalidate>
|
||||
<div class="field">
|
||||
<label for="password"><span class="l-en">New password</span><span class="l-zh">新密码</span></label>
|
||||
<PasswordInput id="password" name="password" autocomplete="new-password" minlength={8} required />
|
||||
<span class="hint"><span class="l-en">At least 8 characters.</span><span class="l-zh">至少 8 个字符。</span></span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="confirm"><span class="l-en">Confirm password</span><span class="l-zh">确认密码</span></label>
|
||||
<PasswordInput id="confirm" name="confirm" autocomplete="new-password" minlength={8} required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-dark"><span class="l-en">Update password</span><span class="l-zh">更新密码</span></button>
|
||||
</form>
|
||||
|
||||
<p class="auth-alt">
|
||||
<a href={`${base}/login/`}><span class="l-en">Back to sign in</span><span class="l-zh">返回登录</span></a>
|
||||
</p>
|
||||
</Account>
|
||||
@@ -1,575 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import CommunityJoin from '../components/CommunityJoin.astro';
|
||||
import SponsorSlot from '../components/SponsorSlot.astro';
|
||||
import SiteHeader from '../components/SiteHeader.astro';
|
||||
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const repo = 'https://github.com/esengine/DeepSeek-Reasonix';
|
||||
const desc = "Browse, publish, and install community skills, plugins, and MCP servers for Reasonix. One command to add any capability to your agent — install_source does the rest, SSRF-guarded and risk-rated.";
|
||||
---
|
||||
<Base
|
||||
title="Reasonix — Skills, Plugins & MCP registry"
|
||||
titleEn="Reasonix — Skills, Plugins & MCP registry"
|
||||
titleZh="Reasonix — 技能、插件与 MCP 市场"
|
||||
description={desc}>
|
||||
|
||||
<SiteHeader active="skills" />
|
||||
|
||||
<main>
|
||||
<section class="reg-head">
|
||||
<div class="wrap">
|
||||
<div class="sec-head split">
|
||||
<span class="kicker"><span class="l-en">Registry</span><span class="l-zh">技能市场</span></span>
|
||||
<h2><span class="l-en">Every skill, one <span class="em">install</span> away.</span><span class="l-zh">每个能力,一条 <span class="em">install</span> 就到手。</span></h2>
|
||||
<p><span class="l-en">Community skills, plugins, and MCP servers for your agent. The registry only stores metadata and a source pointer — installs run client-side through <code>install_source</code>, SSRF-guarded and risk-rated. Nothing executes here.</span><span class="l-zh">社区贡献的技能、插件与 MCP 服务器。市场只存元数据与来源指针——安装在你本机经 <code>install_source</code> 执行(带 SSRF 防护与风险分级),这里不运行任何代码。</span></p>
|
||||
</div>
|
||||
|
||||
<div class="reg-toolbar">
|
||||
<div class="reg-search">
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M8.5 3a5.5 5.5 0 1 0 3.4 9.8l3.6 3.7 1.4-1.4-3.7-3.6A5.5 5.5 0 0 0 8.5 3Zm0 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Z"/></svg>
|
||||
<input id="reg-q" type="search" autocomplete="off" placeholder="Search skills, plugins, MCP servers, themes, tags…" aria-label="Search the registry" />
|
||||
</div>
|
||||
<div class="reg-tabs" data-group="kind" role="tablist">
|
||||
<button class="active" data-kind="all" role="tab"><span class="l-en">All</span><span class="l-zh">全部</span></button>
|
||||
<button data-kind="skill" role="tab">Skills</button>
|
||||
<button data-kind="plugin" role="tab">Plugins</button>
|
||||
<button data-kind="mcp" role="tab">MCP</button>
|
||||
<button data-kind="theme" role="tab">Themes</button>
|
||||
</div>
|
||||
<div class="reg-tabs" data-group="sort" role="tablist">
|
||||
<button class="active" data-sort="recommended" role="tab" title="0.75 × Wilson lower bound of the vote ratio + 0.25 × decayed distinct 30-day installs"><span class="l-en">Recommended</span><span class="l-zh">推荐</span></button>
|
||||
<button data-sort="new" role="tab"><span class="l-en">New</span><span class="l-zh">最新</span></button>
|
||||
<button data-sort="trending" role="tab"><span class="l-en">Trending</span><span class="l-zh">趋势</span></button>
|
||||
<button data-sort="installs" role="tab"><span class="l-en">Top</span><span class="l-zh">最热</span></button>
|
||||
</div>
|
||||
<a class="btn btn-dark reg-publish-cta" id="publish-open" href="#publish">
|
||||
<span class="l-en">Publish</span><span class="l-zh">发布</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="reg-body">
|
||||
<div class="wrap reg-cols">
|
||||
<div class="reg-main">
|
||||
<div id="pkg-grid" class="pkg-grid" data-state="loading" aria-live="polite">
|
||||
<div class="pkg-skel"></div><div class="pkg-skel"></div><div class="pkg-skel"></div><div class="pkg-skel"></div>
|
||||
</div>
|
||||
<div id="reg-empty" class="reg-state" hidden>
|
||||
<h3><span class="l-en">Be the first to publish.</span><span class="l-zh">来当第一个发布者。</span></h3>
|
||||
<p><span class="l-en">No packages match yet. Ship a capability you built and it lands here for everyone.</span><span class="l-zh">还没有匹配的包。把你构建的能力发上来,所有人都能一键装。</span></p>
|
||||
<a class="btn btn-dark" href="#publish" data-publish-jump><span class="l-en">Publish a package</span><span class="l-zh">发布一个包</span></a>
|
||||
</div>
|
||||
<div id="reg-offline" class="reg-state" hidden>
|
||||
<h3><span class="l-en">Registry is launching soon.</span><span class="l-zh">市场即将上线。</span></h3>
|
||||
<p><span class="l-en">The registry service isn't reachable yet. Sign in to get a handle ready — your packages will be namespaced to it.</span><span class="l-zh">市场服务暂时还连不上。先登录占好你的 handle——你的包会以它作命名空间。</span></p>
|
||||
<a class="btn btn-dark" href="/login/?next=%2Fskills%2F"><span class="l-en">Sign in</span><span class="l-zh">登录</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="reg-aside">
|
||||
<div class="feed-card">
|
||||
<div class="feed-head"><span class="pulse"></span><span class="l-en">Live activity</span><span class="l-zh">实时动态</span></div>
|
||||
<ul id="feed" class="feed-list"><li class="feed-skel"></li><li class="feed-skel"></li><li class="feed-skel"></li></ul>
|
||||
<div id="feed-empty" class="feed-empty" hidden><span class="l-en">Quiet for now — the first publish shows up here.</span><span class="l-zh">暂时安静——第一个发布会出现在这里。</span></div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="reg-review" id="review" hidden>
|
||||
<div class="wrap">
|
||||
<div class="review-head">
|
||||
<span class="kicker"><span class="l-en">Review queue</span><span class="l-zh">审核队列</span></span>
|
||||
<span class="review-count" id="review-count"></span>
|
||||
</div>
|
||||
<div id="review-list" class="review-list"></div>
|
||||
<div id="review-empty" class="review-empty" hidden><span class="l-en">Nothing pending — all caught up.</span><span class="l-zh">没有待审——都处理完了。</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="reg-publish" id="publish" hidden>
|
||||
<div class="wrap">
|
||||
<div class="pub-card">
|
||||
<div class="pub-head">
|
||||
<div>
|
||||
<h3><span class="l-en">Publish a package</span><span class="l-zh">发布一个包</span></h3>
|
||||
<p><span class="l-en">Verified accounts only. Submissions are reviewed before they go live; your package is namespaced to your handle (<code>@you/name</code>).</span><span class="l-zh">仅已验证邮箱的账号可提交。提交后经管理员审核才会公开;包以你的 handle 命名(<code>@you/name</code>)。</span></p>
|
||||
</div>
|
||||
<button type="button" class="pub-close" id="publish-close" aria-label="Close">✕</button>
|
||||
</div>
|
||||
<form id="pub-form" class="pub-form" autocomplete="off">
|
||||
<div class="pub-row">
|
||||
<label><span class="l-en">Kind</span><span class="l-zh">类型</span>
|
||||
<select name="kind"><option value="skill">skill</option><option value="plugin">plugin</option><option value="mcp">mcp</option><option value="theme">theme</option></select>
|
||||
</label>
|
||||
<label><span class="l-en">Name</span><span class="l-zh">名称</span>
|
||||
<input name="name" required placeholder="my-package" pattern="[a-z0-9][a-z0-9._-]*" />
|
||||
</label>
|
||||
<label><span class="l-en">Version</span><span class="l-zh">版本</span>
|
||||
<input name="version" placeholder="0.1.0" />
|
||||
</label>
|
||||
</div>
|
||||
<label><span class="l-en">One-line summary</span><span class="l-zh">一句话简介</span>
|
||||
<input name="summary" maxlength="200" placeholder="What does it do?" />
|
||||
</label>
|
||||
<label><span class="l-en">Source</span><span class="l-zh">来源</span>
|
||||
<input name="source" required pattern="(https?://[^ ]+|git:github[.]com/[^ ]+|@?[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)?)" title="A URL (SKILL.md, a Reasonix/Codex/Claude plugin repository, .mcp.json, repository path), a git:github.com/… shorthand, or an MCP package name — not free text or a local path." placeholder="https://… SKILL.md · plugin repo · .mcp.json · MCP package" />
|
||||
<small class="pub-hint"><span class="l-en">Plugins and themes must point to a GitHub repository or path containing <code>reasonix-plugin.json</code>, <code>.codex-plugin/plugin.json</code>, <code>.claude-plugin/plugin.json</code>, or a supported <code>.claude-plugin/marketplace.json</code>.</span><span class="l-zh">插件和主题必须指向包含 <code>reasonix-plugin.json</code>、<code>.codex-plugin/plugin.json</code>、<code>.claude-plugin/plugin.json</code>,或受支持的 <code>.claude-plugin/marketplace.json</code> 的 GitHub 仓库或路径。</span></small>
|
||||
</label>
|
||||
<div class="pub-row">
|
||||
<label><span class="l-en">Repo (optional)</span><span class="l-zh">仓库(可选)</span>
|
||||
<input name="repoUrl" placeholder="https://github.com/…" />
|
||||
</label>
|
||||
<label><span class="l-en">Tags (comma-separated)</span><span class="l-zh">标签(逗号分隔)</span>
|
||||
<input name="tags" placeholder="testing, go, review" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="pub-private"><input type="checkbox" name="private" />
|
||||
<span><span class="l-en">Only me — skip review; in the registry only your account can see and install it (from Reasonix Studio; moderators can still see it). Submit it for review later from Studio's My packages.</span><span class="l-zh">仅自己可见——不提交审核,市场中只有你的账号能看到并安装(在 Reasonix Studio 中;管理员仍能看到)。之后可在 Studio 的「我的发布」里提交审核。</span></span>
|
||||
</label>
|
||||
<div class="pub-actions">
|
||||
<button type="submit" class="btn btn-dark"><span class="l-en">Publish</span><span class="l-zh">发布</span></button>
|
||||
<span id="pub-msg" class="pub-msg" role="status"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<CommunityJoin />
|
||||
<div class="foot-inner">
|
||||
<a class="brand" href={`${base}/`}><img src={`${base}/logo.svg`} alt="Reasonix logo" /><span>Reasonix</span></a>
|
||||
<span><span class="l-en">© 2026 Reasonix · MIT licensed · built to be left running.</span><span class="l-zh">© 2026 Reasonix · MIT 许可 · 为常驻运行而生。</span></span>
|
||||
<nav class="foot-links">
|
||||
<a href={repo}>GitHub</a>
|
||||
<a href={`${base}/`}><span class="l-en">Home</span><span class="l-zh">首页</span></a>
|
||||
<a href={`${base}/docs/`}><span class="l-en">Docs</span><span class="l-zh">文档</span></a>
|
||||
<a href="/login/?next=%2Fskills%2F"><span class="l-en">Sign in</span><span class="l-zh">登录</span></a>
|
||||
</nav>
|
||||
</div>
|
||||
<SponsorSlot />
|
||||
</footer>
|
||||
|
||||
<style is:global>
|
||||
.reg-head { padding: 128px 0 0; }
|
||||
.reg-toolbar {
|
||||
display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
|
||||
margin-top: 32px; padding-bottom: 4px;
|
||||
}
|
||||
.reg-search {
|
||||
display: flex; align-items: center; gap: 8px; flex: 1 1 240px;
|
||||
background: var(--bg-soft); border-radius: 999px; padding: 0 18px;
|
||||
border: 1px solid transparent; transition: border-color .2s, background .2s;
|
||||
}
|
||||
.reg-search:focus-within { background: var(--card); border-color: var(--line); box-shadow: var(--shadow-1); }
|
||||
.reg-search svg { width: 18px; height: 18px; fill: var(--ink-3); flex: none; }
|
||||
.reg-search input {
|
||||
border: none; background: none; outline: none; width: 100%;
|
||||
font-family: var(--font-sans); font-size: 15px; color: var(--ink); padding: 12px 0;
|
||||
}
|
||||
.reg-tabs { display: inline-flex; gap: 2px; padding: 3px; background: var(--bg-soft); border-radius: 999px; }
|
||||
.reg-tabs button {
|
||||
font-family: var(--font-sans); font-size: 13.5px; font-weight: 500; color: var(--ink-2);
|
||||
background: transparent; border: none; cursor: pointer; padding: 8px 16px; border-radius: 999px;
|
||||
transition: background .2s, color .2s, box-shadow .2s;
|
||||
}
|
||||
.reg-tabs button:hover { color: var(--ink); }
|
||||
.reg-tabs button.active { background: var(--card); color: var(--accent); box-shadow: var(--shadow-1); }
|
||||
|
||||
.reg-body { padding: 36px 0 96px; }
|
||||
.reg-cols { display: grid; grid-template-columns: 1fr 320px; gap: 32px; align-items: start; }
|
||||
.reg-main { min-width: 0; }
|
||||
.pkg-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||||
.pkg-grid[data-state="loading"] .pkg-skel { display: block; }
|
||||
|
||||
.pkg-card {
|
||||
display: flex; flex-direction: column; gap: 12px;
|
||||
background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
|
||||
padding: 22px; min-width: 0; transition: box-shadow .25s, border-color .25s, transform .25s;
|
||||
}
|
||||
.pkg-card:hover { box-shadow: var(--shadow-2); border-color: transparent; transform: translateY(-2px); }
|
||||
.pkg-top { display: flex; align-items: center; gap: 8px; }
|
||||
.pkg-kind { font-family: var(--font-mono); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--accent); background: var(--accent-soft); padding: 4px 9px; border-radius: 999px; }
|
||||
.pkg-kind.plugin { color: oklch(0.52 0.14 55); background: color-mix(in oklch, oklch(0.52 0.14 55) 10%, var(--card)); }
|
||||
.pkg-kind.mcp { color: oklch(0.5 0.16 300); background: color-mix(in oklch, oklch(0.5 0.16 300) 9%, var(--card)); }
|
||||
.pkg-kind.theme { color: oklch(0.52 0.13 200); background: color-mix(in oklch, oklch(0.52 0.13 200) 9%, var(--card)); }
|
||||
.pkg-badge { font-size: 11px; font-weight: 600; padding: 4px 9px; border-radius: 999px; }
|
||||
.pkg-badge.verified { color: oklch(0.5 0.13 155); background: color-mix(in oklch, oklch(0.5 0.13 155) 10%, var(--card)); }
|
||||
.pkg-badge.community { color: var(--ink-3); background: var(--bg-soft); }
|
||||
.pkg-stars { margin-left: auto; font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
||||
.pkg-name { font-size: 17px; font-weight: 600; letter-spacing: -.01em; overflow-wrap: anywhere; }
|
||||
.pkg-name .scope { color: var(--ink-3); font-weight: 500; }
|
||||
.pkg-sum { font-size: 14px; line-height: 1.55; color: var(--ink-2); overflow-wrap: anywhere; }
|
||||
.pkg-tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.pkg-tags span { font-size: 12px; color: var(--ink-3); background: var(--bg-soft); padding: 3px 10px; border-radius: 999px; }
|
||||
.pkg-foot { display: flex; align-items: center; gap: 12px; margin-top: 2px; }
|
||||
.pkg-install {
|
||||
display: inline-flex; align-items: center; gap: 10px; flex: 1; min-width: 0;
|
||||
font-family: var(--font-mono); font-size: 12.5px; color: var(--ink);
|
||||
background: var(--bg-soft); border-radius: 999px; padding: 8px 8px 8px 14px;
|
||||
}
|
||||
.pkg-install code { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pkg-install button {
|
||||
margin-left: auto; flex: none; font-family: var(--font-sans); font-size: 12px; font-weight: 600;
|
||||
background: var(--card); color: var(--accent); border: none; cursor: pointer; padding: 6px 13px;
|
||||
border-radius: 999px; box-shadow: var(--shadow-1); transition: background .2s, color .2s;
|
||||
}
|
||||
.pkg-install button.copied { background: oklch(0.55 0.14 155); color: #fff; }
|
||||
.pkg-meta { font-size: 12.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
||||
|
||||
.pkg-skel, .feed-skel { display: none; border-radius: var(--radius); background: linear-gradient(100deg, var(--bg-soft) 30%, var(--line) 50%, var(--bg-soft) 70%); background-size: 200% 100%; animation: sheen 1.2s linear infinite; }
|
||||
.pkg-skel { height: 168px; }
|
||||
.feed-skel { height: 44px; border-radius: 12px; margin-bottom: 10px; }
|
||||
@keyframes sheen { to { background-position: -200% 0; } }
|
||||
|
||||
.reg-state { text-align: center; padding: 56px 20px; border: 1px dashed var(--line); border-radius: var(--radius); }
|
||||
.reg-state h3 { font-size: 20px; font-weight: 600; }
|
||||
.reg-state p { margin: 10px auto 22px; max-width: 40ch; color: var(--ink-2); font-size: 15px; line-height: 1.55; }
|
||||
|
||||
.reg-aside { position: sticky; top: 88px; }
|
||||
.feed-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 20px; }
|
||||
.feed-head { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--ink); margin-bottom: 14px; }
|
||||
.pulse { width: 8px; height: 8px; border-radius: 50%; background: oklch(0.7 0.16 150); box-shadow: 0 0 0 0 oklch(0.7 0.16 150 / .5); animation: pulse 2s var(--ease) infinite; }
|
||||
@keyframes pulse { 70%, 100% { box-shadow: 0 0 0 7px oklch(0.7 0.16 150 / 0); } }
|
||||
.feed-list { list-style: none; display: flex; flex-direction: column; gap: 2px; }
|
||||
.feed-item { display: flex; flex-direction: column; gap: 2px; padding: 9px 0; border-top: 1px solid var(--line); font-size: 13px; }
|
||||
.feed-item:first-child { border-top: none; }
|
||||
.feed-item .fx { color: var(--ink); line-height: 1.45; }
|
||||
.feed-item .fx b { font-weight: 600; }
|
||||
.feed-item .fx a { color: var(--accent); text-decoration: none; }
|
||||
.feed-item .fx a:hover { text-decoration: underline; }
|
||||
.feed-item .ft { color: var(--ink-3); font-size: 11.5px; }
|
||||
.feed-item .ico { font-family: var(--font-mono); }
|
||||
.feed-empty { font-size: 13px; color: var(--ink-3); line-height: 1.5; }
|
||||
|
||||
.reg-review { padding: 8px 0 40px; }
|
||||
.review-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; }
|
||||
.review-count { font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
||||
.review-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.review-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; background: color-mix(in oklch, oklch(0.7 0.16 60) 6%, var(--card)); border: 1px solid color-mix(in oklch, oklch(0.7 0.16 60) 30%, var(--card)); border-radius: var(--radius-sm); padding: 14px 16px; }
|
||||
.review-row .rv-main { flex: 1; min-width: 0; }
|
||||
.review-row .rv-name { font-size: 15px; font-weight: 600; }
|
||||
.review-row .rv-name .scope { color: var(--ink-3); font-weight: 500; }
|
||||
.review-row .rv-sum { font-size: 13px; color: var(--ink-2); margin-top: 2px; overflow-wrap: anywhere; }
|
||||
.review-row .rv-src { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-3); margin-top: 4px; overflow-wrap: anywhere; }
|
||||
.review-row .rv-acts { display: inline-flex; gap: 8px; flex: none; }
|
||||
.rv-btn { font-family: var(--font-sans); font-size: 13px; font-weight: 600; border: none; cursor: pointer; padding: 8px 14px; border-radius: 999px; transition: background .2s, color .2s, box-shadow .2s; }
|
||||
.rv-btn.approve { background: oklch(0.55 0.13 155); color: #fff; }
|
||||
.rv-btn.reject { background: var(--bg-soft); color: oklch(0.5 0.18 25); }
|
||||
.rv-btn.verify { background: var(--card); color: var(--accent); box-shadow: var(--shadow-1); }
|
||||
.rv-btn:disabled { opacity: .5; cursor: default; }
|
||||
.review-empty { font-size: 14px; color: var(--ink-3); padding: 28px 0; }
|
||||
|
||||
.reg-publish { padding: 8px 0 64px; scroll-margin-top: 88px; }
|
||||
.pub-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 28px; box-shadow: var(--shadow-2); }
|
||||
.pub-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
|
||||
.pub-head h3 { font-size: 20px; font-weight: 600; }
|
||||
.pub-head p { margin-top: 6px; color: var(--ink-2); font-size: 14px; }
|
||||
.pub-close { background: var(--bg-soft); border: none; cursor: pointer; width: 32px; height: 32px; border-radius: 50%; color: var(--ink-2); font-size: 14px; flex: none; }
|
||||
.pub-close:hover { color: var(--ink); }
|
||||
.pub-form { display: flex; flex-direction: column; gap: 14px; }
|
||||
.pub-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
||||
.pub-form label { display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; font-weight: 600; color: var(--ink-2); }
|
||||
.pub-hint { color: var(--ink-3); font-size: 11.5px; font-weight: 400; line-height: 1.45; }
|
||||
.pub-form input, .pub-form select {
|
||||
font-family: var(--font-sans); font-size: 14px; color: var(--ink); font-weight: 400;
|
||||
background: var(--bg-soft); border: 1px solid transparent; border-radius: var(--radius-sm);
|
||||
padding: 11px 14px; outline: none; transition: border-color .2s, background .2s;
|
||||
}
|
||||
.pub-form input:focus, .pub-form select:focus { background: var(--card); border-color: var(--accent); }
|
||||
.pub-form label.pub-private { flex-direction: row; align-items: flex-start; gap: 8px; font-weight: 400; }
|
||||
.pub-form .pub-private input { padding: 0; margin-top: 2px; }
|
||||
.pub-actions { display: flex; align-items: center; gap: 16px; margin-top: 4px; }
|
||||
.pub-msg { font-size: 13.5px; color: var(--ink-2); }
|
||||
.pub-msg.err { color: oklch(0.55 0.18 25); }
|
||||
.pub-msg.ok { color: oklch(0.5 0.13 155); }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.reg-cols { grid-template-columns: 1fr; }
|
||||
.reg-aside { position: static; }
|
||||
.pkg-grid { grid-template-columns: 1fr; }
|
||||
.pub-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.reg-head { padding-top: 104px; }
|
||||
.wrap { padding: 0 20px; }
|
||||
.reg-toolbar { gap: 10px; }
|
||||
.reg-tabs { flex: 1 1 auto; justify-content: center; }
|
||||
.pkg-card { padding: 18px; }
|
||||
.pub-card { padding: 20px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { accountSession } from '../scripts/account-nav.js';
|
||||
const REGISTRY = import.meta.env.PUBLIC_REGISTRY_ORIGIN || 'https://crash.reasonix.io';
|
||||
const state = { kind: 'all', sort: 'recommended', q: '' };
|
||||
let currentUser = null;
|
||||
|
||||
const grid = document.getElementById('pkg-grid');
|
||||
const emptyEl = document.getElementById('reg-empty');
|
||||
const offlineEl = document.getElementById('reg-offline');
|
||||
const feedEl = document.getElementById('feed');
|
||||
const feedEmpty = document.getElementById('feed-empty');
|
||||
|
||||
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (m) => (
|
||||
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m]
|
||||
));
|
||||
const kindClass = (kind) => `pkg-kind${kind === 'plugin' ? ' plugin' : kind === 'mcp' ? ' mcp' : kind === 'theme' ? ' theme' : ''}`;
|
||||
// A theme is listed on its own but installs as a plugin package.
|
||||
const installerFor = (kind) => (kind === 'theme' ? 'plugin' : kind);
|
||||
const installRequest = (p) =>
|
||||
`Install this Reasonix ${p.kind} package from ${p.source}. Use install_source with kind="${installerFor(p.kind)}".`;
|
||||
|
||||
function show(el, on) { if (el) el.hidden = !on; }
|
||||
|
||||
function browserInstallId() {
|
||||
const fresh = () => Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join('');
|
||||
try {
|
||||
let id = localStorage.getItem('reasonix-install-id');
|
||||
if (!/^[0-9a-f]{32}$/.test(id || '')) {
|
||||
id = fresh();
|
||||
localStorage.setItem('reasonix-install-id', id);
|
||||
}
|
||||
return id;
|
||||
} catch {
|
||||
return fresh();
|
||||
}
|
||||
}
|
||||
|
||||
function card(p) {
|
||||
const badge = p.verified
|
||||
? '<span class="pkg-badge verified">✓ verified</span>'
|
||||
: '<span class="pkg-badge community">community</span>';
|
||||
const tags = (p.tags || []).slice(0, 4).map((t) => `<span>${esc(t)}</span>`).join('');
|
||||
const votes = (p.upCount || 0) + (p.downCount || 0);
|
||||
const stars = votes > 0 && typeof p.approvalRate === 'number'
|
||||
? `<span class="pkg-stars" title="▲ ${p.upCount || 0} · ▼ ${p.downCount || 0}">▲ ${Math.round(p.approvalRate * 100)}% · ${votes}</span>`
|
||||
: '';
|
||||
const repo = p.repoUrl ? ` · <a href="${esc(p.repoUrl)}" target="_blank" rel="noreferrer">repo</a>` : '';
|
||||
return `<article class="pkg-card">
|
||||
<div class="pkg-top"><span class="${kindClass(p.kind)}">${esc(p.kind)}</span>${badge}${stars}</div>
|
||||
<div class="pkg-name"><span class="scope">${esc(p.handle)}/</span>${esc(p.name)}</div>
|
||||
<div class="pkg-sum">${esc(p.summary) || '<span style="color:var(--ink-3)">No summary.</span>'}</div>
|
||||
${tags ? `<div class="pkg-tags">${tags}</div>` : ''}
|
||||
<div class="pkg-foot">
|
||||
<span class="pkg-install" title="Install source — give it to Reasonix"><code>${esc(p.source)}</code><button data-copy="${esc(installRequest(p))}" data-slug="${esc(p.handle)}/${esc(p.name)}">Copy</button></span>
|
||||
</div>
|
||||
<div class="pkg-meta">↓ ${p.installCount || 0} installs · v${esc(p.latestVersion) || '—'}${repo}</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function renderPackages(list) {
|
||||
grid.removeAttribute('data-state');
|
||||
show(offlineEl, false);
|
||||
if (!list.length) { grid.innerHTML = ''; show(emptyEl, true); return; }
|
||||
show(emptyEl, false);
|
||||
grid.innerHTML = list.map(card).join('');
|
||||
}
|
||||
|
||||
function renderOffline() {
|
||||
grid.removeAttribute('data-state');
|
||||
grid.innerHTML = '';
|
||||
show(emptyEl, false);
|
||||
show(offlineEl, true);
|
||||
}
|
||||
|
||||
const rel = (iso) => {
|
||||
const s = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000);
|
||||
if (s < 60) return 'just now';
|
||||
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
||||
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
||||
return `${Math.floor(s / 86400)}d ago`;
|
||||
};
|
||||
|
||||
function feedLine(e) {
|
||||
const who = e.actor ? `<b>@${esc(e.actor)}</b> ` : '';
|
||||
const link = e.slug ? `<a href="#" data-slug="${esc(e.slug)}">${esc(e.slug)}</a>` : '';
|
||||
const verb = { publish: 'published', update: 'updated', star: 'upvoted', milestone: '' };
|
||||
const ico = { publish: '↑', update: '↻', star: '▲', milestone: '◎' }[e.type] || '•';
|
||||
const text = e.type === 'milestone' ? esc(e.summary) : `${who}${verb[e.type] || e.type} ${link}`;
|
||||
return `<li class="feed-item"><span class="fx"><span class="ico">${ico}</span> ${text}</span><span class="ft">${rel(e.createdAt)}</span></li>`;
|
||||
}
|
||||
|
||||
function renderFeed(events) {
|
||||
if (!events.length) { feedEl.innerHTML = ''; show(feedEmpty, true); return; }
|
||||
show(feedEmpty, false);
|
||||
feedEl.innerHTML = events.map(feedLine).join('');
|
||||
}
|
||||
|
||||
async function loadPackages() {
|
||||
const params = new URLSearchParams({ kind: state.kind, sort: state.sort, q: state.q, limit: '48' });
|
||||
try {
|
||||
const r = await fetch(`${REGISTRY}/v1/packages?${params}`);
|
||||
if (!r.ok) throw new Error(String(r.status));
|
||||
renderPackages((await r.json()).packages || []);
|
||||
} catch { renderOffline(); }
|
||||
}
|
||||
|
||||
async function loadFeed() {
|
||||
try {
|
||||
const r = await fetch(`${REGISTRY}/v1/activity?limit=20`);
|
||||
if (!r.ok) throw new Error(String(r.status));
|
||||
renderFeed((await r.json()).events || []);
|
||||
} catch { feedEl.innerHTML = ''; show(feedEmpty, true); }
|
||||
}
|
||||
|
||||
// Copy install command (cards render after page load, so delegate).
|
||||
grid.addEventListener('click', (ev) => {
|
||||
const btn = ev.target.closest('button[data-copy]');
|
||||
if (!btn) return;
|
||||
navigator.clipboard?.writeText(btn.dataset.copy);
|
||||
btn.classList.add('copied');
|
||||
btn.textContent = '✓';
|
||||
setTimeout(() => { btn.classList.remove('copied'); btn.textContent = 'Copy'; }, 1400);
|
||||
// Best-effort install ping. The registry counts one per (browser id,
|
||||
// package, day); the id is random and lives only in this browser.
|
||||
fetch(`${REGISTRY}/v1/packages/${btn.dataset.slug}/installed`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ installId: browserInstallId() }),
|
||||
}).catch(() => {});
|
||||
});
|
||||
|
||||
// Toolbar
|
||||
let debounce;
|
||||
document.getElementById('reg-q').addEventListener('input', (e) => {
|
||||
clearTimeout(debounce);
|
||||
state.q = e.target.value.trim();
|
||||
debounce = setTimeout(loadPackages, 220);
|
||||
});
|
||||
document.querySelectorAll('.reg-tabs').forEach((group) => {
|
||||
group.addEventListener('click', (ev) => {
|
||||
const btn = ev.target.closest('button');
|
||||
if (!btn) return;
|
||||
group.querySelectorAll('button').forEach((b) => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
if (group.dataset.group === 'kind') state.kind = btn.dataset.kind;
|
||||
else state.sort = btn.dataset.sort;
|
||||
loadPackages();
|
||||
});
|
||||
});
|
||||
|
||||
// Publish panel — sign-in gated: send anonymous visitors to /login, and warn
|
||||
// a signed-in-but-unverified user that publishing needs a confirmed email.
|
||||
const publish = document.getElementById('publish');
|
||||
const loginNext = () => { location.href = '/login?next=' + encodeURIComponent('/skills/'); };
|
||||
const openPublish = (ev) => {
|
||||
if (ev) ev.preventDefault();
|
||||
if (!currentUser) return loginNext();
|
||||
publish.hidden = false;
|
||||
if (!currentUser.emailVerified) setMsg('Verify your email before publishing — check your inbox.', 'err');
|
||||
publish.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
document.getElementById('publish-open').addEventListener('click', openPublish);
|
||||
document.querySelectorAll('[data-publish-jump]').forEach((el) => el.addEventListener('click', openPublish));
|
||||
document.getElementById('publish-close').addEventListener('click', () => { publish.hidden = true; });
|
||||
if (location.hash === '#publish') publish.hidden = false;
|
||||
|
||||
const msg = document.getElementById('pub-msg');
|
||||
const setMsg = (text, cls) => { msg.textContent = text; msg.className = 'pub-msg' + (cls ? ' ' + cls : ''); };
|
||||
|
||||
document.getElementById('pub-form').addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const f = new FormData(ev.target);
|
||||
const tags = String(f.get('tags') || '').split(',').map((t) => t.trim()).filter(Boolean);
|
||||
const body = {
|
||||
kind: f.get('kind'), name: f.get('name'), summary: f.get('summary') || '',
|
||||
source: f.get('source'), installKind: installerFor(f.get('kind')), version: f.get('version') || '',
|
||||
repoUrl: f.get('repoUrl') || '', tags,
|
||||
...(f.get('private') ? { visibility: 'private' } : {}),
|
||||
};
|
||||
setMsg('Publishing…');
|
||||
try {
|
||||
const r = await fetch(`${REGISTRY}/v1/packages`, {
|
||||
method: 'POST', credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
||||
});
|
||||
const data = await r.json().catch(() => ({}));
|
||||
if (r.status === 401) return loginNext();
|
||||
if (!r.ok) { setMsg(data?.error?.message || 'Publish failed.', 'err'); return; }
|
||||
const status = data.package?.status;
|
||||
setMsg(status === 'private'
|
||||
? `Saved ${data.package.slug}@${data.version} as private — only you can see and install it.`
|
||||
: status === 'pending'
|
||||
? `Submitted ${data.package.slug} for review — an admin approves it before it goes live.`
|
||||
: `Published ${data.package.slug}@${data.version}.`, 'ok');
|
||||
ev.target.reset();
|
||||
loadPackages(); loadFeed();
|
||||
} catch { setMsg('Registry is unreachable right now.', 'err'); }
|
||||
});
|
||||
|
||||
// Admin review queue — only initialised for admins.
|
||||
const reviewSection = document.getElementById('review');
|
||||
const reviewList = document.getElementById('review-list');
|
||||
const reviewCount = document.getElementById('review-count');
|
||||
const reviewEmpty = document.getElementById('review-empty');
|
||||
|
||||
function renderReview(pkgs) {
|
||||
reviewCount.textContent = pkgs.length ? `${pkgs.length} pending` : '';
|
||||
if (!pkgs.length) { reviewList.innerHTML = ''; show(reviewEmpty, true); return; }
|
||||
show(reviewEmpty, false);
|
||||
reviewList.innerHTML = pkgs.map((p) => `
|
||||
<div class="review-row" data-slug="${esc(p.handle)}/${esc(p.name)}" data-version="${esc(p.latestVersion)}" data-updated-at="${esc(p.updatedAt)}" data-status="${esc(p.status)}">
|
||||
<div class="rv-main">
|
||||
<div class="rv-name"><span class="scope">${esc(p.handle)}/</span>${esc(p.name)} <span class="${kindClass(p.kind)}">${esc(p.kind)}</span></div>
|
||||
<div class="rv-sum">${esc(p.summary) || '(no summary)'}</div>
|
||||
<div class="rv-src">${esc(p.source)}</div>
|
||||
</div>
|
||||
<div class="rv-acts">
|
||||
<button class="rv-btn approve" data-act="approve">Approve</button>
|
||||
<button class="rv-btn reject" data-act="reject">Reject</button>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
async function loadReview() {
|
||||
try {
|
||||
const r = await fetch(`${REGISTRY}/v1/admin/packages?status=pending`, { credentials: 'include' });
|
||||
if (!r.ok) throw new Error(String(r.status));
|
||||
renderReview((await r.json()).packages || []);
|
||||
} catch { reviewSection.hidden = true; }
|
||||
}
|
||||
|
||||
reviewList.addEventListener('click', async (ev) => {
|
||||
const btn = ev.target.closest('.rv-btn');
|
||||
if (!btn) return;
|
||||
const row = btn.closest('.review-row');
|
||||
row.querySelectorAll('.rv-btn').forEach((b) => (b.disabled = true));
|
||||
try {
|
||||
const r = await fetch(`${REGISTRY}/v1/admin/packages/${row.dataset.slug}/${btn.dataset.act}`, {
|
||||
method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
expectedVersion: row.dataset.version,
|
||||
expectedUpdatedAt: row.dataset.updatedAt,
|
||||
expectedStatus: row.dataset.status,
|
||||
}),
|
||||
});
|
||||
const data = await r.json().catch(() => ({}));
|
||||
if (!r.ok) {
|
||||
row.querySelectorAll('.rv-btn').forEach((b) => (b.disabled = false));
|
||||
setMsg(data?.error?.message || 'Review action failed.', 'err');
|
||||
if (r.status === 409) loadReview();
|
||||
return;
|
||||
}
|
||||
row.remove();
|
||||
loadReview(); loadPackages(); loadFeed();
|
||||
} catch {
|
||||
row.querySelectorAll('.rv-btn').forEach((b) => (b.disabled = false));
|
||||
setMsg('Registry is unreachable right now.', 'err');
|
||||
}
|
||||
});
|
||||
|
||||
accountSession().then((user) => {
|
||||
currentUser = user;
|
||||
if (user && user.role === 'admin') { reviewSection.hidden = false; loadReview(); }
|
||||
});
|
||||
|
||||
loadPackages();
|
||||
loadFeed();
|
||||
</script>
|
||||
</Base>
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
import Account from '../../layouts/Account.astro';
|
||||
---
|
||||
<Account
|
||||
wide
|
||||
title="Public profile — Reasonix"
|
||||
titleEn="Public profile — Reasonix"
|
||||
titleZh="公开主页 — Reasonix"
|
||||
description="View a Reasonix community profile.">
|
||||
<div id="public-profile-gate" class="auth-msg">Loading profile…</div>
|
||||
|
||||
<div id="public-profile" hidden>
|
||||
<div class="acct-id">
|
||||
<img id="public-profile-avatar" hidden width="72" height="72" alt="" style="border-radius:18px;object-fit:cover" />
|
||||
<div>
|
||||
<h1 id="public-profile-name" class="acct-handle"></h1>
|
||||
<div id="public-profile-handle" class="acct-email"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p id="public-profile-bio" style="white-space:pre-wrap;line-height:1.7"></p>
|
||||
<p class="hint"><span class="l-en">Joined </span><span class="l-zh">加入于 </span><span id="public-profile-joined"></span></p>
|
||||
</div>
|
||||
</Account>
|
||||
@@ -1,24 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const auth = readFileSync(new URL("./auth.js", import.meta.url), "utf8");
|
||||
const community = readFileSync(new URL("./community.js", import.meta.url), "utf8");
|
||||
const account = readFileSync(new URL("../pages/account.astro", import.meta.url), "utf8");
|
||||
const profile = readFileSync(new URL("../pages/u/index.astro", import.meta.url), "utf8");
|
||||
|
||||
test("unverified accounts can request a fresh verification email", () => {
|
||||
assert.match(account, /resend-verification-btn/);
|
||||
assert.match(auth, /\/auth\/resend-verification/);
|
||||
});
|
||||
|
||||
test("public profiles have a generated site route backed by the public account API", () => {
|
||||
assert.match(profile, /public-profile/);
|
||||
assert.match(auth, /api\("\/u\/" \+ encodeURIComponent\(handle\)\)/);
|
||||
});
|
||||
|
||||
test("community renders public handles and wires reactions", () => {
|
||||
assert.doesNotMatch(community, /split\(["']@["']\)/);
|
||||
assert.match(community, /\/posts\/\$\{like\.dataset\.like\}\/likes/);
|
||||
assert.match(community, /aria-pressed/);
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { currentAccount } from "./auth.js";
|
||||
import { accountSlotHTML } from "./account-slot.js";
|
||||
|
||||
// The page's one answer to "who is signed in", taken from /me. Every header slot
|
||||
// and every page script reads this, so no two parts of a page can disagree.
|
||||
let pending = null;
|
||||
export function accountSession() {
|
||||
if (!pending) pending = currentAccount();
|
||||
return pending;
|
||||
}
|
||||
|
||||
function render(user) {
|
||||
const here = location.pathname + location.search;
|
||||
document.querySelectorAll("[data-account-slot]").forEach((slot) => {
|
||||
slot.innerHTML = accountSlotHTML(user, here);
|
||||
slot.removeAttribute("aria-busy");
|
||||
});
|
||||
}
|
||||
|
||||
accountSession().then(render);
|
||||
|
||||
// A page restored from the back/forward cache kept the answer it had when it
|
||||
// was left; the session may have changed on another page since.
|
||||
addEventListener("pageshow", (event) => {
|
||||
if (!event.persisted) return;
|
||||
pending = null;
|
||||
accountSession().then(render);
|
||||
});
|
||||
@@ -1,138 +0,0 @@
|
||||
import { backupCount, deviceSummary, feedbackSummary, forumState, forumSummary, initials, levelName, levelProgress, trustName } from "../lib/account-overview.mjs";
|
||||
|
||||
const ACCOUNTS = (import.meta.env.PUBLIC_ACCOUNTS_API || "https://id.reasonix.io").replace(/\/$/, "");
|
||||
const FORUM = (import.meta.env.PUBLIC_FORUM_API || "https://forum.reasonix.io").replace(/\/$/, "");
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||||
const L = (en, zh) => `<span class="l-en">${esc(en)}</span><span class="l-zh">${esc(zh)}</span>`;
|
||||
const num = (n) => (n === null || n === undefined ? "—" : String(n));
|
||||
|
||||
async function get(base, path) {
|
||||
const res = await fetch(base + path, { credentials: "include" });
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function day(iso) {
|
||||
const d = new Date(iso);
|
||||
return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function paintIdentity(user) {
|
||||
const av = $("acct-avatar");
|
||||
if (user.avatarUrl && /^https:\/\//.test(user.avatarUrl)) {
|
||||
const img = document.createElement("img");
|
||||
img.src = user.avatarUrl;
|
||||
img.alt = "";
|
||||
img.referrerPolicy = "no-referrer";
|
||||
img.addEventListener("error", () => { av.textContent = initials(user.handle); img.remove(); });
|
||||
av.replaceChildren(img);
|
||||
} else {
|
||||
av.textContent = initials(user.handle);
|
||||
}
|
||||
$("acct-name").textContent = user.displayName || user.handle;
|
||||
$("acct-verified").innerHTML = user.emailVerified ? L("✓ Email verified", "✓ 邮箱已验证") : L("Email not verified", "邮箱未验证");
|
||||
$("acct-verified").classList.toggle("warn", !user.emailVerified);
|
||||
$("acct-joined").innerHTML = user.createdAt ? `${L("Joined", "加入于")} ${esc(day(user.createdAt))}` : "";
|
||||
}
|
||||
|
||||
function paintForum(raw) {
|
||||
const f = forumSummary(raw);
|
||||
const state = forumState(f);
|
||||
if (state === "unavailable") {
|
||||
$("forum-level").innerHTML = "—";
|
||||
$("forum-level-note").innerHTML = L("Community figures are unavailable right now.", "暂时无法获取社区数据。");
|
||||
return;
|
||||
}
|
||||
if (state === "empty") {
|
||||
$("forum-level").innerHTML = L("Not started", "尚未参与");
|
||||
$("forum-level-note").innerHTML = L("Post your first topic or reply to join the community.", "发一个话题或回复,加入社区。");
|
||||
return;
|
||||
}
|
||||
const name = trustName(f.trust);
|
||||
$("forum-level").innerHTML = name ? `L${f.trust} · ${L(name[0], name[1])}` : "—";
|
||||
$("forum-level-note").innerHTML = f.role === "admin" ? L("Administrator", "管理员") : f.role === "moderator" ? L("Moderator", "版主") : "";
|
||||
$("st-topics").textContent = num(f.topics);
|
||||
$("st-posts").textContent = num(f.posts);
|
||||
$("st-likes").textContent = num(f.likesReceived);
|
||||
$("st-solved").textContent = num(f.solved);
|
||||
const perms = [];
|
||||
if (f.dailyPostCap !== null) perms.push(L(`Up to ${f.dailyPostCap} posts a day`, `每天最多发 ${f.dailyPostCap} 条`));
|
||||
perms.push(f.canPostLinks ? L("Links allowed", "可以发链接") : L("Links unlock at the next level", "升到下一级后可发链接"));
|
||||
if (f.silencedUntil) perms.push(L(`Restricted until ${day(f.silencedUntil)}`, `发帖受限至 ${day(f.silencedUntil)}`));
|
||||
$("forum-perms").innerHTML = perms.join(" · ");
|
||||
$("forum-recent").innerHTML = f.recent.map((r) => `<li><a href="/community/topic/?id=${r.id}">${esc(r.title)}</a><span>${Number.isInteger(r.replyCount) ? r.replyCount : 0} ${r.replyCount === 1 ? L("reply", "回复") : L("replies", "回复")}</span></li>`).join("");
|
||||
}
|
||||
|
||||
function paintDevices(raw) {
|
||||
const d = deviceSummary(raw);
|
||||
$("dev-total").textContent = String(d.total);
|
||||
$("dev-online").innerHTML = d.online === null ? L("Online status is unavailable right now.", "暂时无法获取在线状态。") : L(`${d.online} online now`, `${d.online} 台在线`);
|
||||
}
|
||||
|
||||
const STATUS = {
|
||||
received: ["Received", "已收到"], needs_info: ["Needs info", "需要补充"], answered: ["Answered", "已回复"], recorded: ["Recorded", "已登记"],
|
||||
in_progress: ["In progress", "处理中"], fixed: ["Fixed", "已修复"], wontfix: ["Won't fix", "不予修复"], duplicate: ["Duplicate", "重复"], closed: ["Closed", "已关闭"],
|
||||
};
|
||||
|
||||
const levelText = (n) => {
|
||||
const name = levelName(n);
|
||||
return name ? `L${n} · ${L(name[0], name[1])}` : `L${n}`;
|
||||
};
|
||||
|
||||
function paintFeedback(raw) {
|
||||
const f = feedbackSummary(raw);
|
||||
const track = $("fb-track");
|
||||
track.hidden = true;
|
||||
for (const id of ["fb-note", "fb-limits", "fb-installs", "fb-items"]) $(id).innerHTML = "";
|
||||
if (f.state === "unavailable") {
|
||||
$("fb-level").innerHTML = "—";
|
||||
$("fb-note").innerHTML = L("Your feedback level is unavailable right now.", "暂时无法获取反馈等级。");
|
||||
return;
|
||||
}
|
||||
if (f.state === "none") {
|
||||
$("fb-level").innerHTML = L("Not linked yet", "尚未关联");
|
||||
$("fb-note").innerHTML = L(
|
||||
"Reports you send from Studio earn a level as they ship, from New seed up to Grove. Linking a computer to your account is coming to Studio in an upcoming release; your level will show here once you link.",
|
||||
"在 Studio 里提交的反馈被采纳后会提升等级(新种 → 共林)。把电脑关联到账号的功能将在 Studio 的后续版本提供,关联后你的等级会显示在这里。",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const h = f.headline;
|
||||
if (!h) {
|
||||
$("fb-level").innerHTML = "—";
|
||||
$("fb-note").innerHTML = L("Your feedback level is unavailable right now.", "暂时无法获取反馈等级。");
|
||||
return;
|
||||
}
|
||||
$("fb-level").innerHTML = levelText(h.level);
|
||||
const share = levelProgress(h);
|
||||
if (share !== null) {
|
||||
track.hidden = false;
|
||||
$("fb-fill").style.width = `${(share * 100).toFixed(1)}%`;
|
||||
const next = levelName(h.nextLevel);
|
||||
$("fb-note").innerHTML = L(`${h.adopted} shipped, ${h.remaining ?? "?"} more to reach ${next ? next[0] : "L" + h.nextLevel}`, `已落地 ${h.adopted} 条,再 ${h.remaining ?? "?"} 条升到「${next ? next[1] : "L" + h.nextLevel}」`);
|
||||
} else {
|
||||
$("fb-note").innerHTML = L(`${h.adopted} shipped, the highest level`, `已落地 ${h.adopted} 条,已是最高等级`);
|
||||
}
|
||||
if (h.perHour !== null && h.perDay !== null && h.repliesPerHour !== null) {
|
||||
$("fb-limits").innerHTML = L(`Up to ${h.perHour} reports an hour, ${h.perDay} a day, ${h.repliesPerHour} replies an hour`, `每小时最多 ${h.perHour} 条反馈,每天 ${h.perDay} 条,每小时 ${h.repliesPerHour} 条回复`);
|
||||
}
|
||||
if (f.installs.length > 1) {
|
||||
$("fb-installs").innerHTML = f.installs.map((i) => `<li><span><code>${esc(i.id)}</code></span><span>${i.standing ? levelText(i.standing.level) : "—"}</span></li>`).join("");
|
||||
}
|
||||
$("fb-items").innerHTML = f.items.map((it) => {
|
||||
const st = STATUS[it.status];
|
||||
const tail = it.status === "fixed" && it.version ? ` ${esc(it.version)}` : "";
|
||||
const link = it.issueNumber ? ` <a href="https://github.com/esengine/DeepSeek-Reasonix/issues/${it.issueNumber}" rel="noopener">#${it.issueNumber}</a>` : "";
|
||||
return `<li><span>${esc(it.title)}${link}</span><span>${st ? L(st[0], st[1]) : ""}${tail}</span></li>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
document.addEventListener("account:ready", (e) => {
|
||||
paintIdentity(e.detail);
|
||||
get(FORUM, "/me").then(paintForum).catch(() => paintForum(null));
|
||||
get(ACCOUNTS, "/me/feedback").then(paintFeedback).catch(() => paintFeedback(null));
|
||||
get(ACCOUNTS, "/me/devices").then(paintDevices).catch(() => { $("dev-total").textContent = "—"; });
|
||||
get(ACCOUNTS, "/me/backups").then((d) => { $("bk-total").textContent = num(backupCount(d)); }).catch(() => {});
|
||||
});
|
||||
Loaded 100 of 198 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user