Skip to main content
Glama

智能标签推断

gitea_workflow_infer_labels

Automatically suggests issue labels by analyzing title and body content using keyword matching and pattern recognition for type, priority, and area classification.

Instructions

Infer labels for an issue based on title and body content. Uses keyword matching and pattern recognition to suggest type, priority, and area labels.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
ownerNoRepository owner. Uses context if not provided
repoNoRepository name. Uses context if not provided
issue_numberYesIssue number to analyze
auto_applyNoAutomatically apply inferred labels (default: false)

Implementation Reference

  • Main handler function that fetches the issue, loads workflow config if needed, uses LabelInferenceEngine to get recommended labels, and optionally auto-applies them to the issue via Gitea API.
    export async function workflowInferLabels(
      ctx: WorkflowToolsContext,
      args: {
        owner?: string;
        repo?: string;
        config?: WorkflowConfig;
        issue_number: number;
        auto_apply?: boolean;
      }
    ): Promise<{
      success: boolean;
      issue_number: number;
      inferred_labels: Array<{ label: string; confidence: number; reason: string }>;
      applied_labels?: string[];
      error?: string;
    }> {
      logger.debug({ args: { ...args, config: args.config ? '[provided]' : undefined } }, 'Inferring labels');
    
      const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo);
      const autoApply = args.auto_apply ?? false;
    
      // 获取配置
      let config = args.config;
      if (!config) {
        const loadResult = await workflowLoadConfig(ctx, { owner, repo });
        if (!loadResult.success || !loadResult.config) {
          return {
            success: false,
            issue_number: args.issue_number,
            inferred_labels: [],
            error: loadResult.error || '无法加载配置',
          };
        }
        config = loadResult.config;
      }
    
      try {
        // 获取 Issue
        const issue = await ctx.client.get<Issue>(`/repos/${owner}/${repo}/issues/${args.issue_number}`);
    
        // 推断标签
        const inferenceEngine = new LabelInferenceEngine(config);
        const recommendedLabels = inferenceEngine.getRecommendedLabels(issue);
    
        const appliedLabels: string[] = [];
    
        // 自动应用标签
        if (autoApply && recommendedLabels.length > 0) {
          // 获取仓库的标签 ID 映射
          const repoLabels = await ctx.client.get<Array<{ id: number; name: string }>>(
            `/repos/${owner}/${repo}/labels`
          );
          const labelIdMap = new Map(repoLabels.map((l) => [l.name, l.id]));
    
          for (const item of recommendedLabels) {
            const labelId = labelIdMap.get(item.label);
            if (labelId) {
              try {
                await ctx.client.post(`/repos/${owner}/${repo}/issues/${args.issue_number}/labels`, {
                  labels: [labelId],
                });
                appliedLabels.push(item.label);
              } catch (error) {
                logger.warn({ label: item.label, error }, 'Failed to apply label');
              }
            }
          }
        }
    
        logger.info(
          { owner, repo, issue_number: args.issue_number, inferred: recommendedLabels.length, applied: appliedLabels.length },
          'Labels inferred'
        );
    
        return {
          success: true,
          issue_number: args.issue_number,
          inferred_labels: recommendedLabels,
          applied_labels: autoApply ? appliedLabels : undefined,
        };
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        logger.error({ owner, repo, issue_number: args.issue_number, error: errorMessage }, 'Failed to infer labels');
    
        return {
          success: false,
          issue_number: args.issue_number,
          inferred_labels: [],
          error: errorMessage,
        };
      }
    }
  • Registers the gitea_workflow_infer_labels tool in the MCP server, defines title, description, Zod input schema, and wraps the workflowInferLabels handler with try-catch and standardized JSON response.
    // 6. gitea_workflow_infer_labels - 智能标签推断
    mcpServer.registerTool(
      'gitea_workflow_infer_labels',
      {
        title: '智能标签推断',
        description:
          'Infer labels for an issue based on title and body content. Uses keyword matching and pattern recognition to suggest type, priority, and area labels.',
        inputSchema: z.object({
          owner: z.string().optional().describe('Repository owner. Uses context if not provided'),
          repo: z.string().optional().describe('Repository name. Uses context if not provided'),
          issue_number: z.number().describe('Issue number to analyze'),
          auto_apply: z.boolean().optional().describe('Automatically apply inferred labels (default: false)'),
        }),
      },
      async (args) => {
        try {
          const result = await WorkflowTools.workflowInferLabels(
            { client: ctx.client, contextManager: ctx.contextManager },
            args
          );
          return {
            content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
            isError: !result.success,
          };
        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : String(error);
          return {
            content: [{ type: 'text' as const, text: `Error: ${errorMessage}` }],
            isError: true,
          };
        }
      }
  • Zod schema for tool input validation: optional owner/repo (contextual), required issue_number, optional auto_apply boolean.
    inputSchema: z.object({
      owner: z.string().optional().describe('Repository owner. Uses context if not provided'),
      repo: z.string().optional().describe('Repository name. Uses context if not provided'),
      issue_number: z.number().describe('Issue number to analyze'),
      auto_apply: z.boolean().optional().describe('Automatically apply inferred labels (default: false)'),
    }),
  • Core LabelInferenceEngine class used by the handler for intelligent label inference via keyword matching, pattern recognition on issue title/body, using workflow config keywords; provides getRecommendedLabels() called in handler.
    export class LabelInferenceEngine {
      private config: WorkflowConfig;
    
      constructor(config: WorkflowConfig) {
        this.config = config;
      }
    
      /**
       * 对 Issue 进行完整的标签推断
       */
      inferAll(issue: Issue): LabelInferenceResult {
        const typeResult = this.inferType(issue);
        const priorityResult = this.inferPriority(issue, typeResult?.value);
        const areaResults = this.inferAreas(issue);
    
        // 收集所有推断结果
        const all: Array<{ label: string; confidence: number; reason: string }> = [];
    
        const prefixes = getLabelPrefixes(this.config);
    
        if (typeResult && typeResult.confidence >= this.getConfidenceThreshold()) {
          all.push({
            label: `${prefixes.type}${typeResult.value}`,
            confidence: typeResult.confidence,
            reason: typeResult.reason,
          });
        }
    
        if (priorityResult && priorityResult.confidence >= this.getConfidenceThreshold()) {
          all.push({
            label: `${prefixes.priority}${priorityResult.value}`,
            confidence: priorityResult.confidence,
            reason: priorityResult.reason,
          });
        }
    
        for (const area of areaResults) {
          if (area.confidence >= this.getConfidenceThreshold()) {
            all.push({
              label: `${prefixes.area}${area.value}`,
              confidence: area.confidence,
              reason: area.reason,
            });
          }
        }
    
        return {
          type: typeResult,
          priority: priorityResult,
          areas: areaResults,
          all,
        };
      }
    
      /**
       * 推断 Issue 类型
       */
      inferType(issue: Issue): InferenceResult<string> | null {
        const text = this.normalizeText(issue.title + ' ' + issue.body);
        const keywords = this.config.automation.label_inference.type_keywords;
    
        let bestMatch: InferenceResult<string> | null = null;
        let maxScore = 0;
    
        for (const [type, typeKeywords] of Object.entries(keywords)) {
          const { score, matchedKeywords } = this.calculateKeywordScore(text, typeKeywords);
    
          if (score > maxScore) {
            maxScore = score;
            bestMatch = {
              value: type,
              confidence: Math.min(score, 1.0),
              reason: `匹配关键词: ${matchedKeywords.join(', ')}`,
            };
          }
        }
    
        // 如果没有匹配到,尝试从标题前缀推断
        if (!bestMatch || maxScore < 0.3) {
          const prefixMatch = this.inferTypeFromPrefix(issue.title);
          if (prefixMatch) {
            return prefixMatch;
          }
        }
    
        return bestMatch;
      }
    
      /**
       * 推断 Issue 优先级
       */
      inferPriority(issue: Issue, inferredType?: string): InferenceResult<string> | null {
        const text = this.normalizeText(issue.title + ' ' + issue.body);
        const keywords = this.config.automation.label_inference.priority_keywords;
    
        // 安全问题强制 P0
        if (inferredType === 'security') {
          return {
            value: 'P0',
            confidence: 1.0,
            reason: '安全问题自动标记为紧急',
          };
        }
    
        // 根据关键词匹配
        for (const [priority, priorityKeywords] of Object.entries(keywords)) {
          const { score, matchedKeywords } = this.calculateKeywordScore(text, priorityKeywords);
    
          if (score >= 0.3) {
            return {
              value: priority,
              confidence: Math.min(score + 0.2, 1.0),
              reason: `匹配关键词: ${matchedKeywords.join(', ')}`,
            };
          }
        }
    
        // 默认优先级
        const defaultPriority = this.config.automation.new_issue_defaults.default_priority;
        return {
          value: defaultPriority,
          confidence: 0.5,
          reason: '使用默认优先级',
        };
      }
    
      /**
       * 推断领域标签
       */
      inferAreas(issue: Issue): InferenceResult<string>[] {
        const results: InferenceResult<string>[] = [];
        const text = this.normalizeText(issue.title + ' ' + issue.body);
        const patterns = this.config.automation.label_inference.area_patterns;
    
        if (!patterns) {
          return results;
        }
    
        for (const [area, areaPatterns] of Object.entries(patterns)) {
          for (const pattern of areaPatterns) {
            if (text.includes(pattern.toLowerCase())) {
              results.push({
                value: area,
                confidence: 0.8,
                reason: `匹配路径模式: ${pattern}`,
              });
              break; // 每个领域只匹配一次
            }
          }
        }
    
        // 根据关键词推断领域
        const areaKeywords: Record<string, string[]> = {
          api: ['api', 'endpoint', '接口', 'rest', 'graphql', 'grpc'],
          database: ['database', 'db', '数据库', 'sql', 'migration', 'model', '模型'],
          auth: ['auth', 'authentication', 'authorization', '认证', '授权', 'login', 'token', 'jwt'],
          ui: ['ui', 'component', '组件', 'button', 'form', 'modal', '界面'],
          performance: ['performance', '性能', 'slow', '慢', 'optimize', '优化', 'memory', 'cpu'],
        };
    
        for (const [area, keywords] of Object.entries(areaKeywords)) {
          // 只推断配置中存在的领域
          if (!this.config.labels.area || !this.config.labels.area[area]) {
            continue;
          }
    
          const { score, matchedKeywords } = this.calculateKeywordScore(text, keywords);
          if (score >= 0.3 && !results.some((r) => r.value === area)) {
            results.push({
              value: area,
              confidence: score,
              reason: `匹配关键词: ${matchedKeywords.join(', ')}`,
            });
          }
        }
    
        return results;
      }
    
      /**
       * 检查 Issue 是否缺少必要标签
       */
      checkMissingLabels(issue: Issue): string[] {
        const missing: string[] = [];
        const existingLabels = issue.labels.map((l) => l.name);
        const prefixes = getLabelPrefixes(this.config);
    
        // 检查类型标签
        const hasTypeLabel = existingLabels.some((l) => matchLabel(prefixes.type, l) !== null);
        if (this.config.automation.new_issue_defaults.require_type_label && !hasTypeLabel) {
          missing.push('type');
        }
    
        // 检查优先级标签
        const hasPriorityLabel = existingLabels.some((l) => matchLabel(prefixes.priority, l) !== null);
        if (this.config.automation.new_issue_defaults.require_priority_label && !hasPriorityLabel) {
          missing.push('priority');
        }
    
        // 检查状态标签
        const hasStatusLabel = existingLabels.some((l) => matchLabel(prefixes.status, l) !== null);
        if (!hasStatusLabel) {
          missing.push('status');
        }
    
        return missing;
      }
    
      /**
       * 获取推荐添加的标签
       */
      getRecommendedLabels(issue: Issue): Array<{ label: string; confidence: number; reason: string }> {
        const existingLabels = new Set(issue.labels.map((l) => l.name));
        const inference = this.inferAll(issue);
    
        return inference.all.filter((item) => !existingLabels.has(item.label));
      }
    
      // ============ 私有方法 ============
    
      private normalizeText(text: string): string {
        return text.toLowerCase().trim();
      }
    
      private getConfidenceThreshold(): number {
        return this.config.automation.label_inference.confidence_threshold;
      }
    
      private calculateKeywordScore(
        text: string,
        keywords: string[]
      ): { score: number; matchedKeywords: string[] } {
        const matchedKeywords: string[] = [];
        let matchCount = 0;
    
        for (const keyword of keywords) {
          const normalizedKeyword = keyword.toLowerCase();
          // 使用词边界匹配,避免部分匹配
          const regex = new RegExp(`\\b${this.escapeRegex(normalizedKeyword)}\\b|${this.escapeRegex(normalizedKeyword)}`, 'gi');
          const matches = text.match(regex);
          if (matches) {
            matchCount += matches.length;
            matchedKeywords.push(keyword);
          }
        }
    
        // 计算分数:匹配数量 / 关键词数量,加权处理
        const baseScore = matchedKeywords.length / keywords.length;
        const bonusScore = Math.min(matchCount * 0.1, 0.3); // 多次匹配加分
    
        return {
          score: Math.min(baseScore + bonusScore, 1.0),
          matchedKeywords,
        };
      }
    
      private escapeRegex(str: string): string {
        return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      }
    
      private inferTypeFromPrefix(title: string): InferenceResult<string> | null {
        const prefixPatterns: Array<{ pattern: RegExp; type: string }> = [
          { pattern: /^(fix|bug|修复|修正)[\s::]/i, type: 'bug' },
          { pattern: /^(feat|feature|新增|添加|实现)[\s::]/i, type: 'feature' },
          { pattern: /^(docs|文档|doc)[\s::]/i, type: 'docs' },
          { pattern: /^(refactor|重构)[\s::]/i, type: 'refactor' },
          { pattern: /^(test|测试)[\s::]/i, type: 'test' },
          { pattern: /^(security|安全)[\s::]/i, type: 'security' },
          { pattern: /^(chore|杂项)[\s::]/i, type: 'refactor' },
          { pattern: /^(perf|性能)[\s::]/i, type: 'refactor' },
        ];
    
        for (const { pattern, type } of prefixPatterns) {
          if (pattern.test(title)) {
            return {
              value: type,
              confidence: 0.9,
              reason: `标题前缀匹配: ${pattern.source}`,
            };
          }
        }
    
        return null;
      }
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden but offers limited behavioral insight. It mentions the inference method ('keyword matching and pattern recognition') and label categories, but doesn't disclose critical traits: whether it's read-only or mutating (the 'auto_apply' parameter suggests potential writes), authentication needs, rate limits, error conditions, or output format. For a tool with a boolean 'auto_apply' parameter, this gap is significant.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core action ('Infer labels for an issue') and adds useful detail without waste. Every part earns its place: the target ('issue'), inputs ('title and body content'), methods ('keyword matching and pattern recognition'), and output types ('type, priority, and area labels').

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (inference with potential mutation via 'auto_apply'), lack of annotations, and no output schema, the description is incomplete. It doesn't cover behavioral aspects like safety (read vs. write), permissions, or return values. For a tool that could automatically apply labels, more context is needed to guide an agent effectively.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema fully documents all 4 parameters. The description adds no parameter-specific information beyond what's in the schema (e.g., it doesn't clarify 'owner'/'repo' context usage or 'auto_apply' implications). Baseline 3 is appropriate as the schema handles parameter semantics adequately.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Infer labels for an issue based on title and body content' with specific methods ('keyword matching and pattern recognition') and label types ('type, priority, and area'). It distinguishes from siblings like 'gitea_workflow_sync_labels' (which likely applies labels rather than infers them) and 'gitea_issue_create' (which creates issues). However, it doesn't explicitly name these alternatives, preventing a perfect score.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention prerequisites (e.g., needing an existing issue), exclusions (e.g., not for pull requests), or comparisons to siblings like 'gitea_workflow_sync_labels' or 'gitea_workflow_check_issues'. Usage is implied through the action 'infer labels,' but explicit context is missing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SupenBysz/gitea-mcp-tool'

If you have feedback or need assistance with the MCP directory API, please join our Discord server