design.md 15 KB

Design Document: Role-Based Menu and Project Config

Overview

本设计文档描述了标注平台角色权限菜单优化和项目配置流程改进的技术实现方案。主要包括:

  1. 角色权限菜单:根据用户角色(admin/annotator)显示不同的侧边栏菜单
  2. 外部项目管理页面:新增管理员专属页面,管理外部API创建的项目
  3. 项目管理页面优化:仅显示进行中和已完成的项目
  4. 选卡式项目配置:提供可视化的标注类型选择和标签配置界面
  5. XML配置生成:根据配置自动生成 Label Studio XML

Architecture

系统架构图

graph TB
    subgraph "Frontend"
        subgraph "Components"
            SM[Sidebar Menu]
            TC[Task Type Cards]
            LE[Label Editor]
            XP[XML Preview]
            CW[Config Wizard]
        end
        
        subgraph "Pages"
            PL[Project List Page]
            EP[External Projects Page]
            PC[Project Config Page]
            CP[Create Project Page]
        end
        
        subgraph "State"
            UA[User Auth Atoms]
            PA[Project Atoms]
        end
    end
    
    subgraph "Services"
        XG[XML Generator]
        RF[Role Filter]
    end
    
    SM --> RF
    RF --> UA
    TC --> XG
    LE --> XG
    XG --> XP
    CW --> TC
    CW --> LE
    CW --> XP
    PL --> PA
    EP --> PA
    PC --> PA

页面路由结构

/projects                    # 项目管理(仅显示进行中和已完成)
/projects/:id/config         # 项目配置页面
/projects/create             # 创建项目向导
/external-projects           # 管理外部来源数据(管理员专属)
/my-tasks                    # 我的任务
/annotations                 # 我的标注
/tasks                       # 任务管理(管理员专属)
/users                       # 用户管理(管理员专属)

菜单权限配置

// 菜单项配置
const menuItems = [
  { id: 'projects', label: '项目管理', path: '/projects', adminOnly: false },
  { id: 'external-projects', label: '管理外部来源数据', path: '/external-projects', adminOnly: true },
  { id: 'tasks', label: '任务管理', path: '/tasks', adminOnly: true },
  { id: 'my-tasks', label: '我的任务', path: '/my-tasks', adminOnly: false },
  { id: 'annotations', label: '我的标注', path: '/annotations', adminOnly: false },
  { id: 'users', label: '用户管理', path: '/users', adminOnly: true },
];

// 角色过滤逻辑
const visibleMenuItems = menuItems.filter(item => {
  if (item.adminOnly && currentUser?.role !== 'admin') {
    return false;
  }
  return true;
});

Components and Interfaces

1. Task Type Selector Component

选卡式标注类型选择组件。

interface TaskTypeOption {
  id: TaskType;
  name: string;
  description: string;
  icon: React.ReactNode;
  dataFormat: 'text' | 'image';
}

interface TaskTypeSelectorProps {
  selectedType: TaskType | null;
  onSelect: (type: TaskType) => void;
}

const TASK_TYPE_OPTIONS: TaskTypeOption[] = [
  {
    id: 'text_classification',
    name: '文本分类',
    description: '对文本内容进行分类标注',
    icon: <FileText />,
    dataFormat: 'text',
  },
  {
    id: 'image_classification',
    name: '图像分类',
    description: '对图像进行分类标注',
    icon: <Image />,
    dataFormat: 'image',
  },
  {
    id: 'object_detection',
    name: '目标检测',
    description: '在图像中标注目标边界框',
    icon: <Square />,
    dataFormat: 'image',
  },
  {
    id: 'ner',
    name: '命名实体识别',
    description: '标注文本中的实体',
    icon: <Tag />,
    dataFormat: 'text',
  },
];

2. Label Editor Component

可视化标签编辑组件。

interface LabelConfig {
  id: string;
  name: string;
  color: string;
  hotkey?: string;
}

interface LabelEditorProps {
  labels: LabelConfig[];
  onChange: (labels: LabelConfig[]) => void;
  taskType: TaskType;
}

// 预定义颜色列表
const PRESET_COLORS = [
  '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
  '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F',
  '#BB8FCE', '#85C1E9', '#F8B500', '#00CED1',
];

3. XML Generator Service

根据配置生成 Label Studio XML。

interface XMLGeneratorConfig {
  taskType: TaskType;
  labels: LabelConfig[];
  choiceType?: 'single' | 'multiple'; // 用于分类任务
}

class XMLGenerator {
  static generate(config: XMLGeneratorConfig): string {
    switch (config.taskType) {
      case 'text_classification':
        return this.generateTextClassification(config);
      case 'image_classification':
        return this.generateImageClassification(config);
      case 'object_detection':
        return this.generateObjectDetection(config);
      case 'ner':
        return this.generateNER(config);
      default:
        throw new Error(`Unsupported task type: ${config.taskType}`);
    }
  }

  private static generateTextClassification(config: XMLGeneratorConfig): string {
    const choiceAttr = config.choiceType === 'multiple' ? 'multiple' : 'single';
    const labelsXML = config.labels
      .map(l => `    <Choice value="${l.name}" ${l.hotkey ? `hotkey="${l.hotkey}"` : ''} />`)
      .join('\n');
    
    return `<View>
  <Text name="text" value="$text"/>
  <Choices name="label" toName="text" choice="${choiceAttr}">
${labelsXML}
  </Choices>
</View>`;
  }

  private static generateImageClassification(config: XMLGeneratorConfig): string {
    const choiceAttr = config.choiceType === 'multiple' ? 'multiple' : 'single';
    const labelsXML = config.labels
      .map(l => `    <Choice value="${l.name}" ${l.hotkey ? `hotkey="${l.hotkey}"` : ''} />`)
      .join('\n');
    
    return `<View>
  <Image name="image" value="$image"/>
  <Choices name="label" toName="image" choice="${choiceAttr}">
${labelsXML}
  </Choices>
</View>`;
  }

  private static generateObjectDetection(config: XMLGeneratorConfig): string {
    const labelsXML = config.labels
      .map(l => `    <Label value="${l.name}" background="${l.color}" ${l.hotkey ? `hotkey="${l.hotkey}"` : ''} />`)
      .join('\n');
    
    return `<View>
  <Image name="image" value="$image"/>
  <RectangleLabels name="label" toName="image">
${labelsXML}
  </RectangleLabels>
</View>`;
  }

  private static generateNER(config: XMLGeneratorConfig): string {
    const labelsXML = config.labels
      .map(l => `    <Label value="${l.name}" background="${l.color}" ${l.hotkey ? `hotkey="${l.hotkey}"` : ''} />`)
      .join('\n');
    
    return `<View>
  <Text name="text" value="$text"/>
  <Labels name="label" toName="text">
${labelsXML}
  </Labels>
</View>`;
  }
}

4. Project Config Wizard Component

项目配置向导组件。

type WizardStep = 'task-type' | 'labels' | 'details' | 'preview';

interface ProjectConfigWizardProps {
  initialTaskType?: TaskType;
  initialLabels?: LabelConfig[];
  onComplete: (config: ProjectConfigResult) => void;
  onCancel: () => void;
}

interface ProjectConfigResult {
  taskType: TaskType;
  labels: LabelConfig[];
  xmlConfig: string;
  name?: string;
  description?: string;
}

5. External Projects Page Component

外部项目管理页面。

interface ExternalProjectsPageProps {}

// 页面状态
interface ExternalProjectsState {
  projects: Project[];
  loading: boolean;
  error: string | null;
  statusFilter: ProjectStatus | 'all';
}

// 过滤逻辑:仅显示外部项目
const loadExternalProjects = async (statusFilter?: ProjectStatus) => {
  return await listProjects({
    source: 'external',
    status: statusFilter,
  });
};

6. Updated Project List Page

优化后的项目管理页面。

// 默认过滤条件:仅显示进行中和已完成的项目
const DEFAULT_STATUS_FILTER = ['in_progress', 'completed'];

// 加载项目时应用过滤
const loadProjects = async (sourceFilter?: ProjectSource) => {
  const allProjects = await listProjects({
    source: sourceFilter,
  });
  
  // 过滤只显示进行中和已完成的项目
  return allProjects.filter(p => 
    p.status === 'in_progress' || p.status === 'completed'
  );
};

Data Models

标签配置模型

interface LabelConfig {
  id: string;           // 唯一标识符
  name: string;         // 标签名称
  color: string;        // 标签颜色(十六进制)
  hotkey?: string;      // 快捷键(可选)
}

任务类型枚举

type TaskType = 
  | 'text_classification'
  | 'image_classification'
  | 'object_detection'
  | 'ner';

项目配置扩展

interface ProjectConfig {
  taskType: TaskType;
  labels: LabelConfig[];
  choiceType?: 'single' | 'multiple';
  xmlConfig: string;
}

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: 角色菜单过滤正确性

For any 用户角色,如果角色为 'admin',则可见菜单项数量应等于所有菜单项数量;如果角色为 'annotator',则可见菜单项数量应等于非管理员专属菜单项数量。

Validates: Requirements 1.1, 1.2

Property 2: 角色识别正确性

For any 用户对象,如果 role 字段为 'admin' 则 isAdmin 应为 true,如果 role 字段为 'annotator' 则 isAdmin 应为 false。

Validates: Requirements 1.5, 1.6

Property 3: 项目来源过滤正确性

For any 项目列表,外部项目管理页面应仅包含 source='external' 的项目,项目管理页面应仅包含 status='in_progress' 或 status='completed' 的项目。

Validates: Requirements 2.3, 3.1

Property 4: 状态按钮显示正确性

For any 项目,如果状态为 'draft' 或 'configuring' 则应显示配置按钮,如果状态为 'ready' 则应显示分发按钮。

Validates: Requirements 2.6, 2.7

Property 5: 角色按钮可见性正确性

For any 用户访问项目管理页面,如果用户角色为 'annotator' 则创建项目按钮应隐藏,如果用户角色为 'admin' 则创建项目按钮应显示。

Validates: Requirements 3.7, 3.8

Property 6: XML生成正确性

For any 有效的标注类型和标签配置,生成的 XML 应包含所有配置的标签,且 XML 格式应符合 Label Studio 规范(以 <View> 开头和结尾)。

Validates: Requirements 4.6, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7

Property 7: 标签编辑器功能正确性

For any 标签列表操作(添加、删除、编辑),操作后的标签列表应正确反映变更,且不应存在重复的标签名称。

Validates: Requirements 7.1, 7.2, 7.3, 7.8, 7.9, 7.10, 7.11

Error Handling

错误类型

// 配置验证错误
interface ConfigValidationError {
  type: 'validation';
  field: string;
  message: string;
}

// 标签验证错误
interface LabelValidationError {
  type: 'label';
  labelId: string;
  message: string;
}

// 错误消息
const ERROR_MESSAGES = {
  EMPTY_LABEL_NAME: '标签名称不能为空',
  DUPLICATE_LABEL_NAME: '标签名称不能重复',
  INVALID_HOTKEY: '快捷键格式无效',
  NO_LABELS: '至少需要添加一个标签',
  INVALID_XML: 'XML 配置格式无效',
};

验证逻辑

function validateLabels(labels: LabelConfig[]): LabelValidationError[] {
  const errors: LabelValidationError[] = [];
  const names = new Set<string>();
  
  labels.forEach(label => {
    // 检查空名称
    if (!label.name.trim()) {
      errors.push({
        type: 'label',
        labelId: label.id,
        message: ERROR_MESSAGES.EMPTY_LABEL_NAME,
      });
    }
    
    // 检查重复名称
    if (names.has(label.name)) {
      errors.push({
        type: 'label',
        labelId: label.id,
        message: ERROR_MESSAGES.DUPLICATE_LABEL_NAME,
      });
    }
    names.add(label.name);
  });
  
  return errors;
}

Testing Strategy

单元测试

  1. XML生成器测试:验证不同任务类型生成正确的 XML 结构
  2. 标签验证测试:验证标签名称、颜色、快捷键的验证逻辑
  3. 角色过滤测试:验证菜单项根据角色正确过滤

属性测试(Property-Based Testing)

使用 fast-check 库进行属性测试:

  1. Property 1 测试:生成随机角色,验证菜单过滤逻辑
  2. Property 3 测试:生成随机项目列表,验证过滤结果
  3. Property 6 测试:生成随机标签配置,验证 XML 生成

集成测试

  1. 配置向导流程测试:完整的配置向导流程
  2. 权限控制测试:验证不同角色的页面访问权限
  3. 项目创建流程测试:从选择类型到创建项目的完整流程

测试配置

// jest.config.ts
export default {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

// fast-check 配置
const fcConfig = {
  numRuns: 100,
  verbose: true,
};

UI Components Design

Task Type Card

┌─────────────────────────────────┐
│  [Icon]                         │
│                                 │
│  文本分类                        │
│  对文本内容进行分类标注            │
│                                 │
│  ○ 选择                         │
└─────────────────────────────────┘

Label Editor

┌─────────────────────────────────────────────┐
│ 标签配置                          [+ 添加]   │
├─────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────┐ │
│ │ [■] 正面评价        快捷键: 1    [编辑] [删除] │
│ └─────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ [■] 负面评价        快捷键: 2    [编辑] [删除] │
│ └─────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ [■] 中性评价        快捷键: 3    [编辑] [删除] │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

Config Wizard Steps

步骤指示器:
[1. 选择类型] ─── [2. 配置标签] ─── [3. 项目信息] ─── [4. 预览确认]
     ●                ○                 ○                 ○