Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,34 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'pnpm'

- name: Install pnpm
run: corepack enable && corepack prepare pnpm@latest --activate

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm exec tsc -p tsconfig.json --noEmit
run: pnpm typecheck

- name: Typecheck session store
run: pnpm exec tsc -p packages/session-sqlite/tsconfig.json --noEmit

- name: Typecheck client
run: pnpm typecheck:client

- name: Run unit tests
run: pnpm exec vitest run --reporter verbose
run: pnpm test

- name: Build agent-core
run: pnpm run build:agent-core
run: pnpm build:agent-core

- name: Build client
run: pnpm build:client
54 changes: 54 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Deploy Client to GitHub Pages

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build client
run: pnpm build:client

- name: Configure Pages
uses: actions/configure-pages@v5

- name: Upload Packages Client
uses: actions/upload-pages-artifact@v3
with:
path: packages/client/dist

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ out
.remix-serve-node_modules
.env/*
.agent-sessions/
.pnpm-store/
packages/client/dist/
*.tsbuildinfo
bundle.*
docs/*
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ packages/
prompt/ # 系统提示词、工具说明、模式提示词
tools/ # 环境、文件、命令等本地工具
policy/ # PLAN/BUILD 模式下的工具权限
web/ # GitHub Pages 客户端数据导出
client/ # TypeScript + React 会话历史界面,可部署到 GitHub Pages
tests/ # 项目统一单元测试
vitest.config.ts # Vitest 测试配置
```
Expand Down Expand Up @@ -103,6 +105,12 @@ pnpm exec vitest run --reporter verbose

# 构建 agent-core
pnpm run build:agent-core

# 导出会话历史到 packages/client/public/data.json
pnpm export:web

# 构建 GitHub Pages 客户端
pnpm build:client
```

## 测试说明
Expand Down
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
"dev": "tsx source/app.tsx",
"start": "tsx source/app.tsx",
"typecheck": "tsc -p tsconfig.json --noEmit",
"typecheck:client": "tsc -p packages/client/tsconfig.json --noEmit",
"test": "vitest run --reporter verbose",
"build:agent-core": "tsc -p packages/agent-core/tsconfig.json"
"build:agent-core": "tsc -p packages/agent-core/tsconfig.json",
"export:web": "tsx source/export-web.ts",
"dev:client": "vite --config packages/client/vite.config.ts",
"build:client": "vite build --config packages/client/vite.config.ts",
"preview:client": "vite preview --config packages/client/vite.config.ts"
},
"dependencies": {
"dotenv": "^17.4.2",
Expand All @@ -17,14 +22,20 @@
"ink-text-input": "^6.0.0",
"openai": "^6.34.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"tiktoken": "^1.0.20"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.3",
"@types/node": "^25.6.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.5",
"prettier": "^3.4.2",
"tailwindcss": "^4.3.3",
"tsx": "^4.23.7",
"typescript": "^6.0.3",
"vite": "^8.2.0",
"vitest": "^3.2.4"
}
}
177 changes: 177 additions & 0 deletions packages/agent-core/src/web/export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';
import type {
EntryLike,
SessionStoreLike,
SessionStatsLike,
} from '../session/session-repository';

/** GitHub Pages 客户端读取的会话快照格式。 */
export interface WebEntry {
seq: number;
id: string;
parentId: string | null;
type: string;
role: string;
timestamp: string;
text?: string;
tool?: string | null;
tags?: string[];
payload: unknown;
}

/** 运行记录的最小导出形状。 */
export interface WebRecord {
seq: number;
id: string;
lane: string;
runId: string | null;
type: string;
opKind: string | null;
timestamp: string;
payload: unknown;
}

/** 事实表的最小导出形状。 */
export interface WebFact {
seq: number;
kind: string;
key: string | null;
value: string | null;
}

/** 单个会话的导出结构。 */
export interface WebSession {
id: string;
createdAt: string;
cwd: string;
parentSessionId: string | null;
metadata: Record<string, unknown>;
stats: SessionStatsLike;
entries: WebEntry[];
records?: WebRecord[];
facts?: WebFact[];
}

/** 完整导出文件结构。 */
export interface WebExport {
schemaVersion: 1;
exportedAt: string;
sessions: WebSession[];
}

/** export.ts 需要的存储最小接口,兼容 SessionStoreLike 及 SQLite 扩展方法。 */
type WebStore = SessionStoreLike & {
getRecords?: (sessionId: string, options?: { limit?: number; offset?: number }) => WebRecord[];
listFacts?: (sessionId: string) => WebFact[];
};

const entryText = (payload: unknown): string | undefined => {
if (!payload || typeof payload !== 'object') {
return undefined;
}
const content = (payload as { content?: unknown }).content;
return typeof content === 'string' ? content : undefined;
};

const entryRole = (entry: EntryLike): string => {
const payload = entry.payload as { role?: unknown } | null;
return payload && typeof payload.role === 'string' ? payload.role : entry.type;
};

const entryTool = (payload: unknown): string | null => {
if (!payload || typeof payload !== 'object') {
return null;
}
const tool = (payload as { tool?: unknown }).tool;
return typeof tool === 'string' ? tool : null;
};

const entryTags = (payload: unknown): string[] | undefined => {
if (!payload || typeof payload !== 'object') {
return undefined;
}
const tags = (payload as { tags?: unknown }).tags;
if (!Array.isArray(tags)) {
return undefined;
}
const result = tags.filter((tag): tag is string => typeof tag === 'string');
return result.length > 0 ? result : undefined;
};

const toWebEntry = (entry: EntryLike): WebEntry => {
const role = entryRole(entry);
const text = entryText(entry.payload);
const tool = entryTool(entry.payload);
const tags = entryTags(entry.payload);

return {
seq: entry.seq,
id: entry.id,
parentId: entry.parentId,
type: entry.type,
role,
timestamp: entry.timestamp,
text,
tool,
tags,
payload: entry.payload,
};
};

/**
* 把会话存储导出为静态客户端可用的 JSON 结构。
* records 和 facts 是可选能力,存储未实现时自动省略。
*/
export const buildWebExport = (
store: WebStore,
options: { limit?: number } = {},
): WebExport => {
const sessions = store.listSessions({ limit: options.limit ?? 100 });

return {
schemaVersion: 1,
exportedAt: new Date().toISOString(),
sessions: sessions.map((session) => ({
id: session.id,
createdAt: session.createdAt,
cwd: session.cwd,
parentSessionId: session.parentSessionId,
metadata: session.metadata,
stats: store.getStats(session.id),
entries: store.getEntries(session.id, { limit: 100000 }).map(toWebEntry),
records: store.getRecords
? store.getRecords(session.id).map((record) => ({
seq: record.seq,
id: record.id,
lane: record.lane,
runId: record.runId,
type: record.type,
opKind: record.opKind,
timestamp: record.timestamp,
payload: record.payload,
}))
: undefined,
facts: store.listFacts
? store.listFacts(session.id).map((fact) => ({
seq: fact.seq,
kind: fact.kind,
key: fact.key,
value: fact.value,
}))
: undefined,
})),
};
};

/** 把会话快照写入 packages/client 可读取的静态 JSON 文件。 */
export const writeWebExport = (
store: WebStore,
outputPath: string,
options: { limit?: number } = {},
): WebExport => {
const data = buildWebExport(store, options);
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
return data;
};
41 changes: 41 additions & 0 deletions packages/client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Call Code History Client

这是 CLI 会话历史的静态展示端,基于 TypeScript、React 和 Tailwind CSS 构建,可直接部署到 GitHub Pages。页面默认读取 `data.json`,如果不存在则回退到 `data.example.json` 预览数据;也可以用 `?data=<url>` 指定数据文件,用 `?session=<id>` 直达某个会话。

## 构建

```bash
pnpm build:client
```

产物输出到 `packages/client/dist`,GitHub Actions 会构建后上传该目录。

## 本地开发

```bash
pnpm dev:client
```

## 导出数据

在仓库根目录运行:

```bash
pnpm export:web
```

脚本会把当前 `SESSION_DB_PATH` 指向的会话数据库导出为 `packages/client/public/data.json`。也可以在 CLI 中执行 `/export` 完成同样操作。

导出文件包含会话、消息、工具调用、统计和事实数据。发布到公开 GitHub Pages 前请先检查内容是否包含不该公开的路径、命令输出或密钥。

## 本地预览

预览构建产物:

```bash
pnpm preview:client
```

## 主题

页面内置白色毛玻璃和高级黑两套主题,侧边栏顶部的分段按钮可切换,选择会保存在浏览器本地。
12 changes: 12 additions & 0 deletions packages/client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Call Code History</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading
Loading