Files
zk-data-agent/docs/site/app.js
T

266 lines
9.9 KiB
JavaScript

const flows = {
agent: {
kicker: 'AGENT REQUEST / 7 HOPS',
description:
'浏览器的已登录请求由 Web 转换成兼容 OpenAI 的流式调用;Runtime 独立完成上下文构建、模型调用、工具循环和证据校验。',
nodes: ['browser', 'web', 'runtime', 'provider', 'gateway', 'workspace', 'store']
},
identity: {
kicker: 'IDENTITY / SHORT-LIVED & SERVER-SIDE',
description:
'Web 先验证登录用户并签发短期身份;Runtime 验证一次后,在每次 Gateway 工具调用前重新签发,避免长任务复用过期令牌。',
nodes: ['browser', 'web', 'runtime', 'gateway', 'workspace']
},
tool: {
kicker: 'TOOL EXECUTION / POLICY BEFORE DOCKER',
description:
'Runtime 校验并调度工具,Gateway 再执行身份、路径和资源策略,最后才通过 SSH Docker 进入该用户唯一的工作区。',
nodes: ['runtime', 'gateway', 'workspace', 'store']
},
file: {
kicker: 'FILE DELIVERY / AUTHENTICATED STREAM',
description:
'用户在工作区抽屉发起浏览或下载;Web 验证会话后请求 Gateway,文件或归档以流式响应返回,浏览器从不接触内部服务密钥。',
nodes: ['browser', 'web', 'gateway', 'workspace']
}
};
const stages = {
context: {
kicker: 'CONTEXT / recent-visible-v1',
title: '把有限上下文留给真正影响下一步的内容',
description:
'移除旧的工具详情渲染,按模型预算保留最新可见消息,再注入最多 8 条持久记忆。上下文策略和 Agent Loop 分别版本化。',
points: [
'去掉历史消息中的工具 UI 标记',
'最近消息优先,按字符预算截断',
'记忆只作为补充上下文,不直接取得执行权限'
]
},
model: {
kicker: 'MODEL / SERVER-SIDE CATALOG',
title: '模型提出下一步,平台控制它能看到和能花费什么',
description:
'Runtime 根据公开模型 ID 选择固定 provider、上下文预算、输出预算和循环上限。用户不能从前端注入密钥或任意模型参数。',
points: ['支持流式内容与工具调用', '模型和推理强度明确标注', 'provider 差异在传输层统一']
},
validate: {
kicker: 'VALIDATE / NORMALIZE BEFORE TRUST',
title: '先把模型输出变成可约束的数据,再决定是否执行',
description:
'工具名称、参数、批次数量和权限都在执行前检查。一次模型响应最多接受 8 个工具调用,未知或畸形调用会成为可见错误。',
points: ['JSON 参数规范化', '工具白名单与参数校验', '重复无进展调用守卫']
},
schedule: {
kicker: 'SCHEDULER / safe-parallel-v1',
title: '读取可以加速,写入必须保持可预测',
description:
'连续且标记为 parallel_safe 的工具并发执行;任何文件写入、命令或状态变更都会形成串行边界,结果按请求顺序返回模型。',
points: ['只读调用成组并发', '变更操作严格串行', '批次结果仍保持稳定顺序']
},
execute: {
kicker: 'EXECUTE / TOOLS + BOUNDED DELEGATION',
title: '所有能力都通过可记录、可验证的工具边界发生',
description:
'文件、命令、Git、计划和记忆都有结构化工具。Workspace 工具经 Gateway 进入用户容器;只读子任务可以受限委派。',
points: ['工作区工具与状态工具分离', '每次调用记录开始和结果事件', '子 Agent 不得再次委派']
},
evidence: {
kicker: 'EVIDENCE / COMPLETION IS A POLICY',
title: '“完成”不是一句自然语言,而是一组已经发生的事实',
description:
'如果任务要求生成文件、修改代码或给出测量报告,循环必须看到相应的写入、验证和精确结果。证据不足时会继续循环或明确失败。',
points: ['变更后必须有后续验证', '失败命令必须修复并重跑', '报告必须晚于来源或实验结果']
}
};
const statusLabels = {
baseline: '当前基线',
validated: '已验证',
backlog: '待实验'
};
let experimentData = [];
let activeExperimentFilter = 'all';
function activateFlow(flowName) {
const flow = flows[flowName];
if (!flow) return;
document.querySelectorAll('.flow-tab').forEach((button) => {
const active = button.dataset.flow === flowName;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', String(active));
});
document.querySelectorAll('.system-node').forEach((node) => {
node.classList.toggle('is-flow-active', flow.nodes.includes(node.dataset.node));
});
document.querySelector('.system-map')?.classList.add('has-selection');
document.querySelector('#flow-kicker').textContent = flow.kicker;
document.querySelector('#flow-description').textContent = flow.description;
}
function activateStage(stageName) {
const stage = stages[stageName];
if (!stage) return;
document.querySelectorAll('.loop-stage').forEach((button) => {
const active = button.dataset.stage === stageName;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', String(active));
});
document.querySelector('#stage-kicker').textContent = stage.kicker;
document.querySelector('#stage-title').textContent = stage.title;
document.querySelector('#stage-description').textContent = stage.description;
const list = document.querySelector('#stage-points');
list.replaceChildren(
...stage.points.map((point) => {
const item = document.createElement('li');
item.textContent = point;
return item;
})
);
}
function experimentCard(experiment) {
const article = document.createElement('article');
article.className = 'experiment-card';
const meta = document.createElement('div');
meta.className = 'experiment-meta';
const id = document.createElement('span');
id.textContent = experiment.id;
const badge = document.createElement('span');
badge.className = `status-badge status-${experiment.status}`;
badge.textContent = statusLabels[experiment.status];
meta.append(id, badge);
const title = document.createElement('h3');
title.textContent = experiment.title;
const summary = document.createElement('p');
summary.textContent = experiment.summary;
const footer = document.createElement('div');
footer.className = 'experiment-footer';
[experiment.area, experiment.version, ...(experiment.metrics || [])].filter(Boolean).forEach((text) => {
const tag = document.createElement('span');
tag.textContent = text;
footer.append(tag);
});
article.append(meta, title, summary, footer);
return article;
}
function renderExperiments() {
const query = document.querySelector('#experiment-search').value.trim().toLocaleLowerCase('zh-CN');
const filtered = experimentData.filter((experiment) => {
const statusMatches =
activeExperimentFilter === 'all' || experiment.status === activeExperimentFilter;
const haystack = [
experiment.id,
experiment.title,
experiment.summary,
experiment.area,
experiment.version,
...(experiment.metrics || [])
]
.join(' ')
.toLocaleLowerCase('zh-CN');
return statusMatches && (!query || haystack.includes(query));
});
const grid = document.querySelector('#experiment-grid');
if (filtered.length === 0) {
const empty = document.createElement('article');
empty.className = 'experiment-empty';
empty.textContent = '没有匹配的实验记录。';
grid.replaceChildren(empty);
} else {
grid.replaceChildren(...filtered.map(experimentCard));
}
document.querySelector('#experiment-count').textContent =
`${String(filtered.length).padStart(2, '0')} / ${String(experimentData.length).padStart(2, '0')} RECORDS`;
}
async function loadExperiments() {
const grid = document.querySelector('#experiment-grid');
try {
const response = await fetch('./experiments.json', { cache: 'no-cache' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
experimentData = await response.json();
renderExperiments();
} catch (error) {
const failure = document.createElement('article');
failure.className = 'experiment-empty';
failure.textContent = '实验台账暂时无法读取,请查看仓库中的 docs/experiments.md。';
grid.replaceChildren(failure);
document.querySelector('#experiment-count').textContent = 'DATA UNAVAILABLE';
}
}
function setActiveNavigation(sectionId) {
document.querySelectorAll('.nav-link').forEach((link) => {
link.classList.toggle('is-active', link.getAttribute('href') === `#${sectionId}`);
});
}
document.querySelectorAll('.flow-tab').forEach((button) => {
button.addEventListener('click', () => activateFlow(button.dataset.flow));
});
document.querySelectorAll('.loop-stage').forEach((button) => {
button.addEventListener('click', () => activateStage(button.dataset.stage));
});
document.querySelectorAll('.filter-button').forEach((button) => {
button.addEventListener('click', () => {
activeExperimentFilter = button.dataset.filter;
document
.querySelectorAll('.filter-button')
.forEach((candidate) => candidate.classList.toggle('is-active', candidate === button));
renderExperiments();
});
});
document.querySelector('#experiment-search').addEventListener('input', renderExperiments);
const toggle = document.querySelector('.nav-toggle');
const sidebar = document.querySelector('.sidebar');
toggle.addEventListener('click', () => {
const isOpen = sidebar.classList.toggle('is-open');
toggle.setAttribute('aria-expanded', String(isOpen));
});
document.querySelectorAll('.nav-link').forEach((link) => {
link.addEventListener('click', () => {
sidebar.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false');
});
});
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
if (visible[0]) setActiveNavigation(visible[0].target.id);
},
{ rootMargin: '-20% 0px -62% 0px', threshold: [0.05, 0.3] }
);
document.querySelectorAll('main > section[id]').forEach((section) => observer.observe(section));
}
activateFlow('agent');
activateStage('context');
loadExperiments();