Skip to main content
Glama
approval-manager.ts7.6 kB
/** * 审批管理器 * 管理变更提案的审批流程 */ import * as fs from 'fs/promises'; import * as path from 'path'; import type { ApprovalRecord, ApprovalStatus } from '../types/openspec.js'; export interface ApprovalManagerOptions { cwd?: string; } export class ApprovalManager { private cwd: string; constructor(options?: ApprovalManagerOptions) { this.cwd = options?.cwd || process.cwd(); } /** * 获取审批目录路径 */ private getApprovalsDir(): string { return path.join(this.cwd, 'openspec', 'approvals'); } /** * 获取审批记录文件路径 */ private getApprovalPath(changeId: string): string { return path.join(this.getApprovalsDir(), `${changeId}.json`); } /** * 确保审批目录存在 */ private async ensureApprovalsDir(): Promise<void> { const dir = this.getApprovalsDir(); try { await fs.access(dir); } catch { await fs.mkdir(dir, { recursive: true }); } } /** * 获取审批状态 */ async getApprovalStatus(changeId: string): Promise<ApprovalRecord | null> { const approvalPath = this.getApprovalPath(changeId); try { const content = await fs.readFile(approvalPath, 'utf-8'); return JSON.parse(content) as ApprovalRecord; } catch { return null; } } /** * 创建或更新审批记录 */ private async saveApproval(record: ApprovalRecord): Promise<void> { await this.ensureApprovalsDir(); const approvalPath = this.getApprovalPath(record.changeId); await fs.writeFile(approvalPath, JSON.stringify(record, null, 2), 'utf-8'); } /** * 请求审批 */ async requestApproval( changeId: string, requestedBy: string, reviewers?: string[] ): Promise<ApprovalRecord> { const existing = await this.getApprovalStatus(changeId); const now = new Date().toISOString(); const record: ApprovalRecord = existing || { changeId, status: 'draft', approvals: [], rejections: [], history: [], }; // 更新状态 record.status = 'pending_approval'; record.requestedAt = now; record.requestedBy = requestedBy; if (reviewers) { record.reviewers = reviewers; } // 添加历史记录 record.history.push({ action: 'request_approval', by: requestedBy, at: now, details: reviewers ? `Reviewers: ${reviewers.join(', ')}` : undefined, }); await this.saveApproval(record); return record; } /** * 审批通过 */ async approve( changeId: string, approver: string, comment?: string ): Promise<ApprovalRecord> { const record = await this.getApprovalStatus(changeId); const now = new Date().toISOString(); if (!record) { throw new Error(`No approval record found for change: ${changeId}`); } if (record.status !== 'pending_approval') { throw new Error( `Cannot approve change in status: ${record.status}. Must be pending_approval.` ); } // 添加审批 record.approvals.push({ approver, approvedAt: now, comment, }); // 更新状态 record.status = 'approved'; // 添加历史记录 record.history.push({ action: 'approved', by: approver, at: now, details: comment, }); await this.saveApproval(record); return record; } /** * 审批拒绝 */ async reject( changeId: string, rejector: string, reason: string ): Promise<ApprovalRecord> { const record = await this.getApprovalStatus(changeId); const now = new Date().toISOString(); if (!record) { throw new Error(`No approval record found for change: ${changeId}`); } if (record.status !== 'pending_approval') { throw new Error( `Cannot reject change in status: ${record.status}. Must be pending_approval.` ); } // 添加拒绝 record.rejections.push({ rejector, rejectedAt: now, reason, }); // 更新状态 record.status = 'rejected'; // 添加历史记录 record.history.push({ action: 'rejected', by: rejector, at: now, details: reason, }); await this.saveApproval(record); return record; } /** * 开始实施 */ async startImplementation( changeId: string, implementer: string ): Promise<ApprovalRecord> { const record = await this.getApprovalStatus(changeId); const now = new Date().toISOString(); if (!record) { throw new Error(`No approval record found for change: ${changeId}`); } if (record.status !== 'approved') { throw new Error( `Cannot start implementation in status: ${record.status}. Must be approved.` ); } record.status = 'implementing'; record.history.push({ action: 'start_implementation', by: implementer, at: now, }); await this.saveApproval(record); return record; } /** * 标记完成 */ async markCompleted(changeId: string, completedBy: string): Promise<ApprovalRecord> { const record = await this.getApprovalStatus(changeId); const now = new Date().toISOString(); if (!record) { throw new Error(`No approval record found for change: ${changeId}`); } if (record.status !== 'implementing') { throw new Error( `Cannot mark as completed in status: ${record.status}. Must be implementing.` ); } record.status = 'completed'; record.history.push({ action: 'completed', by: completedBy, at: now, }); await this.saveApproval(record); return record; } /** * 重置为草稿(用于被拒绝后重新提交) */ async resetToDraft(changeId: string, resetBy: string): Promise<ApprovalRecord> { const record = await this.getApprovalStatus(changeId); const now = new Date().toISOString(); if (!record) { // 创建新的草稿记录 const newRecord: ApprovalRecord = { changeId, status: 'draft', approvals: [], rejections: [], history: [ { action: 'created', by: resetBy, at: now, }, ], }; await this.saveApproval(newRecord); return newRecord; } record.status = 'draft'; record.history.push({ action: 'reset_to_draft', by: resetBy, at: now, }); await this.saveApproval(record); return record; } /** * 列出所有审批记录 */ async listApprovals(): Promise<ApprovalRecord[]> { const dir = this.getApprovalsDir(); const records: ApprovalRecord[] = []; try { const files = await fs.readdir(dir); for (const file of files) { if (!file.endsWith('.json')) continue; try { const content = await fs.readFile(path.join(dir, file), 'utf-8'); records.push(JSON.parse(content)); } catch { // 跳过无效文件 } } } catch { // 目录不存在 } return records; } /** * 列出待审批的记录 */ async listPendingApprovals(): Promise<ApprovalRecord[]> { const all = await this.listApprovals(); return all.filter((r) => r.status === 'pending_approval'); } /** * 删除审批记录 */ async deleteApproval(changeId: string): Promise<boolean> { const approvalPath = this.getApprovalPath(changeId); try { await fs.unlink(approvalPath); return true; } catch { return false; } } }

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/Lumiaqian/openspec-mcp'

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