Merge remote-tracking branch 'origin/main' into wsh_dev

# Conflicts:
#	backend/api/server.py
This commit is contained in:
hupenglong1
2026-05-25 11:46:44 +08:00
77 changed files with 11155 additions and 2259 deletions
+5 -1
View File
@@ -11,6 +11,7 @@ import {
accountSessionOutputRoot,
accountSessionRoot,
accountSessionScratchpadRoot,
chownAccountPath,
getCurrentAccount,
} from "@/lib/claw-auth";
@@ -334,14 +335,16 @@ async function getLastUserText(
}
async function ensureSessionDirectories(accountId: string, sessionId: string) {
const sessionRoot = accountSessionRoot(accountId, sessionId);
await Promise.all([
mkdir(accountSessionRoot(accountId, sessionId), { recursive: true }),
mkdir(sessionRoot, { recursive: true }),
mkdir(accountSessionInputRoot(accountId, sessionId), { recursive: true }),
mkdir(accountSessionOutputRoot(accountId, sessionId), { recursive: true }),
mkdir(accountSessionScratchpadRoot(accountId, sessionId), {
recursive: true,
}),
]);
await chownAccountPath(accountId, sessionRoot, true);
}
function renderSessionRuntimeContext(accountId: string, sessionId: string) {
@@ -394,6 +397,7 @@ async function saveFilePart(
await mkdir(uploadDir, { recursive: true });
const filePath = path.join(uploadDir, `${Date.now()}-${filename}`);
await writeFile(filePath, bytes);
await chownAccountPath(accountId, filePath);
return [
`[文件已上传] ${filename}`,
+373
View File
@@ -0,0 +1,373 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import type { Metadata } from "next";
import Image from "next/image";
import { notFound } from "next/navigation";
import type { ReactNode } from "react";
const technicalDocs = {
"01-base-runtime": {
title: "基座 Runtime",
file: "01-base-runtime.md",
image: "/doc-assets/technical/01-base-runtime.png",
},
"02-agent-loop": {
title: "Agent Loop",
file: "02-agent-loop.md",
image: "/doc-assets/technical/02-agent-loop.png",
},
"03-tools": {
title: "工具体系",
file: "03-tools.md",
image: "/doc-assets/technical/03-tools.png",
},
"04-skills": {
title: "Skill 体系",
file: "04-skills.md",
image: "/doc-assets/technical/04-skills.png",
},
"05-workspace-memory-observability": {
title: "工作区、运行态和记忆",
file: "05-workspace-memory-observability.md",
image: "/doc-assets/technical/05-workspace-memory-observability.png",
},
"06-product-data": {
title: "product-data",
file: "06-product-data.md",
image: "/doc-assets/technical/06-product-data.png",
},
"07-online-mining-v2": {
title: "online-mining-v2",
file: "07-online-mining-v2.md",
image: "/doc-assets/technical/07-online-mining-v2.png",
},
"08-label-master": {
title: "标签大师",
file: "08-label-master.md",
image: "/doc-assets/technical/08-label-master.png",
},
"09-external-skills": {
title: "外部系统 Skill",
file: "09-external-skills.md",
image: "/doc-assets/technical/09-external-skills.png",
},
"10-memory-research": {
title: "Agent 记忆机制调研与对比",
file: "10-memory-research.md",
image: null,
},
} as const;
type TechnicalDocSlug = keyof typeof technicalDocs;
function getDoc(slug: string) {
if (slug in technicalDocs) {
return technicalDocs[slug as TechnicalDocSlug];
}
return null;
}
function markdownPath(file: string) {
return path.join(
process.cwd(),
"public",
"doc-assets",
"technical-docs",
file,
);
}
function normalizeMarkdown(markdown: string) {
return markdown
.replace(/^\s*#\s+.+\n+/, "")
.replaceAll("](assets/", "](/doc-assets/technical/");
}
function parseInline(text: string): ReactNode[] {
const nodes: ReactNode[] = [];
const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\[([^\]]+)\]\(([^)]+)\))/g;
let cursor = 0;
while (true) {
const match = pattern.exec(text);
if (match === null) break;
if (match.index > cursor) {
nodes.push(text.slice(cursor, match.index));
}
const token = match[0];
const key = `${match.index}-${token}`;
if (token.startsWith("`")) {
nodes.push(<code key={key}>{token.slice(1, -1)}</code>);
} else if (token.startsWith("**")) {
nodes.push(<strong key={key}>{token.slice(2, -2)}</strong>);
} else {
const href = match[3] ?? "";
const isExternal = /^https?:\/\//.test(href);
nodes.push(
<a
href={href}
key={key}
rel={isExternal ? "noopener" : undefined}
target={isExternal ? "_blank" : undefined}
>
{match[2]}
</a>,
);
}
cursor = pattern.lastIndex;
}
if (cursor < text.length) {
nodes.push(text.slice(cursor));
}
return nodes;
}
function splitTableRow(line: string) {
return line
.trim()
.replace(/^\|/, "")
.replace(/\|$/, "")
.split("|")
.map((cell) => cell.trim());
}
function isTableDivider(line: string) {
return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
}
function startsBlock(line: string, nextLine?: string) {
return (
line.startsWith("```") ||
/^#{1,6}\s+/.test(line) ||
/^[-*]\s+/.test(line) ||
/^\d+\.\s+/.test(line) ||
/^>\s?/.test(line) ||
/^!\[[^\]]*]\([^)]+\)\s*$/.test(line) ||
(line.includes("|") && nextLine !== undefined && isTableDivider(nextLine))
);
}
function renderHeading(depth: number, text: string, key: string) {
if (depth === 1) return <h1 key={key}>{parseInline(text)}</h1>;
if (depth === 2) return <h2 key={key}>{parseInline(text)}</h2>;
if (depth === 3) return <h3 key={key}>{parseInline(text)}</h3>;
if (depth === 4) return <h4 key={key}>{parseInline(text)}</h4>;
if (depth === 5) return <h5 key={key}>{parseInline(text)}</h5>;
return <h6 key={key}>{parseInline(text)}</h6>;
}
function renderMarkdown(markdown: string) {
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
const blocks: ReactNode[] = [];
let index = 0;
while (index < lines.length) {
const line = lines[index];
const trimmed = line.trim();
if (!trimmed) {
index += 1;
continue;
}
if (trimmed.startsWith("```")) {
const language = trimmed.slice(3).trim() || "text";
const codeLines: string[] = [];
index += 1;
while (index < lines.length && !lines[index].trim().startsWith("```")) {
codeLines.push(lines[index]);
index += 1;
}
if (index < lines.length) index += 1;
blocks.push(
<div className="project-doc-code-block" key={`code-${index}`}>
<div>{language}</div>
<pre>
<code>{codeLines.join("\n")}</code>
</pre>
</div>,
);
continue;
}
const heading = trimmed.match(/^(#{1,6})\s+(.*)$/);
if (heading) {
blocks.push(
renderHeading(heading[1].length, heading[2], `heading-${index}`),
);
index += 1;
continue;
}
const image = trimmed.match(/^!\[([^\]]*)]\(([^)]+)\)\s*$/);
if (image) {
blocks.push(
<Image
alt={image[1]}
height={900}
key={`image-${index}`}
src={image[2]}
width={1600}
/>,
);
index += 1;
continue;
}
if (
trimmed.includes("|") &&
index + 1 < lines.length &&
isTableDivider(lines[index + 1])
) {
const headers = splitTableRow(trimmed);
index += 2;
const rows: string[][] = [];
while (index < lines.length && lines[index].trim().includes("|")) {
rows.push(splitTableRow(lines[index]));
index += 1;
}
blocks.push(
<table key={`table-${index}`}>
<thead>
<tr>
{headers.map((cell) => (
<th key={cell}>{parseInline(cell)}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.join("|")}>
{row.map((cell) => (
<td key={`${row.join("|")}-${cell}`}>{parseInline(cell)}</td>
))}
</tr>
))}
</tbody>
</table>,
);
continue;
}
if (/^[-*]\s+/.test(trimmed)) {
const items: string[] = [];
while (index < lines.length && /^[-*]\s+/.test(lines[index].trim())) {
items.push(lines[index].trim().replace(/^[-*]\s+/, ""));
index += 1;
}
blocks.push(
<ul key={`ul-${index}`}>
{items.map((item) => (
<li key={item}>{parseInline(item)}</li>
))}
</ul>,
);
continue;
}
if (/^\d+\.\s+/.test(trimmed)) {
const items: string[] = [];
while (index < lines.length && /^\d+\.\s+/.test(lines[index].trim())) {
items.push(lines[index].trim().replace(/^\d+\.\s+/, ""));
index += 1;
}
blocks.push(
<ol key={`ol-${index}`}>
{items.map((item) => (
<li key={item}>{parseInline(item)}</li>
))}
</ol>,
);
continue;
}
if (/^>\s?/.test(trimmed)) {
const quoteLines: string[] = [];
while (index < lines.length && /^>\s?/.test(lines[index].trim())) {
quoteLines.push(lines[index].trim().replace(/^>\s?/, ""));
index += 1;
}
blocks.push(
<blockquote key={`quote-${index}`}>
{parseInline(quoteLines.join(" "))}
</blockquote>,
);
continue;
}
const paragraphLines = [trimmed];
index += 1;
while (
index < lines.length &&
lines[index].trim() &&
!startsBlock(lines[index].trim(), lines[index + 1]?.trim())
) {
paragraphLines.push(lines[index].trim());
index += 1;
}
blocks.push(
<p key={`p-${index}`}>{parseInline(paragraphLines.join(" "))}</p>,
);
}
return blocks;
}
export function generateStaticParams() {
return Object.keys(technicalDocs).map((slug) => ({ slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const doc = getDoc(slug);
if (!doc) return {};
return {
title: `${doc.title} - ZK Data Agent`,
};
}
export default async function TechnicalDocPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const doc = getDoc(slug);
if (!doc) notFound();
const rawMarkdown = await readFile(markdownPath(doc.file), "utf8");
const markdown = normalizeMarkdown(rawMarkdown);
return (
<main className="project-doc-page project-doc-detail-page">
<header className="project-doc-detail-hero">
<nav className="project-doc-nav">
<a href="/doc">ZK Data Agent</a>
<div>
<a href="/doc"></a>
</div>
</nav>
<div className="project-doc-detail-head">
<p className="project-doc-kicker"></p>
<h1>{doc.title}</h1>
<a className="project-doc-back-link" href="/doc">
</a>
</div>
</header>
<section className="project-doc-detail-shell">
<article className="project-doc-article">
{renderMarkdown(markdown)}
</article>
</section>
</main>
);
}
File diff suppressed because it is too large Load Diff
+566
View File
@@ -0,0 +1,566 @@
"use client";
import {
type CSSProperties,
type PointerEvent,
useMemo,
useRef,
useState,
} from "react";
type ToolPosition = {
id: string;
name: string;
x: number;
y: number;
z: number;
type: string;
color: string;
summary: string;
fit: string;
limit: string;
};
type Point3D = {
x: number;
y: number;
z: number;
};
type ProjectionMode = "xy" | "xz" | "yz" | "free";
const tools: ToolPosition[] = [
{
id: "manual",
name: "纯人工",
x: 24,
y: 82,
z: 22,
type: "人主导",
color: "#64748b",
summary: "最懂业务,但查、写、跑、改、整理都靠人推进。",
fit: "复杂边界判断、探索性分析、最终质量把关。",
limit: "执行成本高,流程复用依赖个人习惯和文档质量。",
},
{
id: "web-ai",
name: "网页版 AI",
x: 54,
y: 24,
z: 18,
type: "通用 AI",
color: "#38bdf8",
summary: "适合问答、总结、生成草稿,启动快。",
fit: "通用解释、文本生成、局部判断辅助。",
limit: "不天然接入内部日志、标签规则、标准产物和团队流程。",
},
{
id: "copilot",
name: "IDE Copilot",
x: 62,
y: 34,
z: 28,
type: "编码辅助",
color: "#60a5fa",
summary: "在编辑器里提升局部编码效率。",
fit: "补全、局部函数、单文件修改。",
limit: "更偏代码片段,不负责跨系统数据流和业务产物链。",
},
{
id: "cursor",
name: "Cursor",
x: 76,
y: 46,
z: 38,
type: "IDE Agent",
color: "#818cf8",
summary: "围绕代码仓库做理解、修改和多文件协作。",
fit: "工程代码迭代、仓库内上下文开发。",
limit: "团队业务流程、线上数据、标准导出和共享 Skill 需要另行组织。",
},
{
id: "code-agent",
name: "Claude Code / Codex",
x: 84,
y: 58,
z: 45,
type: "工程 Agent",
color: "#a78bfa",
summary: "能读代码、调工具、执行多步工程任务。",
fit: "本地工程闭环、复杂代码修改、命令执行。",
limit: "强在个人工作区;团队级业务流程沉淀和 Web 多用户协作不是默认形态。",
},
{
id: "notebook",
name: "个人自动化脚本",
x: 66,
y: 74,
z: 48,
type: "个人自动化",
color: "#f59e0b",
summary: "贴近业务数据,能把部分流程脚本化。",
fit: "数据清洗、临时统计、可重复实验。",
limit: "入口、权限、review、产物管理和团队复用通常需要人额外维护。",
},
{
id: "prompt-doc",
name: "经验文档",
x: 38,
y: 62,
z: 60,
type: "经验沉淀",
color: "#10b981",
summary: "能记录规则和经验,便于传播。",
fit: "标签边界、流程说明、操作规范。",
limit: "本身不执行任务,仍需要人把文档转成操作。",
},
{
id: "zk",
name: "ZK Data Agent",
x: 80,
y: 88,
z: 86,
type: "业务 Agent 工作台",
color: "#2563eb",
summary:
"把 Agent Loop、工具执行、业务知识、review 和产物管理组织成团队流程。",
fit: "数据生成、线上挖掘、标签判断、外部系统 Skill 接入。",
limit: "不替代人的业务判断;重点是把判断节点放进可复用流程。",
},
];
const presets: Array<{
mode: ProjectionMode;
name: string;
rotateX: number;
rotateY: number;
}> = [
{ mode: "xy", name: "研发 × 业务", rotateX: 0, rotateY: 0 },
{ mode: "xz", name: "研发 × 复用", rotateX: -90, rotateY: 0 },
{ mode: "yz", name: "业务 × 复用", rotateX: 0, rotateY: -90 },
];
const dimensionCards = [
{
title: "研发执行效率",
text: "读代码、调工具、跑脚本、生成产物。",
},
{
title: "业务嵌入深度",
text: "接住日志、标签体系、数据格式和团队规则。",
},
{
title: "流程可复用度",
text: "把一次解决方案沉淀成团队能继续调用的能力。",
},
];
const frictionCases = [
{ text: "标签边界依赖经验", weight: 5 },
{ text: "格式手工补齐", weight: 4 },
{ text: "规则散在文档", weight: 4 },
{ text: "线上数据难取", weight: 4 },
{ text: "经验靠口口相传", weight: 3 },
{ text: "脚本只在个人机器", weight: 3 },
{ text: "Review 断在中间", weight: 3 },
{ text: "样例难复用", weight: 2 },
{ text: "产物路径分散", weight: 2 },
{ text: "路径和权限反复确认", weight: 2 },
];
const cubeSize = 360;
const cubeCenter = cubeSize / 2;
const stageCenter = 280;
const perspective = 950;
const axisOrigin = { x: -cubeCenter, y: cubeCenter, z: -cubeCenter };
const axisDefinitions = [
{
id: "x",
name: "研发执行效率",
color: "#2563eb",
start: axisOrigin,
end: { x: cubeCenter, y: cubeCenter, z: -cubeCenter },
},
{
id: "y",
name: "业务嵌入深度",
color: "#0f766e",
start: axisOrigin,
end: { x: -cubeCenter, y: -cubeCenter, z: -cubeCenter },
},
{
id: "z",
name: "流程可复用度",
color: "#9333ea",
start: axisOrigin,
end: { x: -cubeCenter, y: cubeCenter, z: cubeCenter },
},
];
const visibleAxesByMode: Record<ProjectionMode, string[]> = {
xy: ["x", "y"],
xz: ["x", "z"],
yz: ["z", "y"],
free: ["x", "y", "z"],
};
function score(tool: ToolPosition) {
return Math.round((tool.x + tool.y + tool.z) / 3);
}
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
function getPointCoordinates(tool: ToolPosition) {
return {
x: (tool.x / 100) * cubeSize - cubeCenter,
y: cubeCenter - (tool.y / 100) * cubeSize,
z: (tool.z / 100) * cubeSize - cubeCenter,
};
}
function projectPoint(
point: Point3D,
rotateX: number,
rotateY: number,
mode: ProjectionMode,
) {
if (mode === "xy") {
return {
x: stageCenter + point.x,
y: stageCenter + point.y,
z: point.z,
scale: 1,
};
}
if (mode === "xz") {
return {
x: stageCenter + point.x,
y: stageCenter - point.z,
z: -point.y,
scale: 1,
};
}
if (mode === "yz") {
return {
x: stageCenter + point.z,
y: stageCenter + point.y,
z: point.x,
scale: 1,
};
}
const angleX = (rotateX * Math.PI) / 180;
const angleY = (rotateY * Math.PI) / 180;
const cosX = Math.cos(angleX);
const sinX = Math.sin(angleX);
const cosY = Math.cos(angleY);
const sinY = Math.sin(angleY);
const afterY = {
x: point.x * cosY + point.z * sinY,
y: point.y,
z: -point.x * sinY + point.z * cosY,
};
const rotated = {
x: afterY.x,
y: afterY.y * cosX - afterY.z * sinX,
z: afterY.y * sinX + afterY.z * cosX,
};
const scale = perspective / (perspective - rotated.z);
return {
x: stageCenter + rotated.x * scale,
y: stageCenter + rotated.y * scale,
z: rotated.z,
scale,
};
}
function toolAnchorStyle(
tool: ToolPosition,
projected: ReturnType<typeof projectPoint>,
) {
const scale = Math.min(1.08, Math.max(0.88, projected.scale));
return {
"--tool-x": `${projected.x}px`,
"--tool-y": `${projected.y}px`,
"--label-scale": `${scale}`,
"--point-color": tool.color,
zIndex: Math.round(500 + projected.z),
} as CSSProperties;
}
function getProjectedAxes(
rotateX: number,
rotateY: number,
mode: ProjectionMode,
) {
const visibleAxes = new Set(visibleAxesByMode[mode]);
return axisDefinitions
.filter((axis) => visibleAxes.has(axis.id))
.map((axis) => {
const start = projectPoint(axis.start, rotateX, rotateY, mode);
const end = projectPoint(axis.end, rotateX, rotateY, mode);
const vectorX = end.x - start.x;
const vectorY = end.y - start.y;
const length = Math.max(1, Math.hypot(vectorX, vectorY));
const labelX = clamp(end.x + (vectorX / length) * 34, 44, 516);
const labelY = clamp(end.y + (vectorY / length) * 34, 32, 528);
return {
...axis,
start,
end,
labelStyle: {
"--axis-label-x": `${labelX}px`,
"--axis-label-y": `${labelY}px`,
"--axis-color": axis.color,
} as CSSProperties,
};
});
}
export function ToolPositionMap() {
const [projectionMode, setProjectionMode] = useState<ProjectionMode>("xy");
const [rotateX, setRotateX] = useState(0);
const [rotateY, setRotateY] = useState(0);
const [selectedId, setSelectedId] = useState("zk");
const [hoveredId, setHoveredId] = useState<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
const dragRef = useRef<{
x: number;
y: number;
rotateX: number;
rotateY: number;
} | null>(null);
const selected = useMemo(
() => tools.find((tool) => tool.id === selectedId) ?? tools[0],
[selectedId],
);
const projectedTools = useMemo(
() =>
tools
.map((tool) => ({
tool,
projected: projectPoint(
getPointCoordinates(tool),
rotateX,
rotateY,
projectionMode,
),
}))
.sort((a, b) => a.projected.z - b.projected.z),
[rotateX, rotateY, projectionMode],
);
const projectedAxes = useMemo(
() => getProjectedAxes(rotateX, rotateY, projectionMode),
[rotateX, rotateY, projectionMode],
);
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
setProjectionMode("free");
dragRef.current = {
x: event.clientX,
y: event.clientY,
rotateX,
rotateY,
};
setIsDragging(true);
event.currentTarget.setPointerCapture(event.pointerId);
};
const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {
if (!dragRef.current) return;
const deltaX = event.clientX - dragRef.current.x;
const deltaY = event.clientY - dragRef.current.y;
setRotateX(dragRef.current.rotateX - deltaY * 0.45);
setRotateY(dragRef.current.rotateY + deltaX * 0.45);
};
const handlePointerEnd = (event: PointerEvent<HTMLDivElement>) => {
dragRef.current = null;
setIsDragging(false);
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
};
return (
<section className="project-doc-section project-doc-position" id="position">
<div className="position-framing">
<div className="position-framing-copy">
<span></span>
<h3 className="position-formula">
<span></span>
<b></b>
<span></span>
<b>×</b>
<span></span>
<b>×</b>
<span></span>
</h3>
<p>
AI
</p>
</div>
<div className="position-dimension-grid">
{dimensionCards.map((card) => (
<div key={card.title}>
<strong>{card.title}</strong>
<p>{card.text}</p>
</div>
))}
</div>
<div className="position-friction-cloud">
<strong></strong>
{frictionCases.map((item) => (
<span className={`is-weight-${item.weight}`} key={item.text}>
{item.text}
</span>
))}
<em></em>
</div>
</div>
<div className="position-map-layout">
<div className="position-stage-card">
<div className="position-controls">
<div>
<span>
{projectionMode === "free"
? "拖动坐标系自由旋转"
: "二维投影视图"}
</span>
<strong>
{projectionMode === "free"
? `X ${Math.round(rotateX)} / Y ${Math.round(rotateY)}`
: presets.find((preset) => preset.mode === projectionMode)
?.name}
</strong>
</div>
<div className="position-preset-row">
{presets.map((preset) => (
<button
className={
projectionMode === preset.mode ? "is-selected" : undefined
}
key={preset.name}
onClick={() => {
setProjectionMode(preset.mode);
setRotateX(preset.rotateX);
setRotateY(preset.rotateY);
}}
type="button"
>
{preset.name}
</button>
))}
</div>
</div>
<div
className={
isDragging ? "position-stage is-dragging" : "position-stage"
}
onPointerCancel={handlePointerEnd}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerEnd}
>
<div className="position-scene">
<div className="position-space-grid" />
<svg
aria-hidden="true"
className="position-axis-overlay"
viewBox="0 0 560 560"
>
{projectedAxes.map((axis) => (
<line
key={axis.id}
stroke={axis.color}
x1={axis.start.x}
x2={axis.end.x}
y1={axis.start.y}
y2={axis.end.y}
/>
))}
</svg>
<div className="position-axis-label-layer">
{projectedAxes.map((axis) => (
<span
className="position-axis-label"
key={axis.id}
style={axis.labelStyle}
>
{axis.name}
</span>
))}
</div>
<div className="position-tool-layer">
{projectedTools.map(({ tool, projected }) => (
<button
className={[
"position-tool-anchor",
tool.id === selectedId ? "is-selected" : "",
tool.id === hoveredId ? "is-hovered" : "",
]
.filter(Boolean)
.join(" ")}
key={tool.id}
onClick={() => setSelectedId(tool.id)}
onBlur={() => setHoveredId(null)}
onFocus={() => setHoveredId(tool.id)}
onMouseEnter={() => setHoveredId(tool.id)}
onMouseLeave={() => setHoveredId(null)}
onPointerDown={(event) => event.stopPropagation()}
style={toolAnchorStyle(tool, projected)}
type="button"
>
<span className="position-ball" />
<span className="position-tool-label">{tool.name}</span>
</button>
))}
</div>
</div>
</div>
</div>
<aside className="position-detail-card">
<div className="position-score">
<span>{selected.type}</span>
<strong>{score(selected)}</strong>
</div>
<h3>{selected.name}</h3>
<p>{selected.summary}</p>
<div className="position-meter-list">
<div>
<span></span>
<i style={{ width: `${selected.x}%` }} />
</div>
<div>
<span></span>
<i style={{ width: `${selected.y}%` }} />
</div>
<div>
<span></span>
<i style={{ width: `${selected.z}%` }} />
</div>
</div>
<dl>
<div>
<dt></dt>
<dd>{selected.fit}</dd>
</div>
<div>
<dt></dt>
<dd>{selected.limit}</dd>
</div>
</dl>
</aside>
</div>
</section>
);
}
+1094 -1620
View File
File diff suppressed because it is too large Load Diff
+117 -15
View File
@@ -1,17 +1,19 @@
import { spawn } from "node:child_process";
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { cookies } from "next/headers";
const AUTH_ROOT = path.join(
const AUTH_STATE_ROOT = path.join(
/* turbopackIgnore: true */ process.cwd(),
"../../.port_sessions/accounts",
);
const USERS_PATH = path.join(AUTH_ROOT, "users.json");
const SESSIONS_PATH = path.join(AUTH_ROOT, "auth_sessions.json");
const USERS_PATH = path.join(AUTH_STATE_ROOT, "users.json");
const SESSIONS_PATH = path.join(AUTH_STATE_ROOT, "auth_sessions.json");
const COOKIE_NAME = "claw_account_session";
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
const SESSION_REFRESH_INTERVAL_MS = 60 * 60 * 12 * 1000;
const LINUX_ACCOUNT_WORKSPACE_NAME = "zk-agent";
type UserRecord = {
id: string;
@@ -51,6 +53,7 @@ export async function registerAccount(username: string, password: string) {
createdAt: new Date().toISOString(),
};
usersFile.users.push(account);
await ensureLinuxAccount(account.id, password);
await writeUsers(usersFile);
await createAccountDirectories(account.id);
return createSession(account);
@@ -65,6 +68,7 @@ export async function loginAccount(username: string, password: string) {
if (!account || !verifyPassword(password, account.passwordHash)) {
throw new Error("账号或密码错误");
}
await ensureLinuxAccount(account.id, password);
await createAccountDirectories(account.id);
return createSession(account);
}
@@ -153,15 +157,18 @@ export async function getCurrentAccount(): Promise<AccountSession | null> {
}
export function accountUploadRoot(accountId: string) {
return path.join(AUTH_ROOT, accountId, "sessions");
return path.join(accountBaseRoot(accountId), "sessions");
}
export function accountOutputRoot(accountId: string) {
return path.join(AUTH_ROOT, accountId, "sessions");
return path.join(accountBaseRoot(accountId), "sessions");
}
export function accountBaseRoot(accountId: string) {
return path.join(AUTH_ROOT, accountId);
if (linuxAccountsEnabled()) {
return path.join("/home", accountId, LINUX_ACCOUNT_WORKSPACE_NAME);
}
return path.join(AUTH_STATE_ROOT, accountId);
}
export function accountSessionInputRoot(accountId: string, sessionId: string) {
@@ -213,15 +220,18 @@ async function setSessionCookie(token: string) {
}
async function createAccountDirectories(accountId: string) {
await Promise.all(
["sessions"].map((name) =>
mkdir(path.join(AUTH_ROOT, accountId, name), { recursive: true }),
),
);
const base = accountBaseRoot(accountId);
await Promise.all([
mkdir(path.join(base, "sessions"), { recursive: true }),
mkdir(path.join(base, "python"), { recursive: true }),
mkdir(path.join(base, "memory"), { recursive: true }),
mkdir(path.join(base, "integrations"), { recursive: true }),
]);
await chownAccountPath(accountId, base, true);
}
async function readUsers(): Promise<UsersFile> {
await mkdir(AUTH_ROOT, { recursive: true });
await mkdir(AUTH_STATE_ROOT, { recursive: true });
try {
return JSON.parse(await readFile(USERS_PATH, "utf8")) as UsersFile;
} catch {
@@ -230,12 +240,12 @@ async function readUsers(): Promise<UsersFile> {
}
async function writeUsers(payload: UsersFile) {
await mkdir(AUTH_ROOT, { recursive: true });
await mkdir(AUTH_STATE_ROOT, { recursive: true });
await writeFile(USERS_PATH, JSON.stringify(payload, null, 2), "utf8");
}
async function readSessions(): Promise<SessionsFile> {
await mkdir(AUTH_ROOT, { recursive: true });
await mkdir(AUTH_STATE_ROOT, { recursive: true });
try {
return JSON.parse(await readFile(SESSIONS_PATH, "utf8")) as SessionsFile;
} catch {
@@ -244,12 +254,20 @@ async function readSessions(): Promise<SessionsFile> {
}
async function writeSessions(payload: SessionsFile) {
await mkdir(AUTH_ROOT, { recursive: true });
await mkdir(AUTH_STATE_ROOT, { recursive: true });
await writeFile(SESSIONS_PATH, JSON.stringify(payload, null, 2), "utf8");
}
function normalizeUsername(username: string) {
const cleanUsername = username.trim().toLowerCase();
if (linuxAccountsEnabled()) {
if (!/^[a-z_][a-z0-9_-]{1,31}$/.test(cleanUsername)) {
throw new Error(
"启用 Linux 账号时,账号只能包含小写字母、数字、下划线或中划线,且必须以小写字母或下划线开头,长度 2-32",
);
}
return cleanUsername;
}
if (!/^[a-z0-9._-]{2,40}$/.test(cleanUsername)) {
throw new Error(
"账号只能包含小写字母、数字、点、下划线或中划线,长度 2-40",
@@ -282,3 +300,87 @@ function shouldRefreshSession(value?: string) {
if (Number.isNaN(timestamp)) return true;
return Date.now() - timestamp > SESSION_REFRESH_INTERVAL_MS;
}
function linuxAccountsEnabled() {
return ["1", "true", "yes", "on"].includes(
(process.env.CLAW_ENABLE_LINUX_ACCOUNTS ?? "").trim().toLowerCase(),
);
}
async function ensureLinuxAccount(accountId: string, password: string) {
if (!linuxAccountsEnabled()) return;
if (process.platform !== "linux") {
throw new Error("CLAW_ENABLE_LINUX_ACCOUNTS=1 只支持 Linux 部署环境");
}
if (typeof process.getuid === "function" && process.getuid() !== 0) {
throw new Error(
"CLAW_ENABLE_LINUX_ACCOUNTS=1 需要 Web 服务以 root 身份运行",
);
}
await runCommand("id", ["-u", accountId], { allowFailure: true }).then(
async (result) => {
if (result.code === 0) return;
await runCommand("useradd", [
"-m",
"-d",
`/home/${accountId}`,
"-s",
"/bin/bash",
accountId,
]);
},
);
await runCommand("chpasswd", [], {
input: `${accountId}:${password}\n`,
});
}
export async function chownAccountPath(
accountId: string,
targetPath: string,
recursive = false,
) {
if (!linuxAccountsEnabled()) return;
await runCommand("chown", [
...(recursive ? ["-R"] : []),
accountId,
targetPath,
]);
}
function runCommand(
command: string,
args: string[],
options: { input?: string; allowFailure?: boolean } = {},
) {
return new Promise<{ code: number; stdout: string; stderr: string }>(
(resolve, reject) => {
const child = spawn(command, args, {
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("error", reject);
child.on("close", (code) => {
const exitCode = code ?? 1;
if (exitCode !== 0 && !options.allowFailure) {
reject(
new Error(
`${command} ${args.join(" ")} 失败:${stderr || stdout || exitCode}`,
),
);
return;
}
resolve({ code: exitCode, stdout, stderr });
});
if (options.input) child.stdin.write(options.input);
child.stdin.end();
},
);
}
@@ -0,0 +1,197 @@
# 01. 基座 Runtime 架构
![基座 Runtime 架构](assets/01-base-runtime.png)
## 1. 基座解决的问题
基座不绑定某一个业务流程。它提供的是通用 Agent runtime
- 多用户 Web 入口。
- 多会话状态管理。
- 模型选择和 OpenAI-compatible 调用。
- Tool registry 和 tool handler 执行。
- Skill 发现、启用和提示词注入。
- 当前会话工作区。
- 运行态、事件流、活动区和刷新恢复。
- 用户记忆和 Skill 记忆后台。
业务能力例如 `product-data``online-mining-v2``label-master` 都跑在这个基座上。
## 2. 文字架构图
```text
浏览器 / Web UI
|
| 用户消息、模型选择、Skill 启用、文件面板、停止运行
v
frontend/app
|
| /api/chat、/api/claw/*、/admin
v
backend/api/server.py
|
| 账号配置、会话目录、run manager、run_state_store、memory_manager
v
LocalCodingAgent
|
| build_session、prompt sections、tool_specs、Agent loop
v
OpenAICompatClient
|
| messages + tools -> model backend
v
模型
|
| assistant text 或 tool_calls
v
Tool Runtime
|
| read/write/python_exec/data_agent/MCP/search/bash
v
session workspace
|
| input / scratchpad / output / session.json
v
前端活动区和文件面板
运行结束后:
AgentRunResult
-> session_store 持久化
-> memory_manager.enqueue_interaction
-> memory worker 异步整理
-> 下一轮 render_injection 注入
```
## 3. 关键代码入口
### 3.1 Web 后端入口
主要文件:
```text
backend/api/server.py
```
关键职责:
- 维护账号和会话配置。
- 创建或复用 `LocalCodingAgent`
- 给 Agent 注入 `runtime_context`、记忆、Jupyter 上下文。
- 记录 run event,并通过 NDJSON streaming 返回前端。
- 运行结束后写入 elapsed、标题、memory event。
关键函数和位置:
```text
backend/api/server.py:691 enabled_skill_names(...)
backend/api/server.py:822 _build_agent(...)
backend/api/server.py:853 agent_for(...)
backend/api/server.py:866 run_lock_for(...)
backend/api/server.py:1720 /api/chat 运行链路开始
backend/api/server.py:1739 memory_manager.render_injection(...)
backend/api/server.py:1751 emit_agent_event(...)
backend/api/server.py:1897 序列化运行结果和 elapsed
backend/api/server.py:1920 memory_manager.enqueue_interaction(...)
backend/api/server.py:2034 StreamingResponse NDJSON
```
### 3.2 Agent Runtime
主要文件:
```text
src/agent_runtime.py
```
关键职责:
- 初始化 tool registry、plugin runtime、MCP runtime、search runtime 等。
- 新会话用 `run(...)`,旧会话用 `resume(...)`
-`_run_prompt(...)` 中执行完整 Agent loop。
- 维护 usage、cost、tool_calls、events、file_history。
- 在结束时持久化 session。
关键函数和位置:
```text
src/agent_runtime.py:158 class LocalCodingAgent
src/agent_runtime.py:195 __post_init__
src/agent_runtime.py:413 run(...)
src/agent_runtime.py:441 resume(...)
src/agent_runtime.py:520 _run_prompt 主链路
src/agent_runtime.py:552 tool_specs = [tool.to_openai_tool() ...]
src/agent_runtime.py:619 for turn_index in range(...)
src/agent_runtime.py:1008 遍历模型返回的 tool_calls
src/agent_runtime.py:1490 _query_model(...)
```
### 3.3 系统提示词
主要文件:
```text
src/agent_prompting.py
```
关键职责:
- 定义中控 Agent 身份。
- 注入工具使用策略。
- 注入工作空间边界。
- 注入 Skill 列表。
- 注入 ask_user 等 runtime 指导。
关键函数和位置:
```text
src/agent_prompting.py:135 get_intro_section
src/agent_prompting.py:155 get_doing_tasks_section
src/agent_prompting.py:182 get_actions_section
src/agent_prompting.py:196 get_workspace_boundary_section
src/agent_prompting.py:210 get_using_your_tools_section
src/agent_prompting.py:274 get_skill_guidance_section
src/agent_prompting.py:414 get_ask_user_guidance_section
```
## 4. 基座的分层职责
```text
Web UI
负责交互、展示、文件面板、活动区、Skill 勾选、管理后台。
Backend API
负责账号、会话、模型配置、运行态、服务端事件流。
Agent Runtime
负责 Agent loop、模型调用、工具调用、预算和持久化。
Prompting
负责把规则、工具、公约、Skill 列表转成模型可见上下文。
Tool Runtime
负责稳定执行动作,并把结果结构化回传给模型。
Skill System
负责让业务流程、知识、脚本可被 Agent 发现和使用。
Workspace
负责隔离每个用户和每个会话的输入、临时文件和交付产物。
Memory Worker
负责从交互历史中异步整理长期偏好和 Skill 使用经验。
```
## 5. 设计边界
基座只应该承载跨业务复用的稳定能力,例如:
- 工具执行。
- 会话状态。
- 运行态事件。
- 工作区路径。
- Skill 发现和启用。
- 模型适配。
- 记忆后台。
业务流程不应该写死在基座里。数据生成、线上挖掘、标签判断等变化快的流程应该沉到 Skill;确定性脚本应该放在对应 Skill 的 `scripts/` 下。
@@ -0,0 +1,246 @@
# 02. Agent Loop 执行机制
![Agent Loop 执行机制](assets/02-agent-loop.png)
## 1. Agent Loop 的基本形态
Agent 每轮不是只调用一次模型,而是一个循环:
```text
用户输入
-> 构造 session 和 prompt
-> 调模型
-> 模型返回 assistant text 或 tool_calls
-> 如果没有 tool_calls:输出最终回复,结束
-> 如果有 tool_calls:执行工具
-> 工具结果写回 session
-> 下一轮模型继续读取工具结果
-> 直到最终回复、预算超限、取消、max_turns 或 review 停止
```
这个循环允许 Agent 做“观察-执行-再观察”的任务,例如:
- 先读文件,再决定是否需要抽取。
- 先生成 plan,等待用户 review。
- 先运行脚本,再根据校验结果修复。
- 先查询线上数据,再抽样展示。
## 2. 新会话和恢复会话
新会话入口:
```text
src/agent_runtime.py:413 run(...)
```
关键动作:
```text
1. 清理当前 managed_agent_id 和 resume_source_session_id。
2. 创建 session_id。
3. 创建 scratchpad_directory。
4. 绑定 plan_runtime/task_runtime 到 scratchpad。
5. 调 _run_prompt(...)
6. 累计 usage。
```
恢复会话入口:
```text
src/agent_runtime.py:441 resume(...)
```
关键动作:
```text
1. 从 StoredAgentSession 恢复 AgentSessionState。
2. 回放 file_history 和 compaction 信息。
3. 设置 active_session_id 和 last_session_path。
4. 恢复 plugin state。
5. 复用已有 scratchpad_directory。
6. 调 _run_prompt(...)
```
这解释了为什么刷新页面、回到旧会话后,Agent 理论上可以继续同一个 session 的上下文,而不是新建一个任务。
## 3. `_run_prompt` 主链路
核心位置:
```text
src/agent_runtime.py:520 _run_prompt 主体
```
主链路关键步骤:
```text
1. slash command 预处理。
2. hook policy / plugin hook 修改 prompt。
3. agent_manager.start_agent(...) 记录运行。
4. 新建或复用 AgentSessionState。
5. runtime_context prepend 到模型可见的用户消息。
6. session.append_user(...)。
7. tool_context 注入 scratchpad、plan_runtime、task_runtime。
8. 生成 tool_specs。
9. 初始化 usage、cost、tool_calls、events。
10. 进入 turn loop。
```
对应代码点:
```text
src/agent_runtime.py:523 agent_manager.start_agent(...)
src/agent_runtime.py:531 session = base_session or build_session(...)
src/agent_runtime.py:539 _prepend_runtime_context(...)
src/agent_runtime.py:543 session.append_user(...)
src/agent_runtime.py:546 replace(self.tool_context, scratchpad_directory=...)
src/agent_runtime.py:552 tool_specs = [tool.to_openai_tool() ...]
src/agent_runtime.py:582 stream_events = _RuntimeEventBuffer(event_sink)
src/agent_runtime.py:619 for turn_index in range(...)
```
## 4. 模型调用和 tool_calls 判断
模型调用入口:
```text
src/agent_runtime.py:1490 _query_model(...)
```
非流式路径中:
```text
turn = self.client.complete(
session.to_openai_messages(),
tool_specs,
output_schema=...
)
```
模型能否返回 `tool_calls` 取决于:
- 当前 `messages`
- 系统提示词。
- Skill 列表。
- 工具描述和参数 schema。
- 模型自身 tool calling 能力。
基座不会强制某个工具被调用。基座只把工具能力暴露给模型,并在模型返回 `tool_calls` 后负责执行。
## 5. 没有 tool_calls 时
如果模型本轮没有工具调用:
```text
src/agent_runtime.py:763 if not turn.tool_calls
```
后续可能有三种情况:
1. 直接输出最终回复。
2. 如果模型输出被截断,自动追加 continuation prompt。
3. 如果达到 continuation 限制,则追加提示并结束。
最终会:
```text
session.append_assistant(...)
_append_final_text_stream_events(...)
AgentRunResult(...)
_persist_session(...)
```
## 6. 有 tool_calls 时
如果模型返回工具调用:
```text
src/agent_runtime.py:1008 for tool_call in turn.tool_calls
```
每个工具调用会:
```text
1. tool_calls 计数 +1。
2. 检查预算。
3. session.start_tool(...) 写入工具开始消息。
4. stream_events 追加 tool_start。
5. 根据工具名执行 handler。
6. 工具结果写回 session。
7. stream_events 追加工具结果。
8. 下一轮模型读取工具结果继续判断。
```
关键代码点:
```text
src/agent_runtime.py:1054 session.start_tool(...)
src/agent_runtime.py:1060 stream_events.append(type='tool_start')
src/agent_tools.py:60 execute_tool(...)
src/agent_tools.py:76 execute_tool_streaming(...)
```
## 7. 为什么 review 可以暂停流程
review 门禁不是特殊 UI 魔法,而是 Agent loop 的自然结果:
1. Skill 要求模型在某一步调用 review 工具。
2. 工具创建 pending state,并返回需要展示的信息。
3. Agent 生成回复,告诉用户需要确认。
4. 当前 run 结束。
5. 用户下一轮回复“确认”。
6. Agent resume 旧 session,调用 confirm 工具。
7. 后续流程继续。
例如 `product-data`
```text
data_agent_prepare_generation_goal
-> pending goal
-> 停止等待用户确认
data_agent_confirm_generation_goal
-> confirmed_goal_id
-> data_agent_prepare_generation_plan
-> pending plan
-> 停止等待用户确认
data_agent_confirm_generation_plan
-> confirmed_plan_id
-> 允许生成 draft 和转换 records
```
## 8. 预算、取消和 max_turns
基座会在模型调用前后、工具请求前检查预算:
```text
src/agent_runtime.py:592 initial_budget = self._check_budget(...)
src/agent_runtime.py:732 budget_after_model = self._check_budget(...)
src/agent_runtime.py:1014 budget_after_tool_request = self._check_budget(...)
```
如果达到最大轮次:
```text
src/agent_runtime.py:1440 _build_max_turns_output(...)
```
输出会包含最后一次阶段说明,提醒用户可以继续补充指令。
取消由后端 run manager 和 tool process registry 处理。Web 点停止后,后端将 run 标记取消,并让工具执行上下文感知 cancel_event。
## 9. 事件流和前端活动区
Agent loop 内会不断追加 `stream_events`,后端用 `event_sink` 把事件推给前端。
后端关键代码:
```text
backend/api/server.py:1751 emit_agent_event(event)
backend/api/server.py:1756 state.run_manager.record_event(...)
backend/api/server.py:1758 state.run_state_store.record_event(...)
backend/api/server.py:2034 StreamingResponse(..., media_type='application/x-ndjson')
```
前端活动区展示的阶段说明、工具开始、工具完成、最终文本,本质上都来自这些 runtime events 或 session replay。
@@ -0,0 +1,298 @@
# 03. 工具体系和 Tool Handler
![工具体系和 Tool Handler](assets/03-tools.png)
## 1. 工具体系的职责
Tool 是执行层。模型只能“请求调用工具”,不能直接执行工具。
后端根据工具名找到 handler,校验参数,执行动作,并把结果写回 session。
工具适合承载:
- 稳定文件读写。
- Python 执行。
- Python 包安装。
- 数据格式转换和校验。
- review 状态机。
- 外部系统访问。
- 需要权限、取消、路径约束或审计的动作。
## 2. Tool registry 构成
入口:
```text
src/agent_tools.py:105 default_tool_registry()
```
它会合并多类工具:
```text
build_file_tools(...)
build_execution_tools(...)
LSP / web_fetch / search / account / config / task / team / workflow 等
Skill 工具
data_agent 工具
```
关键片段:
```text
src/agent_tools.py:107 build_file_tools
src/agent_tools.py:116 build_execution_tools
src/agent_tools.py:1012 AgentTool(name='Skill')
src/agent_tools.py:1033 build_data_agent_tools
```
返回结果是:
```python
return {tool.name: tool for tool in tools}
```
## 3. Tool spec 注入模型
在 Agent loop 中:
```text
src/agent_runtime.py:552
tool_specs = [tool.to_openai_tool() for tool in self.tool_registry.values()]
```
每个工具包含:
```text
name
description
parameters JSON schema
handler
```
模型看到的是 name、description、parameters。handler 不暴露给模型,只在后端执行。
## 4. Tool handler 执行路径
工具执行入口:
```text
src/agent_tools.py:60 execute_tool(...)
src/agent_tools.py:76 execute_tool_streaming(...)
```
执行逻辑:
```text
1. 从 tool_registry 取 tool。
2. 找不到则返回 Unknown tool。
3. bash 走 streaming。
4. 其他工具调用 tool.execute(arguments, context)。
5. 工具结果序列化回模型和前端。
```
## 5. 文件工具
定义位置:
```text
src/agent_tool_specs/files.py
```
主要工具:
- `list_dir`
- `read_file`
- `write_file`
- `edit_file`
- `notebook_edit`
- `glob_search`
- `grep_search`
### 5.1 `write_file` 的边界
`write_file` 当前明确定位为“小文件工具”:
```text
src/agent_tool_specs/files.py
Write or append a SMALL UTF-8 file inside the workspace.
```
适合:
- 短 Markdown。
- 少量配置。
- 小 JSON。
- 少量 JSONL。
- 小 CSV。
不适合:
- 长 Python 脚本。
- 大 JSON。
- JSONL 数据集。
- 长 CSV。
- 大段包含引号和换行的内容。
原因是模型生成工具参数时,需要把内容嵌入 JSON 参数。长文本、引号、换行会显著增加非法 JSON 参数概率。
因此系统提示词也要求:
```text
复杂写文件优先用 python_exec 通过 pathlib/json/csv 写入。
```
## 6. Python 执行工具
定义位置:
```text
src/agent_tool_specs/execution.py
```
主要工具:
- `python_exec`
- `python_package`
- `bash`
- `sleep`
### 6.1 `python_exec`
定位:
```text
结构化文件分析、JSON/JSONL 处理、批量校验、数据抽样、快速计算。
```
支持两种模式:
```text
code
一次性 Python 代码。
script_path
调用项目或 Skill 内已有脚本。
```
重要约束:
- 不用 `bash python ...`
- 不在 `python_exec.code` 里用 subprocess 二次调用 Python 脚本。
- 临时文件写入 `PYTHON_EXEC_SCRATCHPAD`
- 交付产物写入 session/output。
### 6.2 `python_package`
定位:
```text
给当前用户独立 Python venv 安装缺失包。
```
典型场景:
- `pandas`
- `pyarrow`
- `openpyxl`
- `elasticsearch`
- `urllib3`
设计上避免安装到项目 `.venv` 或系统 Python。
### 6.3 `bash`
`bash` 是兜底工具,不是默认 Python 执行方式。
适合:
- git 只读检查。
- 系统命令。
- 进程控制。
- 用户确认后的依赖安装或服务管理。
不适合:
- Python 数据分析。
- JSON/CSV 转换。
- 包安装。
## 7. 路径解析和 session 路由
关键实现:
```text
src/agent_tools.py:1589 _data_agent_output_path(...)
src/agent_tools.py:1624 _data_agent_session_output_root(...)
src/agent_tools.py:1630 _resolve_path(...)
src/agent_tools.py:1658 _session_logical_path(...)
src/agent_tools.py:1685 _execution_cwd(...)
```
逻辑路径会被映射到当前 session:
```text
output/... -> session/output/...
outputs/... -> session/output/...
scratchpad/... -> session/scratchpad/...
scratch/... -> session/scratchpad/...
input/... -> session/input/...
inputs/... -> session/input/...
```
`python_exec` 的执行 cwd 优先是当前会话 scratchpad
```text
src/agent_tools.py:1685 _execution_cwd(...)
```
## 8. data_agent 工具
声明位置:
```text
src/agent_tool_specs/data_agent.py
```
handler 注册位置:
```text
src/agent_tools.py:1033 build_data_agent_tools(...)
```
主要工具分两类:
### 8.1 前链路和 review 状态机
- `data_agent_load_input_sources`
- `data_agent_render_source_context`
- `data_agent_extract_case_evidence`
- `data_agent_prepare_generation_goal`
- `data_agent_confirm_generation_goal`
- `data_agent_prepare_generation_plan`
- `data_agent_show_generation_plan`
- `data_agent_update_generation_plan`
- `data_agent_confirm_generation_plan`
这类工具当前仍属于平台工具,因为它们维护 pending/confirmed 状态,以及产品数据生成的 review 门禁。
### 8.2 历史格式转换工具
- `data_agent_normalize_dataset_draft`
- `data_agent_validate_dataset_records`
- `data_agent_export_dataset_records`
- `data_agent_export_training_jsonl`
- `data_agent_export_planning_eval_csv`
这部分能力已经逐步迁移到 `skills/product-data/scripts/`,平台工具更多是历史兼容和适配层。
## 9. 工具设计原则
当前工具体系的技术取舍:
```text
模型负责决策。
工具负责执行。
Skill 负责流程经验。
脚本负责可迁移确定性能力。
```
工具描述要足够明确,否则模型会选错工具;参数 schema 要尽量简单,否则不同模型后端可能不兼容。
@@ -0,0 +1,256 @@
# 04. Skill 体系和能力包约定
![Skill 体系和能力包约定](assets/04-skills.png)
## 1. Skill 的定位
Skill 是经验层。它不是单纯 prompt,也不是单纯脚本。
一个 Skill 应该回答:
- 什么场景触发。
- 输入材料是什么。
- Agent 应该按什么流程做。
- 哪些地方必须让用户 review。
- 应该调用哪些工具或脚本。
- 产物应该写到哪里。
- 哪些做法是禁止的。
稳定可执行逻辑不应该长期写在 Skill 文本里,而应该进入:
```text
skills/<skill-name>/scripts/
```
或者平台级工具。
## 2. Skill loader 实现
关键文件:
```text
src/bundled_skills.py
```
项目级 Skill 目录:
```text
skills/<skill-name>/SKILL.md
```
核心数据结构:
```text
src/bundled_skills.py:28 BundledSkill
```
字段:
```text
name
description
when_to_use
aliases
allowed_tools
user_invocable
source
path
get_prompt
```
## 3. SKILL.md 解析
解析逻辑:
```text
src/bundled_skills.py:147 _parse_front_matter
src/bundled_skills.py:176 _load_directory_skill
```
`SKILL.md` 必须有 frontmatter
```yaml
---
name: product-data
description: 从产品/标签定义、手写边界规则或示例 query 中提取标签边界...
when_to_use: 当用户提供产品定义、标签规则...
aliases: definition-data, label-data
allowed_tools: read_file, write_file, python_exec
---
```
解析后:
- frontmatter 进入 `BundledSkill` 元数据。
- body 作为真正的 Skill prompt。
- 如果调用 Skill 时带 args,会追加到 `## Invocation Arguments`
对应实现:
```text
src/bundled_skills.py:138 _directory_skill_prompt
```
## 4. Skill 发现顺序
项目 Skill 发现入口:
```text
src/bundled_skills.py:199 load_directory_skills
src/bundled_skills.py:215 load_project_skills
```
系统提示词中可见 Skill 列表由:
```text
src/bundled_skills.py:270 format_skills_for_system_prompt
```
生成。
Web 后端会按当前账号和 session 配置计算启用 Skill
```text
backend/api/server.py:691 enabled_skill_names
backend/api/server.py:706 set_skill_enabled
backend/api/server.py:738 set_all_skills_enabled
```
这意味着:
- Skill 可以存在于项目中,但不一定对某个 session 启用。
- 启用状态影响系统提示词里的 Skill 列表。
- 被禁用的 Skill 不应该被模型主动选择。
## 5. Skill 工具
Skill 本身也是一个工具:
```text
src/agent_tools.py:1012 AgentTool(name='Skill')
```
模型调用:
```json
{
"skill": "product-data",
"args": "用户原始需求或显式参数"
}
```
执行后,Skill body 会被加入对话,让模型按 Skill 中的流程继续做任务。
## 6. Skill 和 Agent Loop 的关系
Skill 不会替代 Agent loop,而是改变 Agent loop 的下一步决策依据。
典型模式:
```text
用户提出任务
-> 模型从 Skill 列表中选择某个 Skill
-> 调用 Skill 工具
-> Skill.md 正文进入上下文
-> 模型按 Skill 指令调用 read_file/python_exec/data_agent 等工具
-> 工具结果进入上下文
-> 模型继续按 Skill 流程推进
```
因此 Skill 的好坏直接影响:
- 模型能否召回正确能力。
- 是否会过早执行。
- 是否能在 review 门禁停下来。
- 是否能使用正确工具而不是手写不稳定逻辑。
## 7. 推荐 Skill 目录结构
```text
skills/<skill-name>/
SKILL.md
README.md
knowledge/
scripts/
examples/
schemas/
tools.yaml
requirements.txt
```
各部分职责:
```text
SKILL.md
运行时入口,写流程、门禁、工具调用方式和禁止事项。
README.md
给维护者看的说明,不一定进入模型上下文。
knowledge/
业务知识、标签规则、字段说明、边界案例。
scripts/
确定性脚本,优先通过 python_exec.script_path 执行。
examples/
示例输入输出,用于回归和讲解。
schemas/
JSON schema 或字段约定。
tools.yaml
描述 portable scripts 如何注册为工具,便于迁移到其他 Agent。
requirements.txt
Skill 脚本的 Python 依赖。
```
## 8. Skill 更新
Web 后端提供 Skill 更新能力:
```text
backend/api/server.py:765 sync_skills_from_git
```
核心行为:
```text
1. 检查当前目录是否是 git 仓库。
2. 检查 tracked 文件是否干净。
3. git fetch origin。
4. git pull --ff-only origin 当前分支。
5. 清理当前账号 agent cache。
6. 重新读取 get_bundled_skills。
```
这让新增或修改 Skill 后,不一定需要重启服务才能让 Skill 列表刷新。
## 9. Skill 设计边界
适合写在 Skill
- 工作流。
- 何时提问。
- 何时 review。
- 哪些工具优先。
- 输出位置约定。
- 常见失败经验。
不适合长期写在 Skill
- 大段可检索知识。
- 复杂代码。
- 格式转换。
- 查询外部系统的具体实现。
- 需要校验的稳定数据结构。
这些应该分别放到:
```text
knowledge/
scripts/
schemas/
platform tools
```
@@ -0,0 +1,243 @@
# 05. 会话工作区、运行态和记忆
![会话工作区、运行态和记忆](assets/05-workspace-memory-observability.png)
## 1. 会话工作区
每个用户、每个会话都有独立目录:
```text
.port_sessions/accounts/<account_id>/sessions/<session_id>/
input/
scratchpad/
output/
session.json
```
目录职责:
```text
input/
用户上传或明确提供的输入资料。
scratchpad/
临时脚本、中间文件、抽样缓存、断点记录。
output/
最终交付产物。
session.json
会话消息、工具调用、usage、events、file_history、runtime metadata。
```
## 2. 工作区路径路由
路径解析实现:
```text
src/agent_tools.py:1658 _session_logical_path
```
逻辑路径:
```text
output/... -> session/output/...
outputs/... -> session/output/...
scratchpad/... -> session/scratchpad/...
scratch/... -> session/scratchpad/...
input/... -> session/input/...
inputs/... -> session/input/...
```
Python 执行 cwd
```text
src/agent_tools.py:1685 _execution_cwd
```
优先使用当前 session scratchpad。这是为了避免临时脚本和缓存污染项目根目录。
## 3. 平台代码写保护
相关实现:
```text
src/agent_tools.py:1118 _PLATFORM_READONLY_DIRS
src/agent_tools.py:1698 _is_platform_code_path
src/agent_tools.py:1708 _ensure_not_platform_code_write
```
当前平台目录:
```text
src
backend
frontend
scripts
```
在数据 Agent 会话中默认视为只读。除非用户明确进入平台开发任务,否则业务任务不应修改平台代码。
## 4. Run 状态和活动区
后端在 `/api/chat` 运行时创建 run record
```text
backend/api/server.py:1720 run_record = state.run_manager.start(...)
backend/api/server.py:1722 state.run_state_store.start(...)
```
运行事件通过 `emit_agent_event` 记录:
```text
backend/api/server.py:1751 emit_agent_event
backend/api/server.py:1756 run_manager.record_event
backend/api/server.py:1758 run_state_store.record_event
```
返回前端:
```text
backend/api/server.py:2034 StreamingResponse
```
前端活动区看到的内容主要来自:
- `run_started`
- `tool_start`
- `tool_result`
- 模型阶段说明
- final text stream events
- run finish/error/cancel 状态
## 5. 会话持久化
运行结束后,后端序列化结果:
```text
backend/api/server.py:2056 _serialize_run_result
backend/api/server.py:2069 _normalize_transcript_entry
```
会话读取:
```text
backend/api/server.py:2094 _serialize_stored_session
```
`agent_runtime` 在多个结束路径都会调用:
```text
_persist_session(session, result)
```
这让刷新后可以恢复:
- 用户消息。
- assistant 文本。
- tool_calls。
- tool result。
- elapsed。
- file_history。
## 6. 记忆体系
实现位置:
```text
src/personal_memory.py
```
### 6.1 文件和数据库
```text
memory.db
user.md
skills/<skill-name>.md
```
常量:
```text
src/personal_memory.py:26 MEMORY_DB_FILENAME
src/personal_memory.py:27 USER_MEMORY_FILENAME
src/personal_memory.py:28 SKILL_MEMORY_DIRNAME
```
### 6.2 注入逻辑
```text
src/personal_memory.py:102 render_injection
```
注入规则:
```text
1. 读取 user.md。
2. 根据 enabled_skill_names 读取对应 skills/<skill>.md。
3. 拼成 # 个性化记忆。
4. 如果用户本轮要求冲突,以本轮要求为准。
```
后端调用:
```text
backend/api/server.py:1739 memory_manager.render_injection(...)
```
### 6.3 入队逻辑
运行结束后:
```text
backend/api/server.py:1920 memory_manager.enqueue_interaction(...)
```
记忆模块内:
```text
src/personal_memory.py:138 enqueue_interaction
src/personal_memory.py:635 detect_memory_signals
```
会检测:
- 显式记忆词:记住、以后、下次、默认、总是、不要、应该、固定。
- 纠错词:不对、不是这样、格式错、之前说过、还是不行。
- Skill/工具/流程/格式相关表述。
- 工具参数非法 JSON 等工具经验。
### 6.4 后台整理
核心逻辑:
```text
src/personal_memory.py:378 _consolidate_events
src/personal_memory.py:419 _generate_memory_updates
src/personal_memory.py:471 _fallback_memory_updates
```
设计取舍:
- 主链路不直接生成记忆。
- 事件先进入 SQLite 队列。
- 后台 worker 批量整理。
- LLM 失败时有 fallback 规则。
- Markdown 是最终可编辑记忆正文。
## 7. 可观测性设计
当前可观测性来自三个层次:
```text
运行态
run_manager + run_state_store,支持运行中刷新、停止、恢复活动区。
会话态
session.json,保存完整消息和工具调用。
产物态
session/input、scratchpad、output,文件面板可查看和下载。
```
这个设计让用户不仅看到最终回复,也能看到 Agent 做了什么、文件在哪里、失败在哪个工具或阶段。
@@ -0,0 +1,292 @@
# 06. product-data Skill 实现
![product-data Skill 实现](assets/06-product-data.png)
## 1. 定位
`product-data` 是数据开发链路的核心 Skill,负责:
```text
产品定义 / 标签规则 / 手写边界 / 示例 query / badcase
-> 输入文本化
-> generation goal
-> 用户 review
-> generation plan
-> 用户确认数量、标签、边界、路径
-> dataset draft text
-> canonical records
-> validate
-> export records / table / training / eval
```
位置:
```text
skills/product-data/SKILL.md
skills/product-data/knowledge/
skills/product-data/scripts/
```
## 2. Skill 目录结构
当前 `SKILL.md` 中定义的能力组织:
```text
skills/product-data/
SKILL.md
tools.yaml
requirements.txt
knowledge/
dataset_draft_v1.md
canonical_record_v1.md
portable_skill_contract.md
schemas/
scripts/
normalize_dataset_draft.py
validate_dataset_records.py
export_dataset_records.py
export_dataset_table.py
export_training_jsonl.py
export_planning_eval_csv.py
```
分工:
```text
SKILL.md
流程协议、门禁、工具调用顺序、禁止事项。
knowledge/
数据草稿、canonical record 和 portable skill contract。
scripts/
确定性转换、校验和导出。
data_agent_* 平台工具
负责输入文本化和 review 状态机。
```
## 3. 输入类型
Skill 将输入分成三类:
```text
文件定义型
产品定义、标签定义、路由规则、表格、Markdown、CSV。
手写规则型
用户直接描述边界,例如“找附近美食给餐饮服务,导航去某地给地图导航”。
示例归纳型
用户只给 query/example/badcase,需要先归纳边界和标签倾向。
```
三类输入最后统一整理成:
```text
dataset_label
target / target_definitions
plan_hint
coverage
exclusions
open_questions
source_refs
complex 规则
```
这一步称为 `generation_goal`
## 4. Review 门禁
`product-data` 有两个门禁模式。
### 4.1 一次确认模式
适用:
- 用户直接给出清晰手写规则。
- target 表达明确。
- 用户已经希望生成数据。
链路:
```text
data_agent_prepare_generation_plan(direct_review=true)
-> 展示目标 + 数量 + 路径
-> 等待“确认,开始生成”
```
### 4.2 两段确认模式
适用:
- 用户提供文件、表格、badcase、长文档。
- 标签、边界、字段有歧义。
- 需要先从资料中抽取 generation goal。
链路:
```text
data_agent_load_input_sources
-> data_agent_render_source_context
-> Agent 整理 generation_goal
-> data_agent_prepare_generation_goal
-> 用户确认目标
-> data_agent_confirm_generation_goal
-> data_agent_prepare_generation_plan
-> 用户确认计划
-> data_agent_confirm_generation_plan
```
## 5. 和 Agent Loop 的关系
`product-data` 明确利用 Agent loop 做分阶段控制。
```text
第一轮:
模型选择 product-data
调输入工具
调 prepare_generation_goal 或 prepare_generation_plan
输出 review 信息
停止
第二轮:
用户确认目标或计划
Agent resume session
调 confirm 工具
继续下一阶段
第三轮:
用户确认计划
Agent 生成 dataset draft
每批调用 normalize 脚本
调 validate 脚本
调 export 脚本
输出最终文件路径
```
重点是:确认状态不是靠自由文本记忆,而是由工具维护 `confirmed_goal_id``confirmed_plan_id`
## 6. 数据格式设计
### 6.1 dataset draft text
面向模型生成,要求模型用较低结构负担描述:
- 用户 / 小爱 对话。
- 当前 query。
- target。
- complex。
- 场景说明。
设计目标是降低模型直接写 JSONL 的难度。
### 6.2 canonical record
面向工具处理,字段稳定。
用于:
- 校验结构。
- 导出流转 CSV。
- 导出训练 JSONL。
- 导出评测 planningPrompt CSV。
### 6.3 complex 独立维度
`complex` 不属于 target。
内部保存:
```text
complex: false
target: Agent(tag="地图导航")
```
训练输出组合:
```text
complex=false
Agent(tag="地图导航")
```
评测 CSV 里:
```text
code标签: Agent(tag="地图导航")
complex: FALSE
```
## 7. 脚本链路
生成式数据确认后,Skill 要求使用 `python_exec.script_path` 调脚本。
典型顺序:
```text
normalize_dataset_draft.py
输入 draft + confirmed_plan_id
输出/追加 scratchpad/normalized_records.jsonl
validate_dataset_records.py
读取 normalized_records.jsonl
校验字段、target、complex、时间戳、多轮上下文
export_dataset_records.py
导出 output/records.jsonl
同时生成 output/records.csv
export_training_jsonl.py
导出 output/training.jsonl
export_planning_eval_csv.py
导出 output/eval_planning.csv
```
为什么不用 `write_file`
- records、CSV、JSONL 都是强格式数据。
- 模型手写容易出错。
- 脚本能统一 timestamp、prev_session、context、complex/target 组合。
## 8. 输出约束
固定逻辑输出:
```text
output/records.jsonl
output/records.csv
output/training.jsonl
output/eval_planning.csv
```
通过路径路由,实际写入:
```text
.port_sessions/accounts/<account>/sessions/<session>/output/
```
Skill 明确禁止:
- 按数据集名创建随机子目录。
- 把 records 写到项目根目录。
-`write_file` 手写最终 JSONL/CSV。
- 在未确认 plan 前生成数据。
## 9. 设计边界
`product-data` 负责:
- 数据目标对齐。
- 生成计划 review。
- dataset draft 生成协议。
- canonical records 转换和导出。
不负责:
- 判断所有标签知识。
- 查询线上数据。
- ELK 检索。
- 模型训练或 git 数据仓库提交。
标签知识应由 `label-master` 辅助,线上数据由 `online-mining-v2` 或相关 Skill 获取。
@@ -0,0 +1,177 @@
# 07. online-mining-v2 Skill 实现
![online-mining-v2 Skill 实现](assets/07-online-mining-v2.png)
## 1. 定位
`online-mining-v2` 用于从线上 ELK 日志中挖掘 case,并把候选样本转换成 `product-data` 兼容的标准数据。
典型链路:
```text
线上挖掘需求
-> 明确目标标签、complex、筛选特征
-> 探索 ELK 表字段
-> 搜索候选
-> 抽样 review
-> 调整策略
-> 直接转 canonical records
-> 或转 product-data 生成补数
```
位置:
```text
skills/online-mining-v2/SKILL.md
skills/online-mining-v2/knowledge/
skills/online-mining-v2/scripts/
```
## 2. 能力组织
`online-mining-v2` 不新增平台注册工具。它把能力放在 Skill 目录下,通过 `python_exec.script_path` 执行。
```text
scripts/
online_mining_common.py
elk_profile_index.py
elk_search_cases.py
elk_fetch_by_request_ids.py
elk_join_request_logs.py
build_dataset_draft.py
build_online_records.py
```
这样做的原因:
- ELK 查询逻辑属于该 Skill 的业务能力。
- 迁移到其他 Agent 时可以直接跑脚本。
- 平台只需要提供通用 `python_exec``python_package`
## 3. 默认数据源
当前重点支持两类索引:
```text
pre-processing*
前处理日志。
适合拿模型 prompt、模型输出、候选 domain、excellent_domains_result。
arch-flat-nlp-log-f-*
主 NLP 日志。
适合拿 query、domain、func、request_id、device_id、device、tts/text/to_speak。
```
Skill 文档中会引导 Agent
- 先用 `elk_profile_index.py` 看字段和样本。
- 再用 `elk_search_cases.py` 根据 query/domain/model output 搜索。
- 必要时用 request_id 做双表 join。
## 4. 和 Agent Loop 的关系
这个 Skill 的交互不是“一次查询结束”,而是策略迭代:
```text
用户描述需求
-> Agent 整理目标标签和筛选特征
-> 如缺目标标签或字段,先问用户
-> python_exec 调 elk_profile_index.py
-> 模型观察字段和样本
-> python_exec 调 elk_search_cases.py
-> 模型观察候选质量
-> 抽样展示给用户 review
-> 用户说哪里不对
-> 调整 filters / regex / domain / model output 条件
-> 再搜索
```
Agent loop 的价值在这里很明显:每次工具结果都会进入上下文,模型可以基于真实候选调整策略。
## 5. 两条分支
`online-mining-v2` 最重要的设计是强制区分两个分支。
### 5.1 直接线上样本分支
适用:
```text
用户想把线上筛出来的真实 case 作为评测集或专项集。
```
行为:
```text
不生成新 query。
不进入 product-data generation plan。
用 build_online_records.py 直接转 canonical records。
```
典型输出:
```text
output/records.jsonl
output/records.csv
```
### 5.2 补充生成分支
适用:
```text
用户想围绕线上问题扩写更多类似 case。
```
行为:
```text
先分析线上 badcase。
整理错误类型和覆盖目标。
再转 product-data 的 generation goal / plan / draft / records 流程。
```
这条边界避免了旧流程里出现的错误:用户只是想“把候选转样本”,Agent 却误走“生成新数据”。
## 6. 和 product-data 的关系
`online-mining-v2` 后处理复用 `product-data` 的标准:
```text
canonical record v1
records.jsonl
records.csv
training.jsonl
eval_planning.csv
```
复用脚本:
```text
skills/product-data/scripts/normalize_dataset_draft.py
skills/product-data/scripts/validate_dataset_records.py
skills/product-data/scripts/export_dataset_records.py
skills/product-data/scripts/export_training_jsonl.py
skills/product-data/scripts/export_planning_eval_csv.py
```
因此线上挖掘和产品定义生成最终可以进入同一套数据格式。
## 7. 设计边界
`online-mining-v2` 负责:
- ELK 字段探索。
- ELK 条件搜索。
- request_id 补全。
- 候选样本 review。
- 线上候选转 canonical records。
不负责:
- 定义 canonical record 标准。
- 生成全新补数。
- 判断复杂标签知识。
- 训练数据提交。
这些分别交给 `product-data``label-master` 或后续数据仓库 Skill。
@@ -0,0 +1,231 @@
# 08. label-master Skill 实现
![label-master Skill 实现](assets/08-label-master.png)
## 1. 定位
`label-master` 是标签知识和边界分析 Skill。
它不把“给 query 打标签”做成一个黑盒工具,而是让 Agent 逐步读取知识、比较候选、解释依据,并在必要时调用脚本校验最终输出格式。
典型链路:
```text
query / 标签边界问题 / target 校验需求
-> 读取决策流程
-> 读取候选召回索引
-> 找 2-5 个候选标签
-> 读取候选标签卡片
-> 命中混淆时读取边界卡
-> 判断 complex / 多指令 / 自动任务等结构维度
-> 判断输出形态
-> 给出推荐、候选、依据、排除项和不确定点
-> 如要落数据,调用 validate_label_output.py
```
位置:
```text
skills/label-master/SKILL.md
skills/label-master/knowledge/
skills/label-master/scripts/
```
## 2. 知识组织
核心目录:
```text
knowledge/
标签总览.md
决策流程.md
索引/
候选召回索引.md
标签索引.md
维度索引.md
label_manifest.json
判断维度/
复杂度/
多指令/
自动任务/
标注输出形态.md
输出能力/
标签/
边界/
边界索引.md
高频混淆/
领域概览/
迁移记录.md
```
设计原则:
```text
索引先行
不直接读全量标签卡。
维度分离
complex、多指令、自动任务不是业务标签。
标签卡片中文维护
方便人工编辑。
输出能力独立
function、intent、object、Agent 包装放在输出能力层。
边界卡优先人工维护
高频混淆写清楚,不只依赖自动迁移总结。
```
## 3. Agent 使用方式
`label-master` 的关键在于利用 Agent 的多轮阅读和规划能力。
典型 Agent loop
```text
模型选择 label-master
-> read_file knowledge/决策流程.md
-> read_file knowledge/索引/候选召回索引.md
-> read_file 候选标签卡片
-> read_file 高频混淆边界卡
-> 必要时 read_file 判断维度/复杂度/*
-> 必要时 read_file 输出能力/*
-> 输出可 review 判断
```
为什么不做成单个 `classify_query(query) -> label` 工具:
- 标签判断常常需要比较候选和排除项。
- function / intent / Agent 包装要看迁移状态。
- complex、多指令、自动任务是独立维度。
- 人类 review 需要看到依据,而不是只看到最终标签。
因此脚本只做索引和校验,不替代语义判断。
## 4. 输出形态
标签知识中存在多种输出形态:
```text
Agent(tag="xxx")
function program
intent
object + function 组合
多指令 JSON
自动任务 JSON
complex=true/false
```
`complex` 是独立维度,不应该混在 target 里。
例如训练输出可以组合成:
```text
complex=false
Agent(tag="地图导航")
```
但内部判断要拆成:
```text
complex: false
target: Agent(tag="地图导航")
```
## 5. 脚本能力
### 5.1 build_label_manifest.py
用途:
```text
把 Markdown 知识生成机器索引。
```
位置:
```text
skills/label-master/scripts/build_label_manifest.py
```
输出:
```text
skills/label-master/knowledge/索引/label_manifest.json
```
使用场景:
- 新增标签卡。
- 修改输出能力。
- 修改判断维度。
- 修改边界知识。
### 5.2 validate_label_output.py
用途:
```text
校验 target/function/intent/Agent/多指令/自动任务输出格式。
```
位置:
```text
skills/label-master/scripts/validate_label_output.py
```
典型调用:
```text
python_exec(script_path="skills/label-master/scripts/validate_label_output.py", args=["--target", "Agent(tag=\"地图导航\")"])
```
批量校验:
```text
--file output/records.jsonl --field target
```
## 6. 和 product-data 的关系
`product-data` 在需要确认 target 或生成边界数据时,可以读取 `label-master` 的知识。
分工:
```text
label-master
判断标签知识、边界、输出形态、target 合法性。
product-data
做数据生成 review、draft、canonical records、导出。
```
当用户给出模糊标签名时,应该先用 `label-master` 辅助确认:
```text
标签是否存在
使用 Agent 包装还是 function
complex 默认是什么
是否有高频混淆边界
```
再进入 `product-data` 的生成计划。
## 7. 设计边界
`label-master` 负责:
- 标签知识检索。
- 候选标签比较。
- 边界解释。
- 输出形态判断。
- target 格式校验。
不负责:
- 生成数据集。
- 线上日志挖掘。
- 导出训练/评测格式。
- 直接替用户确认争议边界。
@@ -0,0 +1,186 @@
# 09. 外部系统 SkillELK、SQL、模型迭代
![外部系统 Skill 接入模式](assets/09-external-skills.png)
## 1. 这类 Skill 的共同特征
外部系统 Skill 的核心不是复杂 prompt,而是把某个内部系统的调用方式、参数约定、依赖和输出格式打包起来。
典型形态:
```text
SKILL.md
写清楚什么时候用、参数怎么选、哪些动作需要确认。
scripts/
执行真实外部系统调用。
python_exec
作为统一执行入口。
python_package
处理依赖缺失。
output/
查询结果或报表写入当前 session output。
```
## 2. elk-fetch
位置:
```text
skills/elk-fetch/SKILL.md
skills/elk-fetch/elk_query.py
```
定位:
```text
按 request id 或查询条件读取小米内网 ELK 日志。
```
实现方式:
- `SKILL.md` 写明支持的 profile 和查询方式。
- 实际执行必须使用 `python_exec.script_path`
-`elasticsearch``urllib3` 时用 `python_package`
- 不让模型用 bash 手写临时 Python 查询脚本。
典型价值:
```text
把“怎么查 ELK”这类个人经验变成团队共享能力。
```
`online-mining-v2` 的区别:
```text
elk-fetch
更偏单次日志查询和调试。
online-mining-v2
更偏批量挖掘、策略迭代、样本转换。
```
## 3. data-factory-sql
位置:
```text
skills/data-factory-sql/SKILL.md
skills/data-factory-sql/run_sql.py
```
定位:
```text
通过 Kyuubi HTTP API 执行数据工场 SQL,轮询状态并下载 CSV 结果。
```
实现方式:
- 用户直接给 SQL 时,可以确认后执行。
- 用户只给分析需求时,Agent 可以先生成 SQL 草稿,再让用户确认。
- 执行必须通过 `python_exec.script_path`
- 输出默认写入当前 session output。
这个 Skill 的门禁重点是:
```text
SQL 执行前确认。
控制查询范围和 limit。
输出路径清晰。
失败时展示错误和可调整建议。
```
## 4. model-iteration
位置:
```text
skills/model-iteration/SKILL.md
skills/model-iteration/scripts/
```
定位:
```text
模型训练、评估、数据准备和迭代流程辅助。
```
这类 Skill 属于混合工程型:
- 有流程。
- 有脚本。
- 有配置。
- 可能调用外部训练平台。
- 高风险动作较多。
因此它更需要明确:
```text
哪些步骤只是分析。
哪些步骤会启动训练。
哪些步骤需要用户确认。
输出目录和实验记录在哪里。
```
## 5. 外部 Skill 的通用约定
### 5.1 调用方式
优先:
```json
{
"script_path": "skills/<skill-name>/scripts/run.py",
"stdin": "{...}",
"timeout_seconds": 120
}
```
或者脚本在 Skill 根目录时:
```json
{
"script_path": "skills/elk-fetch/elk_query.py",
"args": ["..."]
}
```
### 5.2 依赖处理
依赖缺失时:
```text
python_package(action="install", packages=[...])
```
不要:
```text
bash pip install ...
bash python ...
```
### 5.3 输出处理
外部系统查询结果应该:
- stdout 给结构化摘要。
- 大结果写入 `output/``scratchpad/`
- 返回实际文件路径。
- 避免把大量数据直接塞进最终回复。
### 5.4 安全和确认
需要确认的动作:
- 执行大范围 SQL。
- 启动训练。
- 写外部路径。
- 推送代码或数据仓库。
- 导出可能包含敏感字段的线上数据。
只读、小范围、用户明确指定 rid 或 SQL 的查询,可以直接执行,但仍要控制结果规模。
@@ -0,0 +1,478 @@
# 10. Agent 记忆机制调研与 ZK Data Agent 对比
本文整理主流 Agent / AI 产品的记忆实现方式,并对照 ZK Data Agent 当前实现。目标不是判断哪一种“最好”,而是说明不同记忆机制分别解决什么问题,以及为什么我们当前选择“用户记忆 + Skill 使用记忆 + 异步整理队列 + Markdown 可编辑文件”的路线。
调研时间:2026-05-19。
## 1. 结论摘要
主流记忆实现大致分为六类:
| 类型 | 代表 | 核心做法 | 适合场景 |
|------|------|----------|----------|
| 产品级长期记忆 | ChatGPT Memory | 平台自动保存用户偏好和事实,并在后续对话中注入 | 通用个人助手 |
| 会话状态记忆 | OpenAI Agents SDK Sessions、AutoGen Memory | 自动保存历史消息或把外部记忆注入上下文 | 线程连续对话 |
| 文件化项目记忆 | Claude Code `CLAUDE.md`、Claw 基座 memory files | 通过项目/用户级 Markdown 文件向 Agent 注入稳定规则 | 工程项目、团队约定 |
| 图/向量检索记忆 | LangGraph Store、Mem0、Zep/Graphiti | 抽取事实,存入向量库或知识图谱,按语义检索 | 长期、跨会话、海量事实 |
| Agent 自主管理记忆 | Letta / MemGPT | Agent 有显式 memory blocks 和 archival memory,可读写管理 | 状态型 Agent、长期角色 |
| 框架内置任务记忆 | CrewAI | 短期、长期、实体、上下文记忆组合 | 多 Agent 任务协作 |
ZK Data Agent 当前更接近:
```text
文件化项目记忆
+ 产品级用户记忆
+ Skill 作用域记忆
+ 异步记忆整理队列
```
它没有优先做向量库或知识图谱,而是选择 Markdown 文件作为最终记忆正文。这个取舍适合当前团队场景:记忆内容需要能被用户看到、编辑、删除,并且要按 Skill 作用域精准注入。
## 2. 主流实现机制
### 2.1 ChatGPT Memory:产品级个人长期记忆
ChatGPT Memory 的核心是平台级用户记忆。它会保存用户偏好、事实和历史对话中有持续价值的信息,并在后续对话中使用。用户可以查看、管理、删除保存的记忆,也可以关闭相关能力。
机制特点:
- 记忆作用域是用户账号。
- 由产品后台判断哪些内容值得保存。
- 注入方式对用户透明,用户看到的是“助手更了解我”。
- 适合通用个人助手,不适合表达复杂业务流程结构。
和我们的关系:
ZK Data Agent 的“用户记忆”借鉴了这个方向,但没有把全部记忆做成黑盒。我们把最终正文落到 `user.md`,并在 UI 里允许用户编辑。
### 2.2 OpenAI Agents SDK Sessions:会话状态记忆
OpenAI Agents SDK 的 Sessions 主要解决“同一个会话线程里自动保留历史上下文”。开发者不需要每轮手动传入完整历史,Session 会保存对话项,并在下一轮运行时自动带上。
机制特点:
- 更偏 conversation state,而不是长期个人偏好。
- 适合多轮会话连续执行。
- 常见实现是 SQLite / SQLAlchemy / 自定义 session backend。
- 记忆对象主要是消息历史,不是抽象后的长期知识。
和我们的关系:
ZK Data Agent 也有 session 持久化,但我们把它和“长期记忆”分开:
```text
session.json
保存当前会话消息、工具调用、产物和运行事件。
memory/user.md、memory/skills/*.md
保存跨会话长期偏好和 Skill 使用经验。
```
这个区分很重要:会话历史服务“恢复当前任务”,长期记忆服务“下次任务更懂用户和业务”。
### 2.3 Claude Code / OpenClaw / Claw:文件化项目记忆
Claude Code 使用 `CLAUDE.md` 作为项目或用户级记忆文件,常用于保存仓库规则、构建命令、代码风格、项目约定等。OpenClaw / Claw 类 Code Agent 基座通常也会保留这条路线:从全局或工作目录发现记忆文件,并把内容注入上下文。
在当前仓库里,对应实现主要是:
```text
src/agent_context.py
src/session_memory_compact.py
```
其中 `agent_context.py` 负责发现全局和目录级 memory files`session_memory_compact.py` 负责会话压缩场景下的 session memory 摘要。
机制特点:
- 记忆是文本文件,天然可读、可版本化。
- 非常适合工程项目规则和团队约定。
- 注入通常按目录/项目作用域进行。
- 记忆更新更多依赖人工维护,而不是完全自动。
和我们的关系:
ZK Data Agent 继承了“文件化、可编辑、可解释”的优点,但把作用域进一步细分:
```text
user.md
用户级偏好和稳定习惯。
skills/<skill-name>.md
某个 Skill 的使用经验、踩坑、格式偏好和边界修正。
```
也就是说,我们不是只有“项目记忆”,而是增加了“Skill 记忆”这一层。
### 2.4 LangGraph:线程状态 + 长期 Memory Store
LangGraph 把 memory 分成 short-term memory 和 long-term memory。短期记忆通常跟 thread 绑定,用来维持一次会话;长期记忆通过 store 按 namespace 保存,可以跨 thread 召回。它还把长期记忆进一步拆成 semantic、episodic、procedural 等类型。
机制特点:
- thread state 解决会话内上下文。
- store 解决跨会话长期信息。
- 支持按 user id / namespace 组织记忆。
- 长期记忆可以由应用逻辑或 Agent 写入、搜索、更新。
和我们的关系:
ZK Data Agent 当前没有引入通用 Store / VectorStore,而是用文件系统和 SQLite 队列实现一个轻量版本:
```text
namespace = account_id + memory kind + skill_name
storage = Markdown files + SQLite queue
retrieval = user memory always considered, skill memory按启用 Skill 精准注入
```
这比 LangGraph Store 简单,但更直接服务我们当前的 Skill 工作台。
### 2.5 Mem0:独立记忆层
Mem0 更像一个独立 memory layer。典型链路是:从对话中抽取事实,存入记忆系统;后续根据 query 检索相关记忆,再注入给模型。它强调 add / search / update / delete 这类记忆 API,也支持面向用户、Agent、session 等维度组织。
机制特点:
- 记忆层和 Agent 框架解耦。
- 常见存储后端是向量、图或混合检索。
- 强调自动抽取、去重、更新和语义召回。
- 适合大规模个性化 Agent 或跨应用记忆服务。
和我们的关系:
ZK Data Agent 目前没有把记忆做成独立检索服务。原因是我们的高频需求不是“从海量事实里语义搜索”,而是“把少量稳定经验准确注入到对应 Skill”。如果未来 Skill 记忆膨胀,可以在 Markdown 之外增加 Mem0 类似的检索层。
### 2.6 Letta / MemGPTAgent 自主管理内存
Letta 延续 MemGPT 思路,把 Agent 看成有长期状态的主体。它通常区分 core memory 和 archival memorycore memory 是短小、常驻上下文的重要信息;archival memory 是更大的外部记忆空间,Agent 可以通过工具读写。
机制特点:
- Agent 可以主动管理自己的记忆。
- core memory 常驻,archival memory 需要检索。
- 适合长期角色 Agent、个人助理、需要自我状态连续性的 Agent。
- 复杂度更高,需要更强的记忆写入约束和审计。
和我们的关系:
ZK Data Agent 没有让主 Agent 在执行链路里自由修改记忆。我们把记忆写入放到后台 worker,避免主任务因为记忆整理变慢或出错。这是一个更保守的团队平台取舍。
### 2.7 Zep / Graphiti:时间感知知识图谱记忆
Zep / Graphiti 代表的是 temporal knowledge graph 路线:从对话或事件中抽取实体和关系,形成带时间属性的知识图谱。它解决的问题不是简单偏好记忆,而是“事实如何随时间变化”“实体关系如何演进”。
机制特点:
- 记忆结构是实体、关系、事件、时间。
- 适合复杂事实网络和时间演化。
- 检索结果可以包含关系路径和上下文。
- 实现成本和运维复杂度高于 Markdown 或向量检索。
和我们的关系:
标签边界、业务规则、Skill 使用经验目前更适合文本化规则,不一定需要图谱。但如果未来要做“用户、Skill、数据集、标签、错误类型、修复策略”之间的关系分析,图谱路线会有价值。
### 2.8 CrewAI:多 Agent 任务记忆
CrewAI 的记忆体系主要服务多 Agent 协作,通常包含 short-term memory、long-term memory、entity memory 和 contextual memory。它关注的是任务过程中多个 Agent 如何共享上下文和持续改进。
机制特点:
- 和 Crew / Agent / Task 结构绑定。
- 强调任务协作过程中的上下文复用。
- 对实体、任务经验有独立组织方式。
和我们的关系:
ZK Data Agent 当前不是多 Agent 编排优先,而是单个工作台 Agent + Skill 能力包优先。Skill 记忆在某种程度上承担了“任务经验记忆”的角色。
### 2.9 AutoGenMemory 组件注入上下文
AutoGen 的 AgentChat 提供 Memory 抽象,可以把 list memory、vector memory 等组件挂到 AssistantAgent 上。运行时 Memory 会根据消息更新上下文,或把检索结果添加到模型输入。
机制特点:
- Memory 是 Agent 可插拔组件。
- 可以使用简单列表,也可以接向量检索。
- 更偏框架扩展点,而不是产品级记忆管理 UI。
和我们的关系:
ZK Data Agent 的记忆也可以理解为一个可插拔上下文组件,但我们额外做了用户 UI、Skill 作用域和后台队列。
## 3. ZK Data Agent 当前实现
实现入口:
```text
src/personal_memory.py
backend/api/server.py
frontend/app/components/assistant-ui/threadlist-sidebar.tsx
```
### 3.1 存储结构
每个账号有独立记忆目录:
```text
.port_sessions/accounts/<account_id>/memory/
user.md
skills/
<skill-name>.md
memory.db
```
其中:
- `user.md`:用户级长期记忆。
- `skills/<skill>.md`:某个 Skill 的使用记忆。
- `memory.db`:事件队列、状态和 revision 账本。
### 3.2 注入逻辑
模型调用前,后端调用:
```text
memory_manager.render_injection(account_id, enabled_skill_names)
```
注入规则:
```text
用户记忆
账号级,作为长期偏好注入。
Skill 使用记忆
只读取当前启用 Skill 对应的 skills/<skill>.md。
冲突优先级
用户本轮明确要求 > 个性化记忆。
```
这避免了一个常见问题:所有记忆都无差别注入导致上下文污染。
### 3.3 生成时机
每次交互结束后,后端调用:
```text
memory_manager.enqueue_interaction(...)
```
系统不会每轮同步整理记忆,而是先检测信号:
```text
显式记忆词:
记住、以后、下次、默认、总是、不要、应该、固定
纠错词:
不对、不是这样、格式错、之前说过、还是不行
Skill 经验:
skill、工具、流程、格式
工具经验:
模型返回的工具参数不是合法 JSON
```
命中后写入 SQLite pending 队列。显式记忆优先级更高。
### 3.4 异步整理
后台 worker 每 5 秒扫描账号事件,每次最多处理 8 条 pending event
```text
pending -> processing -> done / failed
```
整理方式:
1. 读取已有 `user.md` 和相关 `skills/<skill>.md`
2. 把一批事件交给模型做“整理式合并”。
3. 模型必须输出 JSON
```json
{
"user_memory": "完整 Markdown 或空字符串",
"skill_memories": {
"skill-name": "完整 Markdown"
}
}
```
4. 如果模型输出不可解析,则走 fallback 规则。
5. 写入 Markdown 文件,并更新 revision。
### 3.5 用户可编辑
前端左下角“记忆”入口支持:
- 查看用户记忆行数。
- 查看 Skill 记忆列表。
- 编辑用户记忆。
- 编辑某个 Skill 记忆。
- 查看记忆队列状态。
管理后台只看队列、用量等统计,不展示其他用户具体记忆内容。
## 4. 对比表
| 维度 | ChatGPT | Claude Code / OpenClaw | LangGraph / Mem0 / Zep | Letta | ZK Data Agent |
|------|---------|--------------------|-------------------------|-------|---------------|
| 主要目标 | 个人助手个性化 | 项目规则注入 | 长期检索记忆 | 状态型长期 Agent | 团队 Skill 工作台 |
| 记忆粒度 | 用户 | 用户/项目/目录 | 用户/线程/实体/namespace | Agent memory block | 用户 + Skill |
| 存储形态 | 平台内部 | Markdown 文件 | Store / 向量 / 图 | Core + archival memory | Markdown + SQLite queue |
| 生成时机 | 产品后台自动 | 多为人工维护 | 自动抽取 / API 写入 | Agent 主动管理 | 交互结束后异步整理 |
| 检索方式 | 平台决定 | 直接注入文件 | 语义搜索 / 图检索 | Core 常驻 + archival 检索 | 用户记忆 + 当前 Skill 记忆注入 |
| 可编辑性 | 用户可管理 | 文件可编辑 | 取决于产品/API | 通常需要工具/API | UI 可编辑 Markdown |
| 适合业务流程沉淀 | 中 | 中 | 高,但工程复杂 | 高,但复杂 | 高,且轻量 |
| 风险 | 黑盒、难按业务作用域隔离 | 容易依赖人工维护 | 检索和更新复杂 | 主链路复杂度高 | 暂无语义召回和图谱能力 |
## 5. 为什么当前方案适合我们
### 5.1 我们需要的是 Skill 使用经验,而不只是用户偏好
通用记忆多关注“用户是谁、用户喜欢什么”。我们的高频需求更像:
```text
product-data 生成数据时,用户偏好什么确认流程?
标签大师判断时,哪些边界经常被纠正?
online-mining-v2 查询线上日志时,哪些字段和表更稳定?
某个 Skill 写文件时,模型容易踩什么坑?
```
这些经验天然和 Skill 绑定。因此 `skills/<skill>.md` 比单一用户记忆更准确。
### 5.2 我们需要可审计、可编辑,而不是完全黑盒
团队平台里,记忆不能只存在模型或向量库内部。用户需要能看到:
- 记住了什么。
- 为什么下一次会注入。
- 哪里可以手动修改。
- 哪些记忆是用户级,哪些是 Skill 级。
Markdown 文件在这点上比纯向量库更直接。
### 5.3 主链路不能被记忆整理拖慢
数据生成、线上挖掘、标签判断本身就是长任务。记忆整理如果同步放在主链路里,会增加延迟和失败面。
当前设计是:
```text
主链路:只读取已有记忆 + 入队事件
后台:异步整理、合并、失败重试/记录
```
这和团队生产工具的稳定性要求更匹配。
### 5.4 Skill 作用域注入能降低上下文污染
如果所有记忆每次都注入,模型会被无关偏好干扰。当前只注入启用 Skill 的记忆:
```text
启用 product-data -> 注入 product-data 使用记忆
启用 label-master -> 注入 label-master 使用记忆
未启用某 Skill -> 不注入该 Skill 记忆
```
这使记忆更像“能力使用手册的增量补丁”,而不是一坨全局上下文。
## 6. 当前不足和后续方向
### 6.1 缺少语义召回
当前 Skill 记忆是按 Skill 文件整体注入,不做向量检索。如果某个 Skill 记忆变得很长,可能需要:
- 按章节拆分。
- 引入轻量 embedding 检索。
- 只注入和当前 query 相关的片段。
### 6.2 缺少结构化 schema
Markdown 易编辑,但不方便做强约束。后续可以让 Skill 记忆同时存在:
```text
human.md
structured.json
```
其中 Markdown 给人看,JSON 给程序做筛选和校验。
### 6.3 缺少记忆质量评估
目前能看到队列状态,但还没有系统评估:
- 哪些记忆被注入。
- 注入后是否减少纠错。
- 哪些记忆过期。
- 哪些 Skill 记忆导致误导。
后续可以把 memory revision 与 session outcome 关联起来。
### 6.4 缺少跨用户团队记忆
当前是账号级记忆。团队共性经验仍主要沉淀在 Skill 本体里。未来可以区分:
```text
个人 Skill 记忆
某个用户自己的偏好和使用习惯。
团队 Skill 记忆
多人使用后沉淀的稳定经验,经 review 后合入 Skill。
```
这样可以形成从“个人经验”到“团队 Skill 知识”的晋升路径。
## 7. 建议的技术路线
短期保持当前架构:
```text
Markdown 可编辑记忆
SQLite 异步队列
按 Skill 注入
UI 可查看可修改
后台可观测队列
```
中期增强:
```text
记忆片段化
记忆注入日志
过期/冲突检测
记忆质量指标
```
长期可选:
```text
向量检索:解决 Skill 记忆膨胀后的相关片段召回。
图谱记忆:解决用户、Skill、标签、数据集、错误类型之间的关系分析。
团队记忆晋升:把多用户共性 Skill 经验 review 后写回 Git Skill。
```
## 8. 资料来源
- OpenAI HelpChatGPT Memory FAQ
https://help.openai.com/en/articles/8590148-memory-faq
- OpenAI Agents SDKSessions
https://openai.github.io/openai-agents-python/sessions/
- Anthropic Claude CodeMemory
https://docs.anthropic.com/en/docs/claude-code/memory
- LangChain / LangGraphMemory concepts
https://docs.langchain.com/oss/python/concepts/memory
- Mem0 documentation
https://docs.mem0.ai/
- Letta documentation
https://docs.letta.com/
- Zep / Graphiti documentation
https://help.getzep.com/
- CrewAI Memory concepts
https://docs.crewai.com/concepts/memory
- Microsoft AutoGen AgentChat Memory
https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/memory.html
- ZK Data Agent 当前实现
`src/personal_memory.py``docs/technical-architecture/05-workspace-memory-observability.md`
@@ -0,0 +1,60 @@
# ZK Data Agent 技术架构说明
这组文档先服务于技术 review 和后续讲解材料沉淀,不做宣传表达,不做产品比较。
每个章节尽量对应当前代码里的真实模块、函数和 Skill 实现,后续 `/doc` 页面可以从这里抽取内容做视觉化呈现。
## 阅读顺序
1. [基座 Runtime 架构](01-base-runtime.md)
2. [Agent Loop 执行机制](02-agent-loop.md)
3. [工具体系和 Tool Handler](03-tools.md)
4. [Skill 体系和能力包约定](04-skills.md)
5. [会话工作区、运行态和记忆](05-workspace-memory-observability.md)
6. [product-data Skill 实现](06-product-data.md)
7. [online-mining-v2 Skill 实现](07-online-mining-v2.md)
8. [label-master Skill 实现](08-label-master.md)
9. [外部系统 Skill:ELK、SQL、模型迭代](09-external-skills.md)
10. [Agent 记忆机制调研与对比](10-memory-research.md)
## 一句话定位
ZK Data Agent 的基座是一个 Web 化、多用户、可观测的 Agent runtime。
业务能力通过 `skills/<skill-name>/SKILL.md``knowledge/``scripts/` 组织;稳定执行能力通过 Tool handler 或 Skill 内 portable scripts 承载;每次运行通过 Agent loop 让模型在“判断、调用工具、观察结果、继续判断”之间循环。
## 当前技术主线
```text
Web UI
-> backend/api/server.py
-> LocalCodingAgent
-> agent_prompting 组装系统提示词和 Skill 列表
-> OpenAICompatClient 调模型
-> 模型返回文本或 tool_calls
-> agent_tools 执行 handler
-> session workspace 保存 input/scratchpad/output/session.json
-> run_state_store / event stream 推给前端活动区
-> personal_memory 后台异步整理用户记忆和 Skill 记忆
```
## 代码入口速查
| 主题 | 主要代码 |
|------|----------|
| Agent runtime | `src/agent_runtime.py` |
| 系统提示词 | `src/agent_prompting.py` |
| Tool registry / handler | `src/agent_tools.py``src/agent_tool_specs/` |
| Skill loader | `src/bundled_skills.py` |
| 模型兼容层 | `src/openai_compat.py` |
| Web 后端 | `backend/api/server.py` |
| 会话持久化 | `src/agent_session.py``src/session_store.py` |
| 记忆后台 | `src/personal_memory.py` |
| 数据生成 Skill | `skills/product-data/` |
| 线上挖掘 Skill | `skills/online-mining-v2/` |
| 标签知识 Skill | `skills/label-master/` |
## 后续维护原则
- 如果是在讲“基座怎么运行”,优先改 01-05。
- 如果是在讲“某个 Skill 怎么做事”,优先改 06-09。
- 如果代码实现发生变化,先更新对应章节,再考虑同步 README 或 `/doc` 页面。
- 页面表达、图形素材、演示脚本不写在这里;这里只保留技术事实和设计边界。
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB