import pandas as pd
import re
import os
import sys
import logging
from datetime import datetime, timezone
from collections import defaultdict
from difflib import SequenceMatcher
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
SIMILARITY_THRESHOLD = 0.8
TAG_MAPPING = {
'[Doc]': '文档问题',
'[Installation]': '安装问题',
'[Usage]': '使用问题',
'[Bug]': '程序Bug',
'[Performance]': '性能提案',
'[Feature]': '新功能提案',
'[RFC]': '架构调整反馈',
'[Roadmap]': '项目规划',
'CVE': '漏洞问题'
}
def get_category(title):
if not title:
return '其他问题'
for tag, category in TAG_MAPPING.items():
if title.startswith(tag) or tag in title:
return category
if title.startswith('CVE'):
return '漏洞问题'
return '其他问题'
def should_skip(title):
if not title:
return False
return '[Roadmap]' in title
def normalize_title(title):
if not title:
return ''
title = re.sub(r'\[.*?\]', '', title)
title = re.sub(r'[^\w\u4e00-\u9fff]', '', title)
return title.lower().strip()
def calculate_similarity(title1, title2):
norm1 = normalize_title(title1)
norm2 = normalize_title(title2)
if not norm1 or not norm2:
return 0
return SequenceMatcher(None, norm1, norm2).ratio()
def escape_markdown(text):
if not text:
return ''
text = text.replace('[', '\\[').replace(']', '\\]')
return text
def format_datetime(dt_str):
if not dt_str:
return ''
try:
dt = datetime.fromisoformat(dt_str.replace('+08:00', '+08:00'))
return dt.strftime('%Y-%m-%d %H:%M:%S')
except (ValueError, TypeError):
return dt_str
def parse_reference_md(md_file):
issues_data = []
with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()
issue_blocks = re.split(r'### \d+\.', content)
for block in issue_blocks[1:]:
issue_data = {}
issue_id_match = re.search(r'\| Issue ID \| `(\d+)` \|', block)
if issue_id_match:
issue_data['issue_id'] = issue_id_match.group(1)
lines = block.strip().split('\n')
if lines:
title_line = lines[0].strip()
title_line = title_line.replace('\\[', '[').replace('\\]', ']')
issue_data['title'] = title_line
comments_match = re.search(r'\*\*💬 评论汇总\*\*:\n\n(.*?)(?=\n---|\n## |$)', block, re.DOTALL)
if comments_match:
comments_text = comments_match.group(1)
comments = []
for line in comments_text.split('\n'):
line = line.strip()
if re.match(r'^\d+\.', line):
comment = re.sub(r'^\d+\.\s*', '', line)
if comment:
comments.append(comment)
issue_data['comments'] = comments
if issue_data.get('issue_id') and issue_data.get('title'):
issues_data.append(issue_data)
return issues_data
def load_all_references(references_dir):
all_issues = []
if not os.path.exists(references_dir):
return all_issues
for filename in os.listdir(references_dir):
if filename.startswith('issue_comments_analysis') and filename.endswith('.md'):
filepath = os.path.join(references_dir, filename)
issues = parse_reference_md(filepath)
for issue in issues:
issue['source_file'] = filename
all_issues.extend(issues)
return all_issues
def find_best_match(open_issue_title, reference_issues):
best_match = None
best_similarity = 0
for ref_issue in reference_issues:
ref_title = ref_issue.get('title', '')
similarity = calculate_similarity(open_issue_title, ref_title)
if similarity >= SIMILARITY_THRESHOLD and similarity > best_similarity:
best_similarity = similarity
best_match = ref_issue
best_match['similarity'] = similarity
return best_match
def main():
if len(sys.argv) < 2:
logger.error("Missing xlsx file path parameter")
logger.info("Usage: python match_open_issues.py <xlsx_file>")
logger.info("Example: python match_open_issues.py mind-cluster_open_issue.xlsx")
return 1
xlsx_file = sys.argv[1]
if not os.path.exists(xlsx_file):
logger.error(f"File {xlsx_file} not exists")
return 1
logger.info(f"Ready to read {xlsx_file}...")
df = pd.read_excel(xlsx_file)
logger.info(f"Read {len(df)} a record of an opened Issue")
script_dir = os.path.dirname(os.path.abspath(__file__))
skill_dir = os.path.join(os.path.dirname(script_dir), '..')
references_dir = os.path.join(skill_dir, 'references')
output_dir = os.path.join(skill_dir, 'output')
os.makedirs(output_dir, exist_ok=True)
repo_match = re.search(r'([^/\\]+)_open_issue\.xlsx', os.path.basename(xlsx_file))
repo = repo_match.group(1) if repo_match else 'unknown'
logger.info(f"Repo name: {repo}")
logger.info("Loading references suggested processing documents in the directory...")
reference_issues = load_all_references(references_dir)
logger.info(f"Loading {len(reference_issues)} Issue")
logger.info("Ready to match...")
matched_issues = defaultdict(list)
skipped_count = 0
unmatched_count = 0
for _, row in df.iterrows():
issue_id = row.get('ID')
issue_title = row.get('标题', '')
issue_url = row.get('URL', '')
issue_state = row.get('状态', '')
issue_created = row.get('创建时间', '')
if should_skip(issue_title):
skipped_count += 1
logger.info(f"Skip [Roadmap] Issue: {issue_title[:50]}...")
continue
category = get_category(issue_title)
best_match = find_best_match(issue_title, reference_issues)
if best_match:
matched_issues[category].append({
'issue_id': issue_id,
'title': issue_title,
'url': issue_url,
'state': issue_state,
'created_at': issue_created,
'repo': repo,
'match': best_match
})
logger.info(f"Match success ({best_match['similarity']:.0%}): {issue_title[:40]}...")
else:
unmatched_count += 1
logger.info(f"Not match: {issue_title[:40]}...")
logger.info("Matching statistics:")
logger.info(f"Skip (Roadmap): {skipped_count}")
logger.info(f"Match success: {sum(len(issues) for issues in matched_issues.values())}")
logger.info(f"Not match: {unmatched_count}")
if not matched_issues:
logger.info("No matching Issues were found, so no output file will be generated.")
return 0
output_file = os.path.join(output_dir, f'{repo}_open_issue_suggestions.md')
with open(output_file, 'w', encoding='utf-8') as f:
f.write(f"# {repo} Open Issue 处理建议\n\n")
f.write(f"**生成时间**: {datetime.now(tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC\n\n")
f.write(f"**数据来源**: {os.path.basename(xlsx_file)}\n\n")
f.write(f"**匹配阈值**: {SIMILARITY_THRESHOLD:.0%}\n\n")
f.write("---\n\n")
f.write("## 📋 目录\n\n")
for category in sorted(matched_issues.keys()):
count = len(matched_issues[category])
anchor = re.sub(r'[^\w\u4e00-\u9fff-]', '', category).lower()
f.write(f"- [{category}](#{anchor}) ({count}个Issue)\n")
f.write("\n---\n\n")
for category in sorted(matched_issues.keys()):
issues = matched_issues[category]
f.write(f"## {category}\n\n")
f.write(f"> 共 **{len(issues)}** 个Issue匹配到处理建议\n\n")
for idx, issue in enumerate(issues, 1):
safe_title = escape_markdown(issue['title'])
match = issue['match']
f.write(f"### {idx}. {safe_title}\n\n")
f.write(f"| 属性 | 值 |\n")
f.write(f"|:-----|:---|\n")
f.write(f"| Issue ID | `{issue['issue_id']}` |\n")
f.write(f"| URL | [{issue['url']}]({issue['url']}) |\n")
f.write(f"| 状态 | {issue['state']} |\n")
f.write(f"| 仓库 | {issue['repo']} |\n")
f.write(f"| 创建时间 | {format_datetime(issue['created_at'])} |\n\n")
f.write(f"**📋 Issue处理建议**:\n\n")
f.write(f"> 匹配来源: `{match['source_file']}` \n")
f.write(f"> 相似度: **{match['similarity']:.0%}** \n")
f.write(f"> 参考Issue ID: `{match['issue_id']}`\n\n")
comments = match.get('comments', [])
if comments:
f.write("**参考评论**:\n\n")
for i, comment in enumerate(comments, 1):
comment_text = comment.replace('\n', ' \n')
f.write(f"{i}. {comment_text}\n")
f.write("\n---\n\n")
f.write("## 📊 统计信息\n\n")
f.write("| 分类 | 匹配数量 |\n")
f.write("|:-----|:----------|\n")
total = 0
for category in sorted(matched_issues.keys()):
count = len(matched_issues[category])
total += count
f.write(f"| {category} | {count} |\n")
f.write(f"\n**总计**: {total} 个Issue匹配到处理建议\n")
logger.info("Matching completed successfully!")
logger.info(f"Output file: {output_file}")
size = os.path.getsize(output_file)
logger.info(f"File size: {size / 1024:.1f} KB")
return 0
if __name__ == "__main__":
sys.exit(main())