11import type { AgentState , LogEntry , ReviewEntry , TaskStatus , EngineEvent } from './types.js'
22import { runKimi , detectKimiCli , type KimiProcess } from './kimi.js'
3- import { getChangedFiles , getFileDiff , gitAdd , gitCommit , gitPush , createBranch , cloneRepo , gitFetch , getBranchDiff , gitDeleteRemoteBranch } from './git.js'
3+ import { getChangedFiles , getStagedFiles , getFileDiff , gitAdd , gitCommit , gitPush , createBranch , cloneRepo , gitFetch , getBranchDiff , gitDeleteRemoteBranch } from './git.js'
44import { createPullRequest , mergePullRequest , getPullRequest , getCheckRuns , getCheckRunLogs } from './github-api.js'
55
66interface SubmitStep {
@@ -334,6 +334,9 @@ export class Agent {
334334
335335 const steps : SubmitStep [ ] = [ ]
336336
337+ let prTitle = `feat: ${ this . state . name } `
338+ let prBody = `由 Kimi Code Swarm Agent 自动创建`
339+
337340 if ( this . state . workspace ) {
338341 // git add
339342 const addRes = await gitAdd ( this . state . workspace )
@@ -342,8 +345,13 @@ export class Agent {
342345 return { ok : false , steps }
343346 }
344347
348+ // 获取 staged 文件列表,生成规范的 commit message 和 PR 描述
349+ const stagedFiles = await getStagedFiles ( this . state . workspace )
350+ const generated = await this . generateCommitAndPrBody ( stagedFiles )
351+ this . log ( 'system' , `生成提交信息: ${ generated . commitMessage } ` )
352+
345353 // git commit
346- const commitRes = await gitCommit ( this . state . workspace , `feat: ${ this . state . name } ` )
354+ const commitRes = await gitCommit ( this . state . workspace , generated . commitMessage )
347355 steps . push ( { name : 'git commit' , stdout : commitRes . stdout , stderr : commitRes . stderr , exitCode : commitRes . exitCode } )
348356 if ( commitRes . exitCode !== 0 ) {
349357 return { ok : false , steps }
@@ -357,6 +365,10 @@ export class Agent {
357365 }
358366
359367 this . log ( 'system' , '代码已推送至远程' )
368+
369+ // 保存生成的 PR 内容(commit 后 staged 文件会被清空,需提前保存)
370+ prTitle = generated . prTitle
371+ prBody = generated . prBody
360372 }
361373
362374 this . setStatus ( 'reviewing' )
@@ -371,7 +383,7 @@ export class Agent {
371383 // 如果有 GitHub Token,调用真实 API 创建 PR
372384 if ( githubToken ) {
373385 try {
374- const pr = await createPullRequest ( githubToken , this . state . repoUrl , this . state . branch , `feat: ${ this . state . name } ` )
386+ const pr = await createPullRequest ( githubToken , this . state . repoUrl , this . state . branch , prTitle , prBody )
375387 if ( pr ) {
376388 this . state . prStatus = 'open'
377389 this . state . prNumber = pr . number
@@ -677,6 +689,120 @@ export class Agent {
677689 }
678690 }
679691
692+ /**
693+ * 基于变更文件列表生成 commit message 和 PR 描述
694+ * 优先调用 Kimi CLI 生成高质量内容,失败时 fallback 到规则生成
695+ */
696+ private async generateCommitAndPrBody ( files : string [ ] ) : Promise < { commitMessage : string ; prTitle : string ; prBody : string } > {
697+ // 1. 尝试用 Kimi CLI 生成
698+ const fileList = files . map ( ( f ) => `- ${ f } ` ) . join ( '\n' )
699+ const prompt = `你是一位资深工程师,请根据以下代码变更文件列表,生成规范的 commit message 和 PR 描述。
700+
701+ 变更文件:
702+ ${ fileList }
703+
704+ 要求:
705+ 1. commit message 符合 Conventional Commits 规范,格式为 type(scope): description(英文,单行,不超过72字符)
706+ 2. PR 标题与 commit message 保持一致
707+ 3. PR 描述用中文 Markdown 格式,简要列出每个新增/修改文件的作用(一句话)
708+
709+ 请严格按以下格式输出(不要有多余内容):
710+ COMMIT: <commit message>
711+ ---
712+ PR_BODY:
713+ <pr body>
714+ `
715+
716+ try {
717+ const output = await this . runInstructionSilent ( prompt , 60000 )
718+ const commitMatch = output . match ( / C O M M I T : \s * ( .+ ) / )
719+ const bodyMatch = output . match ( / P R _ B O D Y : \s * ( [ \s \S ] + ) / )
720+
721+ if ( commitMatch && bodyMatch ) {
722+ const commitMessage = commitMatch [ 1 ] . trim ( )
723+ const prBody = bodyMatch [ 1 ] . trim ( )
724+ return { commitMessage, prTitle : commitMessage , prBody }
725+ }
726+ } catch {
727+ // Kimi CLI 生成失败,继续 fallback
728+ }
729+
730+ // 2. Fallback:基于规则自动生成
731+ const { scope, action } = this . inferScopeAndAction ( files )
732+ const description = this . inferDescription ( files , action )
733+ const commitMessage = `${ action } ${ scope ? `(${ scope } )` : '' } : ${ description } `
734+
735+ const prBodyLines = files . map ( ( f ) => {
736+ const filename = f . split ( '/' ) . pop ( ) || f
737+ if ( f . endsWith ( '.spec.ts' ) || f . endsWith ( '.test.ts' ) ) return `- 补充 \`${ filename } \` 单元测试`
738+ if ( f . endsWith ( '.vue' ) ) return `- 新增/更新 \`${ filename } \` 组件`
739+ if ( f . endsWith ( '.ts' ) || f . endsWith ( '.js' ) ) return `- 新增/更新 \`${ filename } \` 逻辑`
740+ if ( f . endsWith ( '.md' ) ) return `- 更新 \`${ filename } \` 文档`
741+ return `- 变更 \`${ filename } \``
742+ } )
743+
744+ const prBody = prBodyLines . join ( '\n' )
745+ return { commitMessage, prTitle : commitMessage , prBody }
746+ }
747+
748+ /**
749+ * 根据文件路径推断 scope 和 action
750+ */
751+ private inferScopeAndAction ( files : string [ ] ) : { scope : string ; action : string } {
752+ const scopes = new Set < string > ( )
753+ let hasNew = false
754+ let hasModify = false
755+
756+ for ( const f of files ) {
757+ if ( f . startsWith ( 'kimi-code-swarm/src/components/' ) || f . startsWith ( 'kimi-code-swarm/src/composables/' ) || f . startsWith ( 'kimi-code-swarm/src/App.vue' ) ) {
758+ scopes . add ( 'frontend' )
759+ } else if ( f . startsWith ( 'kimi-code-swarm/src/store/' ) || f . startsWith ( 'kimi-code-swarm/src/api/' ) ) {
760+ scopes . add ( 'frontend' )
761+ } else if ( f . startsWith ( 'agent-engine/src/' ) ) {
762+ scopes . add ( 'agent-engine' )
763+ } else if ( f . startsWith ( 'docs/' ) ) {
764+ scopes . add ( 'docs' )
765+ } else if ( f . startsWith ( 'tests/' ) || f . includes ( '.spec.ts' ) || f . includes ( '.test.ts' ) ) {
766+ scopes . add ( 'test' )
767+ } else if ( f . startsWith ( 'ci/' ) ) {
768+ scopes . add ( 'ci' )
769+ } else if ( f . startsWith ( 'ast/' ) ) {
770+ scopes . add ( 'ast' )
771+ } else if ( f . startsWith ( 'src-tauri/' ) ) {
772+ scopes . add ( 'tauri' )
773+ }
774+ // 简单判断新增还是修改(通过文件名特征无法准确判断,默认用 update,如果有测试文件用 add test)
775+ if ( f . includes ( '.spec.ts' ) || f . includes ( '.test.ts' ) ) hasNew = true
776+ else hasModify = true
777+ }
778+
779+ const scope = scopes . size === 1 ? Array . from ( scopes ) [ 0 ] : scopes . size > 1 ? 'multi' : ''
780+ const action = hasNew && ! hasModify ? 'feat' : hasNew && hasModify ? 'feat' : 'refactor'
781+ return { scope, action }
782+ }
783+
784+ /**
785+ * 根据文件名生成描述
786+ */
787+ private inferDescription ( files : string [ ] , action : string ) : string {
788+ const names = files
789+ . map ( ( f ) => f . split ( '/' ) . pop ( ) || f )
790+ . filter ( ( f ) => ! f . endsWith ( '.spec.ts' ) && ! f . endsWith ( '.test.ts' ) )
791+
792+ if ( names . length === 0 ) {
793+ const testFiles = files . map ( ( f ) => f . split ( '/' ) . pop ( ) || f ) . filter ( ( f ) => f . endsWith ( '.spec.ts' ) || f . endsWith ( '.test.ts' ) )
794+ if ( testFiles . length > 0 ) return `add unit tests for ${ testFiles . map ( ( f ) => f . replace ( / \. ( s p e c | t e s t ) \. t s $ / , '' ) ) . join ( ', ' ) } `
795+ }
796+
797+ if ( names . length === 1 ) {
798+ const name = names [ 0 ] . replace ( / \. v u e $ / , '' ) . replace ( / \. t s $ / , '' ) . replace ( / \. j s $ / , '' )
799+ return action === 'feat' ? `add ${ name } ` : `update ${ name } `
800+ }
801+
802+ const baseNames = names . map ( ( n ) => n . replace ( / \. v u e $ / , '' ) . replace ( / \. t s $ / , '' ) . replace ( / \. j s $ / , '' ) )
803+ return action === 'feat' ? `add ${ baseNames . slice ( 0 , 3 ) . join ( ', ' ) } ${ baseNames . length > 3 ? ' and more' : '' } ` : `update multiple files`
804+ }
805+
680806 /**
681807 * 自动审阅指定分支的代码变更
682808 * 返回 { approved, comment }
0 commit comments