diff --git a/CLAUDE.md b/CLAUDE.md index 8f4d49d..609eb5f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,9 +17,9 @@ MVP 为纯前端 React 应用,使用 mock 数据演示。无后端、无真实 ```bash npm run dev # Vite 开发服务器 → http://localhost:5173 npm run build # tsc 类型检查 + vite 构建 -npm test # vitest 运行全部测试(26 个,位于 export.test.ts 和 parseSkill.test.ts) -npm run preview # 预览生产构建 -npx vitest run src/__tests__/export.test.ts # 运行单个测试文件 +npm test # vitest 运行全部测试(2 个文件,export.test.ts + parseSkill.test.ts,共 ~26 个用例) +npm run preview # 预览生产构建 +npx vitest run src/__tests__/export.test.ts # 运行单个测试文件 npx vitest run src/__tests__/parseSkill.test.ts ``` @@ -27,107 +27,112 @@ npx vitest run src/__tests__/parseSkill.test.ts ### 数据模型 (`src/types/protocol.ts`) -`PromptEnvelope { version, model, messages: Message[] }` — 顶层协议文档。 +`PromptEnvelope { version, model?, messages: Message[] }` — 顶层协议文档。可选 `model` 字段用于导出时指定模型名(默认 `"gpt-4-turbo"`)。 -`Message { id, role: "system"|"user"|"assistant"|"tool", segments: Segment[], timestamp }` — 单条聊天消息。 +`Message { id, role: "system"|"user"|"assistant"|"tool", segments: Segment[], timestamp }` — 单条聊天消息。`role: "tool"` 用于独立的工具调用消息。 `Segment` 是基于 `kind` 的可辨识联合类型,共 11 种变体:`text`、`static_var`、`system_prompt`、`memory`、`skills`、`tool_overview`、`tool_call_request`、`tool_call_result`、`document`、`long_text`、`media`。 -部分 Segment 类型(MemorySegment、SkillSegment)有可选 `description` 字段,用于在 UI 中解释这些项的用途。 - -ToolItem 有可选 `schema` 字段(JSON Schema 对象),在工具总览面板中可展开查看。 - -DocumentSegment 有可选 `parsedContent` 字段,点击「查看解析」可展开 AI 对文档内容的提取结果。 +关键类型细节: +- `StaticVarSegment` 有 `name`、`value`、可选 `description` — 会话级变量,如 `{{current_date}}` → `"2026年6月8日"`。**不在消息气泡内渲染**,由 `MessageList.extractSessionVars()` 提取到 `SessionBar` 横栏中 +- `SkillItem` 有 `name`、`description`(L1 始终可见)、`body`(L2 触发时加载的 Markdown 正文)——遵循 Anthropic SKILL.md 规范 +- `ToolItem` 有可选 `schema` 字段(JSON Schema 对象),导出时作为 `tools[].function.parameters` +- `DocumentSegment` 有可选 `parsedContent` 字段,「查看解析」时用 `MarkdownRenderer` 展示 +- `MemorySegment`、`SkillSegment` 有可选 `description` 字段,在 UI 中解释其用途 ### Skills 系统 -Skills 遵循 Anthropic SKILL.md 规范,实现渐进式披露: +遵循 Anthropic SKILL.md 规范,实现渐进式披露。管线: ``` SKILL.md 文件 (YAML frontmatter + Markdown body) │ - ├─ parseSkillMarkdown() → ParsedSkill { name, description, body, ... } - │ (src/utils/parseSkill.ts) YAML 通过极简手写解析器提取(不依赖外部库) + ├─ parseSkillMarkdown() → ParsedSkill { name, description, body, bodyLineCount, ... } + │ (src/utils/parseSkill.ts) 极简手写 YAML 解析器(不依赖外部库),提取顶层 string 键值 │ ├─ skills-loader.ts → PARSED_SKILLS (Record) - │ (src/data/skills-loader.ts) 通过 Vite ?raw 导入 4 个真实 SKILL.md 文件 + │ (src/data/skills-loader.ts) 通过 Vite ?raw 导入 4 个真实 Anthropic SKILL.md 文件 + │ + toSkillItem() / getRealSkills() / getAllRealSkillItems() │ ├─ skills.ts → ALL_SKILLS (Record) - │ (src/data/skills.ts) 8 个内置自定义 skill,用于 Demo C/D/E + │ (src/data/skills.ts) 8 个手写 skill:code-review、deep-research、verify、simplify、 + │ loop、summarize、translate、qa + │ + getSkills(names) 按名称选取 │ - └─ UI: SkillsView → L1 名称+描述(始终可见),L2 body(点击展开) + ├─ demos-loader.ts → buildSkillLookup() 合并两个源 → hydrateSkills(envelope) + │ (src/data/demos/demos-loader.ts) 按 name 匹配,补全 JSON 中只包含 name+description 的 skill items + │ + validateEnvelope() 运行时类型守卫 + │ + loadEnvelope() = validateEnvelope + hydrateSkills + │ + └─ UI: SkillsView → SkillDisclosure 逐项展开:L1 name+description(始终可见),L2 body(点击展开) ``` -**两类 skill 数据源:** -- `src/data/skills.ts` — 8 个手写 SkillItem(code-review、deep-research、verify 等),通过 `getSkills(names)` 按名称选取 -- `src/data/skills-loader.ts` — 4 个真实 Anthropic SKILL.md 文件(webapp-testing、pdf、doc-coauthoring、mcp-builder),通过 `parseSkillMarkdown()` 解析,`getRealSkills(names)` 获取 - -**parseSkillMarkdown 测试:** 位于 `src/__tests__/parseSkill.test.ts`,覆盖 YAML 解析、body 提取、必填字段校验、可选字段、统计信息等 7 个用例。 - ### 渲染管线 +消息在渲染前经过 `MessageList` 两步预处理: + +1. **`extractSessionVars()`** — 从所有消息中提取 `static_var` segment,构建 `varMap`,并将其从消息体中移除。变量展示在 `SessionBar`(对话区顶部横栏) +2. **`extractToolMessages()`** — 将 `tool_call_request`/`tool_call_result` segment 拆分为独立 `role: "tool"` 消息 + +处理后的消息按 Segment kind 分发渲染: + ``` SegmentRenderer (按 segment.kind 分发) - ├─ TextSegmentView → MarkdownRenderer(react-markdown + GFM) - ├─ StaticVarBadge → 蓝色内联 pill 标签 - ├─ SystemPromptView → CollapsiblePanel(灰色,默认折叠) - ├─ MemoryView → CollapsiblePanel(紫色,默认折叠) - ├─ SkillsView → CollapsiblePanel(绿色,默认折叠) - ├─ ToolOverviewView → CollapsiblePanel(橙色,默认折叠,可展开 JSON Schema) - ├─ ToolCallRequestView → 深色终端风格代码块,参数以标签化键值行展示 - ├─ ToolCallResultView → 绿色/红色状态条(失败时默认展开,成功时默认折叠) - ├─ DocumentCard → 文件图标 + 文件名 + 大小 + 摘要预览 - ├─ LongTextView → 折叠态展示前2行预览 + 展开态用 MarkdownRenderer 渲染 - └─ MediaView → 图标 + 类型标签 + alt 文本 + ├─ TextSegmentView → MarkdownRenderer(react-markdown + remark-gfm,所有 HTML 元素自定义 Tailwind) + ├─ static_var → 返回 null(已由 SessionBar 提取) + ├─ SystemPromptView → CollapsiblePanel(灰色),支持 {{var}} 模板可视化渲染 + ├─ MemoryView → CollapsiblePanel(紫色) + ├─ SkillsView → CollapsiblePanel(绿色), SkillDisclosure 逐项逐层展开 + ├─ ToolOverviewView → CollapsiblePanel(橙色),ToolItemRow 可独立展开 JSON Schema + ├─ ToolCallRequestView → 深色终端风格,参数以标签化键值行展示 + 长值折叠 + ├─ ToolCallResultView → 绿/红状态条(失败默认展开,成功默认折叠) + ├─ DocumentCard → 文件图标 + 文件名 + 大小 + snippet,可选「查看解析」展开 Markdown + ├─ LongTextView → 折叠态前2行预览 + 展开态用 MarkdownRenderer 渲染 + └─ MediaView → 图片有 URL 时直接渲染缩略图;加载失败/无URL/非图片则图标占位 ``` -`CollapsiblePanel` 是共享折叠容器组件。折叠/展开状态通过 `useState(segment.collapsed)` 管理——协议数据提供默认值,UI 控制运行时切换。 +`CollapsiblePanel` 是共享折叠容器,接收 `title`、`icon`、`color`、`bgColor`、`defaultCollapsed`、`badge` props。 -`MarkdownRenderer` 封装 `react-markdown` + `remark-gfm`。所有 HTML 元素(h1–h4、p、code、pre、table、blockquote 等)均有自定义 Tailwind 样式,针对聊天气泡内嵌场景做了紧凑化处理。 - -### 状态管理 - -`ChatContext`(`src/context/ChatContext.tsx`)持有当前 `envelope` 和 `activeDemo` 索引。切换 Demo 场景时替换整个 envelope。ChatInput 已禁用(MVP 无真实输入)。 +`MarkdownRenderer` 是所有文本类组的共享 Markdown 渲染组件,覆盖 h1-h4、p、code、pre、table、blockquote、list 等所有常见元素的 Tailwind 样式。 ### 导出管线 (`src/utils/export.ts`) -`exportToOpenAIFormat(envelope)` 输出 OpenAI 兼容 JSON,结构为 `{ model, messages, tools? }`: +`exportToOpenAIFormat(envelope): OpenAIExport` 输出 `{ model, messages: OpenAIMessage[], tools?: OpenAITool[] }`——完整的 OpenAI Chat Completions request body。 -- 结构性 Segment(`system_prompt`、`memory`、`skills`、`tool_overview`)提取为一条 `role: "system"` 的头部消息 -- `tool_overview` 的 items 转为顶层 `tools` 数组(OpenAI function-calling 格式) -- `tool_call_request` → 带 `tool_calls` 数组的 assistant 消息;`tool_call_result` → `role: "tool"` 消息 -- `static_var` 展开为变量值 -- `text` 和 `long_text` 原文输出 -- `media` 用 `altText` 作为回退文本 -- `segmentToText()` 对结构性/tool Segment 返回 `null`——它们在消息级别统一处理 +关键映射: +- `system_prompt`/`memory`/`skills` → 合并为一条 leading `role: "system"` 消息 +- `system_prompt` 中的 `{{var}}` 占位符在导出时展开 +- `tool_overview` → 顶层 `tools[]`,`item.schema` 映射为 `tool.function.parameters` +- `tool_call_request` → `role: "assistant"` 消息带 `tool_calls[]`(自动生成 call_id) +- `tool_call_result` → 独立 `role: "tool"` 消息,通过 call_id 与请求配对 +- `static_var` → user/assistant 消息中展开为值;system 消息中只参与模板展开,**不**直接出现在 system 消息正文中 +- 含图片 media 的 user 消息 → 多模态 `content` 数组 (`[{type: "text"}, {type: "image_url"}]`) +- `document` / `long_text` → 原文输出(带 `[Document: ...]` 标注) +- `segmentToText()` 对结构性/tool Segment 返回 `null`——在消息级别统一处理 -### Demo 数据 (`src/data/demos.ts` → `src/data/demos/`) +### Demo 数据 (`src/data/demos/`) -6 个场景(A–F),每个场景是一个完整的 `PromptEnvelope`,对话内容为中文。默认激活索引 4(场景 F——真实 Anthropic Skills 加载)。 +6 个场景(A–F),每个是独立的 JSON 文件(`demo-{a..f}.json`),通过 `manifest.json` 索引。默认激活场景 A(`"default": true` 在 manifest 中标记为场景 F,但 ChatContext 初始化为 `useState(0)`)。 -| 索引 | 场景 | 内容 | -|------|--------|---------------------------------------------------| -| 0 | 场景 A | 基础对话 + System Prompt + Memory | -| 1 | 场景 B | 工具调用:请求 → 执行(成功 & 失败) | -| 2 | 场景 C | 文档解析:点击「查看解析」看 AI 如何提取文档内容 | -| 3 | 场景 D | 综合:覆盖全部 11 种 Segment | -| 4 | 场景 F | 真实 Anthropic Skills(SKILL.md 文件加载 + parseSkillMarkdown 解析) | -| 5 | 场景 E | 日志分析:异常检测 + 安全审计 + 性能分析 | +加载流程:`JSON import (unknown) → validateEnvelope() → hydrateSkills() → PromptEnvelope` -每个场景文件位于 `src/data/demos/demo-{a..f}.ts`,`demos.ts` 仅做聚合导出。 +### 状态管理 + +`ChatContext`(`src/context/ChatContext.tsx`)持有 `envelope`、`activeDemo`、`demos`。切换场景时替换整个 envelope。默认激活索引 0(场景 A)。ChatInput 已禁用。 ### 颜色系统 -| 颜色 | 对应 Segment | -|--------|--------------------------------------| -| 蓝色 | `static_var` | -| 灰色 | `system_prompt` | -| 紫色 | `memory` | -| 绿色 | `skills` | -| 橙色 | `tool_overview` | -| 深色 | `tool_call_request`(代码块) | -| 绿/红 | `tool_call_result`(成功/失败) | +| 颜色 | Segment / 用途 | +|--------|-------------------------------------| +| 蓝色 | `static_var`(SessionBar + badge) | +| 灰色 | `system_prompt` | +| 紫色 | `memory` | +| 绿色 | `skills` | +| 橙色 | `tool_overview` | +| 深色 | `tool_call_request`(代码块) | +| 绿/红 | `tool_call_result`(成功/失败) | +| 琥珀 | `role: "tool"` 消息气泡 | ### 布局 -双栏:左侧 `ChatView`(MessageList + ChatInput),右侧 `ProtocolPanel`(实时 OpenAI Format JSON 视图,支持复制/下载)。顶栏有 Demo 场景切换按钮。 +双栏:左侧 `ChatView`(SessionBar + MessageList + ChatInput),右侧 `ProtocolPanel`(双 Tab:OpenAI Format / Raw Protocol,支持复制/下载,底部统计信息)。顶栏有 6 个 Demo 场景切换按钮。