This commit is contained in:
Leonard-ThindPad-P50
2026-09-08 10:26:21 +07:00
commit c81f249920
169 changed files with 38726 additions and 0 deletions

View File

@@ -0,0 +1,284 @@
export const meta = {
name: 'generate-proposal',
description: 'Tổng hợp tài liệu SAD thành proposal khách hàng dạng HTML/CSS: content (nội dung thương mại) → build (HTML) → review (rò rỉ nội bộ, trung thực, chất lượng HTML). Mỗi lần gọi chạy 1 stage (args.stage) có gate; "all" chạy liền kèm 1 vòng tự sửa theo review.',
whenToUse: 'Gọi từ skill sad-proposal sau khi SAD đã hoàn thiện (docs/SAD.md) và đã có docs/proposal/proposal-config.md.',
phases: [
{ title: 'Content', detail: 'proposal-writer: SAD + config → proposal-content.md' },
{ title: 'Build', detail: 'proposal-builder: content → index.html + artifact.html' },
{ title: 'Review', detail: 'proposal-reviewer: rò rỉ nội bộ, trung thực, chất lượng HTML' },
],
}
// ---------------------------------------------------------------------------
// Tham số
// stage: content | build | review | all (mặc định: all)
// notes: ghi chú người duyệt khi chạy lại 1 stage (Revise)
// keyFacts: danh sách {fact, source} từ gate content, truyền cho stage review
// autoFix: (all) tự sửa 1 vòng theo review — mặc định true
// sadFile / sectionsDir / briefFile / configFile / outDir: đường dẫn
// ---------------------------------------------------------------------------
const a = args && typeof args === 'object' ? args : {}
const STAGES = ['content', 'build', 'review']
const stage = a.stage || 'all'
if (stage !== 'all' && !STAGES.includes(stage)) {
throw new Error(`args.stage không hợp lệ: "${stage}". Hợp lệ: all | ${STAGES.join(' | ')}`)
}
const SAD = a.sadFile || 'docs/SAD.md'
const S = a.sectionsDir || 'docs/sections'
const BRIEF = a.briefFile || 'docs/00-project-brief.md'
const CFG = a.configFile || 'docs/proposal/proposal-config.md'
const OUT = a.outDir || 'docs/proposal'
const CONTENT = `${OUT}/proposal-content.md`
const HTML = `${OUT}/index.html`
const ART = `${OUT}/artifact.html`
const autoFix = a.autoFix !== false
const notes = stage !== 'all' && a.notes
? `\n\n## Ghi chú từ người duyệt (bắt buộc xử lý trước khi làm gì khác)\n${a.notes}\n`
: ''
const run = (s) => stage === 'all' || stage === s
// ---------------------------------------------------------------------------
// Schema — mọi agent trả structured output để script rẽ nhánh bằng code
// ---------------------------------------------------------------------------
const CONTENT_SCHEMA = {
type: 'object',
properties: {
filesWritten: { type: 'array', items: { type: 'string' } },
sections: {
type: 'array',
items: {
type: 'object',
properties: { id: { type: 'string' }, title: { type: 'string' }, sourceSadSections: { type: 'array', items: { type: 'string' } } },
required: ['id', 'title'],
},
},
placeholders: {
type: 'array',
items: {
type: 'object',
properties: { id: { type: 'string' }, description: { type: 'string' }, section: { type: 'string' } },
required: ['description'],
},
},
excludedInternal: { type: 'array', items: { type: 'string' } },
keyFacts: {
type: 'array',
items: { type: 'object', properties: { fact: { type: 'string' }, source: { type: 'string' } }, required: ['fact', 'source'] },
},
confidence: { type: 'string', enum: ['high', 'medium', 'low'] },
summary: { type: 'string' },
},
required: ['filesWritten', 'sections', 'placeholders', 'confidence', 'summary'],
}
const BUILD_SCHEMA = {
type: 'object',
properties: {
filesWritten: { type: 'array', items: { type: 'string' } },
sectionsRendered: { type: 'array', items: { type: 'string' } },
placeholdersCount: { type: 'number' },
mermaidBlocks: { type: 'number' },
approxSizeKB: { type: 'number' },
checks: {
type: 'object',
properties: {
standaloneDoc: { type: 'boolean' },
artifactVariant: { type: 'boolean' },
title: { type: 'boolean' },
tocAnchors: { type: 'boolean' },
themeTokens: { type: 'boolean' },
printCss: { type: 'boolean' },
responsiveTables: { type: 'boolean' },
mermaidLoaderGuarded: { type: 'boolean' },
},
},
confidence: { type: 'string', enum: ['high', 'medium', 'low'] },
summary: { type: 'string' },
},
required: ['filesWritten', 'checks', 'confidence', 'summary'],
}
const REVIEW_SCHEMA = {
type: 'object',
properties: {
verdict: { type: 'string', enum: ['pass', 'revise'] },
leaks: {
type: 'array',
items: { type: 'object', properties: { file: { type: 'string' }, snippet: { type: 'string' }, why: { type: 'string' } }, required: ['file', 'snippet'] },
},
placeholders: { type: 'array', items: { type: 'string' } },
findings: {
type: 'array',
items: {
type: 'object',
properties: {
target: { type: 'string', enum: ['content', 'html'] },
issue: { type: 'string' },
suggestion: { type: 'string' },
severity: { type: 'string', enum: ['high', 'medium', 'low'] },
},
required: ['target', 'issue', 'severity'],
},
},
factChecks: {
type: 'array',
items: { type: 'object', properties: { claim: { type: 'string' }, sadEvidence: { type: 'string' }, ok: { type: 'boolean' } }, required: ['claim', 'ok'] },
},
summary: { type: 'string' },
},
required: ['verdict', 'leaks', 'findings', 'summary'],
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const sources =
`Nguồn: ${CFG} (thông tin thương mại — nguồn duy nhất cho giá/ngày/tên người), ` +
`${SAD} (nếu chưa có thì đọc ${BRIEF} + toàn bộ ${S}/01–09).`
const fmtFindings = (list) =>
list.map((f, i) => `${i + 1}. [${f.severity}] ${f.issue}${f.suggestion ? ` → ${f.suggestion}` : ''}`).join('\n')
function writeContent(extra) {
return agent(
`${sources}\nSoạn nội dung proposal khách hàng theo đúng cấu trúc và marker trong system prompt của bạn, ghi ra ${CONTENT}. ` +
`Tuyệt đối không đưa nội dung nội bộ (ghi chú rà soát, trạng thái duyệt, câu hỏi mở, findings, mã FR ngoài Phụ lục) vào proposal. ` +
`Không bịa giá/ngày/số liệu — dùng [[CẦN ĐIỀN: ...]] và liệt kê trong placeholders.${extra || ''}`,
{ agentType: 'proposal-writer', label: 'proposal-writer', phase: 'Content', schema: CONTENT_SCHEMA },
)
}
function buildHtml(extra) {
return agent(
`Đọc ${CONTENT} và ${CFG} (brandColor, language, logo nếu có). Dựng ${HTML} (tài liệu HTML độc lập hoàn chỉnh) ` +
`và ${ART} (biến thể Artifact: không doctype/html/head/body, bắt đầu bằng <title> rồi <style>) theo đúng design spec trong system prompt. ` +
`Không đổi câu chữ của content; mọi [[CẦN ĐIỀN]] phải thành <mark class="todo">. Tự kiểm checklist trước khi trả kết quả.${extra || ''}`,
{ agentType: 'proposal-builder', label: 'proposal-builder', phase: 'Build', schema: BUILD_SCHEMA },
)
}
function reviewAll(keyFacts) {
const facts = Array.isArray(keyFacts) && keyFacts.length
? `\nkeyFacts từ proposal-writer cần đối chiếu từng mục:\n${keyFacts.map((k) => `- ${k.fact} (nguồn: ${k.source})`).join('\n')}`
: ''
return agent(
`Rà soát ${CONTENT}, ${HTML}, ${ART} đối chiếu với ${SAD} (hoặc ${BRIEF} + ${S}/) và ${CFG}. ` +
`Ba lăng kính: (1) rò rỉ nội dung nội bộ; (2) độ trung thực số liệu/cam kết; (3) placeholder còn lại và chất lượng HTML theo checklist. ` +
`Không sửa file — chỉ trả findings.${facts}`,
{ agentType: 'proposal-reviewer', label: 'proposal-reviewer', phase: 'Review', schema: REVIEW_SCHEMA },
)
}
function contentGate(c) {
const r = c || {}
return {
stage: 'content', ok: !!c,
filesWritten: r.filesWritten || [],
sections: r.sections || [],
placeholders: r.placeholders || [],
excludedInternal: r.excludedInternal || [],
keyFacts: r.keyFacts || [],
confidence: r.confidence || 'unknown',
summary: r.summary || '',
}
}
function buildGate(b) {
const r = b || {}
const checks = r.checks || {}
const failed = Object.keys(checks).filter((k) => checks[k] === false)
return {
stage: 'build', ok: !!b,
filesWritten: r.filesWritten || [],
sectionsRendered: r.sectionsRendered || [],
placeholdersCount: typeof r.placeholdersCount === 'number' ? r.placeholdersCount : null,
mermaidBlocks: r.mermaidBlocks,
approxSizeKB: r.approxSizeKB,
checks, failedChecks: failed,
confidence: r.confidence || 'unknown',
summary: r.summary || '',
files: { html: HTML, artifact: ART },
}
}
function reviewGate(v) {
const r = v || {}
const leaks = r.leaks || []
const placeholders = r.placeholders || []
const findings = r.findings || []
return {
stage: 'review', ok: !!v,
verdict: r.verdict || 'unknown',
leaks, placeholders, findings,
factChecks: r.factChecks || [],
failedFacts: (r.factChecks || []).filter((f) => f.ok === false),
canSend: r.verdict === 'pass' && leaks.length === 0 && placeholders.length === 0,
summary: r.summary || '',
}
}
// ---------------------------------------------------------------------------
// Pipeline
// ---------------------------------------------------------------------------
const report = { stage, autoFixed: false, files: { content: CONTENT, html: HTML, artifact: ART } }
if (stage === 'all') log('Chế độ "all": content → build → review, tự sửa tối đa 1 vòng theo review. Không có cổng phê duyệt giữa các bước.')
if (run('content')) {
phase('Content')
const c = await writeContent(notes)
report.content = contentGate(c)
if (stage === 'content') return report.content
}
if (run('build')) {
phase('Build')
const b = await buildHtml(notes)
report.build = buildGate(b)
if (stage === 'build') return report.build
}
if (run('review')) {
phase('Review')
let keyFacts = report.content ? report.content.keyFacts : (Array.isArray(a.keyFacts) ? a.keyFacts : [])
const v = await reviewAll(keyFacts)
report.review = reviewGate(v)
if (stage === 'review') return report.review
// --- all: 1 vòng tự sửa ---------------------------------------------------
if (autoFix && v && v.verdict === 'revise') {
const fixable = (v.findings || []).filter((f) => f.severity !== 'low')
const leakNotes = (v.leaks || []).map((l) => ({
severity: 'high', target: 'content',
issue: `Rò rỉ nội dung nội bộ trong ${l.file}: "${l.snippet}"`,
suggestion: l.why || 'Loại bỏ hoặc viết lại theo góc nhìn khách hàng',
}))
const contentNotes = fixable.filter((f) => f.target === 'content').concat(leakNotes)
const htmlNotes = fixable.filter((f) => f.target === 'html')
if (contentNotes.length) {
log(`Tự sửa: ${contentNotes.length} finding nội dung → chạy lại content, build, review.`)
phase('Content')
const c2 = await writeContent(`\n\n## Findings từ reviewer (bắt buộc xử lý)\n${fmtFindings(contentNotes)}\n`)
report.content = contentGate(c2)
keyFacts = report.content.keyFacts
phase('Build')
const b2 = await buildHtml(
htmlNotes.length
? `\n\n## Findings từ reviewer (bắt buộc xử lý)\n${fmtFindings(htmlNotes)}\n\nNội dung cũng đã được sửa — dựng lại từ content mới.`
: '\n\nNội dung đã được sửa theo review — dựng lại HTML từ content mới.',
)
report.build = buildGate(b2)
} else if (htmlNotes.length) {
log(`Tự sửa: ${htmlNotes.length} finding HTML → chạy lại build, review.`)
phase('Build')
const b2 = await buildHtml(`\n\n## Findings từ reviewer (bắt buộc xử lý)\n${fmtFindings(htmlNotes)}\n`)
report.build = buildGate(b2)
}
if (contentNotes.length || htmlNotes.length) {
phase('Review')
const v2 = await reviewAll(keyFacts)
report.review = reviewGate(v2)
report.autoFixed = true
} else {
log('Review yêu cầu sửa nhưng chỉ có finding mức low — bỏ qua tự sửa, để người duyệt quyết định.')
}
}
}
return report