
ãå人éçºãGeminiã«RSSãããããŒã«ãããŠSlackã«éç¥ãããŠã¿ã
ã¯ããã«
ããã«ã¡ã¯ããããã§ãã
çªç¶ã§ãããæ å ±åéã£ãŠå€§å€ãããªãã§ããïŒ
æè¡ããã°ããã¥ãŒã¹ãµã€ãã远ããããããã®ã¯å±±ã»ã©ããã
ZennãQiitaãLifehackerãåäººã®æè¡ããã°âŠã
ããã®èšäºãåŸã§èªããããšæã£ãŠããã¯ããŒã¯ãããã©ãçµå±èªãŸãªãã
ãããªçµéšããããŸãããïŒ
åãããã§ããã
RSSãã£ãŒããç»é²ããŸãã£ãçµæãæ¯æ¥æ°çŸä»¶ã®èšäºãæµããŠããç¶æ ã«ã
ãã©ããèªãã°ãããããããªãã
æ¬æ«è»¢åã§ãã
æ å ±ãåŸãããã«å§ããã¯ããªã®ã«ãæ å ±ãæ¢ãæéã°ããããã£ãŠããã
ããã§èããŸããã
ããããAIã«ããããã°ãããªãïŒã
AIãåæã«RSSãå·¡åããŠãåã«ãšã£ãŠéèŠãªèšäºã ãæããŠããããã
ãããªä»çµã¿ãäœã£ãŠã¿ãŸããã
ãã®èšäºã§ç޹ä»ããããš
Gemini APIã䜿ã£ãRSSã®èªåå·¡åã·ã¹ãã
éèŠåºŠå€å®ã®ããã³ããèšèš
Goã«ãã䞊ååŠçã®å®è£
éçšããŠã¿ãææ³
ãAIã«æ å ±åéãä»»ãããããšãã人ã®åèã«ãªãã°å¬ããã§ãã
ã©ããªä»çµã¿ã
ãŸããå šäœåã説æããŸãã
åŠçãããŒ
Cloud Schedulerã1æ¥3åèµ·åïŒæ8æã»æŒ12æã»å€æ¹17æïŒ
Cloud Runã§Goã®ããã°ã©ã ãåã
ç»é²ããRSSãã£ãŒããååŸ
åèšäºãGemini APIã«æããŠéèŠåºŠãå€å®
éèŠåºŠãé«ãèšäºã ãSlackã«éç¥
Firestoreã«ä¿åããŠåãèšäºãäºåºŠè©äŸ¡ããªã
ãAIãåæã«èªãã§ãéèŠãªãã€ã ãæããŠãããã
èªåå°çšã®ãã¥ãŒã¹ãã¥ã¬ãŒã¿ãŒãçèªããŸããã
æèµ·ããŠSlackãéããšããð° éèŠãªæè¡ãã¥ãŒã¹ã§ããã£ãŠéç¥ãå±ããŠããã
ãããæã£ã以äžã«å¿«é©ãªãã§ãã
åèã«ããèšäº
ãã®ä»çµã¿ãäœãã«ããã£ãŠã以äžã®èšäºãåèã«ããŸããã
ãã¡ãã¯Pythonã§å®è£ ãããŠããŸãããåã¯Goã§å®è£ ããŸããã
çç±ã¯åŸã§èª¬æããŸãã
䜿ã£ãŠããæè¡
Goèšèª ïŒ ã¡ã€ã³ããžãã¯
Gemini API ïŒ éèŠåºŠå€å®ïŒgemini-2.5-flash-liteïŒ
Cloud Run ïŒ ãµãŒããŒã¬ã¹å®è¡
Cloud Scheduler ïŒ å®æå®è¡ïŒ1æ¥3åïŒ
Firestore ïŒ æ¢èªç®¡ç
Slack API ïŒ éç¥
Secret Manager ïŒ APIããŒã®ç®¡ç
ãã¹ãŠGoogle Cloudã§å®çµããŠããŸãã
ã·ã¹ãã æ§æå³
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â Cloud Scheduler â
â (æ¯æ¥ 8:00 / 12:00 / 17:00) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â
âŒ
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â Cloud Run (Go) â
â process_rss_go â
â â
â 1. RSSãã£ãŒããååŸ â
â 2. Firestoreã§æ¢èªãã§ã㯠â
â 3. Gemini APIã§éèŠåºŠã10段éã§å€å® â
â 4. ãããå€ä»¥äžã®èšäºãSlackã«éç¥ â
â 5. Firestoreã«ä¿åïŒæ¢èªç®¡çïŒ â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â
âââââââââââââââââŽââââââââââââââââ
⌠âŒ
âââââââââââââââââââââââââââ âââââââââââââââââââââââââââ
â Slack â â Firestore â
â ãð° éèŠãªãã¥ãŒã¹ã â â æ¢èªèšäºãä¿å â
âââââââââââââââââââââââââââ âââââââââââââââââââââââââââã³ãŒã解説
ããããã¯å®éã®ã³ãŒããèŠãŠãããŸãã
1. ãšã³ããªãŒãã€ã³ã (main.go)
ãŸãã¯ãšã³ããªãŒãã€ã³ããCloud RunããåŒã³åºãããéšåã§ãã
func ProcessRSS(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
projectID := os.Getenv("GCP_PROJECT")
if projectID == "" {
projectID = "yuyayoshiok"
}
// RSS URLsïŒç°å¢å€æ° or ããã©ã«ãïŒ
rssUrlsEnv := os.Getenv("RSS_URLS")
var rssUrls []string
if rssUrlsEnv != "" {
for _, u := range strings.Split(rssUrlsEnv, ",") {
if strings.TrimSpace(u) != "" {
rssUrls = append(rssUrls, strings.TrimSpace(u))
}
}
} else {
rssUrls = []string{
"https://zenn.dev/feed",
"https://qiita.com/popular-items/feed.atom",
"https://www.lifehacker.jp/feed/index.xml",
"https://yamadashy.github.io/tech-blog-rss-feed/feeds/rss.xml",
}
}
// ã·ãŒã¯ã¬ãããååŸ
slackToken, _ := accessSecret(ctx, projectID, "slack-bot-token")
apiKey, _ := accessSecret(ctx, projectID, "gemini-api-key")
// ãã£ã³ãã«IDãç°å¢å€æ°ããååŸ
channelID := os.Getenv("CHANNEL_3_FEED_NOTIFICATIONS_ID")
if channelID == "" {
channelID = "C09R3DHMSFL"
}
// ãã£ãŒããåŠç
count, err := processFeeds(ctx, projectID, apiKey, slackToken, channelID, rssUrls)
if err != nil {
log.Printf("ãã£ãŒãåŠçãšã©ãŒ: %v", err)
http.Error(w, "Failed to process feeds", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"count": count,
})
}ãã€ã³ã:
RSSã®URLã¯ç°å¢å€æ°ã§å€æŽå¯èœã«ããŠãã
ããã©ã«ãã¯ZennãQiitaãLifehackerãªã©
APIããŒã¯Secret ManagerããååŸ
2. éèŠåºŠå€å® (ai.go)
ããããã¢ã§ãã
Gemini APIã«ããã®èšäºãåã«ãšã£ãŠéèŠïŒããšèããŸãã
type ImportanceResult struct {
Score int `json:"score"`
Reason string `json:"reason"`
}
func evaluateImportance(ctx context.Context, apiKey, title, summary, url string) (ImportanceResult, error) {
client, err := genai.NewClient(ctx, &genai.ClientConfig{
APIKey: apiKey,
})
if err != nil {
return ImportanceResult{}, fmt.Errorf("failed to create Gemini client: %w", err)
}
prompt := fmt.Sprintf(`以äžã®æè¡èšäºã®ã¿ã€ãã«ãšèŠçŽããã
ãããããã«ãšã£ãŠã®éèŠåºŠã10段éã§è©äŸ¡ããŠãã ããã
èšäºã¿ã€ãã«: %s
èšäºèŠçŽ: %s
èšäºURL: %s
## ãããããã®ãããã£ãŒã«
- 30代ãšã³ãžãã¢
- èå³åé: Python, Goèšèª, GCP, çæAI (Gemini, ChatGPT), Automation, Obsidian, Apple補å
- 圹å²: å人éçºè
ãããªãŒã©ã³ã¹ããšã³ãžãã¢
- çŽè¿ã®é¢å¿: RSSãªãŒããŒã®éçºãAIãšãŒãžã§ã³ãã®æ§ç¯ãCloud RunãGoèšèª
## è©äŸ¡åºæº
- 10-9: éåžžã«éèŠãå³åº§ã«èªãã¹ããæè¡çãªãã¬ã€ã¯ã¹ã«ãŒããéçºäžã®ãããžã§ã¯ãã«çŽçµããå
容ã
- 8-7: éèŠãåŸã§èªãã¹ããèå³åéã®æ°ããããŒã«ããã¹ããã©ã¯ãã£ã¹ã
- 6-5: æ®éãèå³ãããã°èªããäžè¬çãªæè¡ãã¥ãŒã¹ã
- 4以äž: éèŠã§ã¯ãªãã
## åºåãã©ãŒããã
JSON圢åŒã§åºåããŠãã ãããMarkdownã®ã³ãŒããããã¯ã¯å«ããªãã§ãã ããã
{
"score": (æ°å€),
"reason": "(30æå以å
ã®ç°¡æœãªçç±)"
}
`, title, summary, url)
modelName := "gemini-2.5-flash-lite"
resp, err := client.Models.GenerateContent(ctx, modelName, genai.Text(prompt), nil)
if err != nil {
// ãã©ãŒã«ããã¯: å¥ã®ã¢ãã«ã詊ã
modelName = "gemini-2.5-flash"
resp, err = client.Models.GenerateContent(ctx, modelName, genai.Text(prompt), nil)
if err != nil {
return ImportanceResult{}, fmt.Errorf("Gemini APIãšã©ãŒ: %w", err)
}
}
// ã¬ã¹ãã³ã¹ãããŒã¹
// ...
}ãã€ã³ã:
ãããã£ãŒã«ãæž¡ããŠãã: ãåã«ãšã£ãŠãéèŠãã©ãããå€å®ããã
è©äŸ¡åºæºãæç€º: ãã¬ãé²ãããã«å ·äœçãªåºæºãèšå®
JSONåºåãæå®: ããŒã¹ãããããã©ãŒããã
ãã©ãŒã«ããã¯: gemini-2.5-flash-liteã倱æãããgemini-2.5-flashã詊ã
3. RSSã®åŠç (rss.go)
è€æ°ã®ãã£ãŒãã䞊ååŠçããŠããŸãããããGoã§æžããçç±ã®äžã€ã
func processFeeds(ctx context.Context, projectID, apiKey, slackToken, channelID string, rssUrls []string) (int, error) {
parser := gofeed.NewParser()
now := time.Now()
// æé垯ã«ãã£ãŠãããå€ãå€ãã
baseThreshold := 6
if now.Hour() >= 7 && now.Hour() < 11 {
baseThreshold = 6 // æã¯ç·©ãïŒæ
å ±ãå€ãç¥ãããïŒ
} else if now.Hour() >= 11 && now.Hour() < 15 {
baseThreshold = 7 // æŒã¯æ®é
} else {
baseThreshold = 8 // å€ã¯å³ããïŒæ¬åœã«éèŠãªãã®ã ãïŒ
}
var (
importantArticles []map[string]interface{}
currentAPIKey = apiKey
usingPaidKey = false
mu sync.Mutex
wg sync.WaitGroup
)
// 䞊ååŠçã§ãã£ãŒããååŸã»è©äŸ¡
for _, feedURL := range rssUrls {
wg.Add(1)
go func(url string) {
defer wg.Done()
// ã¿ã€ã ã¢ãŠãä»ãã§ãã£ãŒããååŸ
fetchCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
feed, err := parser.ParseURLWithContext(url, fetchCtx)
cancel()
if err != nil {
log.Printf("ãã£ãŒãã®ããŒã¹ã«å€±æããŸãã %s: %v", url, err)
return
}
// ã©ã³ãã ã«ã·ã£ããã«ïŒåããé²ãïŒ
rand.Shuffle(len(feed.Items), func(i, j int) {
feed.Items[i], feed.Items[j] = feed.Items[j], feed.Items[i]
})
// åèšäºãè©äŸ¡
maxItems := 20
if strings.Contains(url, "yamadashy") {
maxItems = 5 // éçŽãã£ãŒãã¯å°ãªãã«
}
for i := 0; i < maxItems && i < len(feed.Items); i++ {
item := feed.Items[i]
// Firestoreã§æ¢èªãã§ãã¯
exists := checkArticleInFirestore(ctx, projectID, item.Link)
if exists {
continue
}
// ã¬ãŒãå¶é察ç
time.Sleep(100 * time.Millisecond)
// éèŠåºŠãè©äŸ¡
importance, err := evaluateImportance(ctx, currentAPIKey, item.Title, summary, item.Link)
if err != nil {
// ...
}
// ãããå€ãè¶
ãããéèŠèšäºãšããŠä¿å
if importance.Score >= currentThreshold {
// ...
}
}
}(feedURL)
}
wg.Wait()
// äžäœ1ä»¶ã ãéç¥ïŒã¹ãã 鲿¢ïŒ
if len(importantArticles) > 1 {
sort.Slice(importantArticles, func(i, j int) bool {
return importantArticles[i]["importance"].(int) > importantArticles[j]["importance"].(int)
})
importantArticles = importantArticles[:1]
}
// Slackã«éç¥
for _, article := range importantArticles {
msg := fmt.Sprintf("ð° éèŠãªæè¡ãã¥ãŒã¹\n\n%s\n\néèŠåºŠïŒAIå€å®ïŒ: %d/10\nçç±ïŒAIèå¯ïŒ: %s\n\nð %s",
article["title"], article["importance"], article["reason"], article["url"])
postMessage(ctx, slackToken, channelID, msg)
}
return len(importantArticles), nil
}工倫ãã€ã³ã:
æé垯ã§ãããå€ãå€ãã
æïŒ7-11æïŒ: 6ä»¥äž â ç·©ããæã¯æ å ±ãå€ãç¥ããã
æŒïŒ11-15æïŒ: 7ä»¥äž â æ®é
å€ïŒãã以å€ïŒ: 8ä»¥äž â å³ãããæ¬åœã«éèŠãªãã®ã ã
䞊ååŠç
goroutineã§è€æ°ãã£ãŒããåæåŠç
`sync.Mutex`ã§å ±æå€æ°ãä¿è·
ã©ã³ãã ã·ã£ããã«
æ¯ååãé çªã ãšåã
ã©ã³ãã ã«ããããšã§å€æ§ãªèšäºãæŸãã
äžäœ1ä»¶ã ãéç¥
ã¹ãã 鲿¢
æ¬åœã«éèŠãªãã®ã ãå±ãããã«ãã
Firestoreã§æ¢èªç®¡ç
åãèšäºãäœåºŠãè©äŸ¡ããªã
APIã³ã¹ãã®ç¯çŽ
4. APIç¡ææ ã®äžé察ç
Gemini APIã«ã¯ç¡ææ ã®å¶éããããŸãã
äžéã«éãããèªåã§ææããŒã«åãæ¿ããä»çµã¿ãå ¥ããŠããŸãã
if err != nil && (strings.Contains(err.Error(), "429") ||
strings.Contains(err.Error(), "RESOURCE_EXHAUSTED")) {
mu.Lock()
if !usingPaidKey {
log.Printf("â ïž Geminiç¡ææ ã®äžéã«éããŸããã以éã¯ææããŒã䜿çšããŸã...")
paidKey, secretErr := accessSecret(ctx, projectID, "gemini-api-key-paid")
if secretErr == nil && paidKey != "" {
currentAPIKey = paidKey
usingPaidKey = true
} else {
log.Printf("â ïž ææAPIããŒã®ååŸã«å€±æããŸãã: %v", secretErr)
}
}
mu.Unlock()
// ææããŒã§ãªãã©ã€
if usingPaidKey {
importance, err = evaluateImportance(ctx, currentAPIKey, item.Title, summary, item.Link)
}
}ãã€ã³ã:
429ãšã©ãŒïŒRate LimitïŒãæ€ç¥
Secret ManagerããææããŒãååŸ
èªåã§ãªãã©ã€
ããã§ãç¡ææ ã䜿ãåã£ãŠãæ¢ãŸããªãã
æ®æ®µã¯ç¡ææ ã§éçšããŠãè¶³ããªããªã£ããææã«åãæ¿ããã
ãªãGoã§å®è£ ããã
åèèšäºã¯Pythonã§ããããåã¯Goã§å®è£ ããŸããã
çç±1: Cloud Runãšã®çžæ§
Goã¯ã³ã³ãã€ã«èšèªãªã®ã§ãã³ãŒã«ãã¹ã¿ãŒããéãã
Pythonã ãšèµ·åã«æ°ç§ãããããšããããŸãããGoãªãã»ãŒäžç¬ã
Cloud Runã®ãããªãµãŒããŒã¬ã¹ç°å¢ã§ã¯ããã®å·®ã倧ããã
çç±2: 䞊ååŠçãæ¥œ
Goã®goroutineã¯ãã¡ããã¡ã楜ã
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
// åŠç
}(url)
}
wg.Wait()ããã ãã§äžŠååŠçãã§ããã
Pythonã§ã`asyncio`ã`concurrent.futures`ã§ã§ããŸãããGoã®æ¹ãã·ã³ãã«ã
çç±3: ã·ã³ã°ã«ãã€ããª
Goã¯ã³ã³ãã€ã«ãããšã·ã³ã°ã«ãã€ããªã«ãªãã
Dockerã€ã¡ãŒãžãäœãæã楜ã ãããããã€ãç°¡åã
äŸåé¢ä¿ã®å°çããè§£æŸãããã
å®éã«å±ãéç¥
ãããªæãã§Slackã«å±ããŸãã
ð° éèŠãªæè¡ãã¥ãŒã¹
Gemini 2.5ã®æ°æ©èœãçºè¡šãããŸãã
éèŠåºŠïŒAIå€å®ïŒ: 9/10
çç±ïŒAIèå¯ïŒ: éçºäžã®AIãšãŒãžã§ã³ãã«çŽçµ
ð https://example.com/...éèŠåºŠãšçç±ãã»ããã§å±ãã®ããã€ã³ãã
ããªããã®èšäºãéèŠãªã®ãããããããããèªããã©ãã倿ããããã
éçšããŠã¿ãææ³
çŽ1ã¶æéçšããŠã¿ãææ³ã§ãã
ããã£ãããš
æ å ±åéã®æéãæ¿æž
RSSãªãŒããŒãéãå¿ èŠããªããªã£ã
ãèªãèšäºãæ¢ãæéããã»ãŒãŒãã«
èŠéããæžã£ã
以åã¯åãããŠãããæ¬åœã«éèŠãªèšäºããå±ãããã«ãªã£ã
èªåã®èå³ã«åã£ãèšäºã ãæ¥ã
ãã£ã«ã¿ãªã³ã°ã®ç²ŸåºŠãæå€ãšãã
ãããã£ãŒã«ãæž¡ããŠããããã
ãGCPããGeminiããå人éçºããªã©ã«é¢é£ããèšäºã¯ã¡ãããšé«ã¹ã³ã¢
æèµ·ããã®ã楜ãã¿ã«
Slackãéããšãð° éèŠãªæè¡ãã¥ãŒã¹ã§ãããå±ããŠãã
å°å³ã«ã¢ãããŒã·ã§ã³ã«ãªã
æ¹åãããããš
ã¹ã³ã¢ã®èª¿æŽ
æã ãããéèŠïŒããšããèšäºãå±ã
ããã³ããã®èª¿æŽãå¿ èŠ
ãã£ãŒãã®è¿œå
ãã£ãšæè¡ããã°ã远å ããã
æµ·å€ã®ããã°ã远å ãããïŒç¿»è𳿩èœã欲ããïŒ
èŠçŽãä»ããã
èšäºã®èŠçŽãäžç·ã«å±ããšãã£ãšäŸ¿å©
èªããã©ããã®å€æãããã«æ¥œã«ãªã
ã³ã¹ã
ã»ãŒç¡æã§éçšã§ããŠããŸãã
Cloud Run ïŒ Â¥0ã50/æ
Cloud Scheduler ïŒ Â¥0ïŒç¡ææ å
ïŒ
Firestore ïŒ Â¥0ã10/æ
Gemini API ïŒ Â¥0ïŒã»ãŒç¡ææ ã§åãŸãïŒ
åèš ïŒ Â¥0ã60/æ
ç¡ææ ãããŸã䜿ãã°ãã»ãŒã¿ãã
ææããŒã«åãæ¿ããããšãããŸã«ãããŸãããæ°åçšåºŠã
å人éçºãªãåå蚱容ç¯å²ã§ãã
ãŸãšã
Geminiã«RSSãããããŒã«ãããŠãéèŠãªèšäºã ãSlackã«éç¥ããä»çµã¿ãäœããŸããã
ãèªãèšäºãæ¢ãæéãããªããªã£ãã®ãäžçªã®ã¡ãªããã
AIããããèªãã§ãã£ãŠæããŠãããã®ããã£ã¡ã楜ã§ãã
ãã®ä»çµã¿ã®æ¬è³ª
çµå±ããã®ä»çµã¿ããã£ãŠããã®ã¯ããã£ã«ã¿ãªã³ã°ãã§ãã
倧éã®æ å ±ã®äžãããèªåã«ãšã£ãŠéèŠãªãã®ã ããæœåºããã
以åã¯èªåã§ãã£ãŠãããã®äœæ¥ããAIã«ä»»ããã
ããã ãã§ãæ å ±åéãåçã«æ¥œã«ãªããŸããã
èå³ãããæ¹ã¯ããã²äœã£ãŠã¿ãŠãã ããã
åè
ããã§ã¯ããŸã次ã®èšäºã§ã ð°âš
ãæ¥åèªååã»AI掻çšã®ãçžè«ã
æ®æ®µã®ä»äºã§ããã®äœæ¥ãèªååã§ããªãããªã
ãšæã£ãããšã¯ãããŸãããïŒ
GASã»VBAã»Pythonã»AI掻çšã®ãçžè«ããåãããŠããŸãã
â ã³ã³ãã©ã§ä»äºäŸé ŒãèŠã
â noteã§ä»äºäŸé ŒãèŠã