Add Chinese documentation for custom skills development and reference guide

- Created a new document for custom skills development (`custom.md`) detailing the structure, creation, implementation, and best practices for developing custom CCW skills.
- Added an index document (`index.md`) summarizing all built-in skills, their categories, and usage examples.
- Introduced a reference guide (`reference.md`) providing a quick reference for all 33 built-in CCW skills, including triggers and purposes.
This commit is contained in:
catlog22
2026-03-01 13:08:12 +08:00
parent 2fb93d20e0
commit 8ceae6d6fd
78 changed files with 12352 additions and 3638 deletions

View File

@@ -104,41 +104,131 @@ export interface ApiError {
// ========== CSRF Token Handling ==========
/**
* In-memory CSRF token storage
* The token is obtained from X-CSRF-Token response header and stored here
* because the XSRF-TOKEN cookie is HttpOnly and cannot be read by JavaScript
* CSRF token pool for concurrent request support
* The pool maintains multiple tokens to support parallel mutating requests
*/
let csrfToken: string | null = null;
const MAX_CSRF_TOKEN_POOL_SIZE = 5;
// Token pool queue - FIFO for fair distribution
let csrfTokenQueue: string[] = [];
/**
* Get CSRF token from memory
* Get a CSRF token from the pool
* @returns Token string or undefined if pool is empty
*/
function getCsrfToken(): string | null {
return csrfToken;
function getCsrfTokenFromPool(): string | undefined {
return csrfTokenQueue.shift();
}
/**
* Set CSRF token from response header
* Add a CSRF token to the pool with deduplication
* @param token - Token to add
*/
function addCsrfTokenToPool(token: string): void {
if (!token) return;
// Deduplication: don't add if already in pool
if (csrfTokenQueue.includes(token)) return;
// Limit pool size
if (csrfTokenQueue.length >= MAX_CSRF_TOKEN_POOL_SIZE) return;
csrfTokenQueue.push(token);
}
/**
* Get current pool size (for debugging)
*/
export function getCsrfPoolSize(): number {
return csrfTokenQueue.length;
}
/**
* Lock for deduplicating concurrent token fetch requests
* Prevents multiple simultaneous calls to fetchTokenSynchronously
*/
let tokenFetchPromise: Promise<string> | null = null;
/**
* Synchronously fetch a single token when pool is depleted
* This blocks the request until a token is available
* Uses lock mechanism to prevent concurrent fetch deduplication
*/
async function fetchTokenSynchronously(): Promise<string> {
// If a fetch is already in progress, wait for it
if (tokenFetchPromise) {
return tokenFetchPromise;
}
// Create new fetch promise and store as lock
tokenFetchPromise = (async () => {
try {
const response = await fetch('/api/csrf-token', {
credentials: 'same-origin',
});
if (!response.ok) {
throw new Error('Failed to fetch CSRF token');
}
const data = await response.json();
const token = data.csrfToken;
if (!token) {
throw new Error('No CSRF token in response');
}
return token;
} finally {
// Release lock after completion (success or failure)
tokenFetchPromise = null;
}
})();
return tokenFetchPromise;
}
/**
* Set CSRF token from response header (adds to pool)
*/
function updateCsrfToken(response: Response): void {
const token = response.headers.get('X-CSRF-Token');
if (token) {
csrfToken = token;
addCsrfTokenToPool(token);
}
}
/**
* Initialize CSRF token by fetching from server
* Initialize CSRF token pool by fetching multiple tokens from server
* Should be called once on app initialization
*/
export async function initializeCsrfToken(): Promise<void> {
try {
const response = await fetch('/api/csrf-token', {
// Prefetch 5 tokens for pool
const response = await fetch(`/api/csrf-token?count=${MAX_CSRF_TOKEN_POOL_SIZE}`, {
credentials: 'same-origin',
});
updateCsrfToken(response);
if (!response.ok) {
throw new Error('Failed to initialize CSRF token pool');
}
const data = await response.json();
// Handle both single token and batch response formats
if (data.tokens && Array.isArray(data.tokens)) {
// Batch response - add all tokens to pool
for (const token of data.tokens) {
addCsrfTokenToPool(token);
}
} else if (data.csrfToken) {
// Single token response - add to pool
addCsrfTokenToPool(data.csrfToken);
}
console.log(`[CSRF] Token pool initialized with ${csrfTokenQueue.length} tokens`);
} catch (error) {
console.error('[CSRF] Failed to initialize CSRF token:', error);
console.error('[CSRF] Failed to initialize CSRF token pool:', error);
// Fallback: try to get at least one token
try {
const token = await fetchTokenSynchronously();
addCsrfTokenToPool(token);
} catch (fallbackError) {
console.error('[CSRF] Fallback token fetch failed:', fallbackError);
}
}
}
@@ -155,7 +245,18 @@ async function fetchApi<T>(
// Add CSRF token for mutating requests
if (options.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) {
const token = getCsrfToken();
let token = getCsrfTokenFromPool();
// If pool is depleted, synchronously fetch a new token
if (!token) {
console.warn('[CSRF] Token pool depleted, fetching synchronously');
try {
token = await fetchTokenSynchronously();
} catch (error) {
throw new Error('Failed to acquire CSRF token for request');
}
}
if (token) {
headers.set('X-CSRF-Token', token);
}
@@ -172,7 +273,7 @@ async function fetchApi<T>(
credentials: 'same-origin',
});
// Update CSRF token from response header
// Update CSRF token from response header (adds to pool)
updateCsrfToken(response);
if (!response.ok) {
@@ -2963,6 +3064,18 @@ export interface ReviewDimension {
findings: ReviewFinding[];
}
export interface ReviewSummary {
phase?: string;
status?: string;
severityDistribution?: {
critical: number;
high: number;
medium: number;
low: number;
};
criticalFiles?: string[];
}
export interface ReviewSession {
session_id: string;
title?: string;
@@ -2970,6 +3083,7 @@ export interface ReviewSession {
type: 'review';
phase?: string;
reviewDimensions?: ReviewDimension[];
reviewSummary?: ReviewSummary;
_isActive?: boolean;
created_at?: string;
updated_at?: string;
@@ -2986,6 +3100,17 @@ export interface ReviewSessionsResponse {
progress?: unknown;
}>;
};
// New: Support activeSessions with review type
activeSessions?: Array<{
session_id: string;
project?: string;
type?: string;
status?: string;
created_at?: string;
hasReview?: boolean;
reviewSummary?: ReviewSummary;
reviewDimensions?: ReviewDimension[];
}>;
}
/**
@@ -2994,12 +3119,34 @@ export interface ReviewSessionsResponse {
export async function fetchReviewSessions(): Promise<ReviewSession[]> {
const data = await fetchApi<ReviewSessionsResponse>('/api/data');
// If reviewSessions field exists (legacy format), use it
// Priority 1: Use activeSessions with type='review' or hasReview=true
if (data.activeSessions) {
const reviewSessions = data.activeSessions.filter(
session => session.type === 'review' || session.hasReview
);
if (reviewSessions.length > 0) {
return reviewSessions.map(session => ({
session_id: session.session_id,
title: session.project || session.session_id,
description: '',
type: 'review' as const,
phase: session.reviewSummary?.phase,
reviewDimensions: session.reviewDimensions || [],
reviewSummary: session.reviewSummary,
_isActive: true,
created_at: session.created_at,
updated_at: undefined,
status: session.status
}));
}
}
// Priority 2: Legacy reviewSessions field
if (data.reviewSessions && data.reviewSessions.length > 0) {
return data.reviewSessions;
}
// Otherwise, transform reviewData.sessions into ReviewSession format
// Priority 3: Legacy reviewData.sessions format
if (data.reviewData?.sessions) {
return data.reviewData.sessions.map(session => ({
session_id: session.session_id,

View File

@@ -96,6 +96,11 @@
"message": "Try adjusting your filters or search query.",
"noFixProgress": "No fix progress data available"
},
"notExecuted": {
"title": "Review Not Yet Executed",
"message": "This review session has been created but the review process has not been started yet. No findings have been generated.",
"hint": "💡 Tip: Execute the review workflow to start analyzing code and generate findings."
},
"notFound": {
"title": "Review Session Not Found",
"message": "The requested review session could not be found."

View File

@@ -96,6 +96,11 @@
"message": "尝试调整筛选条件或搜索查询。",
"noFixProgress": "无修复进度数据"
},
"notExecuted": {
"title": "审查尚未执行",
"message": "此审查会话已创建,但审查流程尚未启动。尚未生成任何发现结果。",
"hint": "💡 提示:请执行审查工作流以开始分析代码并生成发现结果。"
},
"notFound": {
"title": "未找到审查会话",
"message": "无法找到请求的审查会话。"

View File

@@ -765,13 +765,32 @@ export function ReviewSessionPage() {
{filteredFindings.length === 0 ? (
<Card>
<CardContent className="p-12 text-center">
<FileText className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'reviewSession.empty.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'reviewSession.empty.message' })}
</p>
{/* Check if review hasn't been executed yet */}
{reviewSession?.reviewSummary?.status === 'in_progress' &&
(!reviewSession?.reviewDimensions || reviewSession.reviewDimensions.length === 0) ? (
<>
<AlertTriangle className="h-12 w-12 text-amber-500 mx-auto mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'reviewSession.notExecuted.title' })}
</h3>
<p className="text-sm text-muted-foreground mb-4">
{formatMessage({ id: 'reviewSession.notExecuted.message' })}
</p>
<div className="text-xs text-muted-foreground bg-muted p-3 rounded-lg inline-block">
{formatMessage({ id: 'reviewSession.notExecuted.hint' })}
</div>
</>
) : (
<>
<FileText className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'reviewSession.empty.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'reviewSession.empty.message' })}
</p>
</>
)}
</CardContent>
</Card>
) : (