mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
chore: remove site/, the main-v2 website, from the studio line
reasonix.io is built and deployed from main-v2 only. pages.yml, removed
in 68b031ae5, ran on main-v2 pushes. On studio the directory was a
remnant, and it still read release-notes/releases.json, which that
commit deleted. main-v2 keeps the site; nothing here published it.
- site/ (60 files) is deleted.
- ci.yml loses the site job, which ran site/'s node tests, and the
`site` output of the path filter. The code and desktop skip patterns
no longer name site/.
- .gitignore loses the five site build-output rules.
- The crash-report worker's shell comment names the reasonix.io website
instead of a stylesheet path that no longer exists here.
studio has no branch protection or required status checks, so dropping
the job leaves no check pending. actionlint passes and make lint is
clean. internal/fileref still skips site/.astro in file pickers: that is
workspace behavior for users' Astro projects, not a dependency on this
directory.
This commit is contained in:
1 parent
921a04bda0
commit
04e9bbe4a0
63 files changed
+8
-12336
No files matched your search
@@ -18,7 +18,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Cheap path gate for pull requests. PRs confined to docs/site/release-notes
|
||||
# Cheap path gate for pull requests. PRs confined to docs/release-notes
|
||||
# (or top-level Markdown) skip the heavy Go jobs, and PRs that cannot affect
|
||||
# the desktop module skip the desktop jobs. Job-level `if` reports skipped,
|
||||
# which satisfies the required status checks (lint, race, test) — a
|
||||
@@ -32,7 +32,6 @@ jobs:
|
||||
outputs:
|
||||
code: ${{ steps.filter.outputs.code }}
|
||||
desktop: ${{ steps.filter.outputs.desktop }}
|
||||
site: ${{ steps.filter.outputs.site }}
|
||||
sdk: ${{ steps.filter.outputs.sdk }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -40,7 +39,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- id: filter
|
||||
run: |
|
||||
code=true; desktop=true; site=true; sdk=true
|
||||
code=true; desktop=true; sdk=true
|
||||
base=""
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
base="${{ github.event.pull_request.base.sha }}"
|
||||
@@ -50,21 +49,20 @@ jobs:
|
||||
if [ -n "$base" ] && git cat-file -e "$base^{commit}" 2>/dev/null; then
|
||||
files=$(git diff --name-only "$base" HEAD)
|
||||
if [ -n "$files" ]; then
|
||||
code=false; desktop=false; site=false; sdk=false
|
||||
code=false; desktop=false; sdk=false
|
||||
# Root-module CI: desktop/ is a separate module, so only the
|
||||
# clearly unrelated paths below can skip it.
|
||||
if echo "$files" | grep -qvE '^(docs/|site/|release-notes/|desktop/|workers/|[^/]+\.md$)'; then code=true; fi
|
||||
if echo "$files" | grep -qvE '^(docs/|release-notes/|desktop/|workers/|[^/]+\.md$)'; then code=true; fi
|
||||
# desktop/ imports the root kernel via `replace reasonix => ../`,
|
||||
# so only this clearly-unrelated set is safe to skip.
|
||||
if echo "$files" | grep -qvE '^(docs/|site/|release-notes/|workers/|benchmarks/|npm/|[^/]+\.md$)'; then desktop=true; fi
|
||||
if echo "$files" | grep -q '^site/'; then site=true; fi
|
||||
if echo "$files" | grep -qvE '^(docs/|release-notes/|workers/|benchmarks/|npm/|[^/]+\.md$)'; then desktop=true; fi
|
||||
# sdk/go is a nested module invisible to root `go test ./...`;
|
||||
# its DTOs are generated from internal/extension/protocol, so
|
||||
# both paths must trigger the sdk job.
|
||||
if echo "$files" | grep -qE '^(sdk/|internal/extension/)'; then sdk=true; fi
|
||||
fi
|
||||
fi
|
||||
{ echo "code=$code"; echo "desktop=$desktop"; echo "site=$site"; echo "sdk=$sdk"; } >> "$GITHUB_OUTPUT"
|
||||
{ echo "code=$code"; echo "desktop=$desktop"; echo "sdk=$sdk"; } >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The ruleset requires the per-OS check names (test (ubuntu-latest) etc.).
|
||||
# A matrix job skipped at job level reports no per-leg checks at all, so
|
||||
@@ -464,27 +462,6 @@ jobs:
|
||||
.github/workflows/release-studio.yml \
|
||||
.github/workflows/ci.yml
|
||||
|
||||
# The site/ auth client has security-sensitive redirect-validation logic
|
||||
# (safeNext) covered by node:test unit tests. Those tests use only Node
|
||||
# builtins, so no `npm install` is needed — run them directly on every PR so
|
||||
# a regression in redirect validation fails the build instead of shipping.
|
||||
site:
|
||||
needs: changes
|
||||
if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.site != 'false')
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: site
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: test
|
||||
run: npm test
|
||||
|
||||
govulncheck:
|
||||
needs: changes
|
||||
if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.code != 'false')
|
||||
|
||||
@@ -25,11 +25,6 @@
|
||||
/catalog-detector
|
||||
/tool-surface-cache
|
||||
|
||||
# Website (Astro) build output
|
||||
/site/dist/
|
||||
/site/.astro/
|
||||
/site/_preview.png
|
||||
|
||||
# Go test/build scratch
|
||||
*.test
|
||||
*.out
|
||||
@@ -80,7 +75,6 @@ desktop/**/.reasonix/
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
/site/public/av/
|
||||
|
||||
benchmarks/context-maintenance-e2e/run/
|
||||
|
||||
@@ -90,7 +84,6 @@ temp/
|
||||
# Official theme art intermediate outputs / caches
|
||||
scripts/official-theme-art/out/
|
||||
scripts/official-theme-art/__pycache__/
|
||||
site/.generated/
|
||||
|
||||
# Committed pointer at REASONIX.md; overrides a global CLAUDE.md ignore rule.
|
||||
!/CLAUDE.md
|
||||
|
||||
@@ -1,15 +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)\/?$/.test(page) &&
|
||||
!/\/changelog\/v\d+\.\d+\.\d+-/.test(page),
|
||||
})],
|
||||
});
|
||||
Generated
-4295
File diff suppressed because it is too large.
Load diff
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "reasonix-site",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"prebuild": "node scripts/fetch-community.mjs && node scripts/fetch-publications.mjs",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"test": "node --test src/scripts/*.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^3.7.3",
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||
"astro": "^7.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sharp": "^0.35.3"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "0.28.1"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,50 +0,0 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { validateReleaseEvent } from "../../scripts/release-event.mjs";
|
||||
|
||||
const siteRoot = resolve(import.meta.dirname, "..");
|
||||
const repoRoot = resolve(siteRoot, "..");
|
||||
const catalog = JSON.parse(await readFile(resolve(repoRoot, "release-notes/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,266 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import SiteHeader from './SiteHeader.astro';
|
||||
import type { ReleaseItem, ReleaseRecord } from '../data/releases';
|
||||
import { releasePath } from '../data/releases';
|
||||
import '../styles/changelog.css';
|
||||
|
||||
const { release, releases, canonical } = Astro.props as {
|
||||
release: ReleaseRecord;
|
||||
releases: ReleaseRecord[];
|
||||
canonical?: string;
|
||||
};
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const repo = 'https://github.com/esengine/DeepSeek-Reasonix';
|
||||
const prHref = (ref: number) => `${repo}/pull/${ref}`;
|
||||
|
||||
const isPreview = release.channel === 'prerelease';
|
||||
const latestForChannel = releases[0];
|
||||
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: '插件' },
|
||||
}[surface] ?? { en: surface, zh: surface });
|
||||
|
||||
/* Sections render only when they have content; numbering is sequential. */
|
||||
const sectionDefs = [
|
||||
...(release.guides.length ? [{ id: 'guides', en: 'Guides', zh: '使用攻略' }] : []),
|
||||
{ id: 'highlights', en: 'Highlights', zh: '重点内容' },
|
||||
...(['new', 'improved', 'fixed'] as const)
|
||||
.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: '风险提示' }] : []),
|
||||
...(release.contributors.length ? [{ id: 'thanks', en: 'Thanks', zh: '致谢' }] : []),
|
||||
];
|
||||
const sectionNum = new Map(sectionDefs.map((s, i) => [s.id, String(i + 1).padStart(2, '0')]));
|
||||
const tocItems = [...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 v${release.version} release notes`;
|
||||
const titleZh = `Reasonix 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">
|
||||
<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 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>
|
||||
)}
|
||||
<p class="release-lede"><span class="l-en">{release.summary.en}</span><span class="l-zh">{release.summary.zh}</span></p>
|
||||
</header>
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
|
||||
{(['new', 'improved', 'fixed'] as const).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>
|
||||
<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>
|
||||
<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.contributors.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>{release.contributors.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">Full comparison ↗</span><span class="l-zh">完整差异 ↗</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>
|
||||
|
||||
<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,130 +0,0 @@
|
||||
---
|
||||
type ActiveItem = 'features' | 'surfaces' | 'skills' | 'community' | 'docs' | 'changelog';
|
||||
|
||||
const {
|
||||
active,
|
||||
scrolled = false,
|
||||
accountSlot = false,
|
||||
} = Astro.props as {
|
||||
active?: ActiveItem;
|
||||
scrolled?: boolean;
|
||||
accountSlot?: 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: '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>
|
||||
{accountSlot ? (
|
||||
<span class="nav-acct" data-account-slot></span>
|
||||
) : (
|
||||
<a class="btn btn-ghost nav-sign-in" href={`${base}/login/`}>
|
||||
<span class="l-en">Sign in</span>
|
||||
<span class="l-zh">登录</span>
|
||||
</a>
|
||||
)}
|
||||
<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>
|
||||
{accountSlot ? (
|
||||
<span class="nav-acct" data-account-slot></span>
|
||||
) : (
|
||||
<a class="btn btn-ghost" href={`${base}/login/`}><span class="l-en">Sign in</span><span class="l-zh">登录</span></a>
|
||||
)}
|
||||
<a class="btn btn-dark" href={`${base}/#start`}><span class="l-en">Install</span><span class="l-zh">安装</span></a>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</header>
|
||||
@@ -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,70 +0,0 @@
|
||||
import source from '../../../release-notes/releases.json';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export type LocalizedText = { en: string; zh: string };
|
||||
|
||||
export type ReleaseItem = {
|
||||
title: LocalizedText;
|
||||
body: LocalizedText;
|
||||
refs?: number[];
|
||||
};
|
||||
|
||||
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;
|
||||
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 releases = publishedReleases.filter((release) => 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}/`;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
import Base from './Base.astro';
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const repo = 'https://github.com/esengine/DeepSeek-Reasonix';
|
||||
const { title, description, titleEn, titleZh, wide } = Astro.props;
|
||||
---
|
||||
<Base title={title} description={description} titleEn={titleEn} titleZh={titleZh}>
|
||||
<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={wide ? 'auth-card wide' : 'auth-card'}>
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<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>
|
||||
</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 — a coding agent you can leave running" />
|
||||
<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,103 +0,0 @@
|
||||
---
|
||||
import "../styles/community.css";
|
||||
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>
|
||||
|
||||
<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,37 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.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>
|
||||
</Base>
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
import Account from '../layouts/Account.astro';
|
||||
---
|
||||
<Account
|
||||
wide
|
||||
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">
|
||||
<div>
|
||||
<div id="acct-handle" class="acct-handle"></div>
|
||||
<div id="acct-email" class="acct-email"></div>
|
||||
<a id="acct-profile-link" class="auth-small" href="/u/"><span class="l-en">View public profile</span><span class="l-zh">查看公开主页</span></a>
|
||||
</div>
|
||||
<span id="acct-role" class="acct-role"></span>
|
||||
</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>
|
||||
<input id="currentPassword" type="password" autocomplete="current-password" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="newPassword"><span class="l-en">New password</span><span class="l-zh">新密码</span></label>
|
||||
<input id="newPassword" type="password" 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>
|
||||
|
||||
<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>
|
||||
</Account>
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
import ChangelogPage from '../../components/ChangelogPage.astro';
|
||||
import { allReleases, publishedReleases, releases } 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('/changelog/');
|
||||
}
|
||||
---
|
||||
<ChangelogPage release={release} releases={releases} />
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
import ChangelogPage from '../../components/ChangelogPage.astro';
|
||||
import { latestStableRelease, releases, releasePath } from '../../data/releases';
|
||||
|
||||
/* /changelog/ renders the latest 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,3 +0,0 @@
|
||||
---
|
||||
return Astro.redirect('/changelog/', 301);
|
||||
---
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
return Astro.redirect('/changelog/', 301);
|
||||
---
|
||||
@@ -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,58 +0,0 @@
|
||||
---
|
||||
import Community from "../../layouts/Community.astro";
|
||||
---
|
||||
<Community title="Reasonix Community" description="Ask questions, share skills, and shape a DeepSeek-native agent." active="discussions">
|
||||
<section class="head">
|
||||
<div class="wrap">
|
||||
<div class="row">
|
||||
<div>
|
||||
<div class="eyebrow"><span class="l-en">Reasonix Community</span><span class="l-zh">Reasonix 社区</span></div>
|
||||
<h1><span class="l-en">Discussions</span><span class="l-zh">讨论区</span></h1>
|
||||
<p><span class="l-en">Ask questions, share skills, and shape a DeepSeek-native agent — with the makers and other builders.</span><span class="l-zh">提问、分享技能、一起打磨这个 DeepSeek 原生 Agent —— 和作者及其他构建者。</span></p>
|
||||
</div>
|
||||
<a class="btn btn-primary" href="/community/new/"><span class="l-en">✎ New topic</span><span class="l-zh">✎ 发起话题</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="wrap layout">
|
||||
<div>
|
||||
<div class="sec-title"><h2><span class="l-en">Categories</span><span class="l-zh">分区</span></h2></div>
|
||||
<div class="cats" id="cat-list">
|
||||
<div class="cat"><span class="ico">·</span><div><h3><span class="l-en">Loading…</span><span class="l-zh">加载中…</span></h3></div></div>
|
||||
</div>
|
||||
|
||||
<div class="sec-title">
|
||||
<div class="tabs" id="sort-tabs">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topics" id="topic-list">
|
||||
<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>
|
||||
|
||||
<aside class="side">
|
||||
<div class="card cta">
|
||||
<h4><span class="l-en">Start a discussion</span><span class="l-zh">发起讨论</span></h4>
|
||||
<p class="lead"><span class="l-en">Ask a question or share what you built.</span><span class="l-zh">提个问题,或分享你做的东西。</span></p>
|
||||
<a class="btn btn-primary" href="/community/new/"><span class="l-en">✎ New topic</span><span class="l-zh">✎ 发起话题</span></a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h4 style="margin-bottom:14px"><span class="l-en">Community</span><span class="l-zh">社区</span></h4>
|
||||
<div class="stats-grid" id="stats">
|
||||
<div class="stat-box"><div class="n" id="s-topics">—</div><div class="l"><span class="l-en">topics</span><span class="l-zh">话题</span></div></div>
|
||||
<div class="stat-box"><div class="n" id="s-cats">—</div><div class="l"><span class="l-en">categories</span><span class="l-zh">分区</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h4 style="margin-bottom:14px"><span class="l-en">Guidelines</span><span class="l-zh">社区规范</span></h4>
|
||||
<p class="lead"><span class="l-en">Be kind, search first, and keep code in fenced blocks. New accounts unlock links and more as they participate.</span><span class="l-zh">友善、先搜索、代码放进围栏块。新账号随参与逐步解锁发链接等权限。</span></p>
|
||||
<a class="btn btn-ghost sm" href="/community/guidelines/"><span class="l-en">Read the guidelines</span><span class="l-zh">阅读规范</span></a>
|
||||
</div>
|
||||
</aside>
|
||||
</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,34 +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">Authorize sign-in</span><span class="l-zh">授权登录</span></h1>
|
||||
<p><span class="l-en">Confirm the code shown in your terminal or app.</span><span class="l-zh">请确认终端或应用里显示的验证码。</span></p>
|
||||
</div>
|
||||
|
||||
<div id="device-msg" class="auth-msg" hidden></div>
|
||||
|
||||
<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" style="width:100%"><span class="l-en">Continue</span><span class="l-zh">继续</span></button>
|
||||
|
||||
<p id="device-meta" class="device-meta" 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</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,414 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.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 goVer = 'latest';
|
||||
---
|
||||
<Base
|
||||
title="Docs — Reasonix"
|
||||
titleEn="Docs — Reasonix"
|
||||
titleZh="文档 — Reasonix"
|
||||
description="Install Reasonix, start your first session, search version-matched embedded docs and release notes, connect ACP-compatible editors, configure providers and credentials, and understand prefix cache, permissions, sandboxing, MCP plugins, memory, rewind, desktop and bot workflows.">
|
||||
|
||||
<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="#install"><strong><span class="l-en">Install</span><span class="l-zh">安装入口</span></strong><small><span class="l-en">CLI/TUI and desktop builds</span><span class="l-zh">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">Approvals</span><span class="l-zh">审批与沙箱</span></strong><small><span class="l-en">Ask, Auto, YOLO, sandbox</span><span class="l-zh">Ask、Auto、YOLO、沙箱</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="#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 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="#install">
|
||||
<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="install"><span class="l-en">CLI / TUI</span><span class="l-zh">CLI / TUI</span></h2>
|
||||
<p><span class="l-en">Install the terminal 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 desktop app when</span><span class="l-zh">适合用桌面端</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, Ask, Auto, YOLO, transcript scrolling, and tool approval prompts.</span><span class="l-zh">交互式 TUI 支持 Plan、Ask、Auto、YOLO、对话滚动与工具审批提示。</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, Homebrew, and desktop/release downloads install Reasonix 1.x; the current desktop download build is <strong><span data-release-version="desktop">{goVer}</span></strong>.</span><span class="l-zh">如需安装 legacy <strong>0.x</strong> 版本,请显式 pin: <code>npm i -g [email protected]</code>。上方命令、Homebrew 与桌面端/Release 下载对应 Reasonix 1.x;当前桌面端下载构建为 <strong><span data-release-version="desktop">{goVer}</span></strong>。</span></span></div>
|
||||
|
||||
<h2 id="desktop"><span class="l-en">Desktop app</span><span class="l-zh">桌面端</span></h2>
|
||||
<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.zip`} data-desktop-asset="Reasonix-darwin-arm64.zip" download>
|
||||
<strong>macOS</strong>
|
||||
<span>Apple Silicon zip</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>
|
||||
<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 65–85%, or use <code>--local</code> for a project override. Lower values compact earlier and can reduce prefix-cache reuse.</span><span class="l-zh">自动压缩默认在模型上下文达到 80% 时启动。桌面端设置与 <code>reasonix config compact-ratio</code> 共用用户级设置;可在 65–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">Permissions gate each tool call: <code>deny</code> > <code>ask</code> > <code>allow</code> > fallback. Read-only tools generally pass; writers fall back to <code>[permissions] mode</code>. Ask is not read-only: an approved writer can still run. The sandbox is the enforced capability boundary. Headless <code>reasonix run</code> defaults to Ask and fails closed when a writer needs approval; use <code>-y</code>, <code>--auto</code>, or <code>--permission-mode auto</code> for intentional unattended writes. <code>[permissions] allow_dynamic_bash = true</code> is an advanced opt-in that lets an Allow fallback cover nested or indirect Bash. Explicit ask and deny rules still apply.</span><span class="l-zh">权限逐个把关工具调用:<code>deny</code> > <code>ask</code> > <code>allow</code> > 兜底。只读工具通常放行;写工具回退到 <code>[permissions] mode</code>。Ask 不是只读:writer 获得批准后仍会执行;沙箱才是强制能力边界。无界面的 <code>reasonix run</code> 默认使用 Ask,写操作需要审批时会安全拒绝;要显式启用无人值守写入,请使用 <code>-y</code>、<code>--auto</code> 或 <code>--permission-mode auto</code>。高级用户可设置 <code>[permissions] allow_dynamic_bash = true</code>,让 Allow fallback 覆盖嵌套或间接 Bash。显式 ask 与 deny 规则仍然生效。</span></p>
|
||||
<p><span class="l-en">The sandbox is enforcement: file-writers refuse any path outside <code>[sandbox] workspace_root</code> (default: the current dir), resolving symlinks and <code>..</code> so a link cannot tunnel out. <code>bash = "enforce"</code> jails shell commands when an OS sandbox backend is available (Seatbelt on macOS, bubblewrap on Linux); without one, bash is refused instead of running unconfined. Install the backend or set <code>bash = "off"</code> to explicitly restore the pre-1.16 unconfined behavior.</span><span class="l-zh">沙箱负责强制执行:写文件工具拒绝 <code>[sandbox] workspace_root</code>(默认当前目录)之外的任何路径,并解析符号链接与 <code>..</code>,防止借链接逃逸。<code>bash = "enforce"</code> 会在存在 OS 沙箱后端时隔离 shell 命令(macOS Seatbelt、Linux bubblewrap);没有后端时会拒绝执行 bash,而不是非隔离运行。安装对应后端,或显式设置 <code>bash = "off"</code> 来恢复 1.16 之前的非隔离行为。</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>
|
||||
reasonix acp --profile delivery <span class="c"># economy | balanced | delivery</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, execution setting, 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="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> always ask for fresh approval, even in YOLO. <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> 时,即使在 YOLO 下也会重新请求批准。<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="codeblock">reasonix setup <span class="c"># first-run: pick provider + keys</span>
|
||||
reasonix <span class="c"># interactive session, prompts before writes</span>
|
||||
reasonix run "<task>" <span class="c"># headless Ask; writers fail closed</span>
|
||||
reasonix run -y "<task>" <span class="c"># opt into unattended ordinary writes</span>
|
||||
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 /work-mode /paste-image /goal
|
||||
/reasoning-language /output-style /help</div>
|
||||
<p><span class="l-en"><code>Shift+Tab</code> cycles Ask → Auto → Plan, while <code>Ctrl+Y</code> toggles YOLO independently. Use <code>/theme auto|light|dark</code> for the terminal background mode or choose a named accent from bare <code>/theme</code>. Long drafts grow and then scroll inside the composer; wheel events over the transcript continue to scroll the conversation.</span><span class="l-zh"><code>Shift+Tab</code> 按 Ask → Auto → Plan 循环,<code>Ctrl+Y</code> 独立切换 YOLO。用 <code>/theme auto|light|dark</code> 选择终端背景模式,也可从不带参数的 <code>/theme</code> 中选择命名强调色。长草稿会先增长,超过上限后在输入框内滚动;鼠标位于 transcript 时仍滚动对话。</span></p>
|
||||
<p><span class="l-en">The responsive footer shows the active permission posture and interaction state first. Wide terminals keep model, effort, and execution setting 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 65–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>
|
||||
<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>
|
||||
</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,494 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import SiteHeader from '../components/SiteHeader.astro';
|
||||
import community from '../data/community.json';
|
||||
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const repo = 'https://github.com/esengine/DeepSeek-Reasonix';
|
||||
const repoReleases = `${repo}/releases`;
|
||||
const desktopStableFallback = repoReleases;
|
||||
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 AI coding agent you can leave running. One local engine, four ways in — terminal, desktop app, browser, or your editor over ACP. Plan mode, permissions, a workspace sandbox and checkpoints keep long autonomous runs reviewable. A single Go binary, no runtime to install. 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 — a coding agent you can leave running"
|
||||
titleEn="Reasonix — a coding agent you can leave running"
|
||||
titleZh="Reasonix — 可以一直开着跑的编码 Agent"
|
||||
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 · a single Go binary</span><span class="l-zh">开源 · MIT · 单个 Go 二进制</span></div>
|
||||
<h1 class="reveal" style="--d:0.06s">
|
||||
<span class="l-en">A coding agent you can<br /><span class="tt">leave running</span>.</span>
|
||||
<span class="l-zh">可以<span class="tt">一直开着跑</span>的<br />编码 Agent。</span>
|
||||
</h1>
|
||||
<p class="sub reveal" style="--d:0.12s">
|
||||
<span class="l-en">One local engine, four ways in — terminal, desktop app, browser, or your editor over ACP. Plan mode, permissions, a workspace sandbox and per-turn checkpoints keep a long autonomous run something you can still read and undo.</span>
|
||||
<span class="l-zh">一套本地引擎,四个入口——终端、桌面端、浏览器,或通过 ACP 接入你的编辑器。计划模式、权限、工作区沙箱与逐轮 checkpoint,让长时间自治运行始终可读、可撤销。</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">Desktop app</span><span class="l-zh">桌面端</span></span>
|
||||
<strong><span class="l-en">Visual workspace for sessions, settings, approvals, and updates.</span><span class="l-zh">可视化管理会话、设置、审批与自动更新。</span></strong>
|
||||
<a class="btn btn-dark" data-os-dl data-goto="desktop" href="#start"><span class="l-en">Download desktop</span><span class="l-zh">下载桌面版</span></a>
|
||||
</div>
|
||||
<div class="install-option install-option--cli">
|
||||
<canvas class="particle-canvas"></canvas>
|
||||
<span class="install-kicker"><span class="l-en">CLI / TUI</span><span class="l-zh">CLI / TUI</span></span>
|
||||
<strong><span class="l-en">Terminal-native agent with the same local Reasonix engine.</span><span class="l-zh">终端原生入口,使用同一套本地 Reasonix 引擎。</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="?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</a>
|
||||
</span>
|
||||
</span>
|
||||
<span class="l-zh also-row">
|
||||
<strong class="also-label">更多使用方式:</strong>
|
||||
<span class="also-links">
|
||||
<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">Autonomy you can <span class="em">actually audit.</span></span><span class="l-zh">可以<span class="em">真正审阅</span>的自治。</span></h2>
|
||||
<p><span class="l-en">Letting an agent run for hours is only useful if you can see what it did and undo the parts you don't want. Every layer here exists for that: plan first, gate each tool call, checkpoint every turn.</span><span class="l-zh">让 agent 跑上几个小时,前提是你能看清它做了什么、并撤销你不想要的部分。这里的每一层都为此存在:先规划、逐次工具调用受控、逐轮 checkpoint。</span></p>
|
||||
</div>
|
||||
<div class="feat-grid">
|
||||
<div class="feat reveal">
|
||||
<span class="tag">01 / permissions</span>
|
||||
<h3><span class="l-en">Every tool call is gated</span><span class="l-zh">每次工具调用都受控</span></h3>
|
||||
<p><span class="l-en">Reads, writes, and shell commands each ask separately, and the workspace sandbox bounds what a command can touch even after you say yes.</span><span class="l-zh">读取、写入、shell 命令分别询问;即便你放行,工作区沙箱仍限定命令能触碰的范围。</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">Undo any turn</span><span class="l-zh">任意一轮都可回退</span></h3>
|
||||
<p><span class="l-en">Each turn writes a checkpoint outside git, so a six-hour run rewinds to any point without touching your commit history.</span><span class="l-zh">每轮在 git 之外写入 checkpoint,六小时的运行可回退到任意一点,且不污染你的提交历史。</span></p>
|
||||
</div>
|
||||
<div class="feat reveal" style="--d:0.04s">
|
||||
<span class="tag">03 / plan mode</span>
|
||||
<h3><span class="l-en">Plan before it touches anything</span><span class="l-zh">动手前先给出计划</span></h3>
|
||||
<p><span class="l-en"><code>/plan</code> holds every write until you've read the plan and approved it.</span><span class="l-zh"><code>/plan</code> 会先给出计划,在你读过并批准前不写入任何东西。</span></p>
|
||||
</div>
|
||||
<div class="feat reveal" style="--d:0.1s">
|
||||
<span class="tag">04 / mcp + skills</span>
|
||||
<h3><span class="l-en">Extend it without forking it</span><span class="l-zh">不用 fork 就能扩展</span></h3>
|
||||
<p><span class="l-en">MCP over stdio, SSE, and streamable HTTP merges external tools into one registry. Markdown skills and subagents — explore, research, review, security-review — run with their own isolated tool sets.</span><span class="l-zh">MCP 支持 stdio、SSE 与 streamable HTTP,把外部工具合并进同一 registry。Markdown 技能与子智能体(explore / research / review / security-review)各自持有隔离的工具集。</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">One file, no runtime</span><span class="l-zh">一个文件,零运行时</span></h3>
|
||||
<p><span class="l-en">CGO-free Go, cross-compiled for darwin/linux/windows × amd64/arm64. Nothing to install underneath it.</span><span class="l-zh">无 CGO 的 Go,交叉编译覆盖 darwin/linux/windows × amd64/arm64。底下不需要装任何东西。</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 machine</span><span class="l-zh">你的 Key,你的机器</span></h3>
|
||||
<p><span class="l-en">Bring your own DeepSeek key. The engine runs locally — your code goes to the model you configured and nowhere else. MIT licensed, developed in public.</span><span class="l-zh">使用你自己的 DeepSeek Key。引擎在本地运行——代码只发往你配置的模型,不去别处。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">One engine. <span class="em">Four ways in.</span></span><span class="l-zh">同一个引擎。<span class="em">四个入口。</span></span></h2>
|
||||
<p><span class="l-en">Not four products — one local engine behind four front ends. Sessions, permissions, skills and MCP servers are shared, so a task you start in the terminal is waiting for you in the desktop app.</span><span class="l-zh">不是四个产品,而是一个本地引擎配四个前端。会话、权限、技能与 MCP 服务器都共享——在终端起的任务,打开桌面端就在那儿等着。</span></p>
|
||||
</div>
|
||||
<div class="surf-grid">
|
||||
<div class="surf reveal">
|
||||
<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</span><span class="l-zh">终端</span></h3>
|
||||
<p><span class="l-en">Full-screen TUI — diff review, approvals, slash commands.</span><span class="l-zh">全屏 TUI——diff 审阅、审批、斜杠命令。</span></p>
|
||||
<code>reasonix</code>
|
||||
</div>
|
||||
<div class="surf reveal" style="--d:0.06s">
|
||||
<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</span><span class="l-zh">桌面端</span></h3>
|
||||
<p><span class="l-en">Sessions side by side, settings, approvals, auto-update.</span><span class="l-zh">多会话并列、设置、审批与自动更新。</span></p>
|
||||
<code><span class="l-en">macOS · Windows · Linux</span><span class="l-zh">macOS · Windows · Linux</span></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 local web UI on your own machine. Add auth before you expose it.</span><span class="l-zh">跑在你自己机器上的本地 Web UI。对外暴露前先开认证。</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; anything else that speaks ACP.</span><span class="l-zh">通过扩展接入 VS Code 与 VSCodium;其他支持 ACP 的编辑器同样可用。</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">One command. macOS, Linux, Windows, and WSL.</span><span class="l-zh">一条命令,支持 macOS、Linux、Windows 与 WSL。</span></p>
|
||||
<code>npm i -g reasonix</code>
|
||||
</div>
|
||||
<div class="step reveal" style="--d:0.08s">
|
||||
<span class="num">02</span>
|
||||
<h3><span class="l-en">Point it at a repo</span><span class="l-zh">指向你的仓库</span></h3>
|
||||
<p><span class="l-en">It maps the codebase once and keeps that map for the rest of the session.</span><span class="l-zh">只做一次代码库测绘,这份地图在整场会话里一直有效。</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">Open the web UI</span><span class="l-zh">打开 Web UI</span></h3>
|
||||
<p><span class="l-en">Keep it local, or add token/password auth before sharing a tunnel or remote port.</span><span class="l-zh">本机使用保持默认;通过 tunnel 或远程端口分享前,先启用 token/password 认证。</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">Leave it running</span><span class="l-zh">让它一直跑着</span></h3>
|
||||
<p><span class="l-en">Queue tasks, review diffs, resume anytime. The session never goes cold.</span><span class="l-zh">排任务、看 diff、随时恢复。会话永不冷却。</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">Start a session that <span class="em">lasts.</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. Bring your own DeepSeek API key — your code never leaves your machine and the model.</span><span class="l-zh">MIT 开源。使用你自己的 DeepSeek API Key——代码不会离开你的机器与模型。</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-desktop" type="button" data-pane="desktop" role="tab" aria-controls="download-pane-desktop" aria-selected="true" tabindex="0"><span class="l-en">Desktop</span><span class="l-zh">桌面端</span></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">CLI</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>
|
||||
</div>
|
||||
|
||||
<div class="dl-pane active" id="download-pane-desktop" data-pane="desktop" role="tabpanel" aria-labelledby="download-tab-desktop">
|
||||
<div class="release-channel-summary" data-release-summary="desktop" aria-live="polite">
|
||||
<span><span class="l-en">Current official version <span data-release-version="desktop">latest</span></span><span class="l-zh">当前正式版本 <span data-release-version="desktop">latest</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">latest</span></span><span class="l-zh">下载通用版 · <span data-release-version="desktop">latest</span></span></a>
|
||||
<div class="os-options" aria-label="macOS alternate downloads">
|
||||
<a href={desktopStableFallback} data-desktop-asset="Reasonix-darwin-arm64.zip">Apple Silicon zip</a>
|
||||
<a href={desktopStableFallback} data-desktop-asset="Reasonix-darwin-amd64.zip">Intel zip</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">latest</span></span><span class="l-zh">下载 · <span data-release-version="desktop">latest</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">latest</span></span><span class="l-zh">下载 .deb · <span data-release-version="desktop">latest</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={`${base}/changelog/`} data-release-notes="desktop">release notes →</a> · <a href={`${repo}/releases`}>all releases →</a></span><span class="l-zh">Reasonix 会自动检查正式版本 · <a href={`${base}/changelog/`} 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>
|
||||
</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">Current official version <span data-release-version="cli">latest</span></span><span class="l-zh">当前正式版本 <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 1.x · one binary · no Node · actively developed</span><span class="l-zh">Reasonix 1.x · 单二进制 · 无需 Node · 主力开发中</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>
|
||||
<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="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">Built to be left running.</span><span class="l-zh">为长时间常驻运行而造。</span></span>
|
||||
<span class="ver"><span class="rxv">{goVer}</span> · official</span>
|
||||
</div>
|
||||
</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,36 +0,0 @@
|
||||
---
|
||||
import Account from '../layouts/Account.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>
|
||||
<input id="password" name="password" type="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,38 +0,0 @@
|
||||
---
|
||||
import Account from '../layouts/Account.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>
|
||||
<input id="password" name="password" type="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,33 +0,0 @@
|
||||
---
|
||||
import Account from '../layouts/Account.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>
|
||||
<input id="password" name="password" type="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>
|
||||
<input id="confirm" name="confirm" type="password" 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,563 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.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" accountSlot />
|
||||
|
||||
<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, 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>
|
||||
</div>
|
||||
<div class="reg-tabs" data-group="sort" role="tablist">
|
||||
<button class="active" 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="https://id.reasonix.io"><span class="l-en">Sign in at id.reasonix.io</span><span class="l-zh">前往 id.reasonix.io 登录</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></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 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>
|
||||
<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>
|
||||
<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="https://id.reasonix.io"><span class="l-en">Sign in</span><span class="l-zh">登录</span></a>
|
||||
</nav>
|
||||
</div>
|
||||
</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-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; }
|
||||
|
||||
.nav-acct { display: inline-flex; align-items: center; }
|
||||
.acct { display: inline-flex; align-items: center; gap: 10px; }
|
||||
.acct-name { font-size: 14px; font-weight: 600; color: var(--ink); text-decoration: none; }
|
||||
.acct-name:hover { color: var(--accent); }
|
||||
.acct-logout { font-family: var(--font-sans); font-size: 13px; font-weight: 500; color: var(--ink-2); background: var(--bg-soft); border: none; cursor: pointer; padding: 8px 14px; border-radius: 999px; transition: background .2s, color .2s; }
|
||||
.acct-logout:hover { color: var(--ink); background: var(--line); }
|
||||
.acct-warn { font-size: 11px; font-weight: 600; color: oklch(0.52 0.15 60); background: color-mix(in oklch, oklch(0.7 0.16 60) 16%, var(--card)); padding: 3px 9px; border-radius: 999px; text-decoration: none; }
|
||||
.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-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 { currentAccount, accountLogout } from '../scripts/auth.js';
|
||||
const REGISTRY = import.meta.env.PUBLIC_REGISTRY_ORIGIN || 'https://crash.reasonix.io';
|
||||
const state = { kind: 'all', sort: 'new', 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' : ''}`;
|
||||
const installRequest = (p) =>
|
||||
`Install this Reasonix ${p.kind} package from ${p.source}. Use install_source with kind="${p.kind}".`;
|
||||
|
||||
function show(el, on) { if (el) el.hidden = !on; }
|
||||
|
||||
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 stars = p.starCount > 0 ? `<span class="pkg-stars">★ ${p.starCount}</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: 'starred', 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 — powers the trending rank.
|
||||
fetch(`${REGISTRY}/v1/packages/${btn.dataset.slug}/installed`, { method: 'POST' }).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: f.get('kind'), version: f.get('version') || '',
|
||||
repoUrl: f.get('repoUrl') || '', tags,
|
||||
};
|
||||
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 pending = data.package?.status === 'pending';
|
||||
setMsg(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');
|
||||
}
|
||||
});
|
||||
|
||||
// Nav account state, via the site's shared account client. Signed out → a
|
||||
// sign-in link; signed in → the handle (linking to /account) + a log-out control.
|
||||
async function mountAccount() {
|
||||
const slots = [...document.querySelectorAll('[data-account-slot]')];
|
||||
const user = await currentAccount();
|
||||
if (!slots.length) return null;
|
||||
if (!user) {
|
||||
slots.forEach((slot) => {
|
||||
slot.innerHTML = `<a class="btn btn-ghost" href="/login/?next=${encodeURIComponent('/skills/')}"><span class="l-en">Sign in</span><span class="l-zh">登录</span></a>`;
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const warn = user.emailVerified ? '' : `<a class="acct-warn" href="/account/"><span class="l-en">unverified</span><span class="l-zh">未验证</span></a>`;
|
||||
slots.forEach((slot) => {
|
||||
slot.innerHTML = `<span class="acct">${warn}<a class="acct-name" href="/account/">@${esc(user.handle)}</a><button type="button" class="acct-logout"><span class="l-en">Log out</span><span class="l-zh">退出</span></button></span>`;
|
||||
slot.querySelector('.acct-logout').addEventListener('click', async () => { await accountLogout(); location.reload(); });
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
mountAccount().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,353 +0,0 @@
|
||||
import { safeNext } from "./safe-next.js";
|
||||
|
||||
// Client for the reasonix-accounts API (id.reasonix.io). Cookie-based session,
|
||||
// so every call sends credentials; the API base is build-time configurable.
|
||||
const API = (import.meta.env.PUBLIC_ACCOUNTS_API || "https://id.reasonix.io").replace(/\/$/, "");
|
||||
|
||||
async function api(path, { method = "GET", body } = {}) {
|
||||
const res = await fetch(API + path, {
|
||||
method,
|
||||
credentials: "include",
|
||||
headers: body ? { "content-type": "application/json" } : undefined,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch {}
|
||||
if (!res.ok) {
|
||||
const err = new Error(data?.error?.message || "Something went wrong. Please try again.");
|
||||
err.code = data?.error?.code;
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// Reusable helpers for account-aware pages (nav state, gated actions). Importing
|
||||
// this module also runs the form auto-wiring below, but each block is guarded by
|
||||
// element presence, so pages without auth forms just get these two helpers.
|
||||
export async function currentAccount() {
|
||||
try { return (await api("/me")).user; } catch { return null; }
|
||||
}
|
||||
export async function accountLogout() {
|
||||
try { await api("/auth/logout", { method: "POST" }); } catch {}
|
||||
}
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const qp = new URLSearchParams(location.search);
|
||||
const withBase = (p) => (import.meta.env.BASE_URL.replace(/\/$/, "") + p) || p;
|
||||
|
||||
function msg(el, kind, text) {
|
||||
if (!el) return;
|
||||
el.className = "auth-msg " + kind;
|
||||
el.textContent = text;
|
||||
el.hidden = false;
|
||||
}
|
||||
function clearMsg(el) { if (el) el.hidden = true; }
|
||||
|
||||
function busy(btn, on) {
|
||||
if (!btn) return;
|
||||
btn.disabled = on;
|
||||
btn.classList.toggle("loading", on);
|
||||
}
|
||||
|
||||
// login — POST /auth/login, then continue to ?next or /account.
|
||||
const loginForm = $("login-form");
|
||||
if (loginForm) {
|
||||
const box = $("login-msg");
|
||||
const verified = qp.get("verified");
|
||||
if (verified === "1") msg(box, "ok", "Email confirmed. Sign in to continue.");
|
||||
else if (verified === "0") msg(box, "error", "That confirmation link was invalid or has expired.");
|
||||
loginForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
clearMsg(box);
|
||||
const btn = loginForm.querySelector("button[type=submit]");
|
||||
busy(btn, true);
|
||||
try {
|
||||
await api("/auth/login", { method: "POST", body: { email: $("email").value.trim(), password: $("password").value } });
|
||||
location.href = safeNext(qp.get("next"), location.origin) || withBase("/account/");
|
||||
} catch (err) {
|
||||
msg(box, "error", err.message);
|
||||
busy(btn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// register — enumeration-safe: the API returns the same message either way.
|
||||
const registerForm = $("register-form");
|
||||
if (registerForm) {
|
||||
const box = $("register-msg");
|
||||
registerForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
clearMsg(box);
|
||||
const btn = registerForm.querySelector("button[type=submit]");
|
||||
busy(btn, true);
|
||||
try {
|
||||
const out = await api("/auth/register", {
|
||||
method: "POST",
|
||||
body: {
|
||||
email: $("email").value.trim(),
|
||||
password: $("password").value,
|
||||
displayName: $("displayName").value.trim() || undefined,
|
||||
},
|
||||
});
|
||||
msg(box, "ok", out?.message || "Check your inbox to confirm your account.");
|
||||
registerForm.reset();
|
||||
} catch (err) {
|
||||
msg(box, "error", err.message);
|
||||
} finally {
|
||||
busy(btn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// forgot — always a generic success (never reveals whether the email exists).
|
||||
const forgotForm = $("forgot-form");
|
||||
if (forgotForm) {
|
||||
const box = $("forgot-msg");
|
||||
forgotForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
clearMsg(box);
|
||||
const btn = forgotForm.querySelector("button[type=submit]");
|
||||
busy(btn, true);
|
||||
try {
|
||||
const out = await api("/auth/forgot", { method: "POST", body: { email: $("email").value.trim() } });
|
||||
msg(box, "ok", out?.message || "If that account exists, a reset link is on its way.");
|
||||
forgotForm.reset();
|
||||
} catch (err) {
|
||||
msg(box, "error", err.message);
|
||||
} finally {
|
||||
busy(btn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// reset — token comes from the emailed link.
|
||||
const resetForm = $("reset-form");
|
||||
if (resetForm) {
|
||||
const box = $("reset-msg");
|
||||
const token = qp.get("token") || "";
|
||||
if (!token) {
|
||||
msg(box, "error", "This reset link is incomplete. Request a new one.");
|
||||
resetForm.querySelectorAll("input, button").forEach((el) => (el.disabled = true));
|
||||
}
|
||||
resetForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
clearMsg(box);
|
||||
const password = $("password").value;
|
||||
if (password !== $("confirm").value) {
|
||||
msg(box, "error", "The two passwords don't match.");
|
||||
return;
|
||||
}
|
||||
const btn = resetForm.querySelector("button[type=submit]");
|
||||
busy(btn, true);
|
||||
try {
|
||||
await api("/auth/reset", { method: "POST", body: { token, password } });
|
||||
msg(box, "ok", "Password updated. You can sign in now.");
|
||||
resetForm.reset();
|
||||
resetForm.querySelectorAll("input, button").forEach((el) => (el.disabled = true));
|
||||
} catch (err) {
|
||||
msg(box, "error", err.message);
|
||||
busy(btn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// account — profile view/edit, password change, sign out, delete.
|
||||
const accountView = $("account-view");
|
||||
if (accountView) {
|
||||
const gate = $("account-gate");
|
||||
let accountUser = null;
|
||||
const fill = (user) => {
|
||||
accountUser = user;
|
||||
$("acct-handle").textContent = "@" + user.handle;
|
||||
$("acct-email").textContent = user.email + (user.emailVerified ? "" : " · unconfirmed");
|
||||
$("acct-role").textContent = user.role;
|
||||
$("acct-profile-link").href = withBase("/u/?handle=" + encodeURIComponent(user.handle));
|
||||
$("verification-wrap").hidden = user.emailVerified;
|
||||
$("f-displayName").value = user.displayName || "";
|
||||
$("f-handle").value = user.handle || "";
|
||||
$("f-bio").value = user.bio || "";
|
||||
$("f-avatarUrl").value = user.avatarUrl || "";
|
||||
accountView.hidden = false;
|
||||
if (gate) gate.hidden = true;
|
||||
};
|
||||
|
||||
api("/me")
|
||||
.then((d) => fill(d.user))
|
||||
.catch((err) => {
|
||||
if (err.status === 401) location.href = withBase("/login/?next=/account/");
|
||||
else if (gate) msg(gate, "error", err.message);
|
||||
});
|
||||
|
||||
const profileForm = $("profile-form");
|
||||
const pBox = $("profile-msg");
|
||||
profileForm?.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
clearMsg(pBox);
|
||||
const btn = profileForm.querySelector("button[type=submit]");
|
||||
busy(btn, true);
|
||||
try {
|
||||
const d = await api("/me", {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
displayName: $("f-displayName").value.trim(),
|
||||
handle: $("f-handle").value.trim().toLowerCase(),
|
||||
bio: $("f-bio").value.trim(),
|
||||
avatarUrl: $("f-avatarUrl").value.trim(),
|
||||
},
|
||||
});
|
||||
fill(d.user);
|
||||
msg(pBox, "ok", "Profile saved.");
|
||||
} catch (err) {
|
||||
msg(pBox, "error", err.message);
|
||||
} finally {
|
||||
busy(btn, false);
|
||||
}
|
||||
});
|
||||
|
||||
const passwordForm = $("password-form");
|
||||
const pwBox = $("password-msg");
|
||||
passwordForm?.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
clearMsg(pwBox);
|
||||
const btn = passwordForm.querySelector("button[type=submit]");
|
||||
busy(btn, true);
|
||||
try {
|
||||
await api("/me/password", {
|
||||
method: "POST",
|
||||
body: { currentPassword: $("currentPassword").value, newPassword: $("newPassword").value },
|
||||
});
|
||||
passwordForm.reset();
|
||||
msg(pwBox, "ok", "Password changed.");
|
||||
} catch (err) {
|
||||
msg(pwBox, "error", err.message);
|
||||
} finally {
|
||||
busy(btn, false);
|
||||
}
|
||||
});
|
||||
|
||||
$("logout-btn")?.addEventListener("click", async () => {
|
||||
try { await api("/auth/logout", { method: "POST" }); } catch {}
|
||||
location.href = withBase("/login/");
|
||||
});
|
||||
|
||||
$("resend-verification-btn")?.addEventListener("click", async () => {
|
||||
if (!accountUser || accountUser.emailVerified) return;
|
||||
const btn = $("resend-verification-btn");
|
||||
const box = $("verification-msg");
|
||||
clearMsg(box);
|
||||
busy(btn, true);
|
||||
try {
|
||||
await api("/auth/resend-verification", { method: "POST", body: { email: accountUser.email } });
|
||||
msg(box, "ok", "A new verification email is on its way.");
|
||||
} catch (err) {
|
||||
msg(box, "error", err.message);
|
||||
} finally {
|
||||
busy(btn, false);
|
||||
}
|
||||
});
|
||||
|
||||
$("delete-btn")?.addEventListener("click", async () => {
|
||||
if (!confirm("Delete your account? This cannot be undone.")) return;
|
||||
try {
|
||||
await api("/me", { method: "DELETE" });
|
||||
location.href = withBase("/");
|
||||
} catch (err) {
|
||||
msg(pBox, "error", err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// public profile — the static site uses a query parameter while the accounts
|
||||
// API keeps its canonical /u/:handle endpoint.
|
||||
const publicProfile = $("public-profile");
|
||||
if (publicProfile) {
|
||||
const gate = $("public-profile-gate");
|
||||
const handle = (qp.get("handle") || "").trim().toLowerCase();
|
||||
if (!/^[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/.test(handle) || handle.length < 3 || handle.length > 30) {
|
||||
msg(gate, "error", "That public profile address is invalid.");
|
||||
} else {
|
||||
api("/u/" + encodeURIComponent(handle))
|
||||
.then(({ user }) => {
|
||||
$("public-profile-handle").textContent = "@" + user.handle;
|
||||
$("public-profile-name").textContent = user.displayName || user.handle;
|
||||
$("public-profile-bio").textContent = user.bio || "No bio yet.";
|
||||
$("public-profile-joined").textContent = new Date(user.joinedAt).toLocaleDateString();
|
||||
const avatar = $("public-profile-avatar");
|
||||
if (user.avatarUrl) {
|
||||
try {
|
||||
const url = new URL(user.avatarUrl);
|
||||
if (url.protocol === "https:" || url.protocol === "http:") {
|
||||
avatar.src = url.href;
|
||||
avatar.hidden = false;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
gate.hidden = true;
|
||||
publicProfile.hidden = false;
|
||||
})
|
||||
.catch((err) => msg(gate, "error", err.status === 404 ? "That profile doesn't exist." : err.message));
|
||||
}
|
||||
}
|
||||
|
||||
// device — the human-facing half of the CLI/desktop device-authorization flow.
|
||||
const deviceView = $("device-view");
|
||||
if (deviceView) {
|
||||
const gate = $("device-gate");
|
||||
const box = $("device-msg");
|
||||
const codeInput = $("device-code");
|
||||
const meta = $("device-meta");
|
||||
const actions = $("device-actions");
|
||||
const preset = qp.get("code");
|
||||
if (preset && codeInput) codeInput.value = preset;
|
||||
|
||||
const showGrant = async () => {
|
||||
clearMsg(box);
|
||||
if (meta) meta.hidden = true;
|
||||
if (actions) actions.hidden = true;
|
||||
const code = codeInput.value.trim();
|
||||
if (!code) { msg(box, "error", "Enter the code shown in your terminal."); return; }
|
||||
try {
|
||||
const d = await api("/device/info?userCode=" + encodeURIComponent(code));
|
||||
if (meta) {
|
||||
meta.textContent = `Requested by ${d.grant.userAgent || "a device"}`;
|
||||
meta.hidden = false;
|
||||
}
|
||||
if (actions) actions.hidden = false;
|
||||
} catch (err) {
|
||||
msg(box, "error", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const decide = async (path, okText) => {
|
||||
clearMsg(box);
|
||||
const code = codeInput.value.trim();
|
||||
actions?.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
||||
try {
|
||||
await api(path, { method: "POST", body: { userCode: code } });
|
||||
msg(box, "ok", okText);
|
||||
if (meta) meta.hidden = true;
|
||||
if (actions) actions.hidden = true;
|
||||
codeInput.disabled = true;
|
||||
} catch (err) {
|
||||
msg(box, "error", err.message);
|
||||
actions?.querySelectorAll("button").forEach((b) => (b.disabled = false));
|
||||
}
|
||||
};
|
||||
|
||||
api("/me")
|
||||
.then(() => {
|
||||
deviceView.hidden = false;
|
||||
if (gate) gate.hidden = true;
|
||||
$("device-check")?.addEventListener("click", showGrant);
|
||||
$("device-approve")?.addEventListener("click", () => decide("/device/approve", "Approved. Return to your terminal — you're signed in."));
|
||||
$("device-deny")?.addEventListener("click", () => decide("/device/deny", "The sign-in request was rejected."));
|
||||
if (preset) showGrant();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.status === 401) {
|
||||
const next = "/device/" + (preset ? "?code=" + encodeURIComponent(preset) : "");
|
||||
location.href = withBase("/login/?next=" + encodeURIComponent(next));
|
||||
} else if (gate) msg(gate, "error", err.message);
|
||||
});
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { test } from "node:test";
|
||||
|
||||
const css = await readFile(new URL("../styles/changelog.css", import.meta.url), "utf8");
|
||||
const component = await readFile(new URL("../components/ChangelogPage.astro", import.meta.url), "utf8");
|
||||
const versionPage = await readFile(new URL("../pages/changelog/[version].astro", import.meta.url), "utf8");
|
||||
|
||||
test("historical changelog styling remains readable", () => {
|
||||
assert.doesNotMatch(css, /var\(--(?:muted|paper)\)/);
|
||||
assert.match(css, /\.release-channel\s*\{/);
|
||||
});
|
||||
|
||||
test("every release uses the version-ledger hero", () => {
|
||||
assert.doesNotMatch(component, /isThematicTitle|isThematic/);
|
||||
assert.doesNotMatch(component, /release-hero--compact|release-hero__actions|<blockquote>/);
|
||||
assert.match(component, /<header class="release-hero">/);
|
||||
assert.match(component, /<h1>Reasonix v\{release\.version\}<\/h1>/);
|
||||
assert.match(component, /<p class="release-lede">/);
|
||||
assert.doesNotMatch(css, /\.release-hero--compact|\.release-hero__actions|\.release-hero blockquote/);
|
||||
});
|
||||
|
||||
test("changelog has one official navigation and marks archives noindex", () => {
|
||||
assert.doesNotMatch(component, /release-channel-tabs/);
|
||||
assert.match(component, /Historical archive/);
|
||||
assert.match(component, /noindex=\{isPreview\}/);
|
||||
});
|
||||
|
||||
test("reviewed exact-version routes redirect safely until their publication marker exists", () => {
|
||||
assert.match(versionPage, /publishedReleases/);
|
||||
assert.match(versionPage, /publishedVersions\.has\(release\.version\)/);
|
||||
assert.match(versionPage, /if \(!published\)/);
|
||||
assert.match(versionPage, /return Astro\.redirect/);
|
||||
assert.match(versionPage, /Astro\.redirect\('\/changelog\/'\)/);
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
(function () {
|
||||
const versionSelect = document.querySelector('[data-release-version]');
|
||||
versionSelect?.addEventListener('change', () => {
|
||||
const version = String(versionSelect.value || '').replace(/^v/, '');
|
||||
if (/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
||||
window.location.href = `/changelog/v${version}/`;
|
||||
}
|
||||
});
|
||||
|
||||
const links = Array.from(document.querySelectorAll('.release-toc a[href^="#"]'));
|
||||
const sections = links.map((link) => document.querySelector(link.getAttribute('href'))).filter(Boolean);
|
||||
if (!links.length || !sections.length) return;
|
||||
|
||||
let ticking = false;
|
||||
const update = () => {
|
||||
ticking = false;
|
||||
let current = sections[0];
|
||||
for (const section of sections) {
|
||||
if (section.getBoundingClientRect().top <= 150) current = section;
|
||||
}
|
||||
links.forEach((link) => link.classList.toggle('active', link.hash === `#${current.id}`));
|
||||
};
|
||||
const queue = () => {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(update);
|
||||
};
|
||||
window.addEventListener('scroll', queue, { passive: true });
|
||||
window.addEventListener('resize', queue, { passive: true });
|
||||
update();
|
||||
})();
|
||||
@@ -1,336 +0,0 @@
|
||||
// Reasonix Community client. Renders the forum from the forum.reasonix.io API and
|
||||
// gates posting on the shared id.reasonix.io session (cookie sent cross-subdomain).
|
||||
// Bilingual like the rest of the site: static labels via .l-en/.l-zh spans that the
|
||||
// shared `reasonix-lang` choice toggles; plain-text strings pick the current lang.
|
||||
import { initTheme } from "./theme.js";
|
||||
import { initMobileNav } from "./mobile-nav.js";
|
||||
|
||||
const FORUM = (import.meta.env.PUBLIC_FORUM_API || "https://forum.reasonix.io").replace(/\/$/, "");
|
||||
const ACCOUNTS = (import.meta.env.PUBLIC_ACCOUNTS_API || "https://id.reasonix.io").replace(/\/$/, "");
|
||||
|
||||
const el = (id) => document.getElementById(id);
|
||||
const qp = new URLSearchParams(location.search);
|
||||
|
||||
let lang = "en";
|
||||
const L = (en, zh) => `<span class="l-en">${en}</span><span class="l-zh">${zh}</span>`;
|
||||
const t = (en, zh) => (lang === "zh" ? zh : en);
|
||||
|
||||
function applyLangText() {
|
||||
const attr = lang === "zh" ? "zh" : "en";
|
||||
document.querySelectorAll("[data-ph-en]").forEach((n) => { n.placeholder = n.getAttribute(`data-ph-${attr}`); });
|
||||
document.querySelectorAll("[data-l-en]").forEach((n) => { n.textContent = n.getAttribute(`data-l-${attr}`); });
|
||||
}
|
||||
function setLang(l) {
|
||||
lang = l;
|
||||
document.body.dataset.lang = l;
|
||||
document.documentElement.lang = l === "zh" ? "zh-CN" : "en";
|
||||
document.querySelectorAll(".lang-switch button").forEach((b) => b.classList.toggle("active", b.dataset.lang === l));
|
||||
try { localStorage.setItem("reasonix-lang", l); } catch {}
|
||||
applyLangText();
|
||||
}
|
||||
function initLang() {
|
||||
let saved = "";
|
||||
try { saved = localStorage.getItem("reasonix-lang") || ""; } catch {}
|
||||
setLang(saved || ((navigator.language || "").toLowerCase().startsWith("zh") ? "zh" : "en"));
|
||||
document.querySelectorAll(".lang-switch button").forEach((b) => b.addEventListener("click", () => setLang(b.dataset.lang)));
|
||||
}
|
||||
|
||||
async function api(base, path, opts = {}) {
|
||||
const res = await fetch(base + path, {
|
||||
method: opts.method || "GET",
|
||||
credentials: "include",
|
||||
headers: opts.body ? { "content-type": "application/json" } : undefined,
|
||||
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
||||
});
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch {}
|
||||
if (!res.ok) {
|
||||
const err = new Error(data?.error?.message || "Something went wrong.");
|
||||
err.code = data?.error?.code;
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
const forum = (p, o) => api(FORUM, p, o);
|
||||
|
||||
// Anti-spam / gate errors are localized by code; unknown codes fall back to the
|
||||
// server message.
|
||||
const ERR = {
|
||||
email_unverified: ["Confirm your email address before posting.", "发帖前请先验证你的邮箱。"],
|
||||
links_restricted: ["New members can't post links yet — participate a little to unlock.", "新成员暂时不能发链接——参与一段时间后解锁。"],
|
||||
insufficient_trust: ["You don't have access to post in this category yet.", "你还没有在此分区发帖的权限。"],
|
||||
silenced: ["Your account is temporarily restricted from posting.", "你的账号暂时被限制发帖。"],
|
||||
rate_limited: ["You're posting too fast — take a short break.", "发帖太快了——稍微歇一会儿。"],
|
||||
daily_limit: ["You've hit today's posting limit for your trust level.", "你已达到当前信任等级的每日发帖上限。"],
|
||||
closed: ["This topic is closed to new replies.", "该话题已关闭,不能再回复。"],
|
||||
self_flag: ["You can't report your own post.", "不能举报自己的帖子。"],
|
||||
unauthorized: ["Sign in to continue.", "请先登录。"],
|
||||
};
|
||||
const errText = (err) => (ERR[err.code] ? t(ERR[err.code][0], ERR[err.code][1]) : err.message);
|
||||
|
||||
const CATS = {
|
||||
announcements: ["Announcements", "公告"],
|
||||
help: ["Help & Support", "帮助与支持"],
|
||||
skills: ["Skills & Plugins", "技能与插件"],
|
||||
show: ["Show & Tell", "作品展示"],
|
||||
feedback: ["Feedback & Ideas", "反馈与建议"],
|
||||
};
|
||||
const CATDESC = {
|
||||
announcements: ["Releases, roadmap, and community news.", "版本发布、路线图与社区动态。"],
|
||||
help: ["Stuck on setup, config, or cache behavior? Ask here.", "安装、配置或缓存问题?在这里提问。"],
|
||||
skills: ["Share, request, and review community skills and MCP servers.", "分享、求助、评审社区技能与 MCP 服务。"],
|
||||
show: ["Built something with Reasonix? Show the community.", "用 Reasonix 做了东西?来给社区看看。"],
|
||||
feedback: ["Feature requests and product feedback.", "功能建议与产品反馈。"],
|
||||
};
|
||||
const catName = (slug, apiName) => (CATS[slug] ? L(CATS[slug][0], CATS[slug][1]) : esc(apiName));
|
||||
const catText = (slug, apiName) => (CATS[slug] ? t(CATS[slug][0], CATS[slug][1]) : apiName);
|
||||
const ROLES = { admin: ["admin", "管理员"], moderator: ["moderator", "版主"] };
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||||
}
|
||||
const AV_GRAD = [
|
||||
"var(--accent),var(--violet)",
|
||||
"var(--ok),oklch(0.62 0.15 200)",
|
||||
"var(--warm),var(--rose)",
|
||||
"var(--violet),var(--rose)",
|
||||
"oklch(0.6 0.15 200),var(--accent)",
|
||||
];
|
||||
function avatar(handle, size = "") {
|
||||
const h = handle || "?";
|
||||
let n = 0;
|
||||
for (const ch of h) n = (n + ch.charCodeAt(0)) % AV_GRAD.length;
|
||||
const initials = h.replace(/[^a-zA-Z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
|
||||
return `<span class="av ${size}" style="background:linear-gradient(140deg,${AV_GRAD[n]})">${esc(initials)}</span>`;
|
||||
}
|
||||
function ago(iso) {
|
||||
if (!iso) return "";
|
||||
const s = Math.max(1, (Date.now() - new Date(iso).getTime()) / 1000);
|
||||
const u = [[86400, "d", "天"], [3600, "h", "小时"], [60, "m", "分钟"]];
|
||||
for (const [sec, en, zh] of u) if (s >= sec) { const v = Math.floor(s / sec); return L(`${v}${en} ago`, `${v}${zh}前`); }
|
||||
return L("just now", "刚刚");
|
||||
}
|
||||
function md(body) {
|
||||
const parts = esc(body).split(/```/);
|
||||
let out = "";
|
||||
parts.forEach((chunk, i) => {
|
||||
if (i % 2 === 1) { out += `<pre>${chunk.replace(/^\n/, "")}</pre>`; return; }
|
||||
const paras = chunk.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
|
||||
out += paras.map((p) => `<p>${p.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\n/g, "<br>")}</p>`).join("");
|
||||
});
|
||||
return out || "<p></p>";
|
||||
}
|
||||
|
||||
const CAT_ICONS = { announcements: "📣", help: "🛟", skills: "🧩", show: "✨", feedback: "💡" };
|
||||
const loginUrl = () => `/login/?next=${encodeURIComponent(location.pathname + location.search)}`;
|
||||
|
||||
let account = null;
|
||||
async function loadAccount() {
|
||||
try { account = (await api(ACCOUNTS, "/me")).user; } catch { account = null; }
|
||||
document.querySelectorAll("#nav-account, #mobile-nav-account").forEach((slot) => {
|
||||
slot.innerHTML = account
|
||||
? `<a href="/account/" title="${esc(account.email)}">${avatar(account.handle)}</a>`
|
||||
: `<a class="btn btn-ghost sm" href="${loginUrl()}">${L("Sign in", "登录")}</a>`;
|
||||
});
|
||||
}
|
||||
|
||||
/* ── home ─────────────────────────────────────────── */
|
||||
function renderHome() {
|
||||
const catBox = el("cat-list");
|
||||
const topicBox = el("topic-list");
|
||||
const category = qp.get("category") || "";
|
||||
|
||||
forum("/categories").then((d) => {
|
||||
const cats = d.categories;
|
||||
if (el("s-cats")) el("s-cats").textContent = cats.length;
|
||||
if (el("s-topics")) el("s-topics").textContent = cats.reduce((a, c) => a + (c.topicCount || 0), 0);
|
||||
catBox.innerHTML = cats.map((c) => `
|
||||
<a class="cat" href="/community/?category=${esc(c.slug)}">
|
||||
<span class="ico">${CAT_ICONS[c.slug] || "💬"}</span>
|
||||
<div><h3>${catName(c.slug, c.name)}</h3><p>${CATDESC[c.slug] ? L(CATDESC[c.slug][0], CATDESC[c.slug][1]) : esc(c.description)}</p>
|
||||
<div class="meta">${c.topicCount || 0} ${L("topics", "话题")}${c.lastActivity ? " · " + ago(c.lastActivity) : ""}</div></div>
|
||||
</a>`).join("");
|
||||
}).catch(() => { catBox.innerHTML = `<div class="empty">${L("Couldn't load categories.", "无法加载分区。")}</div>`; });
|
||||
|
||||
const loadTopics = (sort) => {
|
||||
topicBox.innerHTML = `<div class="skeleton"><div class="bar"></div><div class="bar short"></div></div>`.repeat(3);
|
||||
const q = new URLSearchParams();
|
||||
if (category) q.set("category", category);
|
||||
if (sort) q.set("sort", sort);
|
||||
forum("/topics?" + q).then((d) => {
|
||||
if (!d.topics.length) { topicBox.innerHTML = `<div class="empty">${L("No topics yet —", "还没有话题 ——")} <a class="tag" href="/community/new/">${L("start the first one", "来发第一个")}</a>.</div>`; return; }
|
||||
topicBox.innerHTML = d.topics.map((tp) => `
|
||||
<div class="topic">
|
||||
${avatar(tp.author)}
|
||||
<div class="main">
|
||||
<div class="title">
|
||||
${tp.pinned ? `<span class="badge pinned">📌 ${L("Pinned", "置顶")}</span>` : ""}
|
||||
${tp.status === "solved" ? `<span class="badge solved">✓ ${L("Solved", "已解决")}</span>` : ""}
|
||||
<a href="/community/topic/?id=${tp.id}">${esc(tp.title)}</a>
|
||||
</div>
|
||||
<div class="sub"><span class="cat-tag">${catName(tp.category, tp.categoryName)}</span> <span class="who">${esc(tp.author)}</span> · ${ago(tp.createdAt)}</div>
|
||||
</div>
|
||||
<div class="stat"><div class="n">${tp.replyCount}</div><div class="l">${L("replies", "回复")}</div></div>
|
||||
<div class="last">${ago(tp.lastPostAt)}</div>
|
||||
</div>`).join("");
|
||||
}).catch(() => { topicBox.innerHTML = `<div class="empty">${L("Couldn't load discussions.", "无法加载讨论。")}</div>`; });
|
||||
};
|
||||
loadTopics("latest");
|
||||
|
||||
el("sort-tabs")?.addEventListener("click", (e) => {
|
||||
const b = e.target.closest("button[data-sort]");
|
||||
if (!b) return;
|
||||
el("sort-tabs").querySelectorAll("button").forEach((x) => x.classList.toggle("on", x === b));
|
||||
loadTopics(b.dataset.sort);
|
||||
});
|
||||
}
|
||||
|
||||
/* ── thread ───────────────────────────────────────── */
|
||||
let firstPostId = 0;
|
||||
function postHtml(p, topic) {
|
||||
const answer = topic.acceptedPostId && topic.acceptedPostId === p.id;
|
||||
const cls = answer ? "post answer" : p.id === firstPostId ? "post op" : "post";
|
||||
const roleWord = ROLES[p.role] ? L(ROLES[p.role][0], ROLES[p.role][1]) : "";
|
||||
const role = roleWord ? `<span class="badge role ${esc(p.role)}">${roleWord}</span>` : "";
|
||||
return `<article class="${cls}">
|
||||
${avatar(p.handle || p.author, "lg")}
|
||||
<div>
|
||||
${answer ? `<div class="answer-flag">✓ ${L("Accepted answer", "已采纳回答")}</div>` : ""}
|
||||
<div class="who"><span class="name">${esc(p.handle || p.author)}</span>${role}<span class="when">${ago(p.createdAt)}</span></div>
|
||||
<div class="body">${md(p.body)}</div>
|
||||
<div class="actions">
|
||||
<button class="react${p.liked ? " on" : ""}" data-like="${p.id}" data-liked="${p.liked ? "1" : "0"}" aria-pressed="${p.liked ? "true" : "false"}">👍 <span>${p.likeCount || 0}</span></button>
|
||||
<button class="link-act" data-flag="${p.id}">${L("Report", "举报")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
async function renderThread() {
|
||||
const id = Number(qp.get("id"));
|
||||
if (!id) { location.href = "/community/"; return; }
|
||||
let data;
|
||||
try { data = await forum(`/topics/${id}`); }
|
||||
catch { el("posts").innerHTML = `<div class="empty">${L("That discussion doesn't exist or was removed.", "该讨论不存在或已被移除。")}</div>`; return; }
|
||||
const { topic, posts } = data;
|
||||
firstPostId = posts[0]?.id || 0;
|
||||
|
||||
document.title = `${topic.title} — ${t("Reasonix Community", "Reasonix 社区")}`;
|
||||
el("crumb-cat").textContent = catText(topic.category, topic.category);
|
||||
el("crumb-title").textContent = topic.title;
|
||||
el("t-title").textContent = topic.title;
|
||||
el("t-meta").innerHTML =
|
||||
`${topic.status === "solved" ? `<span class="badge solved">✓ ${L("Solved", "已解决")}</span>` : ""}
|
||||
<span>${topic.replyCount} ${L("replies", "回复")} · ${topic.viewCount} ${L("views", "浏览")} · ${L("started", "发起于")} ${ago(topic.createdAt)}</span>`;
|
||||
el("posts").innerHTML = posts.map((p) => postHtml(p, topic)).join("");
|
||||
|
||||
const seen = new Set();
|
||||
el("parti").innerHTML = posts.filter((p) => !seen.has(p.author) && seen.add(p.author)).slice(0, 8).map((p) => avatar(p.handle || p.author)).join("");
|
||||
|
||||
el("posts").addEventListener("click", async (e) => {
|
||||
const like = e.target.closest("[data-like]");
|
||||
if (like && account) {
|
||||
const wasLiked = like.dataset.liked === "1";
|
||||
like.disabled = true;
|
||||
try {
|
||||
const out = await forum(`/posts/${like.dataset.like}/likes`, { method: wasLiked ? "DELETE" : "POST" });
|
||||
like.dataset.liked = out.liked ? "1" : "0";
|
||||
like.setAttribute("aria-pressed", out.liked ? "true" : "false");
|
||||
like.classList.toggle("on", out.liked);
|
||||
like.querySelector("span").textContent = out.likeCount;
|
||||
} catch (err) { alert(errText(err)); }
|
||||
finally { like.disabled = false; }
|
||||
return;
|
||||
} else if (like) {
|
||||
location.href = loginUrl();
|
||||
return;
|
||||
}
|
||||
const flag = e.target.closest("[data-flag]");
|
||||
if (flag && account) {
|
||||
if (!confirm(t("Report this post as spam or abuse?", "举报该帖为垃圾/滥用内容?"))) return;
|
||||
try { await forum(`/posts/${flag.dataset.flag}/flags`, { method: "POST", body: { reason: "spam" } }); flag.innerHTML = L("Reported ✓", "已举报 ✓"); flag.disabled = true; }
|
||||
catch (err) { alert(errText(err)); }
|
||||
} else if (flag) { location.href = loginUrl(); }
|
||||
});
|
||||
|
||||
const zone = el("reply-zone");
|
||||
if (!account) {
|
||||
zone.innerHTML = `<div class="composer"><div class="gate"><p>${L("Sign in with your Reasonix account to reply.", "用你的 Reasonix 账号登录后回复。")}</p><a class="btn btn-primary" href="${loginUrl()}">${L("Sign in", "登录")}</a></div></div>`;
|
||||
return;
|
||||
}
|
||||
zone.innerHTML = `
|
||||
<div class="msg error" id="reply-msg" hidden></div>
|
||||
<div class="composer">
|
||||
<textarea id="reply-body" data-ph-en="Write a reply… Markdown and \`\`\` code blocks supported." data-ph-zh="写下回复… 支持 Markdown 和 \`\`\` 代码块。"></textarea>
|
||||
<div class="foot"><span class="hint">${L("Signed in as", "已登录")} <b>${esc(account.handle)}</b></span><button class="btn btn-primary" id="reply-submit">${L("Post reply", "发布回复")}</button></div>
|
||||
</div>`;
|
||||
applyLangText();
|
||||
el("reply-submit").addEventListener("click", async () => {
|
||||
const body = el("reply-body").value.trim();
|
||||
const msg = el("reply-msg");
|
||||
msg.hidden = true;
|
||||
if (body.length < 2) return;
|
||||
el("reply-submit").disabled = true;
|
||||
try {
|
||||
await forum(`/topics/${id}/posts`, { method: "POST", body: { body } });
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
msg.textContent = errText(err); msg.hidden = false;
|
||||
el("reply-submit").disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── new topic ────────────────────────────────────── */
|
||||
async function renderNew() {
|
||||
if (!account) {
|
||||
el("new-gate").hidden = false;
|
||||
el("gate-login").href = loginUrl();
|
||||
return;
|
||||
}
|
||||
el("new-form").hidden = false;
|
||||
const sel = el("f-category");
|
||||
try {
|
||||
const { categories } = await forum("/categories");
|
||||
for (const c of categories) {
|
||||
const o = document.createElement("option");
|
||||
o.value = c.id;
|
||||
o.setAttribute("data-l-en", CATS[c.slug] ? CATS[c.slug][0] : c.name);
|
||||
o.setAttribute("data-l-zh", CATS[c.slug] ? CATS[c.slug][1] : c.name);
|
||||
o.textContent = catText(c.slug, c.name);
|
||||
sel.appendChild(o);
|
||||
}
|
||||
const pre = qp.get("category");
|
||||
if (pre) { const m = categories.find((c) => c.slug === pre); if (m) sel.value = m.id; }
|
||||
} catch {}
|
||||
applyLangText();
|
||||
|
||||
el("f-submit").addEventListener("click", async () => {
|
||||
const msg = el("new-msg");
|
||||
msg.hidden = true;
|
||||
const categoryId = Number(sel.value);
|
||||
const title = el("f-title").value.trim();
|
||||
const body = el("f-body").value.trim();
|
||||
if (!categoryId) { msg.textContent = t("Choose a category.", "请选择一个分区。"); msg.hidden = false; return; }
|
||||
if (title.length < 6 || body.length < 10) { msg.textContent = t("Add a title (6+ chars) and a bit more detail (10+ chars).", "标题至少 6 个字符,正文至少 10 个字符。"); msg.hidden = false; return; }
|
||||
el("f-submit").disabled = true;
|
||||
try {
|
||||
const { topic } = await forum("/topics", { method: "POST", body: { categoryId, title, body } });
|
||||
location.href = `/community/topic/?id=${topic.id}`;
|
||||
} catch (err) {
|
||||
msg.textContent = errText(err); msg.hidden = false;
|
||||
el("f-submit").disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
(async function () {
|
||||
initTheme();
|
||||
initMobileNav();
|
||||
initLang();
|
||||
await loadAccount();
|
||||
if (el("topic-list")) renderHome();
|
||||
else if (el("posts")) renderThread();
|
||||
else if (el("new-form")) renderNew();
|
||||
})();
|
||||
@@ -1,67 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const configSource = () => readFile(new URL("../../astro.config.mjs", import.meta.url), "utf8");
|
||||
const docsSource = () => readFile(new URL("../pages/docs.astro", import.meta.url), "utf8");
|
||||
const stylesSource = () => readFile(new URL("../styles/global.css", import.meta.url), "utf8");
|
||||
|
||||
test("Astro preserves documentation code sample newlines", async () => {
|
||||
const config = await configSource();
|
||||
|
||||
assert.match(config, /compressHTML:\s*false/);
|
||||
});
|
||||
|
||||
test("copyable documentation code blocks reserve space for their buttons", async () => {
|
||||
const [page, styles] = await Promise.all([docsSource(), stylesSource()]);
|
||||
const codeblocks = [...page.matchAll(/<div class="([^"]*\bcodeblock\b[^"]*)">([\s\S]*?)<\/div>/g)];
|
||||
const copyable = codeblocks.filter(([, , body]) => body.includes("data-copy="));
|
||||
|
||||
assert.ok(copyable.length > 0);
|
||||
for (const [, classes] of copyable) {
|
||||
assert.match(classes, /(?:^|\s)codeblock--copy(?:\s|$)/);
|
||||
}
|
||||
assert.match(styles, /\.codeblock--copy\s*\{\s*padding-top:\s*52px;\s*\}/);
|
||||
});
|
||||
|
||||
test("the macOS quarantine comment and command remain separate source lines", async () => {
|
||||
const page = await docsSource();
|
||||
|
||||
assert.match(
|
||||
page,
|
||||
/# Quit Reasonix first, then run in Terminal\.<\/span>\nsudo xattr -rd com\.apple\.quarantine \/Applications\/Reasonix\.app/,
|
||||
);
|
||||
});
|
||||
|
||||
test("website documents the version-matched built-in docs command", async () => {
|
||||
const page = await docsSource();
|
||||
|
||||
assert.match(page, /id="embedded-docs"/);
|
||||
assert.match(page, /\/docs 1\.19\.5 changelog/);
|
||||
assert.match(page, /AI configured for the current session/);
|
||||
assert.match(page, /normally <code>\/reasonix:docs<\/code>/);
|
||||
assert.match(page, /Reasonix never overwrites the existing command/);
|
||||
assert.match(page, /Release CI rejects a build when its embedded corpus does not match/);
|
||||
});
|
||||
|
||||
test("website exposes the Extension Protocol developer path", async () => {
|
||||
const page = await docsSource();
|
||||
|
||||
assert.match(page, /href="#extensions"/);
|
||||
assert.match(page, /id="extensions"/);
|
||||
assert.match(page, /MCP or Extension Protocol\?/);
|
||||
assert.match(page, /sdk\/go\/examples\/starterextension/);
|
||||
assert.match(page, /plugin_root="\$\(pwd -P\)"/);
|
||||
assert.match(page, /reasonix plugin install "\$plugin_root" --dry-run/);
|
||||
assert.match(page, /reasonix plugin install "\$plugin_root" --link --replace --yes/);
|
||||
assert.match(page, /sdk\/go\/v1\.0\.0/);
|
||||
assert.match(page, /<strong>Full trust:<\/strong>/);
|
||||
assert.match(page, /Native Manifest v2 is an explicit capability boundary/);
|
||||
assert.match(page, /原生 Manifest v2 是显式能力边界/);
|
||||
assert.match(page, /docs\/EXTENSIONS\.md/);
|
||||
assert.match(page, /docs\/PLUGIN_PACKAGES\.md#manifest-v2-extensions/);
|
||||
assert.match(page, /docs\/PLUGIN_PACKAGES\.zh-CN\.md#manifest-v2扩展/);
|
||||
assert.match(page, /sdk\/go\/README\.md/);
|
||||
assert.match(page, /docs\/EXTENSION_PROTOCOL\.md/);
|
||||
assert.doesNotMatch(page, /Manifest v1/);
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
const DOWNLOAD_PANES = new Set(["npm", "brew", "desktop", "cli", "vscode"]);
|
||||
|
||||
// Return the requested install pane only for the homepage download section.
|
||||
// Plain #start links keep the rendered default pane; query links opt into one.
|
||||
export function downloadPaneFromURL(input, base = "https://reasonix.io/") {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(input, base);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
if (url.hash !== "#start") return "";
|
||||
const pane = url.searchParams.get("download") || "";
|
||||
return DOWNLOAD_PANES.has(pane) ? pane : "";
|
||||
}
|
||||
|
||||
export function releaseChannelFromURL(input, base = "https://reasonix.io/") {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(input, base);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
if (url.hash !== "#start") return "";
|
||||
const pane = url.searchParams.get("download") || "";
|
||||
if (pane !== "desktop" && pane !== "cli") return "";
|
||||
return "";
|
||||
}
|
||||
|
||||
export function downloadURLForPane(input, pane, channel, base = "https://reasonix.io/") {
|
||||
if (!DOWNLOAD_PANES.has(pane)) return "";
|
||||
let url;
|
||||
try {
|
||||
url = new URL(input, base);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
url.searchParams.set("download", pane);
|
||||
void channel;
|
||||
url.searchParams.delete("channel");
|
||||
url.hash = "start";
|
||||
return url.href;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { downloadPaneFromURL, downloadURLForPane, releaseChannelFromURL } from "./download-link.js";
|
||||
|
||||
test("desktop updater deep link selects the desktop pane", () => {
|
||||
assert.equal(downloadPaneFromURL("https://reasonix.io/?download=desktop#start"), "desktop");
|
||||
});
|
||||
|
||||
test("plain install links preserve the default pane", () => {
|
||||
assert.equal(downloadPaneFromURL("https://reasonix.io/#start"), "");
|
||||
assert.equal(downloadPaneFromURL("https://reasonix.io/?download=desktop"), "");
|
||||
});
|
||||
|
||||
test("recognized download panes are strict", () => {
|
||||
assert.equal(downloadPaneFromURL("/?download=brew#start"), "brew");
|
||||
assert.equal(downloadPaneFromURL("/?download=npm#start"), "npm");
|
||||
assert.equal(downloadPaneFromURL("/?download=vscode#start"), "vscode");
|
||||
assert.equal(downloadPaneFromURL("/?download=cli#start"), "cli");
|
||||
assert.equal(downloadPaneFromURL("/?download=DESKTOP#start"), "");
|
||||
assert.equal(downloadPaneFromURL("/?download=unknown#start"), "");
|
||||
assert.equal(downloadPaneFromURL("not a url", "not a base"), "");
|
||||
});
|
||||
|
||||
test("legacy release channel deep links never select a public channel", () => {
|
||||
assert.equal(releaseChannelFromURL("/?download=desktop&channel=preview#start"), "");
|
||||
assert.equal(releaseChannelFromURL("/?download=cli&channel=stable#start"), "");
|
||||
assert.equal(releaseChannelFromURL("/?download=desktop&channel=canary#start"), "");
|
||||
});
|
||||
|
||||
test("download tab navigation keeps the pane and removes legacy channels", () => {
|
||||
assert.equal(
|
||||
downloadURLForPane("https://reasonix.io/?download=desktop&channel=preview#start", "cli", "stable"),
|
||||
"https://reasonix.io/?download=cli#start",
|
||||
);
|
||||
assert.equal(
|
||||
downloadURLForPane("https://reasonix.io/?download=desktop&channel=preview#start", "npm", "preview"),
|
||||
"https://reasonix.io/?download=npm#start",
|
||||
);
|
||||
assert.equal(downloadURLForPane("https://reasonix.io/", "desktop", "preview"),
|
||||
"https://reasonix.io/?download=desktop#start");
|
||||
assert.equal(downloadURLForPane("https://reasonix.io/", "unknown", "stable"), "");
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
// Reasonix — motion & FX layer (all effects respect data-motion + prefers-reduced-motion)
|
||||
(function () {
|
||||
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
const rich = () => document.body.dataset.motion === "rich" && !reduced;
|
||||
|
||||
/* spotlight border on cards */
|
||||
document.querySelectorAll(".feat, .os-card, .doc-card").forEach((card) => {
|
||||
card.addEventListener("mousemove", (e) => {
|
||||
const r = card.getBoundingClientRect();
|
||||
card.style.setProperty("--mx", e.clientX - r.left + "px");
|
||||
card.style.setProperty("--my", e.clientY - r.top + "px");
|
||||
});
|
||||
});
|
||||
|
||||
/* stat counters (run when terminal playback finishes) */
|
||||
const counters = Array.from(document.querySelectorAll("[data-cnt]"));
|
||||
const fmt = (el, v) => {
|
||||
if (el.dataset.fmt === "hm") {
|
||||
const m = Math.round(v);
|
||||
return Math.floor(m / 60) + "h " + (m % 60) + "m";
|
||||
}
|
||||
const dec = +(el.dataset.dec || 0);
|
||||
return (el.dataset.pre || "") + v.toFixed(dec) + (el.dataset.suf || "");
|
||||
};
|
||||
const runCounters = () => {
|
||||
counters.forEach((el) => {
|
||||
const target = parseFloat(el.dataset.cnt);
|
||||
if (!rich()) { el.textContent = fmt(el, target); return; }
|
||||
const t0 = performance.now(), dur = 1500;
|
||||
const frame = (t) => {
|
||||
const p = Math.min(1, (t - t0) / dur);
|
||||
const e = 1 - Math.pow(1 - p, 3);
|
||||
el.textContent = fmt(el, target * e);
|
||||
if (p < 1) requestAnimationFrame(frame);
|
||||
};
|
||||
requestAnimationFrame(frame);
|
||||
});
|
||||
};
|
||||
document.addEventListener("rx:term-played", runCounters);
|
||||
})();
|
||||
@@ -1,39 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const source = (path) => readFile(new URL(path, import.meta.url), "utf8");
|
||||
|
||||
test("primary site pages share the same header component", async () => {
|
||||
const pages = await Promise.all([
|
||||
source("../pages/index.astro"),
|
||||
source("../pages/docs.astro"),
|
||||
source("../pages/skills.astro"),
|
||||
source("../components/ChangelogPage.astro"),
|
||||
source("../pages/404.astro"),
|
||||
]);
|
||||
|
||||
for (const page of pages) {
|
||||
assert.match(page, /<SiteHeader\b/);
|
||||
assert.doesNotMatch(page, /<header class="nav/);
|
||||
}
|
||||
});
|
||||
|
||||
test("the shared header owns the complete global navigation", async () => {
|
||||
const header = await source("../components/SiteHeader.astro");
|
||||
|
||||
for (const id of ["features", "surfaces", "skills", "community", "docs", "changelog"]) {
|
||||
assert.match(header, new RegExp(`id: '${id}'`));
|
||||
}
|
||||
|
||||
assert.doesNotMatch(header, /id: 'start'/);
|
||||
assert.equal((header.match(/#start/g) ?? []).length, 2);
|
||||
assert.match(header, /nav-sign-in/);
|
||||
assert.match(header, /nav-install/);
|
||||
assert.match(header, /class="nav-github"/);
|
||||
assert.match(header, /https:\/\/github[.]com\/esengine\/DeepSeek-Reasonix/);
|
||||
assert.match(header, /target="_blank"/);
|
||||
assert.match(header, /aria-label="Reasonix GitHub repository"/);
|
||||
assert.match(header, /data-mobile-nav-toggle/);
|
||||
assert.match(header, /id="site-mobile-nav"/);
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const source = (path) => readFile(new URL(path, import.meta.url), "utf8");
|
||||
|
||||
/* Extract the body of a `@media (max-width: Npx)` block (up to the next
|
||||
at-rule or EOF) so assertions stay scoped to small viewports. */
|
||||
const mediaBlock = (css, px) => {
|
||||
const marker = `@media (max-width: ${px}px)`;
|
||||
const start = css.indexOf(marker);
|
||||
assert.notEqual(start, -1, `missing ${marker}`);
|
||||
const rest = css.slice(start + marker.length);
|
||||
const next = rest.search(/@media|@keyframes/);
|
||||
return next === -1 ? rest : rest.slice(0, next);
|
||||
};
|
||||
|
||||
/* The header must fit narrow viewports while keeping a clear menu trigger. */
|
||||
test("≤640px: marketing nav contracts to fit 390px viewports", async () => {
|
||||
const [css, header] = await Promise.all([
|
||||
source("../styles/global.css"),
|
||||
source("../components/SiteHeader.astro"),
|
||||
]);
|
||||
const block = mediaBlock(css, 640);
|
||||
assert.match(block, /\.nav-sign-in \{ display: none/);
|
||||
assert.match(block, /\.nav \.brand span \{ display: none/);
|
||||
assert.match(block, /\.nav \.brand img \{ width: 64px; \}/);
|
||||
assert.match(header, /<source media=\"\(max-width: 400px\)\" srcset=\{`\$\{base\}\/favicon\.svg`\} \/>/);
|
||||
assert.match(block, /\.theme-switch button \{ padding: 6px 9px/);
|
||||
});
|
||||
|
||||
test("≤1100px: marketing navigation collapses before controls overlap", async () => {
|
||||
const css = await source("../styles/global.css");
|
||||
const tabletBlock = mediaBlock(css, 1100);
|
||||
const mobileBlock = mediaBlock(css, 900);
|
||||
|
||||
assert.match(tabletBlock, /\.nav-links \{ display: none/);
|
||||
assert.match(tabletBlock, /\.nav-github \{ margin-left: auto; padding-inline: 8px/);
|
||||
assert.match(tabletBlock, /\.nav-github-label \{ display: none/);
|
||||
assert.match(tabletBlock, /\.nav-menu-toggle \{ display: inline-flex/);
|
||||
assert.match(mobileBlock, /\.nav-github \{ display: none/);
|
||||
});
|
||||
|
||||
test("≤360px: marketing nav keeps the menu trigger within 320px", async () => {
|
||||
const css = await source("../styles/global.css");
|
||||
const block = mediaBlock(css, 360);
|
||||
assert.match(block, /\.nav-inner \{ padding: 0 12px; gap: 6px/);
|
||||
assert.match(block, /\.lang-switch button \{ padding: 6px 8px/);
|
||||
assert.match(block, /\.theme-switch button \{ padding: 6px 7px/);
|
||||
assert.match(block, /\.nav-install \{ display: none/);
|
||||
assert.match(block, /\.nav-menu-toggle \{ width: 36px; height: 36px/);
|
||||
});
|
||||
|
||||
test("≤400px: marketing nav uses the compact brand mark", async () => {
|
||||
const css = await source("../styles/global.css");
|
||||
const block = mediaBlock(css, 400);
|
||||
assert.match(block, /\.nav \.brand img \{ width: 28px; height: 28px; border-radius: 8px; \}/);
|
||||
});
|
||||
|
||||
test("≤360px: hero install command keeps its copy control inside the bar", async () => {
|
||||
const [css, page] = await Promise.all([
|
||||
source("../styles/global.css"),
|
||||
source("../pages/index.astro"),
|
||||
]);
|
||||
const block = mediaBlock(css, 360);
|
||||
assert.match(page, /class=\"install-command\">npm i -g reasonix<\/span>/);
|
||||
assert.match(css, /\.hero-install \.install-command\s*\{[\s\S]*?min-width:\s*0;/);
|
||||
assert.match(block, /\.hero-install \.install \{ gap: 6px; padding-inline: 6px; \}/);
|
||||
assert.match(block, /\.hero-install \.install button \{ padding-inline: 10px; \}/);
|
||||
});
|
||||
|
||||
test("≤440px: community nav budgets for the async account control", async () => {
|
||||
const css = await source("../styles/community.css");
|
||||
const layout = await source("../layouts/Community.astro");
|
||||
const block = mediaBlock(css, 440);
|
||||
assert.match(layout, /class="brand-name">Reasonix/);
|
||||
assert.match(layout, /id="nav-account"/);
|
||||
assert.match(block, /\.nav \.brand-name \{ display: none/);
|
||||
assert.match(block, /\.nav-right \{ gap: 8px/);
|
||||
assert.match(block, /\.lang-switch button \{ padding: 6px 8px/);
|
||||
assert.match(block, /\.theme-switch button \{ padding: 6px 7px/);
|
||||
assert.match(block, /#nav-account \.btn \{ padding: 7px 10px/);
|
||||
});
|
||||
|
||||
test("shared headers expose an accessible mobile navigation", async () => {
|
||||
const [header, community] = await Promise.all([
|
||||
source("../components/SiteHeader.astro"),
|
||||
source("../layouts/Community.astro"),
|
||||
]);
|
||||
for (const sourceText of [header, community]) {
|
||||
assert.match(sourceText, /data-mobile-nav-toggle/);
|
||||
assert.match(sourceText, /data-mobile-nav/);
|
||||
assert.match(sourceText, /data-mobile-nav-close/);
|
||||
assert.match(sourceText, /aria-expanded="false"/);
|
||||
}
|
||||
});
|
||||
|
||||
test("mobile navigation behavior supports close, Escape, and focus cycling", async () => {
|
||||
const js = await source("./mobile-nav.js");
|
||||
assert.match(js, /event\.key === "Escape"/);
|
||||
assert.match(js, /event\.key !== "Tab"/);
|
||||
assert.match(js, /document\.body\.classList\.add\("mobile-nav-open"\)/);
|
||||
assert.match(js, /document\.body\.classList\.remove\("mobile-nav-open"\)/);
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
// Shared responsive navigation behavior for the marketing and community headers.
|
||||
export function initMobileNav() {
|
||||
document.querySelectorAll("[data-mobile-nav]").forEach((menu) => {
|
||||
const header = menu.closest(".nav");
|
||||
const toggle = header?.querySelector("[data-mobile-nav-toggle]");
|
||||
const panel = menu.querySelector(".mobile-nav-panel");
|
||||
if (!toggle || !panel) return;
|
||||
|
||||
let closeTimer;
|
||||
let stateToken = 0;
|
||||
let returnFocus = toggle;
|
||||
const focusables = () => Array.from(panel.querySelectorAll(
|
||||
'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
||||
)).filter((element) => !element.closest("[hidden]"));
|
||||
|
||||
const close = (restore = true) => {
|
||||
stateToken += 1;
|
||||
clearTimeout(closeTimer);
|
||||
toggle.setAttribute("aria-expanded", "false");
|
||||
menu.setAttribute("aria-hidden", "true");
|
||||
menu.classList.remove("is-open");
|
||||
document.body.classList.remove("mobile-nav-open");
|
||||
closeTimer = window.setTimeout(() => { menu.hidden = true; }, 220);
|
||||
if (restore) returnFocus.focus();
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
const token = ++stateToken;
|
||||
clearTimeout(closeTimer);
|
||||
returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : toggle;
|
||||
menu.hidden = false;
|
||||
menu.setAttribute("aria-hidden", "false");
|
||||
toggle.setAttribute("aria-expanded", "true");
|
||||
document.body.classList.add("mobile-nav-open");
|
||||
requestAnimationFrame(() => {
|
||||
if (token === stateToken && !menu.hidden) menu.classList.add("is-open");
|
||||
});
|
||||
requestAnimationFrame(() => {
|
||||
if (token === stateToken && !menu.hidden) focusables()[0]?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
toggle.addEventListener("click", () => (menu.hidden ? open() : close()));
|
||||
menu.querySelectorAll("[data-mobile-nav-close]").forEach((button) => button.addEventListener("click", () => close()));
|
||||
menu.querySelectorAll("a[href]").forEach((link) => link.addEventListener("click", () => close(false)));
|
||||
menu.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if (event.key !== "Tab") return;
|
||||
const items = focusables();
|
||||
if (!items.length) return;
|
||||
const first = items[0];
|
||||
const last = items[items.length - 1];
|
||||
if (event.shiftKey && document.activeElement === first) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
});
|
||||
menu.addEventListener("transitionend", (event) => {
|
||||
if (event.propertyName === "opacity" && !menu.classList.contains("is-open")) menu.hidden = true;
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
if (window.matchMedia("(min-width: 1101px)").matches && !menu.hidden) close(false);
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
const STABLE_TAG = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
||||
const DESKTOP_DOWNLOAD_PAGE = "https://reasonix.io/?download=desktop#start";
|
||||
const DESKTOP_ASSETS = [
|
||||
["platforms", "darwin-arm64", "Reasonix-darwin-arm64.zip"],
|
||||
["platforms", "darwin-amd64", "Reasonix-darwin-amd64.zip"],
|
||||
["platforms", "windows-amd64", "Reasonix-windows-amd64-installer.exe"],
|
||||
["platforms", "windows-arm64", "Reasonix-windows-arm64-installer.exe"],
|
||||
["platforms", "linux-amd64", "Reasonix-linux-amd64.tar.gz"],
|
||||
["native_packages", "linux-amd64", "Reasonix-linux-amd64.deb"],
|
||||
["downloads", "Reasonix-darwin-universal.dmg", "Reasonix-darwin-universal.dmg"],
|
||||
["downloads", "Reasonix-windows-amd64.zip", "Reasonix-windows-amd64.zip"],
|
||||
];
|
||||
const DESKTOP_ASSET_NAMES = new Set(DESKTOP_ASSETS.map(([, , name]) => name));
|
||||
const OFFICIAL_DESKTOP_RELEASE_TAG = /^(?:desktop-)?(v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*))$/;
|
||||
const SHA256 = /^[0-9a-f]{64}$/;
|
||||
const MAX_RELEASE_ASSET_SIZE = 1 << 30;
|
||||
|
||||
// Keep in lockstep with workers/crash-report CLI asset gate.
|
||||
export const CLI_RELEASE_ASSETS = [
|
||||
"reasonix-darwin-amd64.tar.gz",
|
||||
"reasonix-darwin-arm64.tar.gz",
|
||||
"reasonix-linux-amd64.tar.gz",
|
||||
"reasonix-linux-arm64.tar.gz",
|
||||
"reasonix-windows-amd64.zip",
|
||||
"reasonix-windows-arm64.zip",
|
||||
"SHA256SUMS",
|
||||
];
|
||||
const CLI_RELEASE_ASSET_NAMES = new Set(CLI_RELEASE_ASSETS);
|
||||
|
||||
export const publicReleaseChannels = new Set(["stable"]);
|
||||
|
||||
export function normalizePublicReleaseChannel(_value) {
|
||||
return "stable";
|
||||
}
|
||||
|
||||
export function cliUpgradeCommand(_value) {
|
||||
return "reasonix upgrade";
|
||||
}
|
||||
|
||||
export function releaseVersionLabel(model) {
|
||||
return String(model?.version || "").trim() || "latest";
|
||||
}
|
||||
|
||||
function parsePublicTag(tag) {
|
||||
const value = typeof tag === "string" ? tag : "";
|
||||
let match = value.match(STABLE_TAG);
|
||||
if (match) {
|
||||
return { tag: value, channel: "stable", order: match.slice(1) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function compareDecimal(left, right) {
|
||||
if (left.length !== right.length) return left.length - right.length;
|
||||
return left === right ? 0 : left > right ? 1 : -1;
|
||||
}
|
||||
|
||||
function compareOrder(left, right) {
|
||||
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
||||
const delta = compareDecimal(left[i] || "0", right[i] || "0");
|
||||
if (delta !== 0) return delta;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function safeHTTPSURL(value) {
|
||||
if (typeof value !== "string") return null;
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === "https:" &&
|
||||
Boolean(url.hostname) &&
|
||||
!url.username &&
|
||||
!url.password
|
||||
? url
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function expectedCLIAssetURL(value, tag, name) {
|
||||
if (typeof value !== "string") return null;
|
||||
const url = safeHTTPSURL(value);
|
||||
const path = `/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`;
|
||||
return url &&
|
||||
url.href === value &&
|
||||
url.hostname.toLowerCase() === "github.com" &&
|
||||
!url.port &&
|
||||
url.pathname === path &&
|
||||
!url.search &&
|
||||
!url.hash
|
||||
? url
|
||||
: null;
|
||||
}
|
||||
|
||||
// Returns the 7 required CLI asset URLs, or null when any required asset is
|
||||
// missing. Never synthesizes download URLs — incomplete releases must be
|
||||
// rejected so the site never advertises 404 links.
|
||||
export function releaseAssetMap(release) {
|
||||
const found = {};
|
||||
const seen = new Set();
|
||||
for (const asset of Array.isArray(release?.assets) ? release.assets : []) {
|
||||
const name = String(asset?.name || "");
|
||||
if (!CLI_RELEASE_ASSET_NAMES.has(name)) continue;
|
||||
if (seen.has(name)) return null;
|
||||
seen.add(name);
|
||||
const url = expectedCLIAssetURL(asset?.browser_download_url, String(release?.tag_name || ""), name);
|
||||
if (
|
||||
name &&
|
||||
url &&
|
||||
Number.isSafeInteger(asset?.size) &&
|
||||
asset.size > 0 &&
|
||||
asset.size <= MAX_RELEASE_ASSET_SIZE
|
||||
) {
|
||||
found[name] = url.href;
|
||||
}
|
||||
}
|
||||
const assets = {};
|
||||
for (const name of CLI_RELEASE_ASSETS) {
|
||||
if (!found[name]) return null;
|
||||
assets[name] = found[name];
|
||||
}
|
||||
return assets;
|
||||
}
|
||||
|
||||
export function selectCLIRelease(releases, requestedChannel) {
|
||||
const channel = normalizePublicReleaseChannel(requestedChannel);
|
||||
let selected = null;
|
||||
let selectedTag = null;
|
||||
for (const release of Array.isArray(releases) ? releases : []) {
|
||||
const parsed = parsePublicTag(release?.tag_name);
|
||||
if (!parsed || parsed.channel !== channel) continue;
|
||||
if (Boolean(release?.prerelease)) continue;
|
||||
if (!releaseAssetMap(release)) continue;
|
||||
if (!selectedTag || compareOrder(parsed.order, selectedTag.order) > 0) {
|
||||
selected = release;
|
||||
selectedTag = parsed;
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
export function cliReleaseModel(releases, requestedChannel) {
|
||||
const channel = normalizePublicReleaseChannel(requestedChannel);
|
||||
const release = selectCLIRelease(releases, channel);
|
||||
if (!release) return null;
|
||||
const parsed = parsePublicTag(release.tag_name);
|
||||
if (!parsed) return null;
|
||||
const assets = releaseAssetMap(release);
|
||||
if (!assets) return null;
|
||||
const releaseURL = `https://github.com/esengine/DeepSeek-Reasonix/releases/tag/${parsed.tag}`;
|
||||
const exactChangelogURL = `https://reasonix.io/changelog/${parsed.tag}/`;
|
||||
const changelogURL = release.release_notes_url === exactChangelogURL
|
||||
? exactChangelogURL
|
||||
: "https://reasonix.io/changelog/";
|
||||
return {
|
||||
channel,
|
||||
version: parsed.tag,
|
||||
displayVersion: parsed.tag.slice(1),
|
||||
assets,
|
||||
releaseURL,
|
||||
changelogURL,
|
||||
};
|
||||
}
|
||||
|
||||
function desktopAssetBases(parsed) {
|
||||
const tag = `desktop-${parsed.tag}`;
|
||||
return [
|
||||
`https://dl.reasonix.io/${tag}/`,
|
||||
`https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/`,
|
||||
`https://github.com/esengine/DeepSeek-Reasonix/releases/download/${parsed.tag}/`,
|
||||
];
|
||||
}
|
||||
|
||||
function normalizeDesktopManifest(manifest, requestedChannel) {
|
||||
const channel = normalizePublicReleaseChannel(requestedChannel);
|
||||
const parsed = parsePublicTag(manifest?.version);
|
||||
if (
|
||||
!parsed ||
|
||||
parsed.channel !== channel ||
|
||||
manifest?.download_page !== DESKTOP_DOWNLOAD_PAGE
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allowedBases = desktopAssetBases(parsed);
|
||||
let selectedBase = "";
|
||||
for (const [group, key, name] of DESKTOP_ASSETS) {
|
||||
const asset = manifest?.[group]?.[key];
|
||||
if (
|
||||
!asset ||
|
||||
typeof asset !== "object" ||
|
||||
Array.isArray(asset) ||
|
||||
!Number.isSafeInteger(asset.size) ||
|
||||
asset.size <= 0 ||
|
||||
asset.size > MAX_RELEASE_ASSET_SIZE ||
|
||||
typeof asset.sha256 !== "string" ||
|
||||
!SHA256.test(asset.sha256)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawURL = typeof asset.url === "string" ? asset.url : "";
|
||||
const url = safeHTTPSURL(rawURL);
|
||||
const base = allowedBases.find((candidate) => rawURL === candidate + name);
|
||||
if (
|
||||
!url ||
|
||||
!base ||
|
||||
url.href !== rawURL ||
|
||||
asset.sig !== `${rawURL}.minisig` ||
|
||||
(selectedBase && selectedBase !== base)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
selectedBase = base;
|
||||
}
|
||||
return selectedBase ? { parsed } : null;
|
||||
}
|
||||
|
||||
export function desktopReleaseModel(manifest, requestedChannel) {
|
||||
const normalized = normalizeDesktopManifest(manifest, requestedChannel);
|
||||
if (!normalized) return null;
|
||||
const { parsed } = normalized;
|
||||
const assets = Object.fromEntries(DESKTOP_ASSETS.map(([group, key, name]) => [
|
||||
name,
|
||||
manifest[group][key].url,
|
||||
]));
|
||||
return {
|
||||
channel: parsed.channel,
|
||||
version: parsed.tag,
|
||||
displayVersion: parsed.tag.slice(1),
|
||||
assets,
|
||||
changelogURL: manifest.release_notes_url === `https://reasonix.io/changelog/${parsed.tag}/`
|
||||
? manifest.release_notes_url
|
||||
: "https://reasonix.io/changelog/",
|
||||
};
|
||||
}
|
||||
|
||||
// Accept both historical desktop-v* releases and the combined v* release.
|
||||
export function desktopGitHubReleaseModel(release) {
|
||||
const match = typeof release?.tag_name === "string"
|
||||
? release.tag_name.match(OFFICIAL_DESKTOP_RELEASE_TAG)
|
||||
: null;
|
||||
if (!match || release?.draft !== false || release?.prerelease !== false) return null;
|
||||
|
||||
const tag = release.tag_name;
|
||||
const found = {};
|
||||
const seen = new Set();
|
||||
for (const asset of Array.isArray(release.assets) ? release.assets : []) {
|
||||
const name = String(asset?.name || "");
|
||||
if (!DESKTOP_ASSET_NAMES.has(name)) continue;
|
||||
if (seen.has(name)) return null;
|
||||
seen.add(name);
|
||||
const rawURL = typeof asset?.browser_download_url === "string" ? asset.browser_download_url : "";
|
||||
const url = safeHTTPSURL(rawURL);
|
||||
const expected = `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`;
|
||||
if (
|
||||
!url ||
|
||||
url.href !== rawURL ||
|
||||
rawURL !== expected ||
|
||||
!Number.isSafeInteger(asset?.size) ||
|
||||
asset.size <= 0 ||
|
||||
asset.size > MAX_RELEASE_ASSET_SIZE
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
found[name] = rawURL;
|
||||
}
|
||||
if (DESKTOP_ASSETS.some(([, , name]) => !found[name])) return null;
|
||||
|
||||
return {
|
||||
channel: "stable",
|
||||
version: match[1],
|
||||
displayVersion: match[1].slice(1),
|
||||
assets: Object.fromEntries(DESKTOP_ASSETS.map(([, , name]) => [name, found[name]])),
|
||||
changelogURL: "https://reasonix.io/changelog/",
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchFirstJSON(urls, fetchImpl = fetch, accept = () => true) {
|
||||
const failures = [];
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const response = await fetchImpl(url, { cache: "no-cache" });
|
||||
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim());
|
||||
const payload = await response.json();
|
||||
if (!accept(payload)) throw new Error("invalid release data");
|
||||
return payload;
|
||||
} catch (error) {
|
||||
failures.push(`${url}: ${String(error?.message || error)}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`release data unavailable (${failures.join("; ")})`);
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import {
|
||||
CLI_RELEASE_ASSETS,
|
||||
cliUpgradeCommand,
|
||||
cliReleaseModel,
|
||||
desktopGitHubReleaseModel,
|
||||
desktopReleaseModel,
|
||||
fetchFirstJSON,
|
||||
releaseAssetMap,
|
||||
releaseVersionLabel,
|
||||
selectCLIRelease,
|
||||
} from "./release-channels.js";
|
||||
|
||||
function cliAssets(tag, missing = []) {
|
||||
const skip = new Set(missing);
|
||||
return CLI_RELEASE_ASSETS.filter((name) => !skip.has(name)).map((name) => ({
|
||||
name,
|
||||
browser_download_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`,
|
||||
size: 42,
|
||||
}));
|
||||
}
|
||||
|
||||
const desktopSHA256 = "a".repeat(64);
|
||||
|
||||
function desktopManifest(version, base) {
|
||||
const releaseBase = base || `https://dl.reasonix.io/desktop-${version}/`;
|
||||
const asset = (name) => {
|
||||
const url = releaseBase + name;
|
||||
return { url, sig: `${url}.minisig`, size: 42, sha256: desktopSHA256 };
|
||||
};
|
||||
return {
|
||||
version,
|
||||
download_page: "https://reasonix.io/?download=desktop#start",
|
||||
platforms: {
|
||||
"darwin-arm64": asset("Reasonix-darwin-arm64.zip"),
|
||||
"darwin-amd64": asset("Reasonix-darwin-amd64.zip"),
|
||||
"windows-amd64": asset("Reasonix-windows-amd64-installer.exe"),
|
||||
"windows-arm64": asset("Reasonix-windows-arm64-installer.exe"),
|
||||
"linux-amd64": asset("Reasonix-linux-amd64.tar.gz"),
|
||||
},
|
||||
native_packages: {
|
||||
"linux-amd64": asset("Reasonix-linux-amd64.deb"),
|
||||
},
|
||||
downloads: {
|
||||
"Reasonix-darwin-universal.dmg": asset("Reasonix-darwin-universal.dmg"),
|
||||
"Reasonix-windows-amd64.zip": asset("Reasonix-windows-amd64.zip"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function desktopGitHubRelease(version = "v1.17.21") {
|
||||
const tag = `desktop-${version}`;
|
||||
const names = [
|
||||
"Reasonix-darwin-arm64.zip",
|
||||
"Reasonix-darwin-amd64.zip",
|
||||
"Reasonix-windows-amd64-installer.exe",
|
||||
"Reasonix-windows-arm64-installer.exe",
|
||||
"Reasonix-linux-amd64.tar.gz",
|
||||
"Reasonix-linux-amd64.deb",
|
||||
"Reasonix-darwin-universal.dmg",
|
||||
"Reasonix-windows-amd64.zip",
|
||||
];
|
||||
return {
|
||||
tag_name: tag,
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
assets: names.map((name) => ({
|
||||
name,
|
||||
browser_download_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`,
|
||||
size: 42,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function copy(value) {
|
||||
return structuredClone(value);
|
||||
}
|
||||
|
||||
test("CLI always emits the official upgrade command", () => {
|
||||
assert.equal(cliUpgradeCommand("stable"), "reasonix upgrade");
|
||||
assert.equal(cliUpgradeCommand("preview"), "reasonix upgrade");
|
||||
assert.equal(cliUpgradeCommand("canary"), "reasonix upgrade");
|
||||
});
|
||||
|
||||
test("release labels use a complete version or a readable fallback", () => {
|
||||
assert.equal(releaseVersionLabel({ version: "v1.17.21" }), "v1.17.21");
|
||||
assert.equal(releaseVersionLabel({ version: "v1.18.0-preview.62" }), "v1.18.0-preview.62");
|
||||
assert.equal(releaseVersionLabel(null), "latest");
|
||||
});
|
||||
|
||||
test("site placeholders do not render the synthetic version vlatest", async () => {
|
||||
const [home, docs, siteScript] = await Promise.all([
|
||||
readFile(new URL("../pages/index.astro", import.meta.url), "utf8"),
|
||||
readFile(new URL("../pages/docs.astro", import.meta.url), "utf8"),
|
||||
readFile(new URL("./site.js", import.meta.url), "utf8"),
|
||||
]);
|
||||
assert.doesNotMatch(home, /v<span (?:class="rxv"|data-release-version=)/);
|
||||
assert.doesNotMatch(docs, /v<span data-release-version=/);
|
||||
assert.doesNotMatch(home, /<a[^>]*data-cli-asset=[^>]*\sdownload(?:[ >])/);
|
||||
assert.match(home, /id="download-tab-desktop"[^>]+aria-controls="download-pane-desktop"/);
|
||||
assert.match(home, /id="download-pane-cli"[^>]+role="tabpanel"[^>]+hidden/);
|
||||
assert.doesNotMatch(home, /releases\/latest\/download/);
|
||||
assert.doesNotMatch(siteScript, /releases\/latest\/download/);
|
||||
assert.doesNotMatch(siteScript, /desktopPreviewBase/);
|
||||
});
|
||||
|
||||
test("CLI strictly excludes foreign and all prereleases", () => {
|
||||
const releases = [
|
||||
{ tag_name: "v1.19.0-rc.1", prerelease: true, assets: cliAssets("v1.19.0-rc.1") },
|
||||
{ tag_name: "v1.18.0-preview.2", prerelease: true, assets: cliAssets("v1.18.0-preview.2") },
|
||||
{ tag_name: "desktop-v1.18.0", prerelease: false, assets: cliAssets("desktop-v1.18.0") },
|
||||
{ tag_name: "v1.17.21", prerelease: false, assets: cliAssets("v1.17.21") },
|
||||
{ tag_name: "v1.18.0-preview.12", prerelease: true, assets: cliAssets("v1.18.0-preview.12") },
|
||||
{ tag_name: "v1.18.0-preview.13", prerelease: false, assets: cliAssets("v1.18.0-preview.13") },
|
||||
];
|
||||
assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v1.17.21");
|
||||
assert.equal(selectCLIRelease(releases, "preview")?.tag_name, "v1.17.21");
|
||||
});
|
||||
|
||||
test("CLI selection rejects incomplete releases instead of synthesizing asset URLs", () => {
|
||||
const releases = [
|
||||
{
|
||||
tag_name: "v1.20.0",
|
||||
prerelease: false,
|
||||
assets: cliAssets("v1.20.0", ["reasonix-windows-arm64.zip", "SHA256SUMS"]),
|
||||
},
|
||||
{
|
||||
tag_name: "v1.19.5",
|
||||
prerelease: false,
|
||||
assets: cliAssets("v1.19.5"),
|
||||
},
|
||||
];
|
||||
assert.equal(releaseAssetMap(releases[0]), null);
|
||||
assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v1.19.5");
|
||||
const model = cliReleaseModel(releases, "stable");
|
||||
assert.equal(model?.displayVersion, "1.19.5");
|
||||
assert.equal(
|
||||
model?.assets["reasonix-windows-arm64.zip"],
|
||||
"https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.19.5/reasonix-windows-arm64.zip",
|
||||
);
|
||||
assert.equal(cliReleaseModel([releases[0]], "stable"), null);
|
||||
});
|
||||
|
||||
test("CLI selection compares arbitrarily large numeric version components exactly", () => {
|
||||
const releases = [
|
||||
{
|
||||
tag_name: "v99999999999999999999.999.999",
|
||||
prerelease: false,
|
||||
assets: cliAssets("v99999999999999999999.999.999"),
|
||||
},
|
||||
{
|
||||
tag_name: "v100000000000000000000.0.0",
|
||||
prerelease: false,
|
||||
assets: cliAssets("v100000000000000000000.0.0"),
|
||||
},
|
||||
];
|
||||
assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v100000000000000000000.0.0");
|
||||
});
|
||||
|
||||
test("CLI release links are derived from the validated canonical tag", () => {
|
||||
const release = {
|
||||
tag_name: "v1.18.0",
|
||||
prerelease: false,
|
||||
html_url: "https://evil.invalid/phishing",
|
||||
assets: cliAssets("v1.18.0"),
|
||||
};
|
||||
assert.equal(
|
||||
cliReleaseModel([release], "stable")?.releaseURL,
|
||||
"https://github.com/esengine/DeepSeek-Reasonix/releases/tag/v1.18.0",
|
||||
);
|
||||
assert.equal(
|
||||
cliReleaseModel([release], "stable")?.changelogURL,
|
||||
"https://reasonix.io/changelog/",
|
||||
);
|
||||
release.release_notes_url = "https://reasonix.io/changelog/v1.18.0/";
|
||||
assert.equal(cliReleaseModel([release], "stable")?.changelogURL, release.release_notes_url);
|
||||
});
|
||||
|
||||
test("CLI assets reject spoofed hosts and cross-tag URLs", () => {
|
||||
const valid = {
|
||||
tag_name: "v1.18.0",
|
||||
prerelease: false,
|
||||
assets: cliAssets("v1.18.0"),
|
||||
};
|
||||
const invalidURLs = [
|
||||
"https://evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
|
||||
"https://[email protected]/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
|
||||
"http://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
|
||||
"https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.19.0/reasonix-darwin-amd64.tar.gz",
|
||||
"https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-arm64.tar.gz",
|
||||
];
|
||||
const invalid = invalidURLs.map((browser_download_url) => {
|
||||
const release = {
|
||||
tag_name: "v1.20.0",
|
||||
prerelease: false,
|
||||
assets: cliAssets("v1.20.0"),
|
||||
};
|
||||
release.assets[0].browser_download_url = browser_download_url;
|
||||
return release;
|
||||
});
|
||||
assert.equal(selectCLIRelease([...invalid, valid], "stable")?.tag_name, "v1.18.0");
|
||||
});
|
||||
|
||||
test("CLI assets require a bounded positive safe integer size", () => {
|
||||
for (const size of [0, -1, "42", 1.5, 1073741825, Number.MAX_SAFE_INTEGER + 1, NaN, undefined]) {
|
||||
const release = {
|
||||
tag_name: "v1.20.0",
|
||||
prerelease: false,
|
||||
assets: cliAssets("v1.20.0"),
|
||||
};
|
||||
release.assets[0].size = size;
|
||||
assert.equal(releaseAssetMap(release), null, `size ${String(size)} must be rejected`);
|
||||
}
|
||||
});
|
||||
|
||||
test("CLI releases reject duplicate required asset names", () => {
|
||||
const release = {
|
||||
tag_name: "v1.20.0",
|
||||
prerelease: false,
|
||||
assets: cliAssets("v1.20.0"),
|
||||
};
|
||||
release.assets.push({ ...release.assets[0] });
|
||||
assert.equal(releaseAssetMap(release), null);
|
||||
});
|
||||
|
||||
test("Desktop manifests accept only official versions and old or unified asset bases", () => {
|
||||
const preview = desktopManifest("v1.18.0-preview.62");
|
||||
assert.equal(desktopReleaseModel(preview, "stable"), null);
|
||||
assert.equal(desktopReleaseModel(preview, "preview"), null);
|
||||
|
||||
const stableR2 = desktopManifest("v1.18.0");
|
||||
assert.equal(desktopReleaseModel(stableR2, "stable")?.version, "v1.18.0");
|
||||
const githubBase =
|
||||
"https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.18.0/";
|
||||
const stableGitHub = desktopManifest("v1.18.0", githubBase);
|
||||
assert.equal(
|
||||
desktopReleaseModel(stableGitHub, "stable")?.assets["Reasonix-linux-amd64.deb"],
|
||||
`${githubBase}Reasonix-linux-amd64.deb`,
|
||||
);
|
||||
const unifiedBase = "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.18.0/";
|
||||
assert.equal(desktopReleaseModel(desktopManifest("v1.18.0", unifiedBase))?.assets["Reasonix-linux-amd64.deb"], `${unifiedBase}Reasonix-linux-amd64.deb`);
|
||||
});
|
||||
|
||||
test("Desktop manifests reject hostile URLs and incomplete integrity metadata", () => {
|
||||
const cases = [
|
||||
["malicious host", (manifest) => {
|
||||
const url = "https://evil.invalid/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
|
||||
Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
|
||||
}],
|
||||
["userinfo", (manifest) => {
|
||||
const url = "https://[email protected]/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
|
||||
Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
|
||||
}],
|
||||
["http", (manifest) => {
|
||||
const url = "http://dl.reasonix.io/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
|
||||
Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
|
||||
}],
|
||||
["wrong channel path", (manifest) => {
|
||||
const url = "https://dl.reasonix.io/preview/Reasonix-darwin-arm64.zip";
|
||||
Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
|
||||
}],
|
||||
["wrong filename", (manifest) => {
|
||||
const url = "https://dl.reasonix.io/desktop-v1.18.0-preview.62/Reasonix-darwin-amd64.zip";
|
||||
Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
|
||||
}],
|
||||
["missing platform", (manifest) => {
|
||||
delete manifest.platforms["windows-arm64"];
|
||||
}],
|
||||
["missing website download", (manifest) => {
|
||||
delete manifest.downloads["Reasonix-darwin-universal.dmg"];
|
||||
}],
|
||||
["invalid website download", (manifest) => {
|
||||
manifest.downloads["Reasonix-windows-amd64.zip"].size = 0;
|
||||
}],
|
||||
["bad SHA", (manifest) => {
|
||||
manifest.platforms["darwin-arm64"].sha256 = "A".repeat(64);
|
||||
}],
|
||||
["zero size", (manifest) => {
|
||||
manifest.platforms["darwin-arm64"].size = 0;
|
||||
}],
|
||||
["size above release maximum", (manifest) => {
|
||||
manifest.platforms["darwin-arm64"].size = 1073741825;
|
||||
}],
|
||||
["string size", (manifest) => {
|
||||
manifest.platforms["darwin-arm64"].size = "42";
|
||||
}],
|
||||
["missing size", (manifest) => {
|
||||
delete manifest.platforms["darwin-arm64"].size;
|
||||
}],
|
||||
["bad signature URL", (manifest) => {
|
||||
manifest.platforms["darwin-arm64"].sig += "?mirror=1";
|
||||
}],
|
||||
["wrong download page", (manifest) => {
|
||||
manifest.download_page = "https://evil.invalid/download";
|
||||
}],
|
||||
];
|
||||
|
||||
for (const [name, mutate] of cases) {
|
||||
const manifest = copy(desktopManifest("v1.18.0-preview.62"));
|
||||
mutate(manifest);
|
||||
assert.equal(desktopReleaseModel(manifest, "preview"), null, name);
|
||||
}
|
||||
|
||||
const crossVersion = desktopManifest(
|
||||
"v1.18.0",
|
||||
"https://dl.reasonix.io/desktop-v1.17.9/",
|
||||
);
|
||||
assert.equal(desktopReleaseModel(crossVersion, "stable"), null);
|
||||
});
|
||||
|
||||
test("Desktop Stable falls back to a complete exact GitHub Latest release", () => {
|
||||
const release = desktopGitHubRelease();
|
||||
const model = desktopGitHubReleaseModel(release);
|
||||
assert.equal(model?.version, "v1.17.21");
|
||||
assert.equal(
|
||||
model?.assets["Reasonix-darwin-universal.dmg"],
|
||||
"https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.17.21/Reasonix-darwin-universal.dmg",
|
||||
);
|
||||
|
||||
const invalid = [
|
||||
{ ...release, prerelease: true },
|
||||
{ ...release, tag_name: "v1.17.21" },
|
||||
{ ...release, assets: release.assets.slice(1) },
|
||||
{ ...release, assets: [...release.assets, { ...release.assets[0] }] },
|
||||
];
|
||||
const spoofed = copy(release);
|
||||
spoofed.assets[0].browser_download_url =
|
||||
"https://github.com.evil.invalid/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.17.21/Reasonix-darwin-arm64.zip";
|
||||
invalid.push(spoofed);
|
||||
for (const candidate of invalid) assert.equal(desktopGitHubReleaseModel(candidate), null);
|
||||
});
|
||||
|
||||
test("release JSON fetch falls back in order", async () => {
|
||||
const calls = [];
|
||||
const result = await fetchFirstJSON(["https://one.invalid", "https://two.invalid"], async (url) => {
|
||||
calls.push(url);
|
||||
if (url.includes("one")) return { ok: false, status: 503, statusText: "Unavailable" };
|
||||
return { ok: true, json: async () => ({ version: "v1.2.3" }) };
|
||||
});
|
||||
assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
|
||||
assert.deepEqual(result, { version: "v1.2.3" });
|
||||
});
|
||||
|
||||
test("release JSON fetch skips a successful response that fails channel validation", async () => {
|
||||
const calls = [];
|
||||
const result = await fetchFirstJSON(
|
||||
["https://one.invalid", "https://two.invalid"],
|
||||
async (url) => {
|
||||
calls.push(url);
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ version: url.includes("one") ? "v1.2.3-canary.1" : "v1.2.3-preview.1" }),
|
||||
};
|
||||
},
|
||||
(payload) => payload.version === "v1.2.3-preview.1",
|
||||
);
|
||||
assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
|
||||
assert.deepEqual(result, { version: "v1.2.3-preview.1" });
|
||||
});
|
||||
|
||||
test("Desktop JSON fetch continues after an invalid 200 response", async () => {
|
||||
const invalid = desktopManifest("v1.18.0");
|
||||
invalid.platforms["darwin-arm64"].size = 0;
|
||||
const valid = desktopManifest("v1.17.21");
|
||||
const calls = [];
|
||||
const result = await fetchFirstJSON(
|
||||
["https://one.invalid", "https://two.invalid"],
|
||||
async (url) => {
|
||||
calls.push(url);
|
||||
return { ok: true, json: async () => url.includes("one") ? invalid : valid };
|
||||
},
|
||||
(payload) => Boolean(desktopReleaseModel(payload)),
|
||||
);
|
||||
assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
|
||||
assert.equal(result.version, "v1.17.21");
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
// A post-login `next` redirect target is honoured only if it resolves — via
|
||||
// full URL parsing against the real page origin, so control characters and
|
||||
// protocol-relative tricks get the same normalization a browser applies
|
||||
// before navigating — to a same-origin path, or to an absolute https URL
|
||||
// under reasonix.io. That lets a subdomain (e.g. crash.reasonix.io) return
|
||||
// here after sign-in without opening a redirect to an arbitrary host.
|
||||
//
|
||||
// Validating a raw string prefix (e.g. checking it starts with "/") is not
|
||||
// enough: URLSearchParams decodes percent-encoding, so a value like
|
||||
// "/%09/evil.example" arrives as "/\t/evil.example", which passes a naive
|
||||
// prefix check but the URL parser strips the tab during navigation, leaving
|
||||
// "//evil.example" — a protocol-relative redirect off-site. Parsing with
|
||||
// `new URL()` first and checking the *parsed* origin/host closes that gap
|
||||
// because it uses the same normalization the browser itself applies.
|
||||
//
|
||||
// Both branches return the fully-serialized `u.href`, never a relative
|
||||
// fragment. Returning `u.pathname` would reopen the hole: dot-segment inputs
|
||||
// such as "/.//evil.example" or "/a/..//evil.example" resolve to a
|
||||
// same-origin URL whose *pathname* is "//evil.example", and handing that
|
||||
// bare pathname back to `location.href` re-parses it as a protocol-relative
|
||||
// URL to evil.example. `u.href` keeps the origin attached, so assigning it
|
||||
// can never leave reasonix.io.
|
||||
export function safeNext(next, origin) {
|
||||
if (!next) return null;
|
||||
let u;
|
||||
try {
|
||||
u = new URL(next, origin);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (u.origin === origin) return u.href;
|
||||
if (u.protocol === "https:" && (u.host === "reasonix.io" || u.host.endsWith(".reasonix.io"))) return u.href;
|
||||
return null;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { safeNext } from "./safe-next.js";
|
||||
|
||||
const ORIGIN = "https://reasonix.io";
|
||||
|
||||
// The security invariant: whatever safeNext returns, re-resolving it the way a
|
||||
// browser does when it assigns `location.href` must stay on our origin or an
|
||||
// allowed reasonix.io host. Asserting this (rather than a fixed string) catches
|
||||
// any value that looks same-origin but re-parses off-site.
|
||||
function assertLandsSomewhereSafe(returned) {
|
||||
if (returned === null) return;
|
||||
const landed = new URL(returned, ORIGIN);
|
||||
const ok = landed.origin === ORIGIN || landed.host === "reasonix.io" || landed.host.endsWith(".reasonix.io");
|
||||
assert.ok(ok, `returned ${JSON.stringify(returned)} re-resolves off-site to ${landed.href}`);
|
||||
}
|
||||
|
||||
test("same-origin path passes through as an absolute same-origin URL", () => {
|
||||
const got = safeNext("/account/settings?x=1#y", ORIGIN);
|
||||
assert.equal(got, "https://reasonix.io/account/settings?x=1#y");
|
||||
assertLandsSomewhereSafe(got);
|
||||
});
|
||||
|
||||
test("allowed reasonix.io subdomain passes through", () => {
|
||||
const got = safeNext("https://crash.reasonix.io/x", ORIGIN);
|
||||
assert.equal(got, "https://crash.reasonix.io/x");
|
||||
assertLandsSomewhereSafe(got);
|
||||
});
|
||||
|
||||
test("empty/missing next", () => {
|
||||
assert.equal(safeNext("", ORIGIN), null);
|
||||
assert.equal(safeNext(null, ORIGIN), null);
|
||||
});
|
||||
|
||||
test("rejects a plain protocol-relative redirect", () => {
|
||||
assert.equal(safeNext("//evil.example", ORIGIN), null);
|
||||
});
|
||||
|
||||
test("rejects a backslash-prefixed redirect", () => {
|
||||
assert.equal(safeNext("/\\evil.example", ORIGIN), null);
|
||||
assert.equal(safeNext("/\\/evil.example", ORIGIN), null);
|
||||
});
|
||||
|
||||
test("rejects control characters (as decoded by URLSearchParams) smuggling a protocol-relative redirect", () => {
|
||||
// URLSearchParams.get() decodes %09/%0A/%0D before this function ever sees
|
||||
// the value, e.g. "?next=/%09/evil.example" arrives as "/\t/evil.example".
|
||||
for (const c of ["\t", "\n", "\r"]) {
|
||||
assert.equal(safeNext(`/${c}/evil.example`, ORIGIN), null, JSON.stringify(c));
|
||||
}
|
||||
});
|
||||
|
||||
test("dot-segment inputs that normalize to a //host pathname do not escape", () => {
|
||||
// These resolve to a same-origin URL whose pathname is "//evil.example".
|
||||
// Returning the bare pathname would re-parse as a protocol-relative redirect;
|
||||
// returning the absolute href keeps them on reasonix.io. Assert end to end.
|
||||
for (const p of ["/.//evil.example", "/a/..//evil.example", "/..//evil.example", "/%2e//evil.example"]) {
|
||||
const got = safeNext(decodeURIComponent(p), ORIGIN);
|
||||
assert.notEqual(got, null, p);
|
||||
assert.equal(new URL(got, ORIGIN).origin, ORIGIN, `${p} -> ${got}`);
|
||||
assertLandsSomewhereSafe(got);
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects an unrelated https host and non-https schemes", () => {
|
||||
assert.equal(safeNext("https://evil.example/", ORIGIN), null);
|
||||
assert.equal(safeNext("javascript:alert(1)", ORIGIN), null);
|
||||
});
|
||||
|
||||
test("rejects a reasonix.io-lookalike host", () => {
|
||||
assert.equal(safeNext("https://reasonix.io.evil.example/", ORIGIN), null);
|
||||
});
|
||||
@@ -1,334 +0,0 @@
|
||||
import { downloadPaneFromURL, downloadURLForPane } from "./download-link.js";
|
||||
import {
|
||||
cliReleaseModel,
|
||||
cliUpgradeCommand,
|
||||
desktopGitHubReleaseModel,
|
||||
desktopReleaseModel,
|
||||
fetchFirstJSON,
|
||||
releaseVersionLabel,
|
||||
} from "./release-channels.js";
|
||||
import { initTheme } from "./theme.js";
|
||||
import { initMobileNav } from "./mobile-nav.js";
|
||||
|
||||
// Reasonix site — vanilla interactions
|
||||
(function () {
|
||||
initTheme();
|
||||
initMobileNav();
|
||||
const motionOK = () =>
|
||||
document.body.dataset.motion === "rich" &&
|
||||
!window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
const nav = document.querySelector(".nav");
|
||||
if (nav) {
|
||||
const onScroll = () => nav.classList.toggle("scrolled", window.scrollY > 12);
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
onScroll();
|
||||
}
|
||||
|
||||
const revealEls = Array.from(document.querySelectorAll(".reveal"));
|
||||
const inView = (el, factor) =>
|
||||
el.getBoundingClientRect().top < window.innerHeight * (factor || 0.95);
|
||||
|
||||
const term = document.querySelector(".term");
|
||||
const lines = Array.from(document.querySelectorAll(".term-body .tl"));
|
||||
let played = false;
|
||||
const playTerm = () => {
|
||||
if (played) return;
|
||||
played = true;
|
||||
const fire = () => document.dispatchEvent(new CustomEvent("rx:term-played"));
|
||||
if (!motionOK()) {
|
||||
lines.forEach((l) => l.classList.add("on"));
|
||||
fire();
|
||||
return;
|
||||
}
|
||||
lines.forEach((l, i) => setTimeout(() => l.classList.add("on"), 350 + i * 520));
|
||||
setTimeout(fire, 350 + Math.max(0, lines.length - 2) * 520);
|
||||
};
|
||||
|
||||
let sweepQueued = false;
|
||||
const sweep = () => {
|
||||
sweepQueued = false;
|
||||
revealEls.forEach((el) => {
|
||||
if (!el.classList.contains("in") && inView(el, 0.95)) el.classList.add("in");
|
||||
});
|
||||
if (term && !played && inView(term, 0.85)) playTerm();
|
||||
};
|
||||
const queueSweep = () => {
|
||||
if (sweepQueued) return;
|
||||
sweepQueued = true;
|
||||
requestAnimationFrame(sweep);
|
||||
};
|
||||
window.addEventListener("scroll", queueSweep, { passive: true });
|
||||
window.addEventListener("resize", queueSweep, { passive: true });
|
||||
window.addEventListener("load", queueSweep);
|
||||
sweep();
|
||||
setTimeout(sweep, 400);
|
||||
|
||||
/* contributors marquee — duplicate the server-rendered set for a seamless loop */
|
||||
document.querySelectorAll(".crew-row").forEach((row) => {
|
||||
const set = row.querySelector(".crew-set");
|
||||
if (set) row.appendChild(set.cloneNode(true));
|
||||
});
|
||||
|
||||
/* download / channel tabs */
|
||||
const tabs = Array.from(document.querySelectorAll(".dl-tab"));
|
||||
const panes = Array.from(document.querySelectorAll(".dl-pane"));
|
||||
const activatePane = (name) => {
|
||||
tabs.forEach((b) => {
|
||||
const active = b.dataset.pane === name;
|
||||
b.classList.toggle("active", active);
|
||||
b.setAttribute("aria-selected", active ? "true" : "false");
|
||||
b.tabIndex = active ? 0 : -1;
|
||||
});
|
||||
panes.forEach((p) => {
|
||||
const active = p.dataset.pane === name;
|
||||
p.classList.toggle("active", active);
|
||||
p.hidden = !active;
|
||||
});
|
||||
};
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
activatePane(tab.dataset.pane);
|
||||
reflectPaneURL(tab.dataset.pane);
|
||||
});
|
||||
tab.addEventListener("keydown", (event) => {
|
||||
const current = tabs.indexOf(tab);
|
||||
let next = -1;
|
||||
if (event.key === "ArrowRight" || event.key === "ArrowDown") next = (current + 1) % tabs.length;
|
||||
else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = (current - 1 + tabs.length) % tabs.length;
|
||||
else if (event.key === "Home") next = 0;
|
||||
else if (event.key === "End") next = tabs.length - 1;
|
||||
if (next < 0) return;
|
||||
event.preventDefault();
|
||||
const nextTab = tabs[next];
|
||||
activatePane(nextTab.dataset.pane);
|
||||
reflectPaneURL(nextTab.dataset.pane);
|
||||
nextTab.focus();
|
||||
});
|
||||
});
|
||||
|
||||
/* OS detection — hero download button + card badge + highlight */
|
||||
const ua = navigator.userAgent;
|
||||
const os = /Windows/i.test(ua) ? "win" : /Mac|iPhone|iPad/i.test(ua) ? "mac" : /Linux|X11/i.test(ua) ? "linux" : "mac";
|
||||
const osNames = { mac: "macOS", win: "Windows", linux: "Linux" };
|
||||
document.querySelectorAll("[data-os-dl] .os-name").forEach((s) => (s.textContent = osNames[os]));
|
||||
const osCard = document.querySelector('.os-card[data-os="' + os + '"]');
|
||||
if (osCard) {
|
||||
osCard.classList.add("detected");
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "os-chip";
|
||||
chip.innerHTML = '<span class="l-en">your OS</span><span class="l-zh">当前系统</span>';
|
||||
osCard.appendChild(chip);
|
||||
}
|
||||
|
||||
const flashOSCard = () => {
|
||||
if (!osCard) return;
|
||||
osCard.classList.remove("flash");
|
||||
void osCard.offsetWidth;
|
||||
setTimeout(() => osCard.classList.add("flash"), 450);
|
||||
setTimeout(() => osCard.classList.remove("flash"), 2600);
|
||||
};
|
||||
|
||||
const requestedPane = downloadPaneFromURL(window.location.href);
|
||||
const legacyChannelURL = new URL(window.location.href);
|
||||
if (legacyChannelURL.searchParams.has("channel")) {
|
||||
legacyChannelURL.searchParams.delete("channel");
|
||||
window.history.replaceState(null, "", legacyChannelURL.href);
|
||||
}
|
||||
if (requestedPane) {
|
||||
activatePane(requestedPane);
|
||||
if (requestedPane === "desktop") flashOSCard();
|
||||
requestAnimationFrame(() => {
|
||||
document.getElementById("start")?.scrollIntoView({ block: "start" });
|
||||
queueSweep();
|
||||
});
|
||||
}
|
||||
|
||||
/* links that deep-link into a specific download tab */
|
||||
document.querySelectorAll("[data-goto]").forEach((a) => {
|
||||
a.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
activatePane(a.dataset.goto);
|
||||
reflectPaneURL(a.dataset.goto);
|
||||
if (a.hasAttribute("data-os-dl")) flashOSCard();
|
||||
document.getElementById("start")?.scrollIntoView({ block: "start" });
|
||||
setTimeout(queueSweep, 500);
|
||||
});
|
||||
});
|
||||
|
||||
/* language switch */
|
||||
const LANG_KEY = "reasonix-lang";
|
||||
const langBtns = Array.from(document.querySelectorAll(".lang-switch button"));
|
||||
const setLang = (l, alignHash) => {
|
||||
document.body.dataset.lang = l;
|
||||
document.documentElement.lang = l === "zh" ? "zh-CN" : "en";
|
||||
const t = document.body.dataset[l === "zh" ? "titleZh" : "titleEn"];
|
||||
if (t) document.title = t;
|
||||
langBtns.forEach((b) => b.classList.toggle("active", b.dataset.lang === l));
|
||||
try { localStorage.setItem(LANG_KEY, l); } catch (e) {}
|
||||
if (alignHash && window.location.hash) {
|
||||
const target = document.getElementById(window.location.hash.slice(1));
|
||||
if (target) requestAnimationFrame(() => target.scrollIntoView({ block: "start" }));
|
||||
}
|
||||
};
|
||||
langBtns.forEach((b) => b.addEventListener("click", () => setLang(b.dataset.lang)));
|
||||
let savedLang = "";
|
||||
try { savedLang = localStorage.getItem(LANG_KEY) || ""; } catch (e) {}
|
||||
const requestedLang = new URLSearchParams(window.location.search).get("lang");
|
||||
const initialLang = requestedLang === "zh" || requestedLang === "en"
|
||||
? requestedLang
|
||||
: savedLang || ((navigator.language || "").toLowerCase().startsWith("zh") ? "zh" : "en");
|
||||
setLang(initialLang, true);
|
||||
|
||||
/* docs scrollspy */
|
||||
const sideLinks = Array.from(document.querySelectorAll(".docs-side a[href^='#']"));
|
||||
if (sideLinks.length) {
|
||||
const targets = sideLinks
|
||||
.map((a) => document.getElementById(a.getAttribute("href").slice(1)))
|
||||
.filter(Boolean)
|
||||
// Sidebar links are grouped editorially, so their order differs from the
|
||||
// page order. The spy below picks the last section past the 140px line,
|
||||
// which is only correct when targets are sorted in document order.
|
||||
.sort((a, b) =>
|
||||
a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
||||
const setActive = (id) =>
|
||||
sideLinks.forEach((a) => {
|
||||
const on = a.getAttribute("href") === "#" + id;
|
||||
a.classList.toggle("active", on);
|
||||
if (on) a.setAttribute("aria-current", "true");
|
||||
else a.removeAttribute("aria-current");
|
||||
});
|
||||
// While a click smooth-scrolls to a section, pin the highlight to it so it
|
||||
// doesn't sweep through every section scrolled past on the way. scrollend
|
||||
// releases the pin when the scroll settles — on arrival or when the user
|
||||
// takes over (wheel, touch, keyboard, scrollbar). Browsers without
|
||||
// scrollend just skip pinning: correct destination, no sweep suppression.
|
||||
let pinned = null;
|
||||
const spy = () => {
|
||||
if (pinned) return;
|
||||
let current = targets[0];
|
||||
for (const t of targets) if (t.getBoundingClientRect().top < 140) current = t;
|
||||
if (current) setActive(current.id);
|
||||
};
|
||||
if ("onscrollend" in window) {
|
||||
const ids = new Set(targets.map((t) => t.id));
|
||||
document.querySelectorAll("a[href^='#']").forEach((a) => {
|
||||
const id = a.getAttribute("href").slice(1);
|
||||
if (ids.has(id)) a.addEventListener("click", () => { pinned = id; setActive(id); });
|
||||
});
|
||||
window.addEventListener("scrollend", () => { pinned = null; spy(); }, { passive: true });
|
||||
}
|
||||
window.addEventListener("scroll", spy, { passive: true });
|
||||
spy();
|
||||
}
|
||||
|
||||
/* copy-to-clipboard */
|
||||
document.querySelectorAll("[data-copy]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const text = btn.getAttribute("data-copy");
|
||||
const done = () => {
|
||||
btn.classList.add("copied");
|
||||
const prev = btn.textContent;
|
||||
btn.textContent = "Copied";
|
||||
setTimeout(() => { btn.classList.remove("copied"); btn.textContent = prev; }, 1600);
|
||||
};
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(done).catch(done);
|
||||
} else done();
|
||||
});
|
||||
});
|
||||
|
||||
/* public official releases */
|
||||
const releaseModels = { desktop: null, cli: null };
|
||||
const releasesPage = "https://github.com/esengine/DeepSeek-Reasonix/releases";
|
||||
const reflectPaneURL = (surface) => {
|
||||
const nextURL = downloadURLForPane(window.location.href, surface, "");
|
||||
if (nextURL) window.history.replaceState(null, "", nextURL);
|
||||
};
|
||||
|
||||
// npm / Homebrew / generic product chips track the CLI stable line only.
|
||||
// Desktop and CLI download panes render their own versions via
|
||||
// [data-release-version="<surface>"]; never let Desktop and CLI race on .rxv.
|
||||
const updateCLIPackageVersion = (model) => {
|
||||
if (!model) return;
|
||||
document.querySelectorAll(".rxv").forEach((element) => { element.textContent = releaseVersionLabel(model); });
|
||||
document.querySelectorAll("a.rxnotes").forEach((link) => {
|
||||
link.href = new URL("changelog/v" + model.displayVersion + "/", window.location.origin + "/").href;
|
||||
});
|
||||
};
|
||||
|
||||
// Never synthesize public artifact URLs. If every required asset is not
|
||||
// attested by live release data, fall back to the release list instead of a
|
||||
// plausible-looking URL that may 404.
|
||||
const fallbackReleaseURL = () => releasesPage;
|
||||
|
||||
const renderReleaseSurface = (surface) => {
|
||||
const model = releaseModels[surface];
|
||||
document.querySelectorAll('[data-release-version="' + surface + '"]').forEach((element) => {
|
||||
element.textContent = releaseVersionLabel(model);
|
||||
});
|
||||
document.querySelectorAll('[data-release-notes="' + surface + '"]').forEach((link) => {
|
||||
const path = model?.changelogURL ? new URL(model.changelogURL).pathname : "changelog/";
|
||||
link.href = new URL(path, window.location.origin + "/").href;
|
||||
});
|
||||
|
||||
const assetAttribute = "data-" + surface + "-asset";
|
||||
document.querySelectorAll("[" + assetAttribute + "]").forEach((link) => {
|
||||
const asset = link.getAttribute(assetAttribute);
|
||||
const target = model?.assets?.[asset] || fallbackReleaseURL();
|
||||
link.href = target;
|
||||
if (target === releasesPage) link.removeAttribute("download");
|
||||
else link.setAttribute("download", "");
|
||||
});
|
||||
|
||||
if (surface === "cli") {
|
||||
const command = cliUpgradeCommand();
|
||||
document.querySelectorAll('[data-release-command="cli"]').forEach((element) => { element.textContent = command; });
|
||||
document.querySelectorAll('.release-upgrade-command [data-copy]').forEach((button) => { button.dataset.copy = command; });
|
||||
}
|
||||
};
|
||||
|
||||
renderReleaseSurface("desktop");
|
||||
renderReleaseSurface("cli");
|
||||
if (requestedPane) reflectPaneURL(requestedPane);
|
||||
|
||||
fetchFirstJSON([
|
||||
"https://dl.reasonix.io/latest/latest.json",
|
||||
"https://crash.reasonix.io/v1/desktop/releases/stable/latest.json",
|
||||
], fetch, (manifest) => Boolean(desktopReleaseModel(manifest)))
|
||||
.then((manifest) => desktopReleaseModel(manifest))
|
||||
.catch(() => fetchFirstJSON(
|
||||
["https://api.github.com/repos/esengine/DeepSeek-Reasonix/releases/latest"],
|
||||
fetch,
|
||||
(release) => Boolean(desktopGitHubReleaseModel(release)),
|
||||
).then(desktopGitHubReleaseModel))
|
||||
.then((model) => {
|
||||
if (!model) return;
|
||||
releaseModels.desktop = model;
|
||||
renderReleaseSurface("desktop");
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
let githubCLIReleases;
|
||||
const fallbackCLIReleases = () => {
|
||||
githubCLIReleases ??= fetchFirstJSON([
|
||||
"https://api.github.com/repos/esengine/DeepSeek-Reasonix/releases?per_page=100",
|
||||
]).catch(() => null);
|
||||
return githubCLIReleases;
|
||||
};
|
||||
fetchFirstJSON(
|
||||
["https://crash.reasonix.io/v1/cli/releases/stable/latest.json"],
|
||||
fetch,
|
||||
(payload) => Boolean(cliReleaseModel(Array.isArray(payload) ? payload : [payload])),
|
||||
)
|
||||
.catch(() => fallbackCLIReleases())
|
||||
.then((payload) => {
|
||||
const releases = Array.isArray(payload) ? payload : payload ? [payload] : [];
|
||||
const model = cliReleaseModel(releases);
|
||||
if (!model) return;
|
||||
releaseModels.cli = model;
|
||||
updateCLIPackageVersion(model);
|
||||
renderReleaseSurface("cli");
|
||||
})
|
||||
.catch(() => {});
|
||||
})();
|
||||
@@ -1,48 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const source = () => readFile(new URL("../pages/skills.astro", import.meta.url), "utf8");
|
||||
|
||||
test("the registry publish CTA does not inherit the publish panel layout", async () => {
|
||||
const page = await source();
|
||||
|
||||
assert.match(page, /class="btn btn-dark reg-publish-cta" id="publish-open"/);
|
||||
assert.match(page, /<section class="reg-publish" id="publish" hidden>/);
|
||||
assert.doesNotMatch(page, /class="btn btn-dark reg-publish" id="publish-open"/);
|
||||
});
|
||||
|
||||
test("the registry exposes plugins as a first-class package kind", async () => {
|
||||
const page = await source();
|
||||
|
||||
assert.match(page, /<button data-kind="plugin" role="tab">Plugins<\/button>/);
|
||||
assert.match(page, /<option value="plugin">plugin<\/option>/);
|
||||
assert.match(page, /installKind: f\.get\('kind'\)/);
|
||||
assert.match(page, /Plugins must point to a GitHub repository or path containing/);
|
||||
assert.match(page, /reasonix-plugin\.json/);
|
||||
assert.match(page, /\.codex-plugin\/plugin\.json/);
|
||||
assert.match(page, /\.claude-plugin\/plugin\.json/);
|
||||
assert.match(page, /\.claude-plugin\/marketplace\.json/);
|
||||
assert.match(page, /pattern="\(https\?:\/\/\[\^ \]\+\|git:github\[\.\]com\/\[\^ \]\+/);
|
||||
});
|
||||
|
||||
test("registry copy requests preserve the reviewed package kind", async () => {
|
||||
const page = await source();
|
||||
|
||||
assert.match(page, /Install this Reasonix \$\{p\.kind\} package from \$\{p\.source\}/);
|
||||
assert.match(page, /Use install_source with kind="\$\{p\.kind\}"/);
|
||||
assert.match(page, /data-copy="\$\{esc\(installRequest\(p\)\)\}"/);
|
||||
assert.doesNotMatch(page, /data-copy="\$\{esc\(p\.source\)\}"/);
|
||||
});
|
||||
|
||||
test("admin approval is bound to the package revision shown in the review queue", async () => {
|
||||
const page = await source();
|
||||
|
||||
assert.match(page, /data-version="\$\{esc\(p\.latestVersion\)\}"/);
|
||||
assert.match(page, /data-updated-at="\$\{esc\(p\.updatedAt\)\}"/);
|
||||
assert.match(page, /data-status="\$\{esc\(p\.status\)\}"/);
|
||||
assert.match(page, /expectedVersion: row\.dataset\.version/);
|
||||
assert.match(page, /expectedUpdatedAt: row\.dataset\.updatedAt/);
|
||||
assert.match(page, /expectedStatus: row\.dataset\.status/);
|
||||
assert.match(page, /if \(r\.status === 409\) loadReview\(\)/);
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
// Reasonix — shared three-state theme switch (system / light / dark).
|
||||
// The pre-paint inline script in each layout sets documentElement.dataset.theme
|
||||
// before first paint; this module wires the toggle buttons and keeps the
|
||||
// resolved theme in sync with OS changes while the preference is "system".
|
||||
const THEME_KEY = "reasonix-theme";
|
||||
const META_LIGHT = "#ffffff";
|
||||
const META_DARK = "#1f232b";
|
||||
|
||||
export function currentThemePref() {
|
||||
try {
|
||||
const p = localStorage.getItem(THEME_KEY);
|
||||
return p === "light" || p === "dark" ? p : "system";
|
||||
} catch (e) {
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveTheme(pref) {
|
||||
return pref === "dark" ||
|
||||
(pref === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
export function applyTheme(pref) {
|
||||
const resolved = resolveTheme(pref);
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
const meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) meta.content = resolved === "dark" ? META_DARK : META_LIGHT;
|
||||
document.querySelectorAll(".theme-switch button").forEach((b) => {
|
||||
const on = b.dataset.theme === pref;
|
||||
b.classList.toggle("active", on);
|
||||
if (on) b.setAttribute("aria-pressed", "true");
|
||||
else b.setAttribute("aria-pressed", "false");
|
||||
});
|
||||
}
|
||||
|
||||
export function initTheme() {
|
||||
applyTheme(currentThemePref());
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const onOsChange = () => {
|
||||
const pref = currentThemePref();
|
||||
if (pref === "system") applyTheme(pref);
|
||||
};
|
||||
if (mq.addEventListener) mq.addEventListener("change", onOsChange);
|
||||
document.querySelectorAll(".theme-switch button").forEach((b) => {
|
||||
b.addEventListener("click", () => {
|
||||
try {
|
||||
localStorage.setItem(THEME_KEY, b.dataset.theme);
|
||||
} catch (e) {}
|
||||
applyTheme(b.dataset.theme);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const source = (path) => readFile(new URL(path, import.meta.url), "utf8");
|
||||
|
||||
test("homepage presents one VS Code extension through both registries", async () => {
|
||||
const page = await source("../pages/index.astro");
|
||||
|
||||
assert.match(page, /data-pane="vscode"/);
|
||||
assert.match(page, /data-pane="desktop"[\s\S]*data-pane="npm"[\s\S]*data-pane="brew"[\s\S]*data-pane="vscode"/);
|
||||
assert.match(page, /Editor extension[\s\S]*编辑器扩展/);
|
||||
assert.match(page, /More ways to use Reasonix:[\s\S]*更多使用方式:/);
|
||||
assert.match(page, /Local Web UI:[\s\S]*reasonix serve[\s\S]*本地 Web UI:/);
|
||||
assert.match(page, /ACP editor integration[\s\S]*ACP 编辑器接入/);
|
||||
assert.match(page, /SivanLiu\.reasonix-agent/);
|
||||
assert.match(page, /marketplace\.visualstudio\.com\/items\?itemName=SivanLiu\.reasonix-agent/);
|
||||
assert.match(page, /open-vsx\.org\/extension\/SivanLiu\/reasonix-agent/);
|
||||
assert.match(page, /does not bundle the CLI/);
|
||||
assert.match(page, /data-goto="vscode"/);
|
||||
});
|
||||
|
||||
test("mobile download channels use a two-column tab grid", async () => {
|
||||
const css = await source("../styles/global.css");
|
||||
const mobile = css.slice(css.indexOf("@media (max-width: 640px)"), css.indexOf("@media (max-width: 360px)"));
|
||||
|
||||
assert.match(mobile, /\.dl-tabs \{\s*display: grid; grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/);
|
||||
});
|
||||
@@ -1,443 +0,0 @@
|
||||
/* Palette unified with the global.css base tokens; --release-paper keeps the
|
||||
ledger identity. Dark flips for base tokens live in global.css. */
|
||||
:root {
|
||||
--release-backdrop: var(--bg-soft);
|
||||
--release-paper: #fbfaf7;
|
||||
--release-ink: var(--ink);
|
||||
--release-blue: var(--accent);
|
||||
--release-blue-soft: var(--accent-soft);
|
||||
--release-orange: #d97732;
|
||||
--release-green: #178b67;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--release-paper: var(--card);
|
||||
--release-orange: oklch(0.76 0.13 60);
|
||||
--release-green: oklch(0.76 0.13 160);
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
linear-gradient(90deg, transparent 0 49.94%, color-mix(in oklch, var(--accent) 2.5%, transparent) 50%, transparent 50.06%),
|
||||
var(--release-backdrop);
|
||||
}
|
||||
|
||||
.release-stage {
|
||||
min-height: 100vh;
|
||||
padding: 108px 34px 80px;
|
||||
}
|
||||
|
||||
.release-grid {
|
||||
width: min(1440px, 100%);
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 220px minmax(0, 920px) 190px;
|
||||
gap: 30px;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.release-rail,
|
||||
.release-toc {
|
||||
position: sticky;
|
||||
top: 96px;
|
||||
max-height: calc(100vh - 120px);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.release-rail__eyebrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin: 4px 0 18px 10px;
|
||||
color: var(--ink-2);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.release-rail__mark {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 2px solid var(--release-blue);
|
||||
border-radius: 3px 3px 3px 0;
|
||||
transform: rotate(-12deg);
|
||||
}
|
||||
|
||||
.release-rail nav { display: grid; gap: 18px; }
|
||||
.release-rail__group { display: grid; gap: 7px; }
|
||||
.release-rail__minor {
|
||||
margin: 0 0 2px 10px;
|
||||
color: var(--ink-3);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.release-rail__more summary {
|
||||
margin: 2px 10px 6px;
|
||||
color: var(--ink-3);
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
.release-rail__more summary::-webkit-details-marker { display: none; }
|
||||
.release-rail__more summary:hover { color: var(--release-blue); }
|
||||
.release-rail__more .when-open { display: none; }
|
||||
.release-rail__more[open] .when-open { display: inline; }
|
||||
.release-rail__more[open] .when-closed { display: none; }
|
||||
|
||||
.release-version-link {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 12px 13px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 11px;
|
||||
color: var(--ink-2);
|
||||
text-decoration: none;
|
||||
transition: background .18s, border-color .18s, transform .18s;
|
||||
}
|
||||
|
||||
.release-version-link:hover {
|
||||
transform: translateX(2px);
|
||||
border-color: var(--line);
|
||||
background: color-mix(in oklch, var(--card) 68%, transparent);
|
||||
}
|
||||
|
||||
.release-version-link.active {
|
||||
border-color: color-mix(in srgb, var(--release-blue) 22%, transparent);
|
||||
background: var(--card);
|
||||
color: var(--release-blue);
|
||||
box-shadow: 0 8px 24px rgba(35, 55, 92, .08);
|
||||
}
|
||||
|
||||
.release-version-link__line { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.release-version-link strong { font-family: var(--font-mono); font-size: 13px; }
|
||||
.release-version-link small { color: var(--ink-3); font-size: 11px; }
|
||||
.release-version-link em {
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--release-blue-soft);
|
||||
color: var(--release-blue);
|
||||
font-size: 9px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
letter-spacing: .04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.release-rail__all {
|
||||
display: inline-block;
|
||||
margin: 18px 10px;
|
||||
color: var(--ink-3);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.release-rail__all:hover { color: var(--release-blue); }
|
||||
|
||||
.release-paper {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 20px;
|
||||
background:
|
||||
linear-gradient(color-mix(in oklch, var(--accent) 3.5%, transparent) 1px, transparent 1px) 0 0 / 100% 34px,
|
||||
var(--release-paper);
|
||||
box-shadow: 0 28px 80px -42px rgba(24, 35, 60, .38);
|
||||
}
|
||||
|
||||
.release-hero {
|
||||
position: relative;
|
||||
padding: 48px 62px 44px;
|
||||
background:
|
||||
radial-gradient(circle at 90% 5%, color-mix(in oklch, var(--accent) 14%, transparent), transparent 28%),
|
||||
linear-gradient(180deg, color-mix(in oklch, var(--card) 88%, transparent), color-mix(in oklch, var(--release-paper) 92%, transparent));
|
||||
}
|
||||
|
||||
.release-hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 62px;
|
||||
right: 62px;
|
||||
bottom: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, var(--release-blue), var(--line) 36%, transparent);
|
||||
}
|
||||
|
||||
.release-hero__meta { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
||||
.release-hero__meta time { font-family: var(--font-mono); color: var(--ink-3); font-size: 12px; }
|
||||
.release-channel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--release-green);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.release-channel__dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; box-shadow: 0 0 0 4px color-mix(in srgb, currentColor 14%, transparent); }
|
||||
.release-hero h1 {
|
||||
max-width: 15em;
|
||||
margin: 28px 0 0;
|
||||
color: var(--release-ink);
|
||||
font-size: clamp(32px, 4vw, 46px);
|
||||
font-weight: 600;
|
||||
letter-spacing: -.032em;
|
||||
line-height: 1.08;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.release-surfaces { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 24px; }
|
||||
.release-surfaces > span {
|
||||
padding: 5px 9px;
|
||||
border: 1px solid color-mix(in srgb, var(--release-blue) 18%, var(--line));
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklch, var(--card) 72%, transparent);
|
||||
color: var(--ink-2);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.release-lede { margin: 28px 0 0; max-width: 720px; color: var(--ink-2); font-size: 16.5px; line-height: 1.75; }
|
||||
|
||||
.release-section { padding: 52px 62px 0; scroll-margin-top: 100px; }
|
||||
.release-section__head { display: flex; align-items: center; gap: 14px; margin-bottom: 24px; }
|
||||
.release-section__head > span {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid color-mix(in srgb, var(--release-blue) 24%, var(--line));
|
||||
border-radius: 8px;
|
||||
color: var(--release-blue);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.release-section h2 { color: var(--release-ink); font-size: 27px; font-weight: 650; letter-spacing: -.02em; }
|
||||
.release-section h3 { color: var(--release-ink); font-size: 17px; font-weight: 650; line-height: 1.35; }
|
||||
.release-section p { color: var(--ink-2); font-size: 15px; line-height: 1.68; }
|
||||
|
||||
.release-guide-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.release-guide {
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
min-height: 168px;
|
||||
padding: 22px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: color-mix(in oklch, var(--card) 72%, transparent);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: transform .2s, border-color .2s, box-shadow .2s;
|
||||
}
|
||||
.release-guide:hover { transform: translateY(-3px); border-color: color-mix(in srgb, var(--release-blue) 35%, var(--line)); box-shadow: 0 18px 35px -24px rgba(35, 65, 120, .4); }
|
||||
.release-guide__arrow { position: absolute; top: 18px; right: 18px; color: var(--release-blue); font-size: 18px; }
|
||||
.release-guide h3 { padding-right: 24px; }
|
||||
.release-guide p { margin-top: 12px; font-size: 13.5px; }
|
||||
|
||||
.release-highlight-list { display: grid; gap: 12px; }
|
||||
.release-highlight {
|
||||
display: grid;
|
||||
grid-template-columns: 46px minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: color-mix(in oklch, var(--card) 68%, transparent);
|
||||
}
|
||||
.release-highlight__index { color: color-mix(in srgb, var(--release-blue) 48%, var(--ink-3)); font-family: var(--font-mono); font-size: 12px; padding-top: 4px; }
|
||||
.release-highlight__kind { margin-bottom: 7px; color: var(--release-blue); font-size: 10px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; }
|
||||
.release-highlight[data-kind="security"] .release-highlight__kind { color: var(--release-orange); }
|
||||
.release-highlight[data-kind="fixed"] .release-highlight__kind { color: var(--release-green); }
|
||||
.release-highlight p { margin-top: 8px; }
|
||||
.release-refs { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 13px; }
|
||||
.release-refs a {
|
||||
padding: 3px 7px;
|
||||
border-radius: 6px;
|
||||
background: var(--release-blue-soft);
|
||||
color: var(--release-blue);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.release-change-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.release-change-list article {
|
||||
padding: 20px 22px;
|
||||
border-top: 2px solid color-mix(in srgb, var(--release-blue) 42%, var(--line));
|
||||
background: color-mix(in oklch, var(--card) 46%, transparent);
|
||||
}
|
||||
.release-change-list p { margin-top: 8px; font-size: 14px; }
|
||||
|
||||
.release-notice-list { display: grid; gap: 10px; }
|
||||
.release-notice {
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid color-mix(in srgb, var(--release-blue) 22%, var(--line));
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--release-blue-soft) 56%, var(--card));
|
||||
}
|
||||
.release-notice--warning { border-color: color-mix(in srgb, var(--release-orange) 32%, var(--line)); background: color-mix(in srgb, var(--release-orange) 12%, var(--card)); }
|
||||
.release-notice__icon { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; background: var(--release-blue); color: white; font-family: var(--font-mono); font-size: 12px; font-weight: 700; }
|
||||
.release-notice--warning .release-notice__icon { background: var(--release-orange); }
|
||||
.release-notice p { margin-top: 6px; font-size: 14px; }
|
||||
|
||||
.release-thanks > p { margin-bottom: 14px; }
|
||||
.release-thanks > div { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.release-thanks a { padding: 6px 10px; border: 1px solid var(--line); border-radius: 999px; background: color-mix(in oklch, var(--card) 70%, transparent); color: var(--ink-2); font-family: var(--font-mono); font-size: 11px; text-decoration: none; }
|
||||
.release-thanks a:hover { color: var(--release-blue); border-color: color-mix(in srgb, var(--release-blue) 34%, var(--line)); }
|
||||
|
||||
.release-download {
|
||||
margin: 56px 62px 62px;
|
||||
padding: 30px 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 26px;
|
||||
border-radius: 16px;
|
||||
background: #20242d;
|
||||
}
|
||||
.release-download h2 { max-width: 480px; color: #fff; font-size: 28px; }
|
||||
.release-download__kicker { margin-bottom: 7px; color: #91b8ff !important; font-size: 11px !important; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
|
||||
.release-download__actions { display: flex; align-items: center; gap: 14px; flex-shrink: 0; }
|
||||
.release-compare { color: #c9d4e9; font-size: 13px; font-weight: 600; text-decoration: none; }
|
||||
.release-compare:hover { color: #fff; }
|
||||
|
||||
.release-toc { padding: 6px 0 20px; }
|
||||
.release-toc > p { margin: 0 0 16px 12px; color: var(--release-ink); font-size: 13px; font-weight: 700; }
|
||||
.release-toc nav { display: grid; border-left: 1px solid var(--line); }
|
||||
.release-toc a {
|
||||
position: relative;
|
||||
padding: 7px 0 7px 13px;
|
||||
color: var(--ink-3);
|
||||
font-size: 12px;
|
||||
font-weight: 550;
|
||||
text-decoration: none;
|
||||
transition: color .16s;
|
||||
}
|
||||
.release-toc a::before { content: ""; position: absolute; left: -1px; top: 5px; bottom: 5px; width: 2px; border-radius: 4px; background: transparent; }
|
||||
.release-toc a:hover { color: var(--release-ink); }
|
||||
.release-toc a.active { color: var(--release-blue); }
|
||||
.release-toc a.active::before { background: var(--release-blue); }
|
||||
|
||||
.release-mobile-switcher { display: none; }
|
||||
.release-footer {
|
||||
width: min(1180px, calc(100% - 64px));
|
||||
margin: 0 auto;
|
||||
padding: 28px 0 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--ink-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
.release-footer > span { margin-left: auto; color: var(--ink-3); }
|
||||
.release-footer > a:last-child { color: var(--ink-2); text-decoration: none; }
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.release-grid { grid-template-columns: 200px minmax(0, 850px); }
|
||||
.release-toc { display: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.release-stage { padding: 92px 16px 48px; }
|
||||
.release-grid { display: block; }
|
||||
.release-rail { display: none; }
|
||||
.release-mobile-switcher {
|
||||
width: min(720px, 100%);
|
||||
margin: 0 auto 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--card);
|
||||
color: var(--ink-2);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.release-mobile-switcher select { min-width: 130px; padding: 7px 28px 7px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg-soft); color: var(--ink); font: inherit; }
|
||||
.release-paper { width: min(720px, 100%); margin: 0 auto; border-radius: 15px; }
|
||||
.release-hero { padding: 38px 28px 34px; }
|
||||
.release-hero::after { left: 28px; right: 28px; }
|
||||
.release-section { padding: 40px 28px 0; }
|
||||
.release-guide-grid, .release-change-list { grid-template-columns: 1fr; }
|
||||
.release-download { margin: 44px 28px 28px; padding: 26px; flex-direction: column; align-items: flex-start; }
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.release-hero h1 { font-size: 34px; }
|
||||
.release-highlight { grid-template-columns: 1fr; gap: 8px; padding: 20px; }
|
||||
.release-highlight__index { display: none; }
|
||||
.release-section h2 { font-size: 23px; }
|
||||
.release-download__actions { align-items: flex-start; flex-direction: column; }
|
||||
.release-footer { width: calc(100% - 32px); flex-wrap: wrap; }
|
||||
.release-footer > span { order: 3; width: 100%; margin: 0; }
|
||||
}
|
||||
|
||||
.release-channel-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
margin: 14px 0 18px;
|
||||
}
|
||||
|
||||
.release-channel-tabs a {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
color: var(--ink-2);
|
||||
padding: 7px 10px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.release-channel-tabs a:hover {
|
||||
border-color: color-mix(in oklch, var(--accent) 40%, var(--line));
|
||||
color: var(--accent-deep);
|
||||
}
|
||||
|
||||
.release-channel-tabs a.active {
|
||||
background: var(--accent-soft);
|
||||
border-color: color-mix(in oklch, var(--accent) 46%, var(--line));
|
||||
color: var(--accent-deep);
|
||||
}
|
||||
|
||||
.release-channel-tabs a:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.release-builds {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.release-builds span {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
color: var(--ink-2);
|
||||
font-family: var(--mono);
|
||||
font-size: 0.78rem;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.release-version-link, .release-guide, .release-channel-tabs a { transition: none; }
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
/* Reasonix Community — self-hosted fonts + the reasonix.io token values, so the
|
||||
forum shares the site's visual language without depending on its marketing CSS. */
|
||||
@import "@fontsource-variable/inter";
|
||||
@import "@fontsource-variable/jetbrains-mono";
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--accent: oklch(0.55 0.17 257);
|
||||
--accent-deep: oklch(0.48 0.17 257);
|
||||
--accent-soft: color-mix(in oklch, var(--accent) 8%, var(--bg));
|
||||
--accent-line: color-mix(in oklch, var(--accent) 26%, transparent);
|
||||
--bg: #ffffff;
|
||||
--bg-soft: oklch(0.977 0.0025 247);
|
||||
--card: #ffffff;
|
||||
--ink: oklch(0.21 0.006 260);
|
||||
--ink-2: oklch(0.45 0.008 260);
|
||||
--ink-3: oklch(0.6 0.008 260);
|
||||
--line: oklch(0.905 0.004 247);
|
||||
--ok: oklch(0.6 0.14 155);
|
||||
--ok-soft: color-mix(in oklch, var(--ok) 12%, var(--bg));
|
||||
--err: oklch(0.5 0.19 25);
|
||||
--warm: oklch(0.72 0.13 66);
|
||||
--violet: oklch(0.58 0.17 300);
|
||||
--rose: oklch(0.62 0.19 15);
|
||||
--radius: 13px;
|
||||
--radius-sm: 9px;
|
||||
--shadow-1: 0 1px 2px oklch(0.4 0.02 260 / 0.08);
|
||||
--shadow-2: 0 1px 2px oklch(0.4 0.02 260 / 0.06), 0 12px 32px -16px oklch(0.4 0.02 260 / 0.24);
|
||||
--font-sans: "Inter Variable", "Inter", "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
--font-mono: "JetBrains Mono Variable", "SF Mono", Menlo, monospace;
|
||||
--ease: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--accent: oklch(0.66 0.16 257);
|
||||
--accent-deep: oklch(0.73 0.14 257);
|
||||
--accent-soft: color-mix(in oklch, var(--accent) 16%, var(--bg));
|
||||
--bg: oklch(0.19 0.006 260);
|
||||
--bg-soft: oklch(0.225 0.008 260);
|
||||
--card: oklch(0.222 0.008 260);
|
||||
--ink: oklch(0.93 0.004 260);
|
||||
--ink-2: oklch(0.78 0.008 260);
|
||||
--ink-3: oklch(0.63 0.008 260);
|
||||
--line: oklch(0.31 0.008 260);
|
||||
--ok-soft: color-mix(in oklch, var(--ok) 18%, var(--bg));
|
||||
--shadow-1: 0 1px 2px oklch(0 0 0 / 0.3);
|
||||
--shadow-2: 0 1px 2px oklch(0 0 0 / 0.28), 0 12px 32px -16px oklch(0 0 0 / 0.6);
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body { font-family: var(--font-sans); background: var(--bg); color: var(--ink); -webkit-font-smoothing: antialiased; line-height: 1.5; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
::selection { background: oklch(0.88 0.05 257); }
|
||||
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 32px; }
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
/* bilingual toggle — mirrors the main site (shared reasonix-lang choice) */
|
||||
body[data-lang="en"] .l-zh { display: none !important; }
|
||||
body[data-lang="zh"] .l-en { display: none !important; }
|
||||
.lang-switch, .theme-switch { display: inline-flex; gap: 2px; padding: 3px; background: var(--bg-soft); border-radius: 9px; }
|
||||
.lang-switch button, .theme-switch button { font-family: var(--font-sans); font-size: 12.5px; font-weight: 500; color: var(--ink-2); background: transparent; border: none; cursor: pointer; padding: 6px 12px; border-radius: 6px; transition: background 0.2s, color 0.2s, box-shadow 0.2s; }
|
||||
.lang-switch button:hover, .theme-switch button:hover { color: var(--ink); }
|
||||
.lang-switch button.active, .theme-switch button.active { background: var(--card); color: var(--accent); box-shadow: var(--shadow-1); }
|
||||
.theme-switch button { display: inline-flex; align-items: center; justify-content: center; }
|
||||
.theme-switch svg { display: block; }
|
||||
|
||||
.nav { position: sticky; top: 0; z-index: 50; background: color-mix(in oklch, var(--bg) 86%, transparent); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-bottom: 1px solid var(--line); }
|
||||
.nav-inner { display: flex; align-items: center; gap: 20px; height: 64px; max-width: 1180px; margin: 0 auto; padding: 0 32px; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 17px; letter-spacing: -0.01em; }
|
||||
.brand .mark { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; background: linear-gradient(140deg, var(--accent), var(--violet)); color: #fff; font-weight: 700; font-size: 15px; }
|
||||
.brand .ctag { font-size: 12px; font-weight: 600; color: var(--accent); background: var(--accent-soft); padding: 3px 9px; border-radius: 999px; }
|
||||
.nav-links { display: flex; gap: 4px; margin-left: 12px; }
|
||||
.nav-links a { color: var(--ink-2); font-size: 14.5px; font-weight: 500; padding: 8px 14px; border-radius: 7px; transition: color 0.2s, background 0.2s; }
|
||||
.nav-links a:hover { color: var(--ink); background: var(--bg-soft); }
|
||||
.nav-links a.here { color: var(--accent); background: var(--accent-soft); }
|
||||
.nav-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
|
||||
.nav-menu-toggle {
|
||||
display: none; flex: none; align-items: center; justify-content: center;
|
||||
width: 38px; height: 38px; padding: 0; color: var(--ink);
|
||||
background: var(--bg-soft); border: 1px solid var(--line); border-radius: 9px; cursor: pointer;
|
||||
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||
}
|
||||
.nav-menu-toggle:hover { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }
|
||||
.nav-menu-toggle:focus-visible, .mobile-nav-close:focus-visible, .mobile-nav-backdrop:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.nav-menu-toggle svg, .mobile-nav-close svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
||||
body.mobile-nav-open { overflow: hidden; }
|
||||
.mobile-nav { position: fixed; inset: 0; z-index: 100; height: 100vh; min-height: 100dvh; opacity: 0; transition: opacity 0.22s ease; }
|
||||
.mobile-nav.is-open { opacity: 1; }
|
||||
.mobile-nav-backdrop { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; border: 0; background: oklch(0.12 0.01 260 / 0.42); cursor: pointer; }
|
||||
.mobile-nav-panel { position: absolute; top: 0; right: 0; display: flex; flex-direction: column; width: min(360px, calc(100% - 24px)); height: 100%; padding: 24px; color: var(--ink); background: var(--bg); border-left: 1px solid var(--line); box-shadow: -18px 0 40px oklch(0.12 0.01 260 / 0.16); transform: translateX(100%); transition: transform 0.28s var(--ease); overflow-y: auto; }
|
||||
.mobile-nav.is-open .mobile-nav-panel { transform: translateX(0); }
|
||||
.mobile-nav-head { display: flex; align-items: center; justify-content: space-between; min-height: 38px; font-size: 15px; font-weight: 600; }
|
||||
.mobile-nav-close { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: 0; color: var(--ink-2); background: var(--bg-soft); border: 1px solid var(--line); border-radius: 9px; cursor: pointer; }
|
||||
.mobile-nav-close:hover { color: var(--accent); border-color: var(--accent-line); }
|
||||
.mobile-nav-links { display: flex; flex-direction: column; gap: 4px; margin-top: 26px; }
|
||||
.mobile-nav-links a { display: flex; align-items: center; min-height: 44px; padding: 10px 12px; color: var(--ink-2); font-size: 16px; font-weight: 500; border-radius: 8px; }
|
||||
.mobile-nav-links a:hover, .mobile-nav-links a.here { color: var(--accent); background: var(--accent-soft); }
|
||||
.mobile-nav-actions { display: flex; flex-direction: column; gap: 10px; margin-top: auto; padding-top: 28px; }
|
||||
.mobile-nav-actions > a, .mobile-nav-actions > span { width: 100%; }
|
||||
.mobile-nav-actions > span .btn { width: 100%; }
|
||||
.mobile-nav-actions .btn { min-height: 44px; }
|
||||
.mobile-nav-github { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; color: var(--ink-2); font-size: 14px; font-weight: 500; border: 1px solid var(--line); border-radius: 9px; }
|
||||
.mobile-nav-github:hover { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }
|
||||
.mobile-nav-github svg { width: 18px; height: 18px; }
|
||||
@media (prefers-reduced-motion: reduce) { .mobile-nav, .mobile-nav-panel { transition: none; } }
|
||||
.nav-search { display: flex; align-items: center; gap: 8px; background: var(--bg-soft); border: 1px solid transparent; border-radius: 9px; padding: 8px 14px; width: 220px; transition: border-color 0.2s, background 0.2s, box-shadow 0.2s; }
|
||||
.nav-search:focus-within { background: var(--card); border-color: var(--accent-line); box-shadow: 0 0 0 4px var(--accent-soft); }
|
||||
.nav-search input { border: none; outline: none; background: none; font-family: var(--font-sans); font-size: 14px; color: var(--ink); width: 100%; }
|
||||
.nav-search .ico { color: var(--ink-3); }
|
||||
|
||||
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-family: var(--font-sans); font-size: 14.5px; font-weight: 600; white-space: nowrap; padding: 10px 20px; border-radius: 9px; border: none; cursor: pointer; transition: background 0.2s, box-shadow 0.25s, color 0.2s, border-color 0.2s, transform 0.15s var(--ease); }
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn-primary { background: var(--accent); color: #fff; }
|
||||
.btn-primary:hover { background: var(--accent-deep); box-shadow: var(--shadow-1); }
|
||||
.btn-ghost { background: transparent; color: var(--accent); border: 1px solid var(--line); }
|
||||
.btn-ghost:hover { background: var(--accent-soft); border-color: transparent; }
|
||||
.btn.sm { padding: 7px 14px; font-size: 13px; }
|
||||
.btn[disabled] { opacity: 0.55; pointer-events: none; }
|
||||
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
.av { flex: none; display: grid; place-items: center; border-radius: 50%; width: 40px; height: 40px; font-size: 14px; font-weight: 600; color: #fff; border: 2px solid var(--bg); box-shadow: var(--shadow-1); overflow: hidden; }
|
||||
.av.sm { width: 28px; height: 28px; font-size: 11px; border-width: 1.5px; }
|
||||
.av.lg { width: 52px; height: 52px; font-size: 18px; }
|
||||
|
||||
.badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; padding: 3px 9px; border-radius: 999px; }
|
||||
.badge.solved { color: var(--ok); background: var(--ok-soft); }
|
||||
.badge.pinned { color: var(--accent); background: var(--accent-soft); }
|
||||
.badge.role { color: var(--ink-3); background: var(--bg-soft); box-shadow: inset 0 0 0 1px var(--line); }
|
||||
.badge.role.staff, .badge.role.admin, .badge.role.moderator { color: var(--accent); background: var(--accent-soft); box-shadow: none; }
|
||||
.tag { font-size: 12px; font-weight: 500; color: var(--ink-2); background: var(--bg-soft); padding: 4px 11px; border-radius: 999px; transition: color 0.2s, background 0.2s; }
|
||||
a.tag:hover { color: var(--accent); background: var(--accent-soft); }
|
||||
.cat-tag { font-size: 11.5px; font-weight: 600; padding: 2px 9px; border-radius: 999px; color: var(--accent); background: var(--accent-soft); }
|
||||
|
||||
.head { padding: 40px 0 28px; border-bottom: 1px solid var(--line); background: linear-gradient(180deg, var(--accent-soft), var(--bg) 76%); }
|
||||
.head .row { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
|
||||
.head .eyebrow { font-size: 13px; font-weight: 600; color: var(--accent); }
|
||||
.head h1 { font-size: clamp(30px, 4vw, 42px); font-weight: 600; letter-spacing: -0.02em; margin-top: 8px; }
|
||||
.head p { margin-top: 10px; font-size: 16px; color: var(--ink-2); max-width: 46ch; }
|
||||
|
||||
.layout { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 32px; padding: 32px 0 80px; align-items: start; }
|
||||
.side { position: sticky; top: 88px; display: flex; flex-direction: column; gap: 18px; }
|
||||
.sec-title { display: flex; align-items: center; justify-content: space-between; margin: 4px 2px 14px; }
|
||||
.sec-title h2 { font-size: 15px; font-weight: 600; }
|
||||
.tabs { display: inline-flex; gap: 2px; padding: 3px; background: var(--bg-soft); border-radius: 9px; }
|
||||
.tabs button { font-family: var(--font-sans); font-size: 13.5px; font-weight: 500; color: var(--ink-2); background: none; border: none; cursor: pointer; padding: 7px 15px; border-radius: 6px; transition: background 0.2s, color 0.2s, box-shadow 0.2s; }
|
||||
.tabs button.on { background: var(--card); color: var(--accent); box-shadow: var(--shadow-1); }
|
||||
.tabs button:hover:not(.on) { color: var(--ink); }
|
||||
|
||||
.cats { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 34px; }
|
||||
.cat { display: flex; gap: 14px; align-items: flex-start; background: var(--bg-soft); border-radius: var(--radius-sm); padding: 18px; transition: box-shadow 0.3s, transform 0.3s var(--ease), background 0.3s; }
|
||||
.cat:hover { background: var(--card); box-shadow: var(--shadow-2); transform: translateY(-2px); }
|
||||
.cat .ico { width: 42px; height: 42px; border-radius: 12px; flex: none; display: grid; place-items: center; font-size: 20px; background: var(--accent-soft); }
|
||||
.cat h3 { font-size: 15.5px; font-weight: 600; }
|
||||
.cat p { font-size: 13px; color: var(--ink-2); margin-top: 3px; }
|
||||
.cat .meta { font-size: 12px; color: var(--ink-3); font-family: var(--font-mono); margin-top: 9px; }
|
||||
|
||||
.topics { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--card); }
|
||||
.topic { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; gap: 16px; padding: 16px 20px; border-bottom: 1px solid var(--line); transition: background 0.18s; }
|
||||
.topic:last-child { border-bottom: none; }
|
||||
.topic:hover { background: var(--bg-soft); }
|
||||
.topic .main { min-width: 0; }
|
||||
.topic .title { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.topic .title a { font-size: 15.5px; font-weight: 600; color: var(--ink); }
|
||||
.topic:hover .title a { color: var(--accent); }
|
||||
.topic .sub { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 12.5px; color: var(--ink-3); flex-wrap: wrap; }
|
||||
.topic .sub .who { color: var(--ink-2); font-weight: 500; }
|
||||
.topic .stat { text-align: center; min-width: 52px; }
|
||||
.topic .stat .n { font-size: 15px; font-weight: 600; font-family: var(--font-mono); }
|
||||
.topic .stat .l { font-size: 11px; color: var(--ink-3); }
|
||||
.topic .last { min-width: 96px; text-align: right; font-size: 12px; color: var(--ink-3); }
|
||||
|
||||
.card { background: var(--bg-soft); border-radius: var(--radius-sm); padding: 20px; }
|
||||
.card.cta { background: linear-gradient(160deg, var(--accent-soft), var(--bg) 82%); border: 1px solid var(--accent-line); }
|
||||
.card h4 { font-size: 14.5px; font-weight: 600; margin-bottom: 6px; }
|
||||
.card .lead { font-size: 13px; color: var(--ink-2); line-height: 1.55; margin-bottom: 14px; }
|
||||
.card .btn { width: 100%; }
|
||||
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.stat-box .n { font-size: 22px; font-weight: 700; font-family: var(--font-mono); letter-spacing: -0.02em; }
|
||||
.stat-box .n .dot { color: var(--ok); }
|
||||
.stat-box .l { font-size: 12px; color: var(--ink-3); }
|
||||
.tagcloud { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
|
||||
.empty { padding: 40px 20px; text-align: center; color: var(--ink-3); font-size: 14px; }
|
||||
.skeleton { padding: 16px 20px; border-bottom: 1px solid var(--line); }
|
||||
.skeleton .bar { height: 12px; border-radius: 6px; background: linear-gradient(90deg, var(--bg-soft), var(--line), var(--bg-soft)); background-size: 200% 100%; animation: sk 1.3s linear infinite; }
|
||||
.skeleton .bar.short { width: 40%; margin-top: 8px; }
|
||||
@keyframes sk { to { background-position: -200% 0; } }
|
||||
.msg { margin: 16px 0; font-size: 13.5px; border-radius: var(--radius-sm); padding: 11px 14px; }
|
||||
.msg.error { color: var(--err); background: color-mix(in oklch, var(--err) 9%, var(--bg)); }
|
||||
.msg.ok { color: var(--ok); background: var(--ok-soft); }
|
||||
|
||||
/* thread */
|
||||
.crumb { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-3); margin-bottom: 14px; }
|
||||
.crumb a:hover { color: var(--accent); }
|
||||
.crumb .sep { opacity: 0.5; }
|
||||
.thread-head h1 { font-size: clamp(24px, 3vw, 32px); font-weight: 600; letter-spacing: -0.02em; line-height: 1.2; }
|
||||
.thread-head .meta { display: flex; align-items: center; gap: 10px; margin-top: 14px; flex-wrap: wrap; font-size: 13px; color: var(--ink-3); }
|
||||
.thread-layout { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 32px; padding: 24px 0 80px; align-items: start; }
|
||||
.post { display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 16px; padding: 24px 0; border-top: 1px solid var(--line); }
|
||||
.post:first-child { border-top: none; }
|
||||
.post.op { background: linear-gradient(180deg, var(--accent-soft), transparent 40%); border-radius: var(--radius); padding: 24px; margin: 0 -24px 4px; }
|
||||
.post.answer { background: var(--ok-soft); border: 1px solid color-mix(in oklch, var(--ok) 28%, transparent); border-radius: var(--radius); padding: 22px 24px; margin: 8px -24px; }
|
||||
.post .who { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
|
||||
.post .who .name { font-size: 14.5px; font-weight: 600; }
|
||||
.post .who .when { font-size: 12px; color: var(--ink-3); }
|
||||
.post .answer-flag { display: flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 600; color: var(--ok); margin-bottom: 10px; }
|
||||
.post .body { font-size: 15px; color: var(--ink); line-height: 1.7; overflow-wrap: anywhere; }
|
||||
.post .body p { margin: 0 0 12px; }
|
||||
.post .body p:last-child { margin-bottom: 0; }
|
||||
.post .body code { font-family: var(--font-mono); font-size: 13px; background: var(--bg-soft); padding: 2px 7px; border-radius: 6px; }
|
||||
.post .body pre { background: oklch(0.25 0.006 260); color: oklch(0.9 0.004 260); border-radius: var(--radius-sm); padding: 16px 18px; overflow-x: auto; font-family: var(--font-mono); font-size: 13px; line-height: 1.7; margin: 4px 0 12px; }
|
||||
.post .actions { display: flex; align-items: center; gap: 6px; margin-top: 16px; }
|
||||
.react { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-sans); font-size: 13px; font-weight: 500; color: var(--ink-2); background: var(--bg-soft); border: 1px solid transparent; border-radius: 7px; padding: 6px 13px; cursor: pointer; transition: background 0.2s, border-color 0.2s, color 0.2s; }
|
||||
.react:hover { border-color: var(--accent-line); color: var(--accent); }
|
||||
.react.on { background: var(--accent-soft); color: var(--accent); }
|
||||
.link-act { font-size: 13px; font-weight: 500; color: var(--ink-3); padding: 6px 10px; border-radius: 7px; cursor: pointer; background: none; border: none; font-family: var(--font-sans); }
|
||||
.link-act:hover { color: var(--accent); background: var(--accent-soft); }
|
||||
|
||||
.composer { border: 1px solid var(--line); border-radius: var(--radius); padding: 8px; margin-top: 24px; background: var(--card); transition: border-color 0.2s, box-shadow 0.2s; }
|
||||
.composer:focus-within { border-color: var(--accent-line); box-shadow: 0 0 0 4px var(--accent-soft); }
|
||||
.composer input.title { width: 100%; border: none; outline: none; padding: 12px 14px 4px; font-family: var(--font-sans); font-size: 18px; font-weight: 600; color: var(--ink); background: none; }
|
||||
.composer textarea { width: 100%; border: none; outline: none; resize: vertical; min-height: 96px; padding: 12px 14px; font-family: var(--font-sans); font-size: 15px; color: var(--ink); background: none; line-height: 1.6; }
|
||||
.composer .foot { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px 6px; gap: 12px; flex-wrap: wrap; }
|
||||
.composer .foot .hint { font-size: 12px; color: var(--ink-3); }
|
||||
.composer select { font-family: var(--font-sans); font-size: 13px; color: var(--ink-2); background: var(--bg-soft); border: none; border-radius: 7px; padding: 8px 12px; cursor: pointer; }
|
||||
.gate { text-align: center; padding: 22px; }
|
||||
.gate p { font-size: 14px; color: var(--ink-2); margin-bottom: 14px; }
|
||||
|
||||
.side-card { background: var(--bg-soft); border-radius: var(--radius-sm); padding: 18px 20px; }
|
||||
.side-card h4 { font-size: 13px; font-weight: 600; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 12px; }
|
||||
.parti { display: flex; }
|
||||
.parti .av { margin-left: -8px; }
|
||||
.parti .av:first-child { margin-left: 0; }
|
||||
|
||||
.foot { border-top: 1px solid var(--line); }
|
||||
.foot-inner { max-width: 1180px; margin: 0 auto; padding: 28px 32px; display: flex; align-items: center; gap: 20px; flex-wrap: wrap; font-size: 13.5px; color: var(--ink-3); }
|
||||
.foot-inner .links { display: flex; gap: 4px; margin-left: auto; flex-wrap: wrap; }
|
||||
.foot-inner .links a { padding: 6px 12px; border-radius: 7px; color: var(--ink-2); font-weight: 500; transition: background 0.2s, color 0.2s; }
|
||||
.foot-inner .links a:hover { background: var(--bg-soft); color: var(--ink); }
|
||||
|
||||
@media (max-width: 940px) {
|
||||
.layout, .thread-layout { grid-template-columns: 1fr; }
|
||||
.side { position: static; }
|
||||
.cats { grid-template-columns: 1fr; }
|
||||
.nav-links, .nav-search { display: none; }
|
||||
.nav-menu-toggle { display: inline-flex; }
|
||||
.topic { grid-template-columns: auto minmax(0, 1fr); }
|
||||
.topic .stat, .topic .last { display: none; }
|
||||
.wrap { padding: 0 20px; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
/* drop the Community tag and tighten the shared controls */
|
||||
.nav-inner { padding: 0 16px; gap: 10px; }
|
||||
.brand .ctag { display: none; }
|
||||
#nav-account { display: none; }
|
||||
.theme-switch button { padding: 6px 9px; }
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
/* Include the async account control in the narrow-header budget. */
|
||||
.nav-inner { padding: 0 12px; gap: 8px; }
|
||||
.nav .brand { gap: 0; }
|
||||
.nav .brand-name { display: none; }
|
||||
.nav-right { gap: 8px; }
|
||||
.lang-switch button { padding: 6px 8px; }
|
||||
.theme-switch button { padding: 6px 7px; }
|
||||
#nav-account .btn { padding: 7px 10px; }
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Shared page shell + UI kit. Visual language mirrors site/src/styles/global.css
|
||||
// — white, blue accent, large radii; crash stacks use the site's dark terminal.
|
||||
// Shared page shell + UI kit. Visual language mirrors the reasonix.io website:
|
||||
// white, blue accent, large radii; crash stacks use its dark terminal style.
|
||||
|
||||
export function esc(s: unknown): string {
|
||||
return String(s).replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
||||
|
||||
Reference in new issue
Block a user