ã¯ããã« ã¯ãããŸããŠãæ ªåŒäŒç€Ÿã¹ã¿ã¡ã³ã®ãšã³ãžãã¢ã®éŽæš( @16suzu )ã§ãã åŒç€Ÿã¹ã¿ã¡ã³ã§ã¯ãçµç¹ã®ãšã³ã²ãŒãžã¡ã³ããé«ããããã®ãµãŒãã¹ã§ãã TUNAG ãšããã£ãããµãŒãã¹ã® TUNAGãã£ãã ãéçºã»éå¶ããŠããŸãã åŒç€Ÿã§ã¯ããã°ããŒãã£ã³ã°ã®äžç°ãšããŸããŠããã® TUNAG ãš TUNAGãã£ãããå
šç€Ÿã§æ¥åå©çšããŠããŸãã æ¥ã
ããããã®ãµãŒãã¹ãéå¶ããäžã§ãCSããŒã çµç±ã§ã客æ§ãããåãåãããããã ããŸãã ãããŸã§ã¯ã TUNAGãã£ããäžã§é£çµ¡ãæ¥ãéã«ãPdMãæåã§ GitHub Issue ã起祚ãããšã³ãžãã¢ããŒã ã察å¿ããŠããŸããã ãããããã®ããæ¹ã§ã¯PdMãä»åšããŠããŸãããå·¥æ°è² è·ã課é¡ãšãªã£ãŠãããŸãããä»åãç§ã¯ãã®èª²é¡ã解決ããããã«ã Google Forms ãããªã¬ãŒã« TUNAGãã£ããã®æçš¿ãåéããGemini ã§èŠçŽããäžã§ GitHub ã« Issue ãèªå起祚ããã¯ãŒã¯ãã㌠ã Google Apps ScriptïŒGASïŒã§æ§ç¯ããŸãããæ¬èšäºã§ã¯ãã®èšèšãšå®è£
ã玹ä»ããŸãã â»æ³šæïŒæ¬èšäºã§ã¯æ€èšŒã®ãã AI Studio ã® API ã䜿çšããŠããŸããã宿¥åã§é¡§å®¢ããŒã¿ãªã©æ©å¯æ
å ±ãæ±ãå Žåã¯ãããŒã¿ãåŠç¿ã«å©çšãããªãææãã©ã³ã Google Cloud Vertex AI ã®å©çšãæšå¥šããŸãã èª²é¡ åŸæ¥ã®ãããŒã«ã¯ä»¥äžã®åé¡ç¹ããããŸããã TUNAGãã£ããã®æçš¿å
容ãæåã§ã³ããŒããŠãIssue ãäœæããŠãã 起祚ã®ã¿ã€ãã³ã°ãæ
åœè
ã«äŸåããå¯Ÿå¿æŒããçºçããããšããã£ã ã¯ãŒã¯ãããŒã®å
šäœå ããã§ã以äžã®ãããªã¯ãŒã¯ãããŒãæ§ç¯ããŸããã CS ã¡ã³ããŒã Priority ã倿ã Google Forms ãéä¿¡ â GAS ããã©ãŒã åçãåä¿¡ â TUNAG ãã£ããã® API ã§åãåãããã£ã³ãã«ã®æçš¿ãšã¹ã¬ããæ
å ±ãååŸ â Gemini API ã§æçš¿å
容ãèŠçŽã»æŽåœ¢ â GitHub API ã§ Issue ãèªå起祚 â Issue ããåãåããçšã® GitHub Projects ã«å
¥ã Priority ã Type ãèšå®ãã ãã©ãŒã ã«ã¯ãTunag Chat LinkããPriorityããTicket typeããã¡ãŒã«ã¢ãã¬ã¹ãã®å
¥åé
ç®ãèšãã起祚æã« Priority ããã±ããã¿ã€ããèšå®ã§ããããã«ããŸããã å®è£
次ã«ã³ãŒãã瀺ããŸãã 1. Google Formsã®éä¿¡ãããªã¬ãŒã«åŠçãéå§ ãšã³ããªãŒãã€ã³ããšãªããã¡ã€ã«ã§ãã Google Forms ã®æçš¿ãããªã¬ãŒãšããåã¢ãžã¥ãŒã«ãé çªã«å®è¡ããŠãããŸãã // main.gs const TC_CHANNEL_ID = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' const GITHUB_OWNER = "your-org" const GITHUB_REPO = "your-repo" function submitForm ( event ) { console . info ( event . namedValues ) ; const namedValues = event . namedValues ; const tunagChatLink = namedValues [ "Tunag Chat Link" ][ 0 ] ; const priority = namedValues [ "Priority" ][ 0 ] ; const ticketType = namedValues [ "Ticket type" ][ 0 ] ; const chatId = tunagChatLink . split ( "/pl/" )[ 1 ] ; // åãåããã¹ã¬ããã®ID const email = namedValues [ "ã¡ãŒã«ã¢ãã¬ã¹" ][ 0 ] ; const chatMessages = getChatThread ( chatId ) ; const image_links = imageLinks ( chatMessages ) ; const title = createIssueTitle ( JSON . stringify ( chatMessages )) ; const body = createIssueBody ( JSON . stringify ( chatMessages ) , tunagChatLink ) ; const body_with_image = body + `\n### ç»å \n ${ image_links . join ( "\n" )} ` ; // GitHub Issueãäœæ const issue_url = createGitHubIssue ( GITHUB_OWNER , GITHUB_REPO , title , body_with_image , priority , ticketType ) ; // TUNAGãã£ããã«æçš¿. åŒæ°ã¯ channelId, rootId, issue_url postIssueUrl ( TC_CHANNEL_ID , chatId , issue_url , priority , ticketType , email ) ; } 2. TUNAGãã£ãã ããæçš¿ãååŸ TUNAGãã£ããäžã«ãååãã®æçš¿ããããã®ã§ããããååŸããŸãã工倫ããç¹ãšããŠãã¹ã¬ããå
ã§è¡ãããCSããšã³ãžãã¢ã¡ã³ããŒå士ã®è°è«ãååŸããŠããŸãã ãã®è°è«ãå«ããŠGeminiã«èŠçŽãããããšã§ãGitHub Issueã«èµ·ç¥šãããéã®æç« ã®ä¿¡é ŒåºŠãé«ããŠããŸãã // tunag_chat.gs const CHAT_URL = "https://your-tunag-chat.example.com/" ; // tunag chat ã® url const BOT_ID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; // botã®æçš¿ãgeminiã®è§£æå¯Ÿè±¡ããå€ãã®ã«äœ¿ã // TUNAGãã£ãã åŽã®æçš¿ãå
šãŠååŸãã function getChatThread ( chat_id ) { const tunag_chat_api_token = PropertiesService . getScriptProperties () . getProperty ( 'tunag_chat_api_token' ) const url = ` ${ CHAT_URL } /api/v4/posts/ ${ chat_id } /thread?per_page=200` ; const headers = { 'Authorization' : 'Bearer ' + tunag_chat_api_token } ; const options = { 'method' : "GET" , 'headers' : headers , } ; const response = UrlFetchApp . fetch ( url , options ) ; var jsonData = JSON . parse ( response . getContentText ()) ; let res = [] ; for ( const key of jsonData . order ) { let post = jsonData . posts [ key ] // botããã®æçš¿ã¯å«ããªã if ( post . user_id == BOT_ID ) { continue } res . push ( { id : post . id , message : post . message , user_id : post . user_id , file_ids : post . file_ids , created_at : formatedDate ( post . create_at ) , }) ; } return res ; } // TUNAGãã£ãã ã®å
ã®ã¹ã¬ããã«äœæããIssueã®URLãæçš¿ãã function postIssueUrl ( channelId , rootId , issue_url , priority , ticketType , email ) { const issuerName = email . split ( '@' )[ 0 ] ; const message = ` ${ issuerName } ã Priority \` ${ priority } \` TicketType \` ${ ticketType } \` ã§IssueãäœæããŸããã ${ issue_url } ` const tunag_chat_api_token = PropertiesService . getScriptProperties () . getProperty ( 'tunag_chat_api_token' ) const postUrl = ` ${ CHAT_URL } /api/v4/posts` ; const payload = { channel_id : channelId , message : message , root_id : rootId , // ããã§è¿ä¿¡å
ã®ã¡ãã»ãŒãžIDãæå® } ; const options = { method : 'post' , contentType : 'application/json' , payload : JSON . stringify ( payload ) , headers : { 'Authorization' : `Bearer ${ tunag_chat_api_token } ` } , muteHttpExceptions : true } ; try { const response = UrlFetchApp . fetch ( postUrl , options ) ; const responseCode = response . getResponseCode () ; if ( responseCode === 201 ) { Logger . log ( 'ã¡ãã»ãŒãžãã¹ã¬ããã«æ£åžžã«æçš¿ãããŸããã' ) ; return JSON . parse ( response . getContentText ()) ; } else { Logger . log ( `æçš¿ã«å€±æããŸãããã¹ããŒã¿ã¹ã³ãŒã: ${ responseCode } ` ) ; Logger . log ( `ã¬ã¹ãã³ã¹å
容: ${ response . getContentText ()} ` ) ; return null ; } } catch ( e ) { Logger . log ( `APIãªã¯ãšã¹ãäžã«ãšã©ãŒãçºçããŸãã: ${ e . message } ` ) ; return null ; } } // unix timeãæ¥æ¬èªã®å¹Žææ¥ã®æ¥ä»ã«å€æ function formatedDate ( created_at ){ const date = new Date ( created_at ) ; // timestampã¯ããªç§åäœãDateæåå const year = date . getFullYear () ; const month = String ( date . getMonth () + 1 ) . padStart ( 2 , '0' ) ; // æã¯0å§ãŸããªã®ã§+1 const day = String ( date . getDate ()) . padStart ( 2 , '0' ) ; const hours = String ( date . getHours ()) . padStart ( 2 , '0' ) ; const minutes = String ( date . getMinutes ()) . padStart ( 2 , '0' ) ; return ` ${ year } / ${ month } / ${ day } ${ hours } : ${ minutes } ` ; } /** * ãã¡ã€ã«IDãæã€ãã£ããã¡ãã»ãŒãžãããç»åãªã³ã¯ã®é
åãçæãã颿°ã * @param {Array<Object>} chatMessages - åèŠçŽ ãIDãšãã¡ã€ã«IDãæã€ãã£ããã¡ãã»ãŒãžãªããžã§ã¯ãã®é
åã * @returns {Array<string>} ç»åãžã®URLãªã³ã¯ã®é
åã */ function imageLinks ( chatMessages ) { return chatMessages . filter ( message => message . file_ids && message . file_ids . length > 0 ) . map ( message => ` ${ CHAT_URL } chat/pl/ ${ message . id } ` ) ; } 3. Gemini ã§èŠçŽ TUNAGãã£ããããååŸããããŒã¿ããGemini API ã䜿ã£ãŠãã©ãŒãããã«åŸã£ãŠèŠçŽããŸãã prompt ã«å
¥ã£ãŠããã®ã Gemini ã«å®éã«æããŠããããã³ããæã«ãªããŸãã // gemini.gs // geminiã®ã¢ãã«ãæå® const MODEL_NAME = 'gemini-2.5-flash' ; const GEMINI_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/ ${ MODEL_NAME } :generateContent` ; // gemini ã«Issueã®ã¿ã€ãã«ãäœããã function createIssueTitle ( chat_messages ) { const prompt = "ããã¯TUNAGãšãããµãŒãã¹ã®äžå
·åã®ãåãåããã®ãã£ããã®ã¹ã¬ããã§ãã ããããèŠçŽããŠGitHub Issueã起祚ãããã®ã§ãã®ã¿ã€ãã«ãèããŠãã ããã50æå以å
ã§ãé¡ãããŸãã ãã ãæåã«äŒç€ŸåãšäŒç€ŸIDã[#133 äŒç€Ÿå]ã®åœ¢åŒã§å
¥ããŠãã ãããäŒç€Ÿåãäžæã®å Žåã¯äžèŠã§ãã [ãããããã£ããã®ã¹ã¬ããã§ã] " + chat_messages const API_KEY = PropertiesService . getScriptProperties () . getProperty ( 'gemini_api_token' ) const apiUrl = ` ${ GEMINI_API_URL } ?key= ${ API_KEY } ` ; const payload = { contents : [ { parts : [ { text : prompt , } , ] , } , ] , } ; const options = { method : 'post' , contentType : 'application/json' , payload : JSON . stringify ( payload ) , muteHttpExceptions : true , } ; const response = UrlFetchApp . fetch ( apiUrl , options ) ; const responseBody = response . getContentText () ; const data = JSON . parse ( responseBody ) ; const generatedText = data . candidates [ 0 ] . content . parts [ 0 ] . text ; return generatedText ; } // gemini ã«Issueã®æ¬æãäœããã function createIssueBody ( chat_messages , tunag_chat_link ) { const format = "ããããããã©ãŒããããã§ãã ### èª²é¡ ### æ©èœ ### çºçé »åºŠ ### ç·æ¥åºŠãšéèŠåºŠ ### çºçç°å¢ ### åçŸæé ### ãã£ãããªã³ã¯" ; const prompt = `å
¥åãããããã£ããã¡ãã»ãŒãžãã«å¯ŸããŠãäžèšã®äžå
·åå ±åæžã®ããã©ãŒããããã®åœ¢ã«æŽããäžã§åºåããŠãã ããã衚çŸã¯ç°¡æœã«ãŸãšããŠãã ããã ãã©ãŒãããã®ãã£ãããªã³ã¯é
ç®ã« ${ tunag_chat_link } ãèšå
¥ããŠãã ããã ããããããã£ããã¡ãã»ãŒãžãã§ãã ${ chat_messages } ${ format } ` ; const API_KEY = PropertiesService . getScriptProperties () . getProperty ( 'gemini_api_token' ) ; const apiUrl = ` ${ GEMINI_API_URL } ?key= ${ API_KEY } ` ; const payload = { contents : [ { parts : [ { text : prompt , } , ] , } , ] , } ; const options = { method : 'post' , contentType : 'application/json' , payload : JSON . stringify ( payload ) , muteHttpExceptions : true , } ; const response = UrlFetchApp . fetch ( apiUrl , options ) ; const responseBody = response . getContentText () ; const data = JSON . parse ( responseBody ) ; const generatedText = data . candidates [ 0 ] . content . parts [ 0 ] . text ; return generatedText ; } 4. GitHub ã« Issue ã起祚 æåŸã«ãGitHub ã« Issue ã起祚ããŸãã起祚åŸã« Project ãžã®çŽä»ããšãå Field ãžã®å€ã®èšå®ãè¡ããŸãã // github.gs // GitHub GraphQL API ã§å©çšããID const GITHUB_PROJECT_ID = "xxxxxxxxxxxxxxxxxxxx" ; const GITHUB_FIELD_ID_STATUS = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; const GITHUB_OPTION_ID_TODO = "xxxxxxxx" ; const GITHUB_FIELD_ID_PRIORITY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; const GITHUB_OPTION_ID_LOW = "xxxxxxxx" ; const GITHUB_OPTION_ID_MIDDLE = "xxxxxxxx" ; const GITHUB_OPTION_ID_HIGH = "xxxxxxxx" ; const GITHUB_FIELD_ID_TICKET_TYPE = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; const GITHUB_OPTION_ID_BUG = "xxxxxxxx" ; const GITHUB_OPTION_ID_UPDATE = "xxxxxxxx" ; const GITHUB_OPTION_ID_RESEARCH = "xxxxxxxx" ; function createGitHubIssue ( owner = "your-org" , repo = "your-repo" , title = "" , body = "" , priority , ticketType ) { const pat = PropertiesService . getScriptProperties () . getProperty ( 'GITHUB_PAT' ) ; const url = `https://api.github.com/repos/ ${ owner } / ${ repo } /issues` ; const payload = { title : title , body : body } ; const options = { method : 'post' , contentType : 'application/json' , payload : JSON . stringify ( payload ) , headers : { 'Accept' : 'application/vnd.github.v3+json' , 'Authorization' : `Bearer ${ pat } ` , 'X-GitHub-Api-Version' : '2022-11-28' , } , muteHttpExceptions : true } ; try { const response = UrlFetchApp . fetch ( url , options ) ; const result = JSON . parse ( response . getContentText ()) ; // ã¹ããŒã¿ã¹ã³ãŒãã§æå/倱æãå€å® if ( response . getResponseCode () === 201 ) { Logger . log ( 'Issueãæ£åžžã«äœæãããŸãã: ' + result . html_url ) ; Logger . log ( result . node_id ) ; // Project ã«è¿œå ãã const result_add_to_project = addToProject ( GITHUB_PROJECT_ID , result . node_id ) const item_id = result_add_to_project . data . addProjectV2ItemById . item . id ; // Todo ãã»ãããã setField ( GITHUB_PROJECT_ID , item_id , // projectã«ãããitem id GITHUB_FIELD_ID_STATUS , // fieldId(Status) GITHUB_OPTION_ID_TODO // optionId(Todo) ) // Priority ãã»ãããã setPriority ( item_id , priority ) // Ticket type ãã»ãããã setTicketType ( item_id , ticketType ) return result . html_url } else { Logger . log ( 'Issueäœæã«å€±æããŸãã: ' + JSON . stringify ( result )) ; } } catch ( e ) { Logger . log ( 'APIãªã¯ãšã¹ãäžã«ãšã©ãŒãçºçããŸãã: ' + e . message ) ; } } function setPriority ( item_id , priority ) { var p_id = "" ; switch ( priority ) { case 'High' : p_id = GITHUB_OPTION_ID_HIGH ; break; case 'Middle' : p_id = GITHUB_OPTION_ID_MIDDLE ; break; case 'Low' : p_id = GITHUB_OPTION_ID_LOW ; break; default: // æªæå®ã®å Žåã¯GitHub Issueã®Priorityãèšå®ããªã return; } setField ( GITHUB_PROJECT_ID , item_id , GITHUB_FIELD_ID_PRIORITY , p_id ) } function setTicketType ( item_id , ticketType ){ var tt_id = "" ; switch ( ticketType ) { case 'Bug' : tt_id = GITHUB_OPTION_ID_BUG break; case 'Update' : tt_id = GITHUB_OPTION_ID_UPDATE break; case '仿§èª¿æ»' : tt_id = GITHUB_OPTION_ID_RESEARCH break; default: // æªæå®ã®å Žåã¯TicketTypeãèšå®ããªã return; } setField ( GITHUB_PROJECT_ID , item_id , GITHUB_FIELD_ID_TICKET_TYPE , tt_id ) } // add to github projects function addToProject ( projectId , issueNodeId ){ const mutation = ` mutation addIssue($projectId: ID!, $issueNodeId: ID!) { addProjectV2ItemById(input: { projectId: $projectId, contentId: $issueNodeId }) { item { id } } } ` ; const variables = { projectId , issueNodeId } ; return callGitHubApi ( mutation , variables ) ; } // Fieldãã»ãããã颿° function setField ( projectId , itemId , fieldId , optionId ) { const mutation = ` mutation updateItemStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { updateProjectV2ItemFieldValue(input: { projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: { singleSelectOptionId: $optionId } }) { projectV2Item { id } } } ` ; const variables = { projectId , itemId , fieldId , optionId } ; callGitHubApi ( mutation , variables ) ; } // GitHub GraphQL API ã«ãªã¯ãšã¹ããéä¿¡ãã颿° function callGitHubApi ( query , variables ) { const GITHUB_TOKEN = PropertiesService . getScriptProperties () . getProperty ( 'GITHUB_PAT' ) ; const url = 'https://api.github.com/graphql' ; const headers = { 'Authorization' : `Bearer ${ GITHUB_TOKEN } ` , 'Content-Type' : 'application/json' } ; const payload = JSON . stringify ({ query : query , variables : variables }) ; const options = { 'method' : 'post' , 'headers' : headers , 'payload' : payload } ; try { const response = UrlFetchApp . fetch ( url , options ) ; const data = JSON . parse ( response . getContentText ()) ; if ( data . errors ) { throw new Error ( `GitHub API Error: ${ JSON . stringify ( data . errors )} ` ) ; } return data ; } catch ( e ) { Logger . log ( `API call failed: ${ e . message } ` ) ; throw e ; } } å®éã«èµ·ç¥šããã GitHub Issue ãã¡ãããå®éã«èµ·ç¥šããã Issue ã®ãµã³ãã«ã§ãã Projects ãã»ãããããStatusãšPriorityãä»äžãããŠããŸãã å°å
¥åŸã®å¹æ ãã©ãŒã ãéä¿¡ããã ãã§ Issue ãäœæããããããæ
åœè
ã®è² æ
ã倧å¹
ã«è»œæžãããŸãã 起祚ãèªååãããããšã§ãè°è«ã Issue ãšããŠç¢ºå®ã«æ®ãããã«ãªããŸãã Gemini ã§ã®èŠçŽã»æŽåœ¢ã«ãããIssue ãšããŠèªã¿ããã圢åŒã§æ®ãããã«ãªããŸãã ãããã« ãã®ã¯ãŒã¯ãããŒã¯ Gemini ãšçžè«ããªããçŽ2æ¥ã§äœæããããšãã§ããŸããã ä»åèŠåŽãããšãã㯠GitHub Projects ã« Issue ãã»ããããéšåã§ GitHub GraphQL ãå©çšããç¹ã§ããã GraphQL ã®å©çšçµéšãããŸããªããããéœåºŠèª¿æ»ããªããé²ããŸããã GAS ã¯å€éšãµãŒãã¹ãšã®é£æºã容æã§ãGoogle Forms ã®ããªã¬ãŒããã®ãŸãŸå©çšã§ããç¹ãä»åã®çšéã«é©ããŠããŸããã Gemini ã®æŽ»çšã«ãããåçŽãªè»¢èšã§ã¯ãªããæŽçããã IssueããšããŠèµ·ç¥šã§ããç¹ã倧ããªã¡ãªããã§ãã åæ§ã®èª²é¡ãæ±ããããŒã ã®åèã«ãªãã°å¹žãã§ãã herp.careers