feat: launch Roncarve Harness product page

This commit is contained in:
2026-08-22 21:49:25 +08:00
parent 28574f367a
commit 9cfbb95b1e
18 changed files with 3561 additions and 13 deletions
+10 -1
View File
@@ -1,8 +1,17 @@
.git
.gitignore
.idea
qa-evidence
design-references
.docker-config
.npm-cache
.npm-cache-*
node_modules
dist
design-qa-artifacts
deploy
Dockerfile
docker-compose.yml
.dockerignore
*.md
docker-compose*.yml
Caddyfile.server
+7
View File
@@ -43,3 +43,10 @@ buildNumber.properties
# JDT-specific (Eclipse Java Development Tools)
.classpath
node_modules/
dist/
.npm-cache/
.npm-cache-*/
.docker-config/
design-qa-artifacts/
design-qa.md
+11 -2
View File
@@ -1,6 +1,15 @@
FROM nginx:1.27-alpine
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY . .
RUN npm run build
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY . /usr/share/nginx/html
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
+1
View File
@@ -0,0 +1 @@
94f23ded66ca5b5c5003812faeacf00d
+6
View File
@@ -5,3 +5,9 @@ services:
restart: unless-stopped
ports:
- "127.0.0.1:8081:80"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/products/roncarve-harness/"]
interval: 10s
timeout: 3s
retries: 5
start_period: 5s
+99 -9
View File
@@ -233,6 +233,52 @@
.nav-link:hover, .nav-link.active { color: var(--white); }
.nav-link.active::after { opacity: 1; transform: translateX(-50%) scale(1); }
.product-nav { position: relative; }
.product-trigger {
display: block;
border: 0;
background: transparent;
font-family: inherit;
cursor: pointer;
}
.product-nav.open .product-trigger { color: var(--white); }
.product-nav.open .product-trigger::after { opacity: 1; transform: translateX(-50%) scale(1); }
.product-popover {
position: absolute;
top: calc(100% + 17px);
left: 50%;
z-index: 1100;
width: 430px;
padding: 8px;
border: 1px solid var(--line-strong);
border-radius: 16px;
background: color-mix(in srgb, var(--ink-900) 96%, transparent);
box-shadow: 0 24px 70px rgba(0, 0, 0, .32), inset 0 1px 0 rgba(255, 255, 255, .035);
backdrop-filter: blur(20px) saturate(135%);
-webkit-backdrop-filter: blur(20px) saturate(135%);
opacity: 0;
pointer-events: none;
transform: translate(-50%, -8px) scale(.98);
transform-origin: top center;
transition: opacity .2s ease, transform .2s var(--ease);
}
.product-nav.open .product-popover { opacity: 1; pointer-events: auto; transform: translate(-50%, 0) scale(1); }
.product-card {
display: block;
padding: 20px;
border: 1px solid var(--line);
border-radius: 12px;
background: linear-gradient(140deg, color-mix(in srgb, var(--cyan) 8%, transparent), rgba(255, 255, 255, .018));
transition: border-color .22s ease, background .22s ease, transform .22s var(--ease);
}
.product-card:hover { transform: translateY(-2px); border-color: var(--line-strong); background: color-mix(in srgb, var(--cyan) 10%, var(--ink-900)); }
.product-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 18px; }
.product-card-meta span { color: var(--cyan); font: 700 9px/1 var(--mono); letter-spacing: .13em; }
.product-card-meta strong { padding: 6px 8px; border: 1px solid var(--line); border-radius: 999px; color: var(--text-soft); background: rgba(255, 255, 255, .025); font: 650 9px/1 var(--mono); }
.product-card h3 { margin: 20px 0 4px; color: var(--white); font-size: 22px; letter-spacing: -.03em; }
.product-card small { color: var(--text-faint); font: 550 10px/1 var(--mono); letter-spacing: .1em; }
.product-card p { margin: 14px 0 0; color: var(--text-soft); font-size: 13px; line-height: 1.65; }
.nav-actions { display: flex; align-items: center; gap: 10px; }
.preference-menu { position: relative; }
.preference-trigger {
@@ -784,13 +830,6 @@
.hero-spatial { width: min(100%, 760px); min-height: 420px; margin-left: auto; }
}
/* Let desktop layouts use the available canvas on ultrawide screens. */
@media (min-width: 1440px) {
:root { --max-width: 88vw; }
.hero-layout { gap: clamp(64px, 6vw, 132px); }
.section-header { gap: clamp(48px, 6vw, 120px); }
}
@media (max-width: 860px) {
:root[data-theme="light"] .nav-links { background: rgba(250, 252, 255, .98); }
}
@@ -859,6 +898,24 @@
.nav-links.open { opacity: 1; pointer-events: auto; transform: translateY(0); }
.nav-link { padding: 14px 6px; border-bottom: 1px solid var(--line); font-size: 15px; }
.nav-link::after { display: none; }
.product-nav { width: 100%; }
.product-trigger { width: 100%; text-align: left; }
.product-popover {
position: relative;
top: auto;
left: auto;
width: 100%;
max-height: 0;
margin: 0;
padding: 0 8px;
overflow: hidden;
border-color: transparent;
opacity: 1;
transform: none;
transition: max-height .25s var(--ease), margin .25s ease, padding .25s ease, border-color .25s ease;
}
.product-nav.open .product-popover { max-height: 250px; margin: 8px 0 12px; padding: 8px; border-color: var(--line); transform: none; }
.product-card { padding: 17px; }
.nav-actions { padding-top: 14px; justify-content: space-between; }
.nav-cta { min-height: 42px; }
.hero { padding-top: 126px; }
@@ -954,6 +1011,17 @@
</a>
<div class="nav-links" id="navLinks">
<div class="product-nav" id="productNav">
<button class="nav-link product-trigger" id="productTrigger" type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="productPopover" data-i18n="nav_products">产品</button>
<div class="product-popover" id="productPopover" role="menu" aria-label="产品列表" data-i18n-aria="product_list_label">
<a class="product-card" href="/products/roncarve-harness/" role="menuitem">
<span class="product-card-meta"><span>AGENT INFRA / HARNESS</span><strong data-i18n="product_status">即将开放</strong></span>
<h3 data-i18n="product_harness_name">荣刻驭境</h3>
<small>RONCARVE HARNESS</small>
<p data-i18n="product_harness_desc">本地客户端连接本地项目,平台共享工作空间承载团队协同,并内置精准插件检索与高效热插拔。</p>
</a>
</div>
</div>
<a class="nav-link" href="#about" data-i18n="nav_about">能力链路</a>
<a class="nav-link" href="#services" data-i18n="nav_services">解决方案</a>
<a class="nav-link" href="#advantages" data-i18n="nav_advantages">核心优势</a>
@@ -1187,7 +1255,7 @@
zh: {
title: '海南荣刻科技有限公司 - 数据筑基 · AI 落地', skip: '跳到主要内容', brand: '荣刻科技', nav_label: '主导航', home_label: '荣刻科技首页', lang_label: '语言切换', menu_open: '打开导航菜单', menu_close: '关闭导航菜单',
theme_label: '外观模式选择', theme_system: '跟随系统', theme_light: '浅色外观', theme_dark: '深色外观', theme_system_short: 'Auto', theme_light_short: 'Light', theme_dark_short: 'Dark', language_zh: '简体中文', language_en: 'English',
nav_about: '能力链路', nav_services: '解决方案', nav_advantages: '核心优势', nav_offices: '交付网络', nav_contact: '预约咨询',
nav_products: '产品', product_list_label: '产品列表', product_status: '即将开放', product_harness_name: '荣刻驭境', product_harness_desc: '本地客户端连接本地项目,平台共享工作空间承载团队协同,并内置精准插件检索与高效热插拔。', nav_about: '能力链路', nav_services: '解决方案', nav_advantages: '核心优势', nav_offices: '交付网络', nav_contact: '预约咨询',
hero_eyebrow: '海南荣刻科技有限公司 · AI 与数据基础设施', hero_line1: '让企业数据', hero_line2: '成为 AI 生产力',
hero_desc: '从数据治理、模型优化到私有化部署,荣刻科技为企业构建可落地、可持续、可掌控的 AI 能力体系。',
hero_primary: '预约 AI 场景诊断', hero_secondary: '查看解决方案', proof_data: '7 年数据技术积累', proof_private: '支持私有化部署', proof_delivery: '成都 / 海口双城交付',
@@ -1221,7 +1289,7 @@
en: {
title: 'Hainan Roncarve Technology - Data Foundation · AI Delivery', skip: 'Skip to main content', brand: 'Roncarve Tech', nav_label: 'Main navigation', home_label: 'Roncarve Tech home', lang_label: 'Language selector', menu_open: 'Open navigation menu', menu_close: 'Close navigation menu',
theme_label: 'Appearance selector', theme_system: 'Follow System', theme_light: 'Light appearance', theme_dark: 'Dark appearance', theme_system_short: 'Auto', theme_light_short: 'Light', theme_dark_short: 'Dark', language_zh: '简体中文', language_en: 'English',
nav_about: 'Capabilities', nav_services: 'Solutions', nav_advantages: 'Advantages', nav_offices: 'Delivery', nav_contact: 'Book a Call',
nav_products: 'Products', product_list_label: 'Product list', product_status: 'Coming Soon', product_harness_name: 'Roncarve Harness', product_harness_desc: 'A local client for browser-based local projects and a shared workspace for team collaboration, with precise plugin discovery and hot swapping.', nav_about: 'Capabilities', nav_services: 'Solutions', nav_advantages: 'Advantages', nav_offices: 'Delivery', nav_contact: 'Book a Call',
hero_eyebrow: 'Hainan Roncarve Technology Co., Ltd. · AI + DATA INFRASTRUCTURE', hero_line1: 'Turn enterprise data', hero_line2: 'into AI productivity',
hero_desc: 'From data governance and model optimization to private deployment, Roncarve builds practical, sustainable and controllable enterprise AI systems.',
hero_primary: 'Book an AI Discovery Call', hero_secondary: 'Explore Solutions', proof_data: '7 years in data technology', proof_private: 'Private deployment ready', proof_delivery: 'Chengdu / Haikou delivery',
@@ -1257,6 +1325,8 @@
const navbar = document.getElementById('navbar');
const menuBtn = document.getElementById('menuBtn');
const navLinks = document.getElementById('navLinks');
const productNav = document.getElementById('productNav');
const productTrigger = document.getElementById('productTrigger');
const langSwitch = document.getElementById('langSwitch');
const themeSwitch = document.getElementById('themeSwitch');
const themeQuery = window.matchMedia('(prefers-color-scheme: dark)');
@@ -1336,6 +1406,12 @@
menuBtn.setAttribute('aria-expanded', 'false');
menuBtn.setAttribute('aria-label', i18n[currentLanguage].menu_open);
document.body.classList.remove('menu-open');
closeProductMenu();
}
function closeProductMenu() {
productNav.classList.remove('open');
productTrigger.setAttribute('aria-expanded', 'false');
}
function closePreferenceMenus(except) {
@@ -1351,12 +1427,24 @@
const trigger = menu.querySelector('.preference-trigger');
trigger.addEventListener('click', function () {
const open = !menu.classList.contains('open');
closeProductMenu();
closePreferenceMenus(menu);
menu.classList.toggle('open', open);
trigger.setAttribute('aria-expanded', String(open));
});
});
productTrigger.addEventListener('click', function () {
const open = !productNav.classList.contains('open');
closePreferenceMenus();
productNav.classList.toggle('open', open);
productTrigger.setAttribute('aria-expanded', String(open));
if (open && window.innerWidth > 860) {
const firstProduct = productNav.querySelector('[role="menuitem"]');
if (firstProduct) requestAnimationFrame(function () { firstProduct.focus(); });
}
});
langSwitch.addEventListener('click', function (event) {
const button = event.target.closest('[data-lang]');
if (!button) return;
@@ -1393,10 +1481,12 @@
if (event.key === 'Escape') {
closeMenu();
closePreferenceMenus();
closeProductMenu();
}
});
document.addEventListener('click', function (event) {
if (!event.target.closest('.preference-menu')) closePreferenceMenus();
if (!event.target.closest('.product-nav')) closeProductMenu();
});
window.addEventListener('scroll', function () { navbar.classList.toggle('scrolled', window.scrollY > 24); }, { passive: true });
+9
View File
@@ -5,10 +5,19 @@ server {
root /usr/share/nginx/html;
index index.html;
location = /products/roncarve-harness {
return 301 /products/roncarve-harness/;
}
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.html$ {
add_header Cache-Control "no-cache";
try_files $uri =404;
}
location ~* \.(?:css|js|png|jpg|jpeg|gif|svg|webp|ico|woff2?)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800, immutable";
+1115
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "roncarve-official-web",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && node scripts/copy-static.mjs",
"preview": "vite preview --host 0.0.0.0"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
"@react-three/fiber": "^9.7.0",
"gsap": "^3.15.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"three": "^0.185.1"
},
"devDependencies": {
"vite": "^8.2.2"
}
}
+40
View File
@@ -0,0 +1,40 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#05070a">
<meta name="description" content="荣刻驭境是一套面向大语言模型与 Agent 的可组合 Harness,个人版通过本地客户端在浏览器中操作本地项目,平台版提供团队共享工作空间。">
<meta name="keywords" content="荣刻驭境,Roncarve Harness,Agent Harness,插件引擎,插件检索,热插拔,工作空间">
<link rel="canonical" href="https://www.roncarve.com/products/roncarve-harness/">
<link rel="icon" type="image/png" href="/assets/brand-logo-final.png">
<meta property="og:type" content="website">
<meta property="og:locale" content="zh_CN">
<meta property="og:title" content="荣刻驭境 Roncarve Harness|插件驱动的 Agent 运行环境">
<meta property="og:description" content="精准检索、高效热插拔,从浏览器操作本地项目到团队共享工作空间。">
<meta property="og:url" content="https://www.roncarve.com/products/roncarve-harness/">
<meta property="og:image" content="https://www.roncarve.com/assets/brand-logo-final.png">
<title>荣刻驭境 Roncarve Harness|插件驱动的 Agent 运行环境 - 荣刻科技</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "荣刻驭境",
"alternateName": "Roncarve Harness",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Web, Local Client, Private Deployment",
"description": "面向大语言模型与 Agent 的可组合 Harness,个人版通过本地客户端在浏览器中操作本地项目,平台版提供团队共享工作空间。",
"url": "https://www.roncarve.com/products/roncarve-harness/",
"publisher": {
"@type": "Organization",
"name": "海南荣刻科技有限公司",
"url": "https://www.roncarve.com/"
}
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/harness/main.jsx"></script>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
import { cp, copyFile, mkdir } from 'node:fs/promises';
import { resolve } from 'node:path';
const root = process.cwd();
const output = resolve(root, 'dist');
await mkdir(resolve(output, 'assets'), { recursive: true });
await cp(resolve(root, 'assets'), resolve(output, 'assets'), {
recursive: true,
force: true
});
await Promise.all([
copyFile(resolve(root, 'robots.txt'), resolve(output, 'robots.txt')),
copyFile(resolve(root, 'sitemap.xml'), resolve(output, 'sitemap.xml')),
copyFile(
resolve(root, 'baidu_verify_codeva-Jnq0QS7SFC.html'),
resolve(output, 'baidu_verify_codeva-Jnq0QS7SFC.html')
)
]);
+7 -1
View File
@@ -2,8 +2,14 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.roncarve.com/</loc>
<lastmod>2026-08-16</lastmod>
<lastmod>2026-08-22</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://www.roncarve.com/products/roncarve-harness/</loc>
<lastmod>2026-08-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
</urlset>
+471
View File
@@ -0,0 +1,471 @@
import {
ArrowRight,
Brain,
Check,
Code,
Database,
Globe,
Lightning,
List,
MagnifyingGlass,
Plug,
ShieldCheck,
Stack,
UsersThree,
X
} from '@phosphor-icons/react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { ParticleStage } from './ParticleStage';
gsap.registerPlugin(ScrollTrigger);
const copy = {
zh: {
brand: '荣刻科技',
navProduct: '荣刻驭境',
navEngine: '插件引擎',
navEditions: '版本能力',
navWorkflow: '使用路径',
email: '邮件咨询',
language: 'EN',
menu: '打开导航',
close: '关闭导航',
eyebrow: 'RONCARVE HARNESS / 荣刻驭境',
status: '即将开放',
title: '让 Agent 驾驭\n每一种能力',
summary: '面向大语言模型的可组合 Harness,以定制插件引擎实现精准检索与高效热插拔;个人版在浏览器中操作本地项目,平台版以共享工作空间连接团队协作。',
primary: '邮件咨询',
secondary: '查看版本能力',
proof: ['精准插件检索', '高效热插拔', '个人 / 平台双版本'],
formula: 'PLUGIN IS ALL YOU NEED',
valueTitle: '插件构成能力,\nHarness 组织运行',
valueSummary: '把插件发现、装配、替换和协作放进同一套运行环境,让 Agent 能力持续扩展,而不必反复重建。',
valueCards: [
['海量插件精准检索', '按任务语义、能力类型、兼容性与工作空间策略定位合适插件。'],
['更高效率热插拔', '在明确依赖与生命周期的前提下快速加载、卸载、替换和组合。'],
['本地项目到共享协作', '个人版通过本地客户端操作本地项目;平台版让所有成员进入同一个共享工作空间协同。']
],
scroll: '滚动展开空间',
engineKicker: 'CUSTOM PLUGIN ENGINE',
engineTitle: '为插件规模化而优化的引擎',
engineSummary: '从定制插件数据库中发现能力,在运行时完成装配;个人版作用于本地项目,平台版把有效组合沉淀为团队共享能力。',
demoTitle: '插件检索演示',
demoStatus: '交互演示',
searchPlaceholder: '搜索能力、插件或场景',
categories: ['全部', '知识', '数据', '开发', '网络'],
candidates: '个候选插件',
compose: '装配',
composed: '已装配',
runtime: '当前运行组合',
hotSwap: '点击任一插件可模拟热插拔',
engineSteps: [
['01', '发现', '连接定制插件数据库,建立能力、版本、依赖与兼容性索引。'],
['02', '匹配', '结合任务语义、运行环境与工作空间策略完成候选筛选。'],
['03', '装配', '按需加载、卸载和替换插件,降低重建 Agent 的成本。'],
['04', '观测', '记录插件状态、调用与异常,让有效组合可以复用。']
],
editionsKicker: 'PERSONAL / PLATFORM',
editionsTitle: '从本地项目客户端,\n到团队共享工作空间',
editionsSummary: '同一套插件引擎,两种工作形态:个人版连接本地项目,平台版让所有成员在共享工作空间中协同。产品当前处于即将开放阶段。',
personal: '个人版',
personalEn: 'PERSONAL',
personalDesc: '作为本地客户端运行,并通过浏览器界面操作当前设备上的本地项目。',
personalFeatures: ['本地客户端部署', '浏览器操作本地项目', '本地文件与运行环境保持在当前设备', '完整插件检索与热插拔', '本地项目预设与运行记录'],
platform: '平台版',
platformEn: 'PLATFORM',
platformDesc: '面向团队的共享工作空间,让所有成员围绕同一项目、插件和运行上下文协同。',
platformFeatures: ['团队共享工作空间', '成员与角色协同', '共享项目、插件组合与配置', '组织级插件目录与版本管理', '共享活动记录与运行上下文'],
askPersonal: '邮件咨询个人版',
askPlatform: '邮件咨询平台版',
workflowKicker: 'FROM TASK TO TEAM CAPABILITY',
workflowTitle: '从一个任务,生长为团队能力',
workflowSummary: '一次有效的插件组合,不应止步于一次会话。荣刻驭境让它成为可以保存、复用和协作的工作资产。',
workflowSteps: [
['01', '描述目标', '从具体任务、数据边界和运行环境开始。'],
['02', '检索插件', '从定制数据库中找到符合语义与策略的能力。'],
['03', '运行装配', '在运行中加载、替换并验证插件组合。'],
['04', '共享协同', '个人版保存到本地项目;平台版同步到团队共享工作空间。']
],
ctaKicker: 'RONCARVE HARNESS / COMING SOON',
ctaTitle: '让下一套 Agent 能力,\n从可组合开始',
ctaSummary: '告诉我们你的本地项目形态、团队协作方式与部署环境,一起定义最合适的个人版或平台版方案。',
ctaButton: '发送咨询邮件',
originLine: '基于开源 DeepSeek Harness 二次开发的独立产品。',
copyright: '海南荣刻科技有限公司',
footerSignal: '数据驱动 · AI 赋能 · 安全交付',
policeAlt: '公安备案图标'
},
en: {
brand: 'Roncarve Tech',
navProduct: 'Harness',
navEngine: 'Plugin Engine',
navEditions: 'Editions',
navWorkflow: 'Workflow',
email: 'Email Us',
language: '中文',
menu: 'Open navigation',
close: 'Close navigation',
eyebrow: 'RONCARVE HARNESS',
status: 'COMING SOON',
title: 'Harness every capability\nfor your Agent',
summary: 'A composable Harness for LLMs with precise plugin discovery and efficient hot swapping. Personal is a local client for browser-based work on local projects; Platform gives the whole team one shared workspace.',
primary: 'Email Us',
secondary: 'Compare Editions',
proof: ['Precise plugin discovery', 'Efficient hot swapping', 'Personal / Platform'],
formula: 'PLUGIN IS ALL YOU NEED',
valueTitle: 'Plugins provide capability.\nHarness organizes the work.',
valueSummary: 'Bring discovery, composition, replacement, and collaboration into one runtime so Agents can keep evolving without being rebuilt.',
valueCards: [
['Precise discovery at scale', 'Find the right plugin by task, capability, compatibility, and workspace policy.'],
['Efficient hot swapping', 'Load, unload, replace, and compose plugins with explicit lifecycle and dependency rules.'],
['Local projects to shared collaboration', 'Personal works with local projects through a local client; Platform brings every member into one shared workspace.']
],
scroll: 'Scroll to expand',
engineKicker: 'CUSTOM PLUGIN ENGINE',
engineTitle: 'An engine optimized for plugin scale',
engineSummary: 'Discover and compose capabilities at runtime. Personal applies them to local projects, while Platform turns effective combinations into shared team capabilities.',
demoTitle: 'Plugin discovery demo',
demoStatus: 'Interactive demo',
searchPlaceholder: 'Search capabilities, plugins, or use cases',
categories: ['All', 'Knowledge', 'Data', 'Developer', 'Web'],
candidates: 'candidate plugins',
compose: 'Compose',
composed: 'Composed',
runtime: 'Active runtime',
hotSwap: 'Select a plugin to simulate hot swapping',
engineSteps: [
['01', 'Discover', 'Index capabilities, versions, dependencies, and compatibility.'],
['02', 'Match', 'Filter candidates by intent, environment, and workspace policy.'],
['03', 'Compose', 'Load, unload, or replace plugins without rebuilding the Agent.'],
['04', 'Observe', 'Record plugin state, calls, and errors for repeatable operation.']
],
editionsKicker: 'PERSONAL / PLATFORM',
editionsTitle: 'From a local project client\nto a shared team workspace',
editionsSummary: 'One plugin engine, two operating models: Personal connects to local projects; Platform brings every member into one shared workspace. Roncarve Harness is coming soon.',
personal: 'Personal',
personalEn: 'PERSONAL',
personalDesc: 'Runs as a local client with a browser interface for working on projects stored on the current device.',
personalFeatures: ['Local client deployment', 'Browser-based control of local projects', 'Local files and runtime stay on the current device', 'Full discovery and hot swapping', 'Local project presets and run history'],
platform: 'Platform',
platformEn: 'PLATFORM',
platformDesc: 'A shared workspace where every team member collaborates around the same projects, plugins, and runtime context.',
platformFeatures: ['Shared team workspace', 'Member and role collaboration', 'Shared projects, plugin sets, and configs', 'Organization plugin catalog and version management', 'Shared activity and runtime context'],
askPersonal: 'Ask about Personal',
askPlatform: 'Ask about Platform',
workflowKicker: 'FROM TASK TO TEAM CAPABILITY',
workflowTitle: 'Grow one task into\na team capability',
workflowSummary: 'A successful plugin composition should outlive a single session. Preserve it as a reusable and collaborative working asset.',
workflowSteps: [
['01', 'Describe the goal', 'Start with a real task, data boundary, and runtime.'],
['02', 'Discover plugins', 'Find capabilities that fit intent and policy.'],
['03', 'Compose at runtime', 'Load, replace, and validate a working plugin set.'],
['04', 'Share and collaborate', 'Save to a local project in Personal, or sync into the shared team workspace in Platform.']
],
ctaKicker: 'RONCARVE HARNESS / COMING SOON',
ctaTitle: 'Start your next Agent\nwith composability',
ctaSummary: 'Tell us about your local project model, collaboration workflow, and deployment environment. We will help shape the right Personal or Platform setup.',
ctaButton: 'Send an email',
originLine: 'An independently developed product based on the open-source DeepSeek Harness.',
copyright: 'Hainan Roncarve Technology Co., Ltd',
footerSignal: 'DATA-DRIVEN · AI-POWERED · SECURE DELIVERY',
policeAlt: 'Public security filing icon'
}
};
const pluginCatalog = [
{ id: 'knowledge', category: 1, icon: Brain, zh: ['知识检索增强', '连接知识库与语义索引,为 Agent 提供可追溯的上下文。', '知识'], en: ['Knowledge Retrieval', 'Connect knowledge bases and semantic indexes with traceable context.', 'Knowledge'] },
{ id: 'database', category: 2, icon: Database, zh: ['数据桥接器', '以受控连接访问结构化数据和业务数据源。', '数据'], en: ['Data Bridge', 'Access structured and business data through governed connections.', 'Data'] },
{ id: 'code', category: 3, icon: Code, zh: ['代码执行环境', '在隔离环境中组合代码、命令与文件操作。', '开发'], en: ['Code Runtime', 'Compose code, commands, and file operations in an isolated runtime.', 'Developer'] },
{ id: 'web', category: 4, icon: Globe, zh: ['网页研究工具', '检索、读取并整理可验证的公开网页信息。', '网络'], en: ['Web Research', 'Search, read, and organize verifiable public web information.', 'Web'] },
{ id: 'workflow', category: 3, icon: Stack, zh: ['工作流编排', '把多轮工具调用组合为稳定、可复用的执行路径。', '开发'], en: ['Workflow Composer', 'Turn multi-step tool calls into reusable execution paths.', 'Developer'] },
{ id: 'policy', category: 2, icon: ShieldCheck, zh: ['策略边界', '按工作空间控制插件可见范围与运行权限。', '数据'], en: ['Policy Boundary', 'Control plugin visibility and runtime permissions by workspace.', 'Data'] }
];
function emailHref(language, edition = '') {
const subject = language === 'zh'
? `[荣刻驭境] ${edition ? `${edition}咨询` : '产品咨询'}`
: `[Roncarve Harness] ${edition ? `${edition} inquiry` : 'Product inquiry'}`;
const body = language === 'zh'
? '您好,我想了解荣刻驭境。\n\n关注版本:\n使用场景:\n部署环境:\n团队规模:\n联系人:'
: 'Hello, I would like to learn more about Roncarve Harness.\n\nEdition:\nUse case:\nDeployment:\nTeam size:\nContact:';
return `mailto:contact@roncarve.com?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
}
function ProductNav({ language, setLanguage }) {
const t = copy[language];
const [open, setOpen] = useState(false);
const close = () => setOpen(false);
return (
<nav className="harness-nav" aria-label={language === 'zh' ? '产品导航' : 'Product navigation'}>
<div className="harness-nav-inner">
<a className="harness-brand" href="/" aria-label={language === 'zh' ? '返回荣刻科技首页' : 'Back to Roncarve home'}>
<img src="/assets/brand-logo-final.png" alt="" />
<span><strong>{t.brand}</strong><small>RONCARVE TECHNOLOGY</small></span>
</a>
<button className="harness-menu-button" type="button" onClick={() => setOpen(!open)} aria-expanded={open} aria-controls="harnessNavLinks" aria-label={open ? t.close : t.menu}>
{open ? <X size={22} /> : <List size={22} />}
</button>
<div className={`harness-nav-links ${open ? 'is-open' : ''}`} id="harnessNavLinks">
<a href="#top" onClick={close}>{t.navProduct}</a>
<a href="#engine" onClick={close}>{t.navEngine}</a>
<a href="#editions" onClick={close}>{t.navEditions}</a>
<a href="#workflow" onClick={close}>{t.navWorkflow}</a>
<button className="language-button" type="button" onClick={() => setLanguage(language === 'zh' ? 'en' : 'zh')}>{t.language}</button>
<a className="nav-email-button" href={emailHref(language)} onClick={close}>{t.email}<ArrowRight size={16} weight="bold" /></a>
</div>
</div>
</nav>
);
}
function PluginDemo({ language }) {
const t = copy[language];
const [query, setQuery] = useState('');
const [category, setCategory] = useState(0);
const [activePlugin, setActivePlugin] = useState('knowledge');
const filtered = pluginCatalog.filter((plugin) => {
const translated = plugin[language];
const matchesCategory = category === 0 || plugin.category === category;
const haystack = translated.join(' ').toLowerCase();
return matchesCategory && haystack.includes(query.trim().toLowerCase());
});
const active = pluginCatalog.find((plugin) => plugin.id === activePlugin);
return (
<div className="plugin-demo">
<div className="demo-toolbar">
<div><span className="status-dot" />{t.demoStatus}</div>
<span>RONCARVE PLUGIN INDEX</span>
</div>
<div className="demo-search-row">
<label className="demo-search">
<MagnifyingGlass size={19} aria-hidden="true" />
<span className="sr-only">{t.searchPlaceholder}</span>
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder={t.searchPlaceholder} />
</label>
<div className="category-tabs" aria-label={language === 'zh' ? '插件分类' : 'Plugin categories'}>
{t.categories.map((label, index) => (
<button key={label} type="button" className={category === index ? 'is-active' : ''} onClick={() => setCategory(index)}>{label}</button>
))}
</div>
</div>
<div className="demo-result-meta"><strong>{filtered.length}</strong> {t.candidates}</div>
<div className="plugin-result-list">
{filtered.map((plugin) => {
const [name, description, tag] = plugin[language];
const Icon = plugin.icon;
const isActive = activePlugin === plugin.id;
return (
<article className={`plugin-result ${isActive ? 'is-active' : ''}`} key={plugin.id}>
<div className="plugin-icon"><Icon size={23} weight="duotone" /></div>
<div className="plugin-copy"><span>{tag}</span><h3>{name}</h3><p>{description}</p></div>
<button type="button" onClick={() => setActivePlugin(plugin.id)} aria-pressed={isActive}>{isActive ? t.composed : t.compose}</button>
</article>
);
})}
{filtered.length === 0 && <div className="empty-result">{language === 'zh' ? '没有匹配结果,请尝试其他关键词。' : 'No matches. Try another keyword.'}</div>}
</div>
<div className="runtime-strip" aria-live="polite">
<div><Lightning size={18} weight="fill" /><span><small>{t.runtime}</small><strong>{active?.[language][0]}</strong></span></div>
<p>{t.hotSwap}</p>
</div>
</div>
);
}
function EditionCard({ title, code, description, features, href, featured, cta }) {
return (
<article className={`edition-card ${featured ? 'is-featured' : ''}`}>
<div className="edition-head"><span>{code}</span>{featured && <UsersThree size={26} weight="duotone" />}</div>
<h3>{title}</h3>
<p>{description}</p>
<ul>{features.map((feature) => <li key={feature}><Check size={17} weight="bold" />{feature}</li>)}</ul>
<a href={href}>{cta}<ArrowRight size={17} weight="bold" /></a>
</article>
);
}
export function HarnessProductPage() {
const [language, setLanguageState] = useState(() => {
try { return localStorage.getItem('site_lang') === 'en' ? 'en' : 'zh'; } catch { return 'zh'; }
});
const storyRef = useRef(null);
const pageRef = useRef(null);
const progressRef = useRef(0);
const t = copy[language];
const setLanguage = (next) => {
setLanguageState(next);
try { localStorage.setItem('site_lang', next); } catch { /* storage may be disabled */ }
};
const meta = useMemo(() => language === 'zh' ? {
title: '荣刻驭境 Roncarve Harness|插件驱动的 Agent 运行环境 - 荣刻科技',
description: '荣刻驭境是一套面向大语言模型与 Agent 的可组合 Harness,通过定制插件引擎提供精准插件检索、高效热插拔,支持浏览器操作本地项目的个人客户端与团队共享工作空间。'
} : {
title: 'Roncarve Harness | A plugin-driven runtime for Agents',
description: 'A composable Harness for LLMs and Agents with precise plugin discovery, efficient hot swapping, a local client for browser-based local projects, and a shared team workspace.'
}, [language]);
useLayoutEffect(() => {
document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en';
document.title = meta.title;
document.querySelector('meta[name="description"]')?.setAttribute('content', meta.description);
}, [language, meta]);
useLayoutEffect(() => {
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const context = gsap.context(() => {
if (reduced) {
progressRef.current = 0;
gsap.set(['.hero-copy', '.value-copy'], { opacity: 1, y: 0 });
return;
}
const timeline = gsap.timeline({
scrollTrigger: {
trigger: storyRef.current,
start: 'top top',
end: 'bottom bottom',
scrub: 0.85,
onUpdate: (self) => { progressRef.current = self.progress; }
}
});
timeline
.to('.hero-copy', { opacity: 0, y: -92, duration: 0.2, ease: 'none' }, 0.11)
.fromTo('.value-copy', { opacity: 0, y: 80 }, { opacity: 1, y: 0, duration: 0.2, ease: 'none' }, 0.26)
.fromTo('.value-card', { opacity: 0, y: 44 }, { opacity: 1, y: 0, stagger: 0.035, duration: 0.15, ease: 'none' }, 0.39)
.to('.story-scroll-cue', { opacity: 0, duration: 0.08 }, 0.05)
.to('.value-copy', { opacity: 0.82, y: -18, duration: 0.13, ease: 'none' }, 0.84);
gsap.utils.toArray('.reveal-section').forEach((section) => {
gsap.fromTo(section, { opacity: 0, y: 52 }, {
opacity: 1,
y: 0,
duration: 0.85,
ease: 'power3.out',
scrollTrigger: { trigger: section, start: 'top 82%', once: true }
});
});
}, pageRef);
return () => context.revert();
}, [language]);
return (
<div className="harness-page" ref={pageRef}>
<a className="skip-link" href="#main">{language === 'zh' ? '跳到主要内容' : 'Skip to main content'}</a>
<ProductNav language={language} setLanguage={setLanguage} />
<main id="main">
<section className="harness-story" id="top" ref={storyRef}>
<div className="story-sticky">
<div className="story-grid" />
<div className="story-glow" />
<ParticleStage progressRef={progressRef} />
<div className="story-shell">
<div className="hero-copy">
<div className="hero-meta"><span>{t.eyebrow}</span><strong>{t.status}</strong></div>
<h1>{t.title.split('\n').map((line) => <span key={line}>{line}</span>)}</h1>
<p>{t.summary}</p>
<div className="hero-actions">
<a className="button-primary" href={emailHref(language)}>{t.primary}<ArrowRight size={18} weight="bold" /></a>
<a className="button-secondary" href="#editions">{t.secondary}</a>
</div>
<div className="hero-proof">{t.proof.map((item) => <span key={item}>{item}</span>)}</div>
</div>
<div className="value-copy">
<span className="section-kicker">{t.formula}</span>
<h2>{t.valueTitle.split('\n').map((line) => <span key={line}>{line}</span>)}</h2>
<p>{t.valueSummary}</p>
<div className="value-grid">
{t.valueCards.map(([title, description], index) => {
const Icon = [MagnifyingGlass, Plug, UsersThree][index];
return <article className="value-card" key={title}><Icon size={27} weight="duotone" /><span>0{index + 1}</span><h3>{title}</h3><p>{description}</p></article>;
})}
</div>
</div>
</div>
<div className="story-scroll-cue"><span />{t.scroll}</div>
</div>
</section>
<section className="content-section engine-section" id="engine">
<div className="content-shell">
<header className="section-header reveal-section">
<div><span className="section-kicker">{t.engineKicker}</span><h2>{t.engineTitle}</h2></div>
<p>{t.engineSummary}</p>
</header>
<aside className="opensource-note reveal-section">
<span>{language === 'zh' ? '开源来源' : 'OPEN-SOURCE FOUNDATION'}</span>
<p>{t.originLine}</p>
</aside>
<div className="engine-layout">
<div className="reveal-section"><PluginDemo language={language} /></div>
<div className="engine-steps reveal-section">
{t.engineSteps.map(([number, title, description]) => (
<article key={number}><span>{number}</span><div><h3>{title}</h3><p>{description}</p></div></article>
))}
</div>
</div>
</div>
</section>
<section className="content-section editions-section" id="editions">
<div className="content-shell">
<header className="section-header reveal-section">
<div><span className="section-kicker">{t.editionsKicker}</span><h2>{t.editionsTitle.split('\n').map((line) => <span key={line}>{line}</span>)}</h2></div>
<p>{t.editionsSummary}</p>
</header>
<div className="edition-grid reveal-section">
<EditionCard title={t.personal} code={t.personalEn} description={t.personalDesc} features={t.personalFeatures} href={emailHref(language, t.personal)} cta={t.askPersonal} />
<EditionCard title={t.platform} code={t.platformEn} description={t.platformDesc} features={t.platformFeatures} href={emailHref(language, t.platform)} cta={t.askPlatform} featured />
</div>
</div>
</section>
<section className="content-section workflow-section" id="workflow">
<div className="content-shell">
<header className="section-header reveal-section">
<div><span className="section-kicker">{t.workflowKicker}</span><h2>{t.workflowTitle.split('\n').map((line) => <span key={line}>{line}</span>)}</h2></div>
<p>{t.workflowSummary}</p>
</header>
<div className="workflow-grid reveal-section">
{t.workflowSteps.map(([number, title, description], index) => (
<article key={number}><span>{number}</span><div className="workflow-node"><Plug size={20} weight={index === 2 ? 'fill' : 'duotone'} /></div><h3>{title}</h3><p>{description}</p></article>
))}
</div>
</div>
</section>
<section className="product-cta">
<div className="cta-orbit" aria-hidden="true"><img src="/assets/brand-logo-final.png" alt="" /></div>
<div className="content-shell reveal-section">
<span className="section-kicker">{t.ctaKicker}</span>
<h2>{t.ctaTitle.split('\n').map((line) => <span key={line}>{line}</span>)}</h2>
<p>{t.ctaSummary}</p>
<a href={emailHref(language)}>{t.ctaButton}<ArrowRight size={19} weight="bold" /></a>
<strong>contact@roncarve.com</strong>
</div>
</section>
</main>
<footer className="harness-footer">
<div className="content-shell">
<div className="harness-footer-row">
<div className="harness-footer-company">
<span>© {new Date().getFullYear()} {t.copyright}. All Rights Reserved.</span>
<a className="harness-footer-record" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">琼ICP备2026011549号</a>
<a className="harness-footer-record harness-footer-police" href="https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=46010002001924" target="_blank" rel="noopener noreferrer">
<img src="/assets/beian-police.png" alt={t.policeAlt} />
琼公网安备46010002001924号
</a>
</div>
<span className="harness-footer-signal">{t.footerSignal}</span>
</div>
</div>
</footer>
</div>
);
}
+402
View File
@@ -0,0 +1,402 @@
import { Canvas, useFrame } from '@react-three/fiber';
import gsap from 'gsap';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import * as THREE from 'three';
const vertexShader = `
precision highp float;
attribute vec3 aLogoPosition;
attribute vec3 aSpreadPosition;
attribute vec3 aIntroPosition;
attribute float aIntroDelay;
attribute float aIntroBend;
attribute float aSeed;
attribute float aScale;
attribute float aColorMix;
uniform float uTime;
uniform float uScroll;
uniform float uIntro;
uniform float uGather;
uniform float uPixelRatio;
uniform float uPointerActive;
uniform float uAspect;
uniform vec2 uPointer;
varying float vAlpha;
varying float vBrightness;
varying float vColorMix;
mat3 rotateX(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat3(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c);
}
mat3 rotateY(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat3(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c);
}
mat3 rotateZ(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat3(c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0);
}
void main() {
float expansion = smoothstep(0.055, 0.72, uScroll);
float rotationStrength = smoothstep(0.09, 0.48, uScroll);
float settle = smoothstep(0.74, 1.0, uScroll);
float introLinear = clamp((uIntro - aIntroDelay) / 0.38, 0.0, 1.0);
float introProgress = 1.0 - pow(1.0 - introLinear, 3.0);
vec2 introTravel = normalize(aLogoPosition.xy - aIntroPosition.xy + vec2(0.0001));
vec2 introTangent = vec2(-introTravel.y, introTravel.x);
float introArc = sin(introLinear * 3.14159265) * aIntroBend * (1.0 - introProgress * 0.32);
vec3 introPosition = mix(aIntroPosition, aLogoPosition, introProgress);
introPosition.xy += introTangent * introArc;
introPosition.z += sin(introLinear * 3.14159265) * abs(aIntroBend) * 0.68;
float idleStrength = (1.0 - smoothstep(0.045, 0.24, uScroll)) * introProgress;
vec3 position = mix(introPosition, aSpreadPosition, expansion * introProgress);
vec2 logoRadial = normalize(aLogoPosition.xy + vec2(0.0001));
vec2 logoTangent = vec2(-logoRadial.y, logoRadial.x);
float idleBreath = sin(uTime * 0.62) * 0.011;
float idleFloat = sin(uTime * 0.78 + aSeed * 21.0) * 0.032;
float idleSway = cos(uTime * 0.46 + aSeed * 13.0) * 0.016;
position.xy *= 1.0 + idleBreath * idleStrength;
position.xy += (logoRadial * idleFloat + logoTangent * idleSway) * idleStrength;
position.z += sin(uTime * 0.58 + aSeed * 17.0) * 0.11 * idleStrength;
position = rotateZ(sin(uTime * 0.28) * 0.007 * idleStrength) * position;
float drift = sin(uTime * 0.42 + aSeed * 18.0) * 0.12 * rotationStrength;
position += normalize(vec3(position.xy + 0.001, 0.35)) * drift;
float spinY = (uTime * 0.085 + uScroll * 1.18) * rotationStrength;
float spinX = (0.16 + sin(uTime * 0.14) * 0.08) * rotationStrength;
float spinZ = sin(uTime * 0.11) * 0.08 * rotationStrength;
position = rotateY(spinY) * rotateX(spinX) * rotateZ(spinZ) * position;
position.xy *= mix(1.0, 0.82, settle);
position.z -= settle * 0.85;
position = mix(position, aLogoPosition, uGather);
vec4 viewPosition = modelViewMatrix * vec4(position, 1.0);
vec4 clipPosition = projectionMatrix * viewPosition;
vec2 particleNdc = clipPosition.xy / max(clipPosition.w, 0.0001);
vec2 pointerDelta = particleNdc - uPointer;
vec2 aspectPointerDelta = vec2(pointerDelta.x * uAspect, pointerDelta.y);
float pointerDistance = length(aspectPointerDelta);
float repel = smoothstep(0.3, 0.0, pointerDistance) * uPointerActive * (1.0 - uGather);
vec2 repelDirection = normalize(aspectPointerDelta + vec2(0.0001));
vec2 ndcDirection = vec2(repelDirection.x / max(uAspect, 0.0001), repelDirection.y);
clipPosition.xy += ndcDirection * repel * 0.16 * clipPosition.w;
float nearFactor = clamp((position.z + 4.5) / 9.0, 0.0, 1.0);
float perspectiveSize = 10.0 / max(3.0, -viewPosition.z);
gl_PointSize = aScale * mix(1.75, 4.35, nearFactor) * uPixelRatio * perspectiveSize * (1.0 + repel * 0.12);
gl_Position = clipPosition;
float contentFade = 1.0 - smoothstep(0.22, 0.48, uScroll) * 0.76;
float idleShimmer = 1.0 + sin(uTime * 0.92 + aSeed * 29.0) * 0.07 * idleStrength;
float introFade = smoothstep(0.0, 0.2, introLinear);
vAlpha = mix(0.28, 0.78, nearFactor) * contentFade * (1.0 - settle * 0.18) * idleShimmer * introFade;
vBrightness = mix(0.46, 1.0, nearFactor) * (1.0 + sin(uTime * 0.54 + aSeed * 9.0) * 0.045 * idleStrength);
vColorMix = aColorMix;
}
`;
const fragmentShader = `
precision highp float;
varying float vAlpha;
varying float vBrightness;
varying float vColorMix;
void main() {
vec2 point = gl_PointCoord - 0.5;
float distanceToCenter = length(point);
float body = 1.0 - smoothstep(0.28, 0.5, distanceToCenter);
float core = 1.0 - smoothstep(0.0, 0.22, distanceToCenter);
if (body <= 0.001) discard;
vec3 cyan = vec3(0.278, 0.898, 0.937);
vec3 blue = vec3(0.259, 0.647, 1.0);
vec3 violet = vec3(0.537, 0.510, 1.0);
vec3 color = mix(cyan, blue, smoothstep(0.18, 0.62, vColorMix));
color = mix(color, violet, smoothstep(0.66, 1.0, vColorMix));
color *= vBrightness + core * 0.22;
gl_FragColor = vec4(color, body * vAlpha);
}
`;
function randomUnit(seed) {
const value = Math.sin(seed * 12.9898 + 78.233) * 43758.5453;
return value - Math.floor(value);
}
function buildGeometry(image, count) {
const sampleSize = 256;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = sampleSize;
canvas.height = sampleSize;
context.clearRect(0, 0, sampleSize, sampleSize);
context.drawImage(image, 0, 0, sampleSize, sampleSize);
const pixels = context.getImageData(0, 0, sampleSize, sampleSize).data;
const candidates = [];
for (let y = 0; y < sampleSize; y += 1) {
for (let x = 0; x < sampleSize; x += 1) {
const alpha = pixels[(y * sampleSize + x) * 4 + 3];
if (alpha > 34) candidates.push([x, y, alpha / 255]);
}
}
const positions = new Float32Array(count * 3);
const logoPositions = new Float32Array(count * 3);
const spreadPositions = new Float32Array(count * 3);
const introPositions = new Float32Array(count * 3);
const introDelays = new Float32Array(count);
const introBends = new Float32Array(count);
const seeds = new Float32Array(count);
const scales = new Float32Array(count);
const colors = new Float32Array(count);
for (let index = 0; index < count; index += 1) {
const seed = randomUnit(index + 1);
const candidate = candidates[Math.floor(seed * candidates.length)] || [sampleSize / 2, sampleSize / 2, 1];
const jitterX = (randomUnit(index * 3.17 + 4) - 0.5) * 0.032;
const jitterY = (randomUnit(index * 5.23 + 7) - 0.5) * 0.032;
const x = ((candidate[0] / sampleSize) - 0.5) * 7.35 + jitterX;
const y = (0.5 - (candidate[1] / sampleSize)) * 7.35 + jitterY;
const radius = Math.hypot(x, y);
const angle = Math.atan2(y, x);
const orbit = randomUnit(index * 7.91 + 11);
const depth = (randomUnit(index * 11.41 + 3) - 0.5) * 7.8;
const spreadRadius = radius * (0.58 + orbit * 0.58) + orbit * 1.25;
const spreadAngle = angle + (orbit - 0.5) * 1.3;
const offset = index * 3;
positions[offset] = x;
positions[offset + 1] = y;
positions[offset + 2] = 0;
logoPositions[offset] = x;
logoPositions[offset + 1] = y;
logoPositions[offset + 2] = 0;
spreadPositions[offset] = Math.cos(spreadAngle) * spreadRadius + (randomUnit(index + 27) - 0.5) * 1.1;
spreadPositions[offset + 1] = Math.sin(spreadAngle) * spreadRadius * 0.72 + (randomUnit(index + 39) - 0.5) * 1.1;
spreadPositions[offset + 2] = depth;
const introLane = Math.floor(randomUnit(index * 13.77 + 17) * 8);
const introAngle = introLane * (Math.PI / 4) + (randomUnit(index * 4.61 + 29) - 0.5) * 0.52;
const introRadius = 0.78 + randomUnit(index * 9.43 + 47) * 0.5;
introPositions[offset] = Math.cos(introAngle) * 10.8 * introRadius + (randomUnit(index * 2.19 + 61) - 0.5) * 1.2;
introPositions[offset + 1] = Math.sin(introAngle) * 6.7 * introRadius + (randomUnit(index * 8.31 + 73) - 0.5) * 0.9;
introPositions[offset + 2] = (randomUnit(index * 6.07 + 83) - 0.5) * 10.5;
introDelays[index] = randomUnit(index * 5.37 + 97) * 0.6;
const bendSeed = randomUnit(index * 7.13 + 109);
introBends[index] = (bendSeed < 0.5 ? -1 : 1) * (0.34 + Math.abs(bendSeed - 0.5) * 1.8);
seeds[index] = seed;
scales[index] = 0.68 + randomUnit(index * 2.73 + 5) * 1.08;
colors[index] = Math.min(1, Math.max(0, orbit * 0.74 + candidate[2] * 0.24));
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aLogoPosition', new THREE.BufferAttribute(logoPositions, 3));
geometry.setAttribute('aSpreadPosition', new THREE.BufferAttribute(spreadPositions, 3));
geometry.setAttribute('aIntroPosition', new THREE.BufferAttribute(introPositions, 3));
geometry.setAttribute('aIntroDelay', new THREE.BufferAttribute(introDelays, 1));
geometry.setAttribute('aIntroBend', new THREE.BufferAttribute(introBends, 1));
geometry.setAttribute('aSeed', new THREE.BufferAttribute(seeds, 1));
geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));
geometry.setAttribute('aColorMix', new THREE.BufferAttribute(colors, 1));
geometry.computeBoundingSphere();
return geometry;
}
function LogoParticles({ progressRef, introRef, gatherRef, pointerRef, pointerActiveRef, onReady }) {
const materialRef = useRef(null);
const [geometry, setGeometry] = useState(null);
const particleTransform = useMemo(() => {
if (window.innerWidth > 900) return { position: [1.92, 0, 0], scale: 0.86 };
if (window.innerWidth > 640) return { position: [1.45, 0, 0], scale: 0.5 };
return { position: [0, 0, 0], scale: 1 };
}, []);
const particleCount = useMemo(() => {
if (typeof window === 'undefined') return 18000;
if (window.innerWidth < 480) return 7000;
if (window.innerWidth < 768) return 10000;
if (window.innerWidth < 1200) return 18000;
return 24000;
}, []);
const uniforms = useMemo(() => ({
uTime: { value: 0 },
uScroll: { value: 0 },
uIntro: { value: 0 },
uGather: { value: 0 },
uPixelRatio: { value: Math.min(window.devicePixelRatio || 1, 1.5) },
uPointerActive: { value: 0 },
uAspect: { value: 1 },
uPointer: { value: new THREE.Vector2(20, 20) }
}), []);
useEffect(() => {
let disposed = false;
const image = new Image();
image.decoding = 'async';
image.src = '/assets/brand-logo-final.png';
image.onload = () => {
if (disposed) return;
const nextGeometry = buildGeometry(image, particleCount);
setGeometry(nextGeometry);
onReady?.();
};
return () => {
disposed = true;
};
}, [onReady, particleCount]);
useEffect(() => () => geometry?.dispose(), [geometry]);
useFrame(({ clock, size }) => {
if (!materialRef.current) return;
materialRef.current.uniforms.uTime.value = clock.getElapsedTime();
materialRef.current.uniforms.uScroll.value = progressRef.current;
materialRef.current.uniforms.uIntro.value = introRef.current.value;
materialRef.current.uniforms.uGather.value = gatherRef.current.value;
materialRef.current.uniforms.uAspect.value = size.width / Math.max(size.height, 1);
materialRef.current.uniforms.uPointerActive.value += (
Number(pointerActiveRef.current) - materialRef.current.uniforms.uPointerActive.value
) * 0.18;
materialRef.current.uniforms.uPointer.value.copy(pointerRef.current);
});
if (!geometry) return null;
return (
<points
geometry={geometry}
frustumCulled={false}
position={particleTransform.position}
scale={particleTransform.scale}
>
<shaderMaterial
ref={materialRef}
uniforms={uniforms}
vertexShader={vertexShader}
fragmentShader={fragmentShader}
transparent
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</points>
);
}
function useReducedMotion() {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
const update = () => setReduced(media.matches);
update();
media.addEventListener?.('change', update);
return () => media.removeEventListener?.('change', update);
}, []);
return reduced;
}
export function ParticleStage({ progressRef }) {
const stageRef = useRef(null);
const pointerRef = useRef(new THREE.Vector2(20, 20));
const pointerActiveRef = useRef(false);
const introRef = useRef({ value: 0 });
const gatherRef = useRef({ value: 0 });
const [ready, setReady] = useState(false);
const [active, setActive] = useState(true);
const [failed, setFailed] = useState(false);
const reducedMotion = useReducedMotion();
const handleReady = useCallback(() => setReady(true), []);
useEffect(() => {
if (!ready) return undefined;
gsap.killTweensOf(introRef.current);
if (reducedMotion) {
introRef.current.value = 1;
return undefined;
}
introRef.current.value = 0;
const tween = gsap.to(introRef.current, {
value: 1,
duration: 3.15,
ease: 'none'
});
return () => tween.kill();
}, [ready, reducedMotion]);
useEffect(() => {
if (!stageRef.current) return undefined;
const observer = new IntersectionObserver(([entry]) => setActive(entry.isIntersecting), { rootMargin: '120px 0px' });
observer.observe(stageRef.current);
return () => observer.disconnect();
}, []);
const handlePointerMove = (event) => {
if (reducedMotion || !window.matchMedia('(pointer: fine)').matches) return;
const bounds = stageRef.current.getBoundingClientRect();
const normalizedX = ((event.clientX - bounds.left) / bounds.width) * 2 - 1;
const normalizedY = 1 - ((event.clientY - bounds.top) / bounds.height) * 2;
pointerRef.current.set(normalizedX, normalizedY);
pointerActiveRef.current = true;
};
const handleGather = (event) => {
if (reducedMotion || event.button !== 0) return;
gsap.killTweensOf(introRef.current);
gsap.killTweensOf(gatherRef.current);
gsap.to(introRef.current, { value: 1, duration: 0.48, ease: 'power2.out' });
gsap.timeline()
.to(gatherRef.current, { value: 1, duration: 0.62, ease: 'power3.out' })
.to(gatherRef.current, { value: 1, duration: 0.26 })
.to(gatherRef.current, { value: 0, duration: 0.9, ease: 'power2.inOut' });
};
return (
<div
className={`particle-stage ${ready ? 'is-ready' : ''} ${failed ? 'has-failed' : ''} ${reducedMotion ? 'is-reduced' : ''}`}
ref={stageRef}
onPointerMove={handlePointerMove}
onPointerLeave={() => { pointerActiveRef.current = false; }}
onPointerDown={handleGather}
aria-hidden="true"
>
<img className="particle-static-logo" src="/assets/brand-logo-final.png" alt="" />
{!reducedMotion && !failed && (
<Canvas
className="particle-canvas"
dpr={[1, 1.5]}
camera={{ position: [0, 0, 10], fov: 42, near: 0.1, far: 50 }}
gl={{ alpha: true, antialias: false, powerPreference: 'high-performance' }}
frameloop={active ? 'always' : 'demand'}
onCreated={({ gl }) => {
gl.setClearColor(0x000000, 0);
gl.domElement.addEventListener('webglcontextlost', () => setFailed(true), { once: true });
}}
>
<LogoParticles
progressRef={progressRef}
introRef={introRef}
gatherRef={gatherRef}
pointerRef={pointerRef}
pointerActiveRef={pointerActiveRef}
onReady={handleReady}
/>
</Canvas>
)}
</div>
);
}
+590
View File
@@ -0,0 +1,590 @@
*, *::before, *::after { box-sizing: border-box; }
:root {
color-scheme: dark;
--h-bg: #05070a;
--h-bg-soft: #07131c;
--h-surface: rgba(13, 29, 41, .72);
--h-surface-solid: #0b1822;
--h-text: #f4fbff;
--h-text-soft: rgba(229, 244, 247, .66);
--h-text-faint: rgba(229, 244, 247, .42);
--h-cyan: #47e5ef;
--h-cyan-bright: #8bfbff;
--h-blue: #42a5ff;
--h-violet: #8982ff;
--h-line: rgba(183, 229, 231, .13);
--h-line-strong: rgba(139, 251, 255, .34);
--h-ease: cubic-bezier(.2, .8, .2, 1);
--h-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
--h-shell: min(1200px, calc(100vw - 48px));
}
html { scroll-behavior: smooth; background: var(--h-bg); }
body {
margin: 0;
min-width: 320px;
overflow-x: hidden;
background: var(--h-bg);
color: var(--h-text);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body, button, input { font: inherit; }
a { color: inherit; text-decoration: none; }
button { color: inherit; }
img { display: block; max-width: 100%; }
::selection { color: #031013; background: var(--h-cyan-bright); }
:focus-visible {
outline: 2px solid var(--h-cyan-bright);
outline-offset: 4px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.skip-link {
position: fixed;
top: 12px;
left: 12px;
z-index: 2000;
padding: 10px 16px;
border-radius: 9px;
color: #031013;
background: var(--h-cyan-bright);
transform: translateY(-160%);
transition: transform .2s ease;
}
.skip-link:focus { transform: translateY(0); }
.content-shell { width: var(--h-shell); margin-inline: auto; }
.harness-page {
min-height: 100vh;
overflow: clip;
background: var(--h-bg);
}
.harness-nav {
position: fixed;
inset: 0 0 auto;
z-index: 1000;
height: 72px;
border-bottom: 1px solid rgba(183, 229, 231, .08);
background: rgba(5, 7, 10, .72);
backdrop-filter: blur(18px) saturate(135%);
-webkit-backdrop-filter: blur(18px) saturate(135%);
}
.harness-nav-inner {
width: var(--h-shell);
height: 100%;
margin-inline: auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 28px;
}
.harness-brand {
display: inline-flex;
align-items: center;
gap: 11px;
flex: 0 0 auto;
}
.harness-brand img {
width: 37px;
height: 37px;
object-fit: contain;
filter: brightness(1.95) saturate(1.15) drop-shadow(0 0 12px rgba(71, 229, 239, .2));
}
.harness-brand span { display: flex; flex-direction: column; line-height: 1.05; }
.harness-brand strong { font-size: 14px; letter-spacing: .04em; }
.harness-brand small { margin-top: 6px; color: var(--h-text-faint); font: 500 8px/1 var(--h-mono); letter-spacing: .14em; }
.harness-nav-links { display: flex; align-items: center; gap: 26px; }
.harness-nav-links > a:not(.nav-email-button) {
position: relative;
color: var(--h-text-soft);
font-size: 14px;
font-weight: 600;
transition: color .2s ease;
}
.harness-nav-links > a:not(.nav-email-button)::after {
content: "";
position: absolute;
inset: auto 50% -10px;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--h-cyan);
opacity: 0;
transform: translateX(-50%) scale(0);
transition: opacity .2s ease, transform .2s ease;
}
.harness-nav-links > a:not(.nav-email-button):hover { color: var(--h-text); }
.harness-nav-links > a:not(.nav-email-button):hover::after { opacity: 1; transform: translateX(-50%) scale(1); }
.language-button {
min-height: 34px;
padding: 0 10px;
border: 1px solid var(--h-line);
border-radius: 8px;
background: rgba(255, 255, 255, .025);
color: var(--h-text-soft);
font: 650 11px/1 var(--h-mono);
cursor: pointer;
}
.language-button:hover { color: var(--h-text); border-color: var(--h-line-strong); }
.nav-email-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 38px;
padding: 0 15px;
border-radius: 9px;
color: #041014;
background: var(--h-cyan);
font-size: 13px;
font-weight: 750;
transition: transform .22s var(--h-ease), box-shadow .22s ease;
}
.nav-email-button:hover { transform: translateY(-2px); box-shadow: 0 12px 32px rgba(71, 229, 239, .18); }
.harness-menu-button {
display: none;
width: 42px;
height: 42px;
place-items: center;
border: 1px solid var(--h-line);
border-radius: 10px;
background: rgba(255, 255, 255, .035);
cursor: pointer;
}
.harness-story { position: relative; height: 220svh; background: var(--h-bg); }
.story-sticky {
position: sticky;
top: 0;
height: 100svh;
min-height: 680px;
overflow: hidden;
isolation: isolate;
background:
radial-gradient(circle at 76% 28%, rgba(66, 165, 255, .14), transparent 24%),
radial-gradient(circle at 58% 54%, rgba(71, 229, 239, .08), transparent 34%),
linear-gradient(180deg, #07111a 0%, #05070a 72%, #05070a 100%);
}
.story-sticky::after {
content: "";
position: absolute;
inset: auto 0 0;
z-index: 5;
height: 180px;
pointer-events: none;
background: linear-gradient(transparent, var(--h-bg));
}
.story-grid {
position: absolute;
inset: 0;
z-index: 0;
opacity: .52;
background-image:
linear-gradient(rgba(183, 229, 231, .045) 1px, transparent 1px),
linear-gradient(90deg, rgba(183, 229, 231, .045) 1px, transparent 1px);
background-size: 72px 72px;
mask-image: radial-gradient(circle at 68% 44%, #000 0%, rgba(0, 0, 0, .82) 42%, transparent 88%);
-webkit-mask-image: radial-gradient(circle at 68% 44%, #000 0%, rgba(0, 0, 0, .82) 42%, transparent 88%);
}
.story-glow {
position: absolute;
z-index: 0;
top: 8%;
right: 3%;
width: min(55vw, 850px);
aspect-ratio: 1;
border-radius: 50%;
opacity: .46;
background: conic-gradient(from 210deg, transparent, rgba(66, 165, 255, .18), rgba(137, 130, 255, .16), transparent 42%);
filter: blur(42px);
pointer-events: none;
}
.particle-stage { position: absolute; inset: 0; z-index: 1; cursor: crosshair; }
.particle-canvas { position: absolute !important; inset: 0; width: 100% !important; height: 100% !important; }
.particle-static-logo {
position: absolute;
top: 50%;
right: 13%;
width: min(45vw, 610px);
transform: translateY(-50%);
opacity: 0;
object-fit: contain;
filter: brightness(1.6) saturate(1.55) hue-rotate(155deg) drop-shadow(0 0 38px rgba(66, 165, 255, .24));
transition: opacity .18s ease;
}
.particle-stage.is-ready .particle-static-logo { opacity: 0; }
.particle-stage.has-failed .particle-static-logo,
.particle-stage.is-reduced .particle-static-logo { opacity: .82; }
.story-shell {
position: relative;
z-index: 3;
width: var(--h-shell);
height: 100%;
margin-inline: auto;
pointer-events: none;
}
.hero-copy {
position: absolute;
top: 50%;
left: 0;
width: min(47%, 560px);
transform: translateY(-48%);
pointer-events: none;
}
.hero-meta { display: flex; align-items: center; gap: 12px; margin-bottom: 26px; }
.hero-meta > span, .section-kicker { color: var(--h-cyan); font: 700 11px/1.35 var(--h-mono); letter-spacing: .13em; }
.hero-meta strong { padding: 7px 10px; border: 1px solid rgba(139, 251, 255, .22); border-radius: 999px; color: var(--h-text); background: rgba(71, 229, 239, .07); font: 650 10px/1 var(--h-mono); letter-spacing: .1em; }
.hero-copy h1, .value-copy h2, .section-header h2, .product-cta h2 {
margin: 0;
color: var(--h-text);
font-weight: 680;
line-height: 1.06;
letter-spacing: -.05em;
}
.hero-copy h1 { font-size: clamp(48px, 5.35vw, 78px); }
.hero-copy h1 span, .value-copy h2 span, .section-header h2 span, .product-cta h2 span { display: block; }
.hero-copy > p {
max-width: 470px;
margin: 28px 0 0;
color: var(--h-text-soft);
font-size: 17px;
line-height: 1.82;
}
.hero-actions { display: flex; gap: 12px; margin-top: 34px; pointer-events: auto; }
.button-primary, .button-secondary, .product-cta a {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 50px;
padding: 0 20px;
border: 1px solid transparent;
border-radius: 10px;
font-size: 14px;
font-weight: 750;
transition: transform .24s var(--h-ease), border-color .24s ease, background .24s ease, box-shadow .24s ease;
}
.button-primary, .product-cta a { color: #031013; background: var(--h-cyan); }
.button-primary:hover, .product-cta a:hover { transform: translateY(-3px); box-shadow: 0 16px 40px rgba(71, 229, 239, .2); }
.button-secondary { border-color: var(--h-line); color: var(--h-text); background: rgba(255, 255, 255, .035); }
.button-secondary:hover { transform: translateY(-3px); border-color: var(--h-line-strong); background: rgba(255, 255, 255, .07); }
.hero-proof { display: flex; flex-wrap: wrap; gap: 8px 22px; margin-top: 30px; color: var(--h-text-faint); font-size: 12px; }
.hero-proof span { position: relative; padding-left: 13px; }
.hero-proof span::before { content: ""; position: absolute; left: 0; top: 50%; width: 5px; height: 5px; border-radius: 50%; background: var(--h-cyan); transform: translateY(-50%); box-shadow: 0 0 10px rgba(71, 229, 239, .65); }
.value-copy {
position: absolute;
inset: 15% 0 auto;
opacity: 0;
text-align: center;
pointer-events: none;
}
.value-copy h2 { margin-top: 22px; font-size: clamp(40px, 4.5vw, 64px); }
.value-copy > p { width: min(650px, 80%); margin: 22px auto 0; color: var(--h-text-soft); font-size: 17px; line-height: 1.8; }
.value-copy > .section-kicker { display: inline-flex; align-items: center; gap: 9px; padding: 8px 12px; border: 1px solid rgba(139, 251, 255, .22); border-radius: 999px; background: rgba(71, 229, 239, .06); box-shadow: 0 0 28px rgba(71, 229, 239, .07); letter-spacing: .17em; }
.value-copy > .section-kicker::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--h-cyan); box-shadow: 0 0 10px rgba(71, 229, 239, .75); }
.value-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-top: 48px; text-align: left; }
.value-card {
position: relative;
min-height: 208px;
padding: 24px;
border: 1px solid var(--h-line);
border-radius: 16px;
background: linear-gradient(150deg, rgba(14, 32, 44, .76), rgba(8, 18, 26, .48));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .025);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
.value-card > svg { color: var(--h-cyan); }
.value-card > span { position: absolute; top: 24px; right: 24px; color: var(--h-text-faint); font: 600 10px/1 var(--h-mono); }
.value-card h3 { margin: 30px 0 10px; font-size: 19px; letter-spacing: -.025em; }
.value-card p { margin: 0; color: var(--h-text-soft); font-size: 13px; line-height: 1.7; }
.story-scroll-cue {
position: absolute;
z-index: 6;
left: 50%;
bottom: 34px;
display: flex;
align-items: center;
gap: 10px;
color: var(--h-text-faint);
font: 600 9px/1 var(--h-mono);
letter-spacing: .14em;
transform: translateX(-50%);
}
.story-scroll-cue span { width: 34px; height: 1px; background: linear-gradient(90deg, transparent, var(--h-cyan)); animation: cue-pulse 1.8s ease-in-out infinite; }
@keyframes cue-pulse { 50% { width: 52px; opacity: .4; } }
.content-section { position: relative; padding: 132px 0; overflow: hidden; }
.content-section::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .36; background-image: linear-gradient(rgba(183, 229, 231, .035) 1px, transparent 1px), linear-gradient(90deg, rgba(183, 229, 231, .035) 1px, transparent 1px); background-size: 72px 72px; mask-image: linear-gradient(transparent, #000 18%, #000 82%, transparent); -webkit-mask-image: linear-gradient(transparent, #000 18%, #000 82%, transparent); }
.engine-section { background: linear-gradient(180deg, var(--h-bg), #071019 48%, var(--h-bg)); }
.editions-section { background: radial-gradient(circle at 78% 40%, rgba(137, 130, 255, .09), transparent 30%), var(--h-bg); }
.workflow-section { background: linear-gradient(180deg, var(--h-bg), #07131c); }
.section-header { position: relative; z-index: 1; display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(320px, .62fr); align-items: end; gap: 80px; margin-bottom: 62px; }
.section-header h2 { margin-top: 22px; font-size: clamp(38px, 4.2vw, 60px); }
.section-header > p { margin: 0; color: var(--h-text-soft); font-size: 17px; line-height: 1.82; }
.opensource-note { position: relative; z-index: 1; display: flex; align-items: center; gap: 22px; margin: -28px 0 32px; padding: 14px 18px; border: 1px solid var(--h-line); border-radius: 11px; color: var(--h-text-faint); background: rgba(255, 255, 255, .018); }
.opensource-note > span { flex: 0 0 auto; color: var(--h-cyan); font: 650 9px/1.4 var(--h-mono); letter-spacing: .12em; }
.opensource-note > p { margin: 0; font-size: 11px; line-height: 1.6; }
.engine-layout { position: relative; z-index: 1; display: grid; grid-template-columns: minmax(0, 1.48fr) minmax(300px, .62fr); gap: 22px; align-items: start; }
.plugin-demo { overflow: hidden; border: 1px solid var(--h-line); border-radius: 20px; background: rgba(7, 18, 27, .87); box-shadow: 0 30px 90px rgba(0, 0, 0, .25), inset 0 1px 0 rgba(255, 255, 255, .03); }
.demo-toolbar { display: flex; align-items: center; justify-content: space-between; min-height: 52px; padding: 0 18px; border-bottom: 1px solid var(--h-line); color: var(--h-text-faint); background: rgba(255, 255, 255, .02); font: 650 9px/1 var(--h-mono); letter-spacing: .12em; }
.demo-toolbar > div { display: flex; align-items: center; gap: 8px; color: var(--h-text-soft); }
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--h-cyan); box-shadow: 0 0 14px rgba(71, 229, 239, .8); }
.demo-search-row { padding: 18px 18px 12px; }
.demo-search { display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 14px; border: 1px solid var(--h-line); border-radius: 11px; background: rgba(255, 255, 255, .025); color: var(--h-text-faint); }
.demo-search:focus-within { border-color: var(--h-line-strong); box-shadow: 0 0 0 3px rgba(71, 229, 239, .07); }
.demo-search input { width: 100%; border: 0; outline: 0; color: var(--h-text); background: transparent; font-size: 14px; }
.demo-search input::placeholder { color: var(--h-text-faint); }
.category-tabs { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; }
.category-tabs button { min-height: 30px; padding: 0 11px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--h-text-faint); font-size: 11px; cursor: pointer; }
.category-tabs button:hover, .category-tabs button.is-active { border-color: var(--h-line); color: var(--h-text); background: rgba(71, 229, 239, .065); }
.demo-result-meta { padding: 0 18px 12px; color: var(--h-text-faint); font: 550 10px/1 var(--h-mono); }
.demo-result-meta strong { color: var(--h-cyan); }
.plugin-result-list { display: grid; gap: 8px; max-height: 416px; padding: 0 18px 18px; overflow: auto; scrollbar-width: thin; scrollbar-color: rgba(139, 251, 255, .2) transparent; }
.plugin-result { display: grid; grid-template-columns: 42px 1fr auto; align-items: center; gap: 12px; min-height: 92px; padding: 13px; border: 1px solid var(--h-line); border-radius: 13px; background: rgba(255, 255, 255, .018); transition: border-color .2s ease, background .2s ease, transform .2s var(--h-ease); }
.plugin-result:hover { transform: translateY(-2px); border-color: rgba(139, 251, 255, .23); background: rgba(255, 255, 255, .032); }
.plugin-result.is-active { border-color: rgba(71, 229, 239, .42); background: linear-gradient(100deg, rgba(71, 229, 239, .08), rgba(66, 165, 255, .035)); }
.plugin-icon { display: grid; place-items: center; width: 42px; height: 42px; border: 1px solid var(--h-line); border-radius: 11px; color: var(--h-cyan); background: rgba(71, 229, 239, .05); }
.plugin-copy > span { color: var(--h-cyan); font: 650 8px/1 var(--h-mono); letter-spacing: .12em; }
.plugin-copy h3 { margin: 7px 0 4px; font-size: 14px; }
.plugin-copy p { margin: 0; color: var(--h-text-faint); font-size: 11px; line-height: 1.5; }
.plugin-result button { min-height: 34px; padding: 0 12px; border: 1px solid var(--h-line); border-radius: 8px; color: var(--h-text-soft); background: rgba(255, 255, 255, .025); font-size: 11px; cursor: pointer; }
.plugin-result button:hover, .plugin-result button[aria-pressed="true"] { border-color: var(--h-line-strong); color: #031013; background: var(--h-cyan); }
.empty-result { padding: 36px 18px; color: var(--h-text-faint); text-align: center; font-size: 13px; }
.runtime-strip { display: flex; align-items: center; justify-content: space-between; gap: 18px; min-height: 68px; padding: 12px 18px; border-top: 1px solid var(--h-line); background: rgba(71, 229, 239, .04); }
.runtime-strip > div { display: flex; align-items: center; gap: 10px; color: var(--h-cyan); }
.runtime-strip > div span { display: flex; flex-direction: column; gap: 5px; }
.runtime-strip small { color: var(--h-text-faint); font: 600 8px/1 var(--h-mono); letter-spacing: .1em; text-transform: uppercase; }
.runtime-strip strong { color: var(--h-text); font-size: 12px; }
.runtime-strip p { margin: 0; color: var(--h-text-faint); font-size: 10px; }
.engine-steps { display: grid; gap: 10px; }
.engine-steps article { display: grid; grid-template-columns: 44px 1fr; gap: 16px; min-height: 135px; padding: 20px; border: 1px solid var(--h-line); border-radius: 15px; background: rgba(255, 255, 255, .018); }
.engine-steps article > span { color: var(--h-cyan); font: 650 10px/1 var(--h-mono); letter-spacing: .1em; }
.engine-steps h3 { margin: 0 0 9px; font-size: 18px; }
.engine-steps p { margin: 0; color: var(--h-text-soft); font-size: 12px; line-height: 1.68; }
.edition-grid { position: relative; z-index: 1; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
.edition-card { position: relative; min-height: 570px; padding: 34px; border: 1px solid var(--h-line); border-radius: 22px; background: linear-gradient(155deg, rgba(15, 31, 43, .72), rgba(6, 14, 21, .78)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .03); }
.edition-card.is-featured { border-color: rgba(139, 251, 255, .28); background: radial-gradient(circle at 86% 6%, rgba(137, 130, 255, .15), transparent 28%), linear-gradient(155deg, rgba(15, 34, 47, .85), rgba(6, 14, 21, .82)); }
.edition-head { display: flex; align-items: center; justify-content: space-between; color: var(--h-cyan); }
.edition-head span { font: 700 10px/1 var(--h-mono); letter-spacing: .14em; }
.edition-card h3 { margin: 46px 0 15px; font-size: 34px; letter-spacing: -.035em; }
.edition-card > p { max-width: 510px; margin: 0; color: var(--h-text-soft); font-size: 15px; line-height: 1.72; }
.edition-card ul { display: grid; gap: 14px; margin: 38px 0 0; padding: 28px 0 0; border-top: 1px solid var(--h-line); list-style: none; }
.edition-card li { display: flex; align-items: flex-start; gap: 10px; color: var(--h-text-soft); font-size: 14px; }
.edition-card li svg { flex: 0 0 auto; margin-top: 2px; color: var(--h-cyan); }
.edition-card > a { position: absolute; inset: auto 34px 34px; display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 48px; border: 1px solid var(--h-line); border-radius: 10px; color: var(--h-text); background: rgba(255, 255, 255, .03); font-size: 13px; font-weight: 750; transition: border-color .2s ease, background .2s ease, transform .2s var(--h-ease); }
.edition-card.is-featured > a { color: #031013; border-color: var(--h-cyan); background: var(--h-cyan); }
.edition-card > a:hover { transform: translateY(-2px); border-color: var(--h-line-strong); background: rgba(71, 229, 239, .09); }
.edition-card.is-featured > a:hover { background: var(--h-cyan-bright); }
.workflow-grid { position: relative; z-index: 1; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid var(--h-line); border-radius: 20px; overflow: hidden; background: rgba(5, 13, 19, .7); }
.workflow-grid article { position: relative; min-height: 310px; padding: 28px; }
.workflow-grid article + article { border-left: 1px solid var(--h-line); }
.workflow-grid article > span { color: var(--h-text-faint); font: 650 10px/1 var(--h-mono); letter-spacing: .12em; }
.workflow-node { display: grid; place-items: center; width: 48px; height: 48px; margin-top: 54px; border: 1px solid var(--h-line-strong); border-radius: 50%; color: var(--h-cyan); background: rgba(71, 229, 239, .055); box-shadow: 0 0 28px rgba(71, 229, 239, .08); }
.workflow-grid article:not(:last-child) .workflow-node::after { content: ""; position: absolute; top: 111px; left: 76px; width: calc(100% - 48px); height: 1px; background: linear-gradient(90deg, rgba(71, 229, 239, .4), transparent); }
.workflow-grid h3 { margin: 34px 0 10px; font-size: 19px; }
.workflow-grid p { margin: 0; color: var(--h-text-soft); font-size: 13px; line-height: 1.68; }
.product-cta { position: relative; min-height: 760px; display: grid; place-items: center; overflow: hidden; text-align: center; background: radial-gradient(circle at 50% 48%, rgba(71, 229, 239, .105), transparent 30%), linear-gradient(180deg, #07131c, var(--h-bg)); }
.product-cta::before { content: ""; position: absolute; inset: 0; opacity: .45; background-image: linear-gradient(rgba(183, 229, 231, .04) 1px, transparent 1px), linear-gradient(90deg, rgba(183, 229, 231, .04) 1px, transparent 1px); background-size: 72px 72px; mask-image: radial-gradient(circle, #000 0%, transparent 70%); -webkit-mask-image: radial-gradient(circle, #000 0%, transparent 70%); }
.product-cta .content-shell { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; }
.product-cta h2 { margin-top: 24px; font-size: clamp(43px, 5vw, 70px); }
.product-cta p { width: min(640px, 88%); margin: 26px auto 0; color: var(--h-text-soft); font-size: 17px; line-height: 1.78; }
.product-cta a { margin-top: 34px; }
.product-cta strong { margin-top: 18px; color: var(--h-text-faint); font: 550 11px/1 var(--h-mono); letter-spacing: .08em; }
.cta-orbit { position: absolute; z-index: 1; width: min(68vw, 780px); aspect-ratio: 1; opacity: .11; animation: orbit-rotate 38s linear infinite; }
.cta-orbit img { width: 100%; height: 100%; object-fit: contain; filter: brightness(2.2) saturate(1.4) hue-rotate(155deg); }
@keyframes orbit-rotate { to { transform: rotate(360deg); } }
.harness-footer { padding: 28px 0 32px; border-top: 1px solid var(--h-line); background: var(--h-bg); }
.harness-footer-row { display: flex; align-items: center; justify-content: space-between; gap: 24px; }
.harness-footer-company { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px; }
.harness-footer-company > span, .harness-footer-signal { color: var(--h-text-faint); font: 550 9px/1.5 var(--h-mono); }
.harness-footer-record { color: var(--h-text-faint); font-size: 11px; line-height: 1.5; transition: color .2s ease; }
.harness-footer-record:hover { color: var(--h-cyan-bright); }
.harness-footer-police { display: inline-flex; align-items: center; gap: 5px; }
.harness-footer-police img { width: 18px; height: 20px; object-fit: contain; }
.harness-footer-signal { flex: 0 0 auto; letter-spacing: .08em; }
@media (min-width: 1500px) {
:root { --h-shell: min(1380px, calc(100vw - 8vw)); }
.hero-copy { width: min(43%, 610px); }
}
@media (max-width: 1050px) {
.harness-nav-links { gap: 18px; }
.harness-nav-links > a:not(.nav-email-button) { font-size: 13px; }
.hero-copy { width: min(50%, 540px); }
.engine-layout { grid-template-columns: 1fr; }
.engine-steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.workflow-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.workflow-grid article:nth-child(3) { border-left: 0; border-top: 1px solid var(--h-line); }
.workflow-grid article:nth-child(4) { border-top: 1px solid var(--h-line); }
.workflow-grid article:nth-child(2) .workflow-node::after { display: none; }
}
@media (max-width: 820px) {
:root { --h-shell: min(100% - 36px, 720px); }
.harness-nav { height: 66px; }
.harness-menu-button { display: grid; }
.harness-nav-links {
position: fixed;
inset: 66px 0 auto;
display: grid;
gap: 0;
padding: 16px 18px 20px;
border-bottom: 1px solid var(--h-line);
background: rgba(5, 7, 10, .98);
opacity: 0;
pointer-events: none;
transform: translateY(-12px);
transition: opacity .22s ease, transform .22s var(--h-ease);
}
.harness-nav-links.is-open { opacity: 1; pointer-events: auto; transform: translateY(0); }
.harness-nav-links > a:not(.nav-email-button) { padding: 13px 4px; border-bottom: 1px solid var(--h-line); }
.harness-nav-links > a:not(.nav-email-button)::after { display: none; }
.language-button { width: 100%; margin-top: 12px; }
.nav-email-button { margin-top: 10px; }
.story-sticky { min-height: 660px; }
.hero-copy { top: 48%; width: 58%; }
.hero-copy > p { max-width: 330px; }
.hero-copy h1 { font-size: clamp(44px, 8vw, 64px); }
.particle-static-logo { right: -1%; width: 54vw; }
.value-copy { inset: 12% 0 auto; }
.value-grid { gap: 10px; margin-top: 34px; }
.value-card { min-height: 190px; padding: 20px; }
.section-header { grid-template-columns: 1fr; gap: 24px; }
.edition-card { min-height: 590px; padding: 28px; }
.edition-card > a { inset: auto 28px 28px; }
}
@media (max-width: 640px) {
:root { --h-shell: min(100% - 28px, 520px); }
.harness-brand small { display: none; }
.harness-brand img { width: 34px; height: 34px; }
.harness-story { height: 230svh; }
.story-sticky { min-height: 620px; }
.story-grid, .content-section::before, .product-cta::before { background-size: 44px 44px; }
.particle-stage { opacity: .72; }
.particle-static-logo { top: 28%; right: 8%; width: 64vw; }
.hero-copy { top: 47%; width: 100%; transform: translateY(-42%); }
.hero-meta { margin-bottom: 20px; }
.hero-copy h1 { max-width: 500px; font-size: clamp(42px, 13.5vw, 59px); }
.hero-copy > p { margin-top: 22px; font-size: 15px; line-height: 1.72; }
.hero-actions { display: grid; gap: 9px; margin-top: 26px; }
.button-primary, .button-secondary { width: 100%; min-height: 47px; }
.hero-proof { display: grid; gap: 8px; margin-top: 20px; }
.value-copy { inset: 9% 0 auto; }
.value-copy h2 { margin-top: 17px; font-size: clamp(34px, 10vw, 46px); }
.value-copy > p { width: 100%; margin-top: 16px; font-size: 14px; line-height: 1.65; }
.value-grid { grid-template-columns: 1fr; gap: 8px; margin-top: 24px; }
.value-card { display: grid; grid-template-columns: 30px 1fr; align-items: center; gap: 3px 12px; min-height: 0; padding: 14px; }
.value-card > svg { grid-row: 1 / 3; }
.value-card > span { top: 14px; right: 14px; }
.value-card h3 { margin: 0; padding-right: 28px; font-size: 15px; }
.value-card p { margin: 0; font-size: 11px; line-height: 1.45; }
.story-scroll-cue { bottom: 18px; }
.content-section { padding: 92px 0; }
.section-header { margin-bottom: 40px; }
.section-header h2 { font-size: clamp(36px, 10vw, 48px); }
.section-header > p { font-size: 15px; }
.demo-toolbar > span { display: none; }
.demo-search-row { padding: 14px 14px 10px; }
.demo-result-meta { padding-inline: 14px; }
.plugin-result-list { padding: 0 14px 14px; }
.plugin-result { grid-template-columns: 38px 1fr; min-height: 0; }
.plugin-icon { width: 38px; height: 38px; }
.plugin-result button { grid-column: 1 / -1; width: 100%; }
.runtime-strip { align-items: flex-start; flex-direction: column; }
.engine-steps { grid-template-columns: 1fr; }
.engine-steps article { min-height: 0; }
.edition-grid { grid-template-columns: 1fr; }
.edition-card { min-height: 560px; padding: 24px; }
.edition-card > a { inset: auto 24px 24px; }
.workflow-grid { grid-template-columns: 1fr; }
.workflow-grid article { min-height: 250px; }
.workflow-grid article + article { border-left: 0; border-top: 1px solid var(--h-line); }
.workflow-grid article .workflow-node::after { display: none !important; }
.workflow-node { margin-top: 34px; }
.opensource-note { align-items: flex-start; flex-direction: column; gap: 7px; margin-top: -30px; }
.product-cta { min-height: 690px; }
.product-cta h2 { font-size: clamp(40px, 11vw, 54px); }
.product-cta p { width: 100%; font-size: 15px; }
.product-cta a { width: 100%; }
.cta-orbit { width: 100vw; }
.harness-footer-row { align-items: flex-start; flex-direction: column; gap: 14px; }
.harness-footer-company { align-items: flex-start; flex-direction: column; gap: 9px; }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
.harness-story { height: auto; }
.story-sticky { position: relative; min-height: 1180px; height: auto; }
.hero-copy { top: 180px; transform: none; }
.value-copy { top: 650px; opacity: 1; }
.particle-static-logo { opacity: .32; }
.reveal-section { opacity: 1 !important; transform: none !important; }
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { HarnessProductPage } from './HarnessProductPage';
import './harness.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<HarnessProductPage />
</StrictMode>
);
+24
View File
@@ -0,0 +1,24 @@
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
export default defineConfig({
appType: 'mpa',
base: '/',
server: {
host: '0.0.0.0',
allowedHosts: ['localhost', '127.0.0.1', 'terminal.local']
},
preview: {
host: '0.0.0.0'
},
build: {
outDir: 'dist',
emptyOutDir: true,
rollupOptions: {
input: {
home: resolve(process.cwd(), 'index.html'),
harness: resolve(process.cwd(), 'products/roncarve-harness/index.html')
}
}
}
});
@@ -0,0 +1,717 @@
# 荣刻驭境(Roncarve Harness)产品入口与产品介绍页设计文档
> 文档状态:设计基线 v1.1(关键决策已确认)
> 编写日期:2026-08-22
> 适用范围:荣刻科技官网导航、产品列表入口、荣刻驭境产品介绍页
> 参考页面:[DeepSeek Harness](https://www.deepseek.com/harness/)
> 当前仓库基线:单页静态官网 `index.html` + 原生 CSS/JavaScript + Three.js Hero
## 1. 设计结论
本次新增内容由两个层级组成:
1. 官网主导航新增“产品 / Products”入口,在下拉产品列表中展示“荣刻驭境 / Roncarve Harness”。
2. 点击产品卡片进入独立产品介绍页,推荐正式路由为 `/products/roncarve-harness/`
产品页采用“深色空间舞台 + 品牌 Logo 粒子 + 滚动叙事”的视觉方向。首屏加载时,粒子从四面八方与不同景深层进入,并在约 3 秒内减速汇聚为完整荣刻 Logo;汇聚完成后维持轻微漂浮,只有在用户开始滚动后才逐渐进入 3D 旋转、景深缩放和空间展开。鼠标靠近粒子产生局部排斥,点击非交互背景时粒子短暂重新聚拢,再回到当前滚动位置对应的空间状态。
页面表达的核心不是“又一个 Harness”,而是三项差异化价值:
- 定制优化的插件引擎:面向海量插件进行精准检索、匹配与发现。
- 更高效率的热插拔:插件可以快速加载、卸载、替换和组合。
- 从本地到协同:个人版以本地客户端连接本地项目,并通过浏览器界面操作;平台版以团队共享工作空间承载所有成员协同。
视觉上参考 DeepSeek Harness 的深色、网格、光晕、克制排版与滚动节奏,但不复制其品牌标识、文案、图形资产或页面结构细节。荣刻页面继续使用现有官网的蓝青紫品牌色、圆角、按钮和字体体系。
### 1.1 已确认决策
| 项目 | 决策 |
| --- | --- |
| 产品状态 | 统一展示为“即将开放 / Coming Soon” |
| 转化渠道 | 当前只使用 `contact@roncarve.com` 邮箱 |
| 版本边界 | 采用本文第 5.4 节定义的个人版/平台版首发能力 |
| 演示参考 | 使用 DeepSeek Harness 官方 Web UI、插件状态页和运行轨迹界面作为研究参考 |
| 上游许可 | 已确认允许二次开发;发布物继续保留上游许可证与必要署名 |
| 前端架构 | 确认引入 React/Vite,产品页采用 React Three Fiber + GSAP |
## 2. 背景与目标
### 2.1 产品背景
产品中文名为“荣刻驭境”,英文名为“Roncarve Harness”。产品基于 DeepSeek Harness 进行二次开发,面向大语言模型与 Agent 场景,重点强化插件检索、插件热插拔、浏览器操作本地项目的个人客户端,以及团队共享工作空间。
“驭境”对应 Harness 对运行环境、工具与能力的组织和驾驭;英文名称保留 Harness,便于开发者快速理解产品类别。
### 2.2 设计目标
- 在官网中建立清晰、可扩展的产品入口,为后续新增产品预留结构。
- 在首屏 10 秒内让访客理解产品类别、差异能力和适用对象。
- 用品牌 Logo 粒子从“完整标识”到“空间插件生态”的变化,形成产品记忆点。
- 第二屏立即解释 Harness 的价值,避免页面只有视觉效果而缺乏产品信息。
- 同时服务技术决策者、开发者、企业团队负责人三类受众。
- 在桌面端保持沉浸感,在移动端、低性能设备和减少动态效果模式下保持可读与可用。
### 2.3 非目标
- 本阶段不设计完整控制台、插件市场或工作空间内部界面。
- 不在官网中展示未经确认的插件数量、性能倍数、客户数量或兼容模型数量。
- 不复制 DeepSeek Harness 的 Logo、动态图形、代码卡片、具体文案与视觉资产。
- 不让粒子交互覆盖导航、按钮、表单等核心操作。
- 不要求首页现有 Three.js Hero 与产品页共用同一套 3D 场景。
## 3. 产品定位与信息优先级
### 3.1 一句话定位
> 荣刻驭境是一套面向大语言模型与 Agent 的可组合 Harness,通过定制插件引擎实现海量插件精准检索和高效热插拔;个人版以本地客户端连接本地项目,平台版以共享工作空间连接团队协作。
### 3.2 目标受众
| 受众 | 主要任务 | 页面需要回答的问题 |
| --- | --- | --- |
| AI 开发者 | 快速组合模型、工具和插件 | 插件如何发现、加载、替换与调试? |
| 技术负责人 | 评估 Agent 基础设施 | 是否可私有部署、可扩展、可治理? |
| 企业团队负责人 | 组织多人协作 | 如何划定工作空间、管理成员与共享能力? |
| 本地项目使用者 | 通过浏览器操作当前设备上的项目 | 客户端如何连接本地文件、运行环境与插件? |
### 3.3 信息层级
访客应按以下顺序理解产品:
1. 这是什么:面向大语言模型与 Agent 的 Harness。
2. 为什么不同:定制插件引擎、精准检索、高效热插拔。
3. 如何使用:个人版通过本地客户端在浏览器中操作本地项目。
4. 团队如何用:平台版让所有成员在共享工作空间中协同。
5. 下一步是什么:通过邮箱咨询产品、选择版本或查看技术资料。
## 4. 官网产品入口设计
### 4.1 导航位置
建议主导航顺序调整为:
`产品``能力链路``解决方案``核心优势``交付网络``邮件咨询`
“产品”放在能力与解决方案之前,先呈现可购买/可体验的具体对象,再承接企业能力说明。英文导航使用 `Products`
### 4.2 桌面端交互
- “产品”是带下拉面板的导航项,点击和键盘操作都可打开;悬停只作为增强,不作为唯一触发方式。
- 面板位于导航下方,宽度建议 `420520px`,使用现有深色半透明表面、`16px` 圆角和低对比边框。
- 当前只有一个产品时,仍使用产品卡片结构,为后续扩充产品保留空间。
- 点击卡片任意非禁用区域进入 `/products/roncarve-harness/`
- 点击外部、按 `Esc`、选择产品或导航离开后关闭面板。
- 打开时首个产品卡获得逻辑焦点;焦点不能被困在面板中。
### 4.3 移动端交互
- “产品”作为一级折叠项,点击后在移动导航中展开产品列表。
- 产品卡改为全宽纵向条目,不使用桌面浮层定位。
- 展开按钮使用 `aria-expanded`,列表与按钮通过 `aria-controls` 关联。
- 产品卡最小触控高度 `56px`,与语言、主题和邮件咨询按钮保持足够间距。
### 4.4 产品列表卡文案
**中文**
- 分类标签:`AGENT INFRA / HARNESS`
- 产品名:`荣刻驭境`
- 英文名:`Roncarve Harness`
- 简介:`本地客户端连接本地项目,平台共享工作空间承载团队协同,并内置精准插件检索与高效热插拔。`
- 状态标签:`即将开放`
**英文**
- Product: `Roncarve Harness`
- Description: `A local client for browser-based local projects and a shared workspace for team collaboration, with precise plugin discovery and hot swapping.`
- Status: `Coming Soon`
### 4.5 路由与返回逻辑
- 推荐正式路由:`/products/roncarve-harness/`
- 推荐面包屑:`首页 / 产品 / 荣刻驭境`,视觉上可弱化,但保留语义和返回能力。
- 产品页品牌 Logo 点击返回官网首页;“产品”导航保持当前激活态。
- 现有 Nginx `try_files` 规则支持目录式静态路由,但实现阶段仍需确认构建产物确实生成 `products/roncarve-harness/index.html`
- 上线时同步更新 `sitemap.xml`、canonical、Open Graph 和结构化数据。
## 5. 产品介绍页信息架构
页面采用 6 个内容阶段。粒子舞台在前两屏连续存在,第二屏结束后逐渐退为环境背景,正文接管注意力。
| 阶段 | 建议高度 | 核心任务 | 主要内容 | 粒子状态 |
| --- | ---: | --- | --- | --- |
| 01 Hero | `100svh`,最小 `760px` | 说明产品是什么 | 品牌、主标题、简介、CTA、能力摘要 | 四向入场后汇聚为完整 Logo,不旋转、不展开 |
| 02 Harness 价值 | `100120svh` | 说明为什么需要它 | 三项核心能力与一句方法论 | Logo 展开为空间插件网络,进入景深与旋转 |
| 03 插件引擎 | 内容自适应 | 解释差异化能力 | 精准检索、匹配、热插拔、运行可观测 | 粒子退到背景,形成节点流 |
| 04 双版本 | 内容自适应 | 帮助选择版本 | 本地客户端、团队共享工作空间、协同 | 仅保留低密度环境粒子 |
| 05 使用路径 | 内容自适应 | 建立落地感 | 发现插件 → 装配能力 → 运行 → 团队沉淀 | 局部连线与状态点 |
| 06 CTA | `7090svh` | 推动转化 | 邮件咨询、版本选择、技术资料 | 粒子再次轻度聚拢为品牌轮廓 |
### 5.1 第一屏:完整 Logo 与产品价值
首屏必须在 WebGL 初始化前先显示可读文案,粒子就绪后直接从画面边缘入场,不先闪现完整静态 Logo。静态 Logo 仅用于减少动态效果模式或 WebGL 失败回退。
建议内容:
- 状态标签:`即将开放 / COMING SOON`
- Eyebrow`RONCARVE HARNESS / 荣刻驭境`
- H1`让 Agent 驾驭每一种能力`
- 副标题:`面向大语言模型的可组合 Harness,以定制插件引擎实现精准检索与高效热插拔;个人版在浏览器中操作本地项目,平台版以共享工作空间连接团队协作。`
- 主按钮:`邮件咨询`
- 次按钮:`查看版本能力`
- 能力摘要:`精准插件检索``高效热插拔``个人 / 平台双版本`
英文对应文案:状态为 `COMING SOON`,主按钮为 `Email Us`,次按钮为 `Compare Editions`
桌面端采用左侧文案、右侧/居中偏右 Logo 粒子构图。Logo 不应被正文遮挡,文字区占页面宽度约 `3844%`,空间舞台占 `5662%`
首屏背景由近黑渐变、低对比网格、青蓝环境光晕和边缘暗角组成。网格与光晕是环境层,不随鼠标进行大幅移动。
### 5.2 第二屏:Harness 价值与空间展开
第二屏是本设计的关键说明屏,不允许只展示粒子动画。
标题建议:
> `插件构成能力,Harness 组织运行`
说明建议:
> `荣刻驭境把插件发现、装配、替换和协作放进同一套运行环境,让 Agent 能力可以持续扩展,而不必反复重建。`
三张能力卡:
1. `海量插件精准检索`:从定制插件数据库中按任务、能力、兼容性和工作空间策略检索合适插件。
2. `更高效率热插拔`:在明确生命周期与依赖关系的前提下完成加载、卸载、替换和组合。
3. `本地项目到共享协作`:个人版通过本地客户端操作本地项目;平台版让所有成员进入同一个共享工作空间协同。
滚动进入第二屏后,Logo 的轮廓开始出现深度:外环向不同 Z 层展开,右上角品牌切面形成高亮近景粒子,部分粒子转化为插件节点。卡片依次进入,但不与粒子同时进行大幅运动。
### 5.3 第三屏:定制插件引擎
标题建议:`为插件规模化而优化的引擎`
内容使用四步能力链:
- `发现 / Discover`:连接定制插件数据库,建立能力、版本、依赖与兼容性索引。
- `匹配 / Match`:结合任务语义、运行环境、权限和工作空间策略完成候选筛选。
- `装配 / Compose`:按需加载、卸载和替换插件,降低重新构建 Agent 的成本。
- `观测 / Observe`:记录插件状态、依赖、调用和异常,为团队复用与治理提供依据。
视觉建议使用真实 UI 风格的信息卡、状态标签和节点连线,不使用无含义的伪监控数字。
### 5.4 第四屏:个人版与平台版
采用两张并列版本卡。平台版可以略大,但不使用“推荐”标签和价格信息;产品当前统一显示“即将开放”。
| 能力 | 个人版 Personal | 平台版 Platform |
| --- | --- | --- |
| 部署对象 | 当前设备上的本地项目 | 需要共同协作的团队 |
| 部署形态 | 本地客户端 + 浏览器界面 | 组织级平台服务 |
| 定制插件引擎 | 精准检索、安装、卸载、替换与热插拔 | 同等引擎能力,并增加组织级策略 |
| 插件数据库 | 本地项目索引、收藏与本地配置 | 团队共享目录、可见范围与版本管理 |
| 工作空间 | 本地项目上下文 | 所有成员共享的团队工作空间 |
| 成员与角色 | 本地客户端,不包含多人协同 | 成员、角色与协作权限 |
| 配置复用 | 本地项目预设、导入与导出 | 团队预设、共享插件组合与配置 |
| 运行记录 | 本地项目运行记录 | 团队共享活动与运行历史 |
| 凭据边界 | 本地连接配置保留在当前设备 | 团队工作空间凭据引用,页面不回显明文 |
| CTA | `邮件咨询个人版` | `邮件咨询平台版` |
#### 5.4.1 首发能力决策
**个人版首发范围**
- 采用本地客户端形态,通过浏览器界面操作当前设备上的本地项目。
- 完整使用定制插件引擎与定制插件数据库。
- 支持插件语义检索、筛选、安装、卸载、替换、版本固定和热插拔。
- 本地文件、运行环境、项目预设和运行记录保留在当前设备。
- 不提供团队成员、角色、组织策略和跨用户共享。
**平台版首发范围**
- 面向组织部署,所有成员围绕同一个团队共享工作空间协作。
- 通过成员与角色权限管理项目、策略、插件组合、共享配置和记录。
- 支持组织级插件目录、团队可见范围、版本管理和基础允许列表。
- 支持共享项目、团队预设、插件组合、配置、活动记录和运行上下文。
- 支持工作空间级凭据引用;前端仅展示凭据名称和状态,不回显明文。
**不纳入首发公开承诺**
- SSO、SCIM、跨组织协作和复杂的字段级权限。
- 多级插件审批流、企业审计导出和合规报表。
- 商业计费、资源配额、用量结算与成本中心。
- 多地域高可用、自动容灾和托管云服务等级协议。
这些能力可以进入平台版后续路线图,但“即将开放”页面不将其包装为已具备能力。
### 5.5 第五屏:使用路径
标题建议:`从一个任务,生长为团队能力`
内容顺序:
1. 描述目标或任务。
2. 检索并选择匹配插件。
3. 在运行中装配或替换能力。
4. 个人版将有效配置保存到本地项目;平台版同步到团队共享工作空间。
该段用于把抽象的 Harness 转化为可理解的工作流。若后续已有真实产品截图,应优先替换概念图,不长期使用占位界面。
#### 5.5.1 官方演示环境参考
截至 2026-08-22DeepSeek Harness 官方提供的可验证演示形态是本地 Web UI,而非一个无需配置即可登录的公共 SaaS 演示账号:安装 Node.js 后可运行 `npx @deepseek-ai/dsh web`,默认启动于 `http://127.0.0.1:3080`。官方使用流程是配置模型、选择工作空间、创建会话并运行任务。
内部设计研究可参考以下官方材料:
- [DeepSeek Harness 官方介绍页](https://www.deepseek.com/harness/)
- [官方 Web UI 使用指南](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/index.md)
- [官方插件安装状态界面](https://www.deepseek.com/harness/images/harness/feat-plugin.en.png)
- [官方运行轨迹界面](https://www.deepseek.com/harness/images/harness/trajectory-real-view.en.png)
- [官方页面演示视频](https://www.deepseek.com/harness/videos/demo.mp4)
荣刻驭境可借鉴其“配置模型 → 选择工作空间 → 运行任务 → 查看插件与运行记录”的信息组织,但对外演示必须使用荣刻自己的界面、品牌和数据。建议后续完成一个可运行的最小演示环境并截取四个真实状态:
1. 在定制插件数据库中搜索并筛选插件。
2. 查看已安装插件、依赖、版本和热插拔状态。
3. 在本地项目与团队共享工作空间两种形态中验证配置和成员协作。
4. 查看一次运行的插件调用与活动记录。
在荣刻真实演示尚未完成前,官网保留文字能力卡和粒子视觉,不制作容易被误认为真实产品的伪控制台截图,也不直接把 DeepSeek 官方截图作为荣刻产品截图发布。
### 5.6 第六屏:转化 CTA
标题建议:`让下一套 Agent 能力,从可组合开始`
- 主 CTA`邮件咨询荣刻驭境`
- 次 CTA`咨询个人版 / 平台版`
- 可选链接:`查看技术资料`,仅在资料真实可访问时展示。
所有对外 CTA 统一打开 `contact@roncarve.com`。基础链接为 `mailto:contact@roncarve.com?subject=%5B%E8%8D%A3%E5%88%BB%E9%A9%AD%E5%A2%83%5D%20%E4%BA%A7%E5%93%81%E5%92%A8%E8%AF%A2`;版本卡分别追加“个人版”或“平台版”主题。邮件正文可预填以下字段:关注版本、使用场景、部署环境、团队规模和联系人。页面不再展示尚不存在的预约表单、企业微信或独立申请入口。
## 6. 视觉系统
### 6.1 与现有官网的关系
沿用现有官网:
- 蓝青主色、蓝紫辅助色和近黑背景。
- `Inter / PingFang SC / Microsoft YaHei` 正文字体体系。
- 等宽字体用于标签、状态和技术元信息。
- `10px / 16px / 24px` 圆角层级。
- 低透明边框、克制光晕、明确 `:focus-visible`
- 最大内容宽度 `1200px`,导航高度桌面 `72px`
产品页第一版建议固定为深色艺术方向,不提供浅色粒子主题。官网首页的主题偏好不被清除;进入产品页时可隐藏外观切换,仅保留语言切换,离开产品页后恢复用户原有偏好。这样避免产品页在视觉上承诺一个尚未设计的浅色 3D 场景。
### 6.2 建议颜色令牌
| 用途 | 令牌 | 建议值 |
| --- | --- | --- |
| 页面底色 | `--harness-bg` | `#05070A` |
| 次级底色 | `--harness-bg-soft` | `#07131C` |
| 表面 | `--harness-surface` | `rgba(13, 29, 41, .72)` |
| 主文字 | `--harness-text` | `#F4FBFF` |
| 次文字 | `--harness-text-soft` | `rgba(229, 244, 247, .66)` |
| 主强调 | `--harness-cyan` | `#47E5EF` |
| 高亮强调 | `--harness-cyan-bright` | `#8BFBFF` |
| 深度蓝 | `--harness-blue` | `#42A5FF` |
| 空间紫 | `--harness-violet` | `#8982FF` |
| 边框 | `--harness-line` | `rgba(183, 229, 231, .13)` |
| 强边框 | `--harness-line-strong` | `rgba(139, 251, 255, .34)` |
粒子颜色以青蓝为主,紫色只用于深度和状态过渡。近景粒子更亮但不使用纯白大面积闪烁。
### 6.3 背景与网格
- 基础使用纵向近黑渐变,避免纯黑导致层次丢失。
- 网格建议 `6480px` 单元,线宽 `1px`,透明度控制在 `0.0250.055`
- Hero 光晕集中在 Logo 后方和页面右上区域,正文后方保持较干净的暗色。
- 使用 CSS mask 让网格在页面边缘、正文区域和段落过渡处自然淡出。
- 内容卡片只使用局部玻璃效果,不在全页叠加大面积 `backdrop-filter`
### 6.4 字体与排版
- H1 桌面:`clamp(46px, 5.2vw, 76px)`,字重 `650750`,行高 `1.041.1`
- H2 桌面:`clamp(36px, 4vw, 58px)`
- 正文:桌面 `1719px`,移动端 `16px`,最大行宽 `3642em`
- 技术标签:`1012px` 等宽字体,字距 `0.100.16em`
- 中英文名称第一次出现时并列;正文不反复堆叠双语,语言切换后使用单一语言。
### 6.5 按钮与卡片
- 主按钮使用青色实底和深色文字,延续现有官网 CTA。
- 次按钮使用透明底、低透明边框;悬停时提高边框和背景亮度。
- 卡片采用 `16px` 圆角、`1px` 边框和轻微内高光。
- Hover 位移不超过 `4px`,粒子活跃时卡片动效进一步收敛。
- 所有按钮必须有与 Hover 不同的键盘焦点状态。
## 7. Logo 空间粒子体验
### 7.1 粒子来源
使用现有 `assets/brand-logo-final.png` 的透明通道作为采样掩模。不得凭空绘制近似 Logo。
建议流程:
1. 将 Logo 绘制到离屏 Canvas。
2. 从 alpha 高于阈值的像素中进行分层采样。
3. 生成每个粒子的二维 Logo 目标坐标 `aLogoPosition`
4. 按八个方位生成椭圆形远场入场坐标 `aIntroPosition`,并分配稳定的延迟和弧线路径。
5. 为每个粒子生成稳定随机种子、深度目标、旋转半径和颜色权重。
6. 使用统一的 `uIntro` 进度把粒子从远场坐标平滑汇聚到 Logo 目标坐标;汇聚完成前不启用滚动展开旋转。
入场位置必须是稳定、可复现的 BufferGeometry 属性,不在每帧重新随机。粒子沿八个方向和不同 Z 深度进入,通过错峰延迟与阻尼缓动汇聚;动画完成后才以 Logo 目标坐标为基础叠加轻微漂浮,避免形成无序爆炸或加载即被 3D 投影打散的观感。
### 7.2 滚动状态
| 页面滚动进度 | 粒子混合 | 旋转 | 景深与大小 | 内容状态 |
| ---: | --- | --- | --- | --- |
| `00.06` | 100% 完整 Logo | 无持续旋转 | Z 接近平面,大小统一 | Hero 完整可读 |
| `0.060.22` | Logo 轮廓轻微松动 | 仅轻微 Y 轴偏转 | 少量 Z 深度 | Hero 文案开始上移 |
| `0.220.55` | 向插件节点空间展开 | X/Y/Z 三轴缓慢旋转 | 近大远小、近亮远暗 | 第二屏标题与卡片进入 |
| `0.550.82` | 形成稳定空间网络 | 持续慢旋转 | 景深最明显 | 三项能力完整展示 |
| `0.821.00` | 粒子向背景节点过渡 | 旋转减弱 | 大粒子数量下降 | 进入正文内容 |
滚动进度只驱动“展开强度、空间位置、整体旋转基准、粒子尺寸范围和透明度”。时间 `uTime` 只负责持续但缓慢的微旋转与漂移,避免滚动停止后场景完全冻结。
### 7.3 鼠标排斥
- 仅在 `pointer: fine` 且未启用减少动态效果时开启。
- 将鼠标位置投射到粒子主平面,计算每个粒子到指针的距离。
- 在排斥半径内使用平滑衰减,不使用硬边界。
- 靠近中心的粒子位移更大,边缘粒子渐近归零。
- 指针离开舞台后,粒子使用阻尼回到当前滚动状态对应的目标,而不是回到页面顶部的 Logo 状态。
- 排斥不改变卡片、文字和可点击元素的位置。
建议 Shader 参数:
- 排斥半径:桌面视觉半径约 `90140px`
- 最大位移:Logo 尺寸的 `36%`
- 回弹阻尼:约 `350650ms` 的视觉恢复感。
### 7.4 点击重新聚拢
- 点击粒子画布或非交互背景时触发;点击导航、链接、按钮、卡片和表单不得触发。
- `uGather` 在约 `500800ms` 内升至 1,使粒子重新聚拢为完整 Logo。
- 保持约 `250400ms` 后,`uGather` 回落,粒子返回“当前滚动进度”对应的空间状态。
- 点击不强制滚动回顶部,不打断用户阅读位置。
- 连续点击时复用当前动画,不创建叠加 tween。
- 触屏设备可通过点击舞台触发同样效果,但不实现持续触摸排斥。
### 7.5 透视、大小与亮度
顶点 Shader 根据视图空间 Z 值计算点大小:远处粒子更小,近处粒子更大。片元 Shader 同时根据深度、Logo 权重和指针作用计算透明度与亮度。
推荐属性:
- `aLogoPosition`Logo 聚拢位置。
- `aIntroPosition`:首屏远场入场位置。
- `aIntroDelay`:每个粒子的错峰入场延迟。
- `aIntroBend`:入场弧线的方向与幅度。
- `aSpreadPosition`:空间展开位置。
- `aSeed`:稳定随机种子。
- `aScale`:个体大小扰动。
- `aColorMix`:青、蓝、紫的颜色混合权重。
推荐 uniforms
- `uTime`:持续旋转与漂移。
- `uIntro`:首屏从四周入场并汇聚为 Logo 的统一进度。
- `uScroll`:前两屏滚动进度。
- `uGather`:点击重新聚拢强度。
- `uPointer`:标准化指针坐标。
- `uRepelRadius` / `uRepelStrength`:排斥参数。
- `uPixelRatio`DPR 控制。
- `uReducedMotion`:减少动态效果状态。
点精灵使用柔和圆点或轻微六边形切面,边缘通过 `smoothstep` 淡出;避免每个粒子都是高亮十字星。
### 7.6 初始加载保护
为确保首屏是完整 Logo
- HTML 先渲染静态 Logo 轮廓或品牌 PNG。
- WebGL 初始化完成后,粒子以与静态 Logo 完全一致的位置和尺寸出现。
- 两层短暂交叉淡化,静态 Logo 再退出。
- `uScroll` 初值强制为 0,并在读取实际滚动位置后再同步;若用户通过历史记录恢复到页面中部,则直接进入对应滚动状态,不播放首屏入场。
- 相机初始视角、Logo 平面尺寸和容器比例必须预先确定,不能依赖第一帧之后的随机校正。
## 8. 技术方案
### 8.1 架构决策
已确认引入 React/Vite 构建链。当前仓库首页仍使用原生 HTML、JavaScript 和本地 Three.js 文件;产品页采用隔离的 React/Vite 入口,构建结果输出到现有静态站点目录,不改写首页运行方式。首页导航只负责链接到产品路由,不加载 React、R3F 或 GSAP 包。
推荐结构:
```text
src/harness/
main.tsx
HarnessProductPage.tsx
components/
ProductNav.tsx
ParticleStage.tsx
LogoParticleSystem.tsx
FeatureSection.tsx
EditionComparison.tsx
ProductCTA.tsx
shaders/
logo-particles.vert.glsl
logo-particles.frag.glsl
styles/
harness.css
public/assets/harness/
dist/products/roncarve-harness/
```
根目录新增统一的 `package.json` 与 Vite 配置,开发入口指向 `src/harness/`,生产构建固定输出 `dist/products/roncarve-harness/index.html` 及带内容哈希的资源文件。部署流程需要把该目录与现有首页静态文件一起发布。产品页只维护 React Three Fiber 场景,不再增加一套原生 Three.js 粒子实现。
DeepSeek Harness 官方当前仍将上游标记为开发者预览,并明确提示可能出现破坏性兼容变更。产品研发应固定已验证的上游提交或发行版本,把定制插件引擎、工作空间扩展和上游适配层分开维护,并为升级建立兼容性测试,不直接让生产版本持续追踪上游 `master`
### 8.2 推荐技术栈
- React:产品页组件与内容组织。
- Three.js / React Three Fiber:粒子场景、相机和渲染循环。
- 自定义 Shader:位置混合、排斥、深度尺寸、亮度和点精灵外观。
- GSAP + ScrollTrigger:前两屏滚动进度、内容进入和场景参数编排。
- CSS mask:网格渐隐、画布边缘暗角、章节过渡与局部光晕裁切。
- 原生 CSS variables:复用荣刻品牌颜色、圆角、排版和响应式断点。
### 8.3 组件职责
| 组件 | 职责 |
| --- | --- |
| `HarnessProductPage` | 页面语义结构、语言和 CTA 数据 |
| `ProductNav` | 产品页导航、返回首页、语言切换 |
| `ParticleStage` | Canvas、相机、DPR、可见性和降级策略 |
| `LogoParticleSystem` | Logo 采样、BufferGeometry、自定义 Shader、交互状态 |
| `ScrollController` | 将 ScrollTrigger 进度写入 R3F/Shader 状态 |
| `FeatureSection` | 第二屏及插件引擎内容 |
| `EditionComparison` | 个人版/平台版对比与 CTA |
| `ProductCTA` | 页面底部转化与联系渠道 |
### 8.4 状态优先级
粒子最终位置按以下优先级组合:
1. 基础状态:由 `uScroll` 在 Logo 位置与空间位置之间混合。
2. 时间状态:根据 `uTime` 添加旋转和微漂移,强度受 `uScroll` 控制。
3. 指针状态:在基础目标上叠加局部排斥。
4. 点击状态:`uGather` 临时覆盖前述展开与排斥,使粒子重新聚拢。
这样可以保证点击反馈确定、滚动状态可恢复、指针离开后粒子不会回到错误位置。
### 8.5 ScrollTrigger 编排
- 前两屏外层使用约 `200220svh` 的滚动容器。
- 粒子舞台使用 `position: sticky; top: 0; height: 100svh`,文案层在其上方滚动。
- 使用一个主 ScrollTrigger 计算归一化进度,避免多个 trigger 同时写入相同粒子属性。
- 内容卡片可使用独立 trigger,但只控制 `opacity``transform`
- `scrub` 建议使用轻微阻尼,不追求完全逐像素同步。
- 组件卸载时销毁 ScrollTrigger,防止重复注册。
### 8.6 CSS mask 用法
- 网格层:中心清晰、边缘淡出。
- 粒子画布:正文侧增加暗色遮罩,保证文字对比度。
- 第二屏过渡:让粒子从完整舞台逐步退到背景,而不是突然隐藏 Canvas。
- 卡片高光:仅在上边缘或角部使用渐变 mask,不做全边框跑马灯。
## 9. 响应式设计
| 视口 | 布局 | 粒子策略 | 交互策略 |
| --- | --- | --- | --- |
| `≥ 1200px` | Hero 左文右景,内容宽 `1200px` | `28k42k` 粒子,完整 Z 深度 | 鼠标排斥 + 点击聚拢 + 滚动旋转 |
| `7681199px` | 文案与粒子更接近 1:1 | `18k28k` 粒子,降低近景尺寸 | 保留排斥,减小半径与旋转幅度 |
| `480767px` | 单列,文案在上、Logo 居中 | `8k14k` 粒子,压缩 Z 深度 | 无悬停排斥,点击聚拢,滚动效果减弱 |
| `< 480px` | 单列、按钮全宽或双行 | `6k10k` 或静态回退 | 只保留轻量滚动过渡与点击反馈 |
移动端注意:
- 使用 `svh` 处理浏览器地址栏高度变化。
- Logo 不能被标题和按钮完全覆盖。
- 第二屏卡片纵向排列,先保证文案,再呈现空间效果。
- 不使用横向滚动作为核心阅读方式。
- 触摸滚动时不得因为 Canvas 指针事件造成页面无法滚动。
## 10. 性能与降级
### 10.1 性能目标
- Hero 文案不等待 Three.js 或 Shader 加载即可显示。
- 桌面主流设备目标为稳定接近 `60fps`;移动端目标不低于可感知流畅的 `30fps`
- DPR 桌面建议上限 `1.5`,移动端建议上限 `1.25`
- 粒子舞台离开可视区域后停止持续渲染或降到按需渲染。
- 页面隐藏时暂停渲染。
- Resize 使用 `ResizeObserver`,避免每帧读取布局。
- 不开启 `preserveDrawingBuffer`,不使用大面积实时阴影和后期模糊。
### 10.2 自适应质量
启动时根据视口、DPR、`prefers-reduced-motion` 和基础设备能力选择质量档位:
- High:完整粒子数量、景深和颜色层次。
- Medium:减少粒子与旋转幅度,关闭昂贵混合。
- Low:静态 Logo + CSS 光晕 + 轻量内容入场。
不通过读取敏感或不稳定的设备信息做复杂设备指纹判断。
### 10.3 WebGL 失败回退
- 保留静态品牌 Logo PNG。
- 背景使用 CSS 网格、径向光晕和渐变。
- 页面内容、导航、CTA 和版本对比不依赖 Canvas。
- 发生 `webglcontextlost` 时切换回静态视觉,不反复重建渲染器。
## 11. 无障碍与可用性
- Canvas 使用 `aria-hidden="true"`;所有产品信息由 HTML 文本提供。
- 提供“跳到主要内容”链接。
- 导航产品菜单支持键盘打开、方向移动、`Esc` 关闭和清晰焦点。
- 普通正文对比度目标至少达到 WCAG AA。
- `prefers-reduced-motion: reduce` 下:关闭粒子旋转、鼠标排斥、滚动 scrub 和大幅位移,显示完整静态 Logo。
- 不使用快速闪烁、高频缩放和突然的相机推进。
- 点击聚拢只绑定非交互背景,不抢占按钮和链接事件。
- 所有 CTA 文案应明确说明动作,不使用只有“了解更多”的模糊文本作为唯一按钮。
- 语言切换后同步更新 `lang`、title、description、导航和按钮文案。
## 12. SEO、分析与合规
### 12.1 SEO
建议中文 title
`荣刻驭境 Roncarve Harness|插件驱动的 Agent 运行环境 - 荣刻科技`
建议中文 description
`荣刻驭境是一套面向大语言模型与 Agent 的可组合 Harness,通过定制插件引擎提供精准插件检索、高效热插拔,支持浏览器操作本地项目的个人客户端与团队共享工作空间。`
同时补充:
- canonical`https://www.roncarve.com/products/roncarve-harness/`
- Open Graph 图片:后续制作独立产品分享图,不直接截取动态粒子帧。
- JSON-LD:根据实际发布形态选择 `SoftwareApplication``Product`,不填写未经确认的价格和评分。
- sitemap 新增产品页 URL。
### 12.2 建议分析事件
- `product_menu_open`
- `product_card_click`,参数 `product=roncarve_harness`
- `harness_email_click`,参数 `placement=hero|personal|platform|footer`
- `harness_edition_view`
- `harness_personal_cta_click`
- `harness_platform_cta_click`
粒子悬停和每次点击聚拢不建议上报,避免制造高频、低价值事件。
### 12.3 品牌与许可边界
- 页面可以说明产品的技术来源,但不得使用 DeepSeek 品牌视觉或暗示官方联合、授权或背书。
- 已确认上游允许二次开发;截至 2026-08-22DeepSeek Harness 官方仓库采用 [MIT License](https://github.com/deepseek-ai/deepseek-harness/blob/master/LICENSE)。
- 发布物必须保留上游版权声明和 MIT 许可文本,并建立第三方依赖许可证清单。
- MIT 许可不等同于商标授权。产品页在“定制插件引擎”内容区展示“基于开源 DeepSeek Harness 二次开发的独立产品”,不在页脚重复来源说明;页脚仅保留公司、版权与备案信息。
- “PLUGIN IS ALL YOU NEED”作为首屏滚动展开后第二幕的核心口号,与“插件构成能力,Harness 组织运行”的价值主张共同出现。
- 页面状态固定为“即将开放”;不使用 DeepSeek 的“开发者预览”状态,也不在当前阶段宣称荣刻驭境已开源。
- “兼容某模型”、性能提升比例和插件数量仍需测试数据支撑后才能进入对外文案。
## 13. 验收标准
### 13.1 导航与路由
- [ ] 首页主导航存在“产品 / Products”。
- [ ] 桌面端产品菜单可点击、可键盘操作、可按 `Esc` 关闭。
- [ ] 移动端产品列表可折叠展开。
- [ ] 点击“荣刻驭境”进入 `/products/roncarve-harness/`
- [ ] 产品页 Logo 可返回官网首页,“产品”保持激活态。
### 13.2 首屏粒子
- [ ] 初始加载呈现完整荣刻 Logo,不从随机散点开始。
- [ ] 未滚动时不发生明显 3D 展开或大幅旋转。
- [ ] 开始滚动后,粒子逐渐出现 Z 深度、透视缩放和空间旋转。
- [ ] 远处粒子更小、更暗;近处粒子更大、更亮。
- [ ] 粒子持续缓慢空间旋转,但内容阅读不受干扰。
- [ ] 鼠标靠近产生平滑排斥,离开后回到当前滚动目标。
- [ ] 点击非交互背景时重新聚拢,再恢复当前滚动状态。
### 13.3 内容与第二屏
- [ ] 第二屏明确呈现精准检索、热插拔、个人/平台双版本三项价值。
- [ ] 第二屏进入与粒子展开同步,但文字不依赖 Canvas 才能显示。
- [ ] 后续内容解释插件引擎、版本差异、工作空间和使用路径。
- [ ] Hero、版本卡和页尾 CTA 均可打开 `contact@roncarve.com`,并带有正确的邮件主题。
### 13.4 响应式与无障碍
- [ ] 在 `375px``768px``1280px``1440px` 视口检查布局。
- [ ] 移动端滚动不被 Canvas 阻塞。
- [ ] 减少动态效果模式显示完整静态 Logo,无持续旋转与排斥。
- [ ] WebGL 不可用时页面仍完整可读、可导航、可转化。
- [ ] 所有交互元素具有清晰 Hover、Active 和 Focus 状态。
### 13.5 性能与稳定性
- [ ] Canvas 离开可视区域后暂停或显著降频。
- [ ] 页面隐藏时暂停渲染。
- [ ] 重复进入/离开页面不会叠加 ScrollTrigger 或事件监听器。
- [ ] Resize、语言切换和返回历史位置后粒子状态正确。
- [ ] WebGL context 丢失后能安全降级。
## 14. 实施顺序
### 阶段 A:入口与静态内容
- 新增“产品”导航与产品下拉列表。
- 建立 `/products/roncarve-harness/` 路由。
- 完成产品页语义结构、双语文案、CTA、版本对比与静态视觉。
- 更新 SEO、sitemap 和分享信息。
### 阶段 B:粒子原型
- 以真实品牌 Logo alpha mask 生成粒子。
- 先完成“首屏完整聚拢 → 滚动展开 → 第二屏稳定空间状态”。
- 再增加景深尺寸、亮度、持续旋转和画布遮罩。
### 阶段 C:交互与降级
- 增加鼠标排斥与点击重新聚拢。
- 增加触屏策略、减少动态效果和 WebGL 回退。
- 完成性能档位与可见性暂停。
### 阶段 D:视觉与体验验收
- 同视口比较设计目标与实现截图。
- 检查卡片间距、文字行宽、光晕强度、Logo 比例和滚动节奏。
- 完成键盘、移动端、低性能和 context lost 测试。
## 15. 设计决策记录
| 编号 | 原待确认项 | 最终决策 | 对实现的影响 |
| ---: | --- | --- | --- |
| 1 | CTA 目标 | 仅使用 `contact@roncarve.com` | 所有主要 CTA 使用带预填主题的 `mailto:`,不建设表单 |
| 2 | 产品状态 | 即将开放 | 中英文统一为“即将开放 / Coming Soon”,不提供下载或立即体验按钮 |
| 3 | 版本能力边界 | 采用第 5.4 节首发方案 | 个人版聚焦本地客户端和本地项目;平台版聚焦团队共享工作空间、成员协同、共享配置和活动记录 |
| 4 | 演示环境 | 参考 DeepSeek Harness 官方 Web UI | 研究插件状态、工作空间和运行轨迹;对外只使用荣刻自有界面 |
| 5 | 上游许可 | 已确认允许二次开发 | 保留 MIT 许可与版权声明,并避免商标或官方背书暗示 |
| 6 | 前端构建链 | 引入 React/Vite | 产品页使用 R3F、Shader 和 GSAP;首页继续保持原生静态实现 |
当前设计已无阻塞性待确认项。开发阶段若真实产品能力与第 5.4 节不一致,应先调整公开文案和版本表,再进入上线验收。
## 16. 参考页观察摘要
本设计对 DeepSeek Harness 页面的借鉴限于设计方法:
- 首屏在深色空间背景中直接提出一句核心产品主张。
- 首屏提供明确行动按钮,而不是把视觉效果当作唯一内容。
- 第二屏马上解释 `MODEL + HARNESS` 的关系,并用三张卡片拆解能力。
- 页面使用近黑背景、低透明表面、细边框、胶囊按钮、网格和少量蓝色强调。
- 长页通过连续章节解释设计理念、运行模式和开始使用方式。
荣刻驭境的差异化处理:
- 用荣刻真实 Logo 作为粒子聚拢目标。
- 视觉重点从“插件构成一切”转向“本地项目控制、精准插件检索、高效热插拔和团队共享协同”。
- 沿用荣刻官网蓝青紫色与现有组件语言。
- 第二屏直接连接个人版的本地项目形态与平台版的共享协同价值,强化商业产品定位。
- 采用独立文案、独立结构和独立粒子运动逻辑,不复刻参考页资产。