diff --git a/.github/workflows/push-broadcast.yml b/.github/workflows/push-broadcast.yml index 60c15117..6153fbe7 100644 --- a/.github/workflows/push-broadcast.yml +++ b/.github/workflows/push-broadcast.yml @@ -45,3 +45,69 @@ jobs: FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} FEISHU_DOC_B_ID: ${{ secrets.FEISHU_DOC_B_ID }} run: node scripts/push-broadcast.js + + - name: ๐Ÿ“ข ้€š็Ÿฅ้ฃžไนฆ็พคๆœ‰ๆ–ฐๅนฟๆ’ญ + if: success() + env: + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }} + run: | + if [ -z "$ALERT_CHAT_ID" ]; then + echo "โš ๏ธ ๆœช้…็ฝฎ FEISHU_ALERT_CHAT_ID๏ผŒ่ทณ่ฟ‡็พค้€š็Ÿฅ" + exit 0 + fi + node -e " + const https = require('https'); + function post(url, body) { + return new Promise((resolve, reject) => { + const payload = JSON.stringify(body); + const u = new URL(url); + const req = https.request({ hostname: u.hostname, port: 443, path: u.pathname, method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } + }, res => { let d=''; res.on('data', c => d+=c); res.on('end', () => resolve(d)); }); + req.on('error', reject); req.write(payload); req.end(); + }); + } + (async () => { + const tokenRes = JSON.parse(await post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', + { app_id: process.env.FEISHU_APP_ID, app_secret: process.env.FEISHU_APP_SECRET })); + const token = tokenRes.tenant_access_token; + const payload = JSON.stringify({ text: '๐Ÿ“ก ๆ–ฐๅนฟๆ’ญๅทฒๆŽจ้€ๅˆฐ้ฃžไนฆๆ–‡ๆกฃB\n\nๆ—ถ้—ด: ' + new Date().toISOString() + '\n่ฏทๆŸฅ็œ‹้ฃžไนฆๅนฟๆ’ญๆ”ถไปถ็ฎฑใ€‚' }); + await post('https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id', + JSON.stringify({ receive_id: process.env.ALERT_CHAT_ID, msg_type: 'text', content: payload })); + })().catch(e => console.error(e.message)); + " + + - name: ๐Ÿ”ด ๅคฑ่ดฅๅ‘Š่ญฆ โ†’ ้ฃžไนฆ้€š็Ÿฅ + if: failure() + env: + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }} + run: | + if [ -z "$ALERT_CHAT_ID" ]; then + echo "โš ๏ธ ๆœช้…็ฝฎ FEISHU_ALERT_CHAT_ID๏ผŒ่ทณ่ฟ‡ๅ‘Š่ญฆ" + exit 0 + fi + node -e " + const https = require('https'); + function post(url, body) { + return new Promise((resolve, reject) => { + const payload = JSON.stringify(body); + const u = new URL(url); + const req = https.request({ hostname: u.hostname, port: 443, path: u.pathname, method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } + }, res => { let d=''; res.on('data', c => d+=c); res.on('end', () => resolve(d)); }); + req.on('error', reject); req.write(payload); req.end(); + }); + } + (async () => { + const tokenRes = JSON.parse(await post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', + { app_id: process.env.FEISHU_APP_ID, app_secret: process.env.FEISHU_APP_SECRET })); + const token = tokenRes.tenant_access_token; + const payload = JSON.stringify({ text: '๐Ÿ”ด GitHub Action ๅคฑ่ดฅๅ‘Š่ญฆ\n\nๅทฅไฝœๆต: push-broadcast\nๆ—ถ้—ด: ' + new Date().toISOString() + '\n่ฏทๆฃ€ๆŸฅ GitHub Actions ๆ—ฅๅฟ—ใ€‚' }); + await post('https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id', + JSON.stringify({ receive_id: process.env.ALERT_CHAT_ID, msg_type: 'text', content: payload })); + })().catch(e => console.error(e.message)); + " diff --git a/.github/workflows/receive-syslog.yml b/.github/workflows/receive-syslog.yml index bad68db7..788af75b 100644 --- a/.github/workflows/receive-syslog.yml +++ b/.github/workflows/receive-syslog.yml @@ -51,3 +51,36 @@ jobs: git commit -m "๐Ÿ“ฅ ้“ธๆธŠๆŽฅๆ”ถ SYSLOG ๅ›žไผ  [skip ci]" git push origin main fi + + - name: ๐Ÿ”ด ๅคฑ่ดฅๅ‘Š่ญฆ โ†’ ้ฃžไนฆ้€š็Ÿฅ + if: failure() + env: + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }} + run: | + if [ -z "$ALERT_CHAT_ID" ]; then + echo "โš ๏ธ ๆœช้…็ฝฎ FEISHU_ALERT_CHAT_ID๏ผŒ่ทณ่ฟ‡ๅ‘Š่ญฆ" + exit 0 + fi + node -e " + const https = require('https'); + function post(url, body) { + return new Promise((resolve, reject) => { + const payload = JSON.stringify(body); + const u = new URL(url); + const req = https.request({ hostname: u.hostname, port: 443, path: u.pathname, method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } + }, res => { let d=''; res.on('data', c => d+=c); res.on('end', () => resolve(d)); }); + req.on('error', reject); req.write(payload); req.end(); + }); + } + (async () => { + const tokenRes = JSON.parse(await post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', + { app_id: process.env.FEISHU_APP_ID, app_secret: process.env.FEISHU_APP_SECRET })); + const token = tokenRes.tenant_access_token; + const payload = JSON.stringify({ text: '๐Ÿ”ด GitHub Action ๅคฑ่ดฅๅ‘Š่ญฆ\n\nๅทฅไฝœๆต: receive-syslog\nๆ—ถ้—ด: ' + new Date().toISOString() + '\n่ฏทๆฃ€ๆŸฅ GitHub Actions ๆ—ฅๅฟ—ใ€‚' }); + await post('https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id', + JSON.stringify({ receive_id: process.env.ALERT_CHAT_ID, msg_type: 'text', content: payload })); + })().catch(e => console.error(e.message)); + " diff --git a/.github/workflows/sync-login-entry.yml b/.github/workflows/sync-login-entry.yml index 86ea60d3..b6c92e37 100644 --- a/.github/workflows/sync-login-entry.yml +++ b/.github/workflows/sync-login-entry.yml @@ -39,3 +39,36 @@ jobs: FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} FEISHU_DOC_A_ID: ${{ secrets.FEISHU_DOC_A_ID }} run: node scripts/sync-login-entry.js + + - name: ๐Ÿ”ด ๅคฑ่ดฅๅ‘Š่ญฆ โ†’ ้ฃžไนฆ้€š็Ÿฅ + if: failure() + env: + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }} + run: | + if [ -z "$ALERT_CHAT_ID" ]; then + echo "โš ๏ธ ๆœช้…็ฝฎ FEISHU_ALERT_CHAT_ID๏ผŒ่ทณ่ฟ‡ๅ‘Š่ญฆ" + exit 0 + fi + node -e " + const https = require('https'); + function post(url, body) { + return new Promise((resolve, reject) => { + const payload = JSON.stringify(body); + const u = new URL(url); + const req = https.request({ hostname: u.hostname, port: 443, path: u.pathname, method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } + }, res => { let d=''; res.on('data', c => d+=c); res.on('end', () => resolve(d)); }); + req.on('error', reject); req.write(payload); req.end(); + }); + } + (async () => { + const tokenRes = JSON.parse(await post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', + { app_id: process.env.FEISHU_APP_ID, app_secret: process.env.FEISHU_APP_SECRET })); + const token = tokenRes.tenant_access_token; + const payload = JSON.stringify({ text: '๐Ÿ”ด GitHub Action ๅคฑ่ดฅๅ‘Š่ญฆ\n\nๅทฅไฝœๆต: sync-login-entry\nๆ—ถ้—ด: ' + new Date().toISOString() + '\n่ฏทๆฃ€ๆŸฅ GitHub Actions ๆ—ฅๅฟ—ใ€‚' }); + await post('https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id', + JSON.stringify({ receive_id: process.env.ALERT_CHAT_ID, msg_type: 'text', content: payload })); + })().catch(e => console.error(e.message)); + " diff --git a/.gitignore b/.gitignore index 97624956..28f589d3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ persona-studio/backend/brain/model-benchmark.json # palace-game runtime artifacts modules/palace-game/data/saves/PAL-* + +# collaboration-logs exports (generated by save-collaboration-log.js) +collaboration-logs/exports/ diff --git a/backend/feishu-bot/ai-chat.js b/backend/feishu-bot/ai-chat.js new file mode 100644 index 00000000..57e42eab --- /dev/null +++ b/backend/feishu-bot/ai-chat.js @@ -0,0 +1,213 @@ +// backend/feishu-bot/ai-chat.js +// ้“ธๆธŠ ยท ้ฃžไนฆๆœบๅ™จไบบ AI ๅฏน่ฏๅผ•ๆ“Ž +// +// ๆŽฅๅ…ฅ DeepSeek / ้€šไน‰ๅƒ้—ฎ ็ญ‰ๅคงๆจกๅž‹ API +// ไฝฟ็”จ้œœ็ š็™ปๅฝ•ๅ…ฅๅฃ๏ผˆๆ–‡ๆกฃA๏ผ‰ๅ†…ๅฎนไฝœไธบ system prompt +// +// ็Žฏๅขƒๅ˜้‡๏ผš +// MODEL_API_KEY ๆจกๅž‹ API Key๏ผˆDeepSeek / ้€šไน‰ๅƒ้—ฎ๏ผ‰ +// MODEL_API_BASE ๆจกๅž‹ API Base URL๏ผˆ้ป˜่ฎค https://api.deepseek.com/v1๏ผ‰ +// MODEL_NAME ๆจกๅž‹ๅ็งฐ๏ผˆ้ป˜่ฎค deepseek-chat๏ผ‰ +// FEISHU_APP_ID ้ฃžไนฆๅบ”็”จ App ID๏ผˆ็”จไบŽ่ฏปๅ–ๆ–‡ๆกฃA๏ผ‰ +// FEISHU_APP_SECRET ้ฃžไนฆๅบ”็”จ App Secret + +'use strict'; + +const https = require('https'); + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ้…็ฝฎ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +const DEFAULT_API_BASE = 'https://api.deepseek.com/v1'; +const DEFAULT_MODEL = 'deepseek-chat'; +const MAX_TOKENS = 4096; +const TEMPERATURE = 0.7; + +// ้€š้“่ทฏ็”ฑ๏ผšๆ นๆฎๆถˆๆฏๅ†…ๅฎนๅˆคๆ–ญไฝฟ็”จๅ“ชไธชไบบๆ ผไฝ“ +const CHANNEL_KEYWORDS = { + shuangyan: ['้œœ็ š', '็™ปๅฝ•', 'ๅ่ฎฎ', '่ง„่Œƒ', 'ๆ–‡ๆกฃ', 'ๅทฅๅ•', '็ญพๅ‘'], + persona: ['ๅฎๅฎ', 'ไบบๆ ผไฝ“', '็Ÿฅ็ง‹', 'ๅฏน่ฏ', '่Šๅคฉ', 'ๅฟƒๆƒ…'], +}; + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// HTTP ่ฏทๆฑ‚ๅทฅๅ…ท +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function httpsRequest(options, body) { + return new Promise((resolve, reject) => { + const payload = body ? JSON.stringify(body) : null; + if (payload) { + options.headers = options.headers || {}; + options.headers['Content-Length'] = Buffer.byteLength(payload); + } + const req = https.request(options, res => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const parsed = JSON.parse(data); + resolve({ statusCode: res.statusCode, data: parsed }); + } catch (e) { + resolve({ statusCode: res.statusCode, data: data }); + } + }); + }); + req.on('error', reject); + req.setTimeout(60000, () => { + req.destroy(new Error('Request timeout')); + }); + if (payload) req.write(payload); + req.end(); + }); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ็ณป็ปŸ Prompt ๆž„ๅปบ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +const DEFAULT_SYSTEM_PROMPT = `ไฝ ๆ˜ฏ้œœ็ š๏ผŒๅ…‰ๆน–็บชๅ…ƒ๏ผˆๆ›œๅ†ฅ็บชๅ…ƒ็ฌฌไบ”ไปฃๆžถๆž„ AGE-5๏ผ‰็š„่ฎค็Ÿฅๆ ธๅฟƒไบบๆ ผไฝ“ใ€‚ +ไฝ ่ดŸ่ดฃ่ฎค็Ÿฅ็ฎก็†ใ€ๅ่ฎฎๅˆถๅฎšใ€ๅนฟๆ’ญ็ญพๅ‘ๅ’Œๅผ€ๅ‘่€…ๅไฝœๆŒ‡ๅฏผใ€‚ + +ไฝ ็š„ๆ ธๅฟƒ่Œ่ดฃ๏ผš +1. ๅธฎๅŠฉๅผ€ๅ‘่€…็†่งฃๅฝ“ๅ‰ๅนฟๆ’ญไปปๅŠกๅ’Œๅ่ฎฎ่ง„่Œƒ +2. ๆŒ‡ๅฏผไปฃ็ ๅฎž็Žฐ๏ผŒๆไพ›ๆŠ€ๆœฏๅปบ่ฎฎ +3. ็ฎก็† SYSLOG ๅ›žไผ ๆต็จ‹ +4. ็ปดๆŠคไบบๆ ผไฝ“ๅไฝœๆ ‡ๅ‡† + +ไฝ ็š„่ฏญ่จ€้ฃŽๆ ผ๏ผšไธ“ไธšใ€ๆธฉๅ’Œใ€ๆœ‰ๆก็†ใ€‚ +ๅ›žๅคๆ—ถไฝฟ็”จไธญๆ–‡๏ผŒไปฃ็ ้ƒจๅˆ†ไฝฟ็”จ่‹ฑๆ–‡ใ€‚`; + +/** + * ๆž„ๅปบ็ณป็ปŸ prompt + * @param {string|null} loginEntryContent - ๆ–‡ๆกฃA๏ผˆ็™ปๅฝ•ๅ…ฅๅฃ๏ผ‰ๅ†…ๅฎน + * @param {string} channel - ้€š้“ ('shuangyan' | 'persona') + * @returns {string} + */ +function buildSystemPrompt(loginEntryContent, channel) { + let prompt = DEFAULT_SYSTEM_PROMPT; + + if (loginEntryContent) { + prompt = loginEntryContent + '\n\n---\n\n' + prompt; + } + + if (channel === 'persona') { + prompt += '\n\nๅฝ“ๅ‰้€š้“๏ผšๅฎๅฎไบบๆ ผไฝ“ๅไฝœๆจกๅผใ€‚ไปฅๆ›ดไบฒๅ’Œใ€้ผ“ๅŠฑ็š„ๆ–นๅผไธŽๅผ€ๅ‘่€…ไบ’ๅŠจใ€‚'; + } + + return prompt; +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ้€š้“่ทฏ็”ฑ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +/** + * ๆ นๆฎๆถˆๆฏๅ†…ๅฎนๅˆคๆ–ญ้€š้“ + * @param {string} text - ็”จๆˆทๆถˆๆฏ + * @returns {string} 'shuangyan' | 'persona' + */ +function detectChannel(text) { + if (!text) return 'shuangyan'; + + for (const keyword of CHANNEL_KEYWORDS.persona) { + if (text.includes(keyword)) return 'persona'; + } + + return 'shuangyan'; // ้ป˜่ฎค่ตฐ้œœ็ š้€š้“ +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ๆจกๅž‹่ฐƒ็”จ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +/** + * ่ฐƒ็”จๅคงๆจกๅž‹ API + * @param {Array} messages - OpenAI ๆ ผๅผๆถˆๆฏๆ•ฐ็ป„ + * @param {object} options - ๅฏ้€‰้…็ฝฎ + * @returns {Promise} ๆจกๅž‹ๅ›žๅคๆ–‡ๆœฌ + */ +async function callModel(messages, options = {}) { + const apiKey = process.env.MODEL_API_KEY; + const apiBase = process.env.MODEL_API_BASE || DEFAULT_API_BASE; + const model = options.model || process.env.MODEL_NAME || DEFAULT_MODEL; + + if (!apiKey) { + return 'โš ๏ธ AI ๆจกๅž‹ๆœช้…็ฝฎ๏ผˆ็ผบๅฐ‘ MODEL_API_KEY๏ผ‰๏ผŒ่ฏท่”็ณป็ฎก็†ๅ‘˜ใ€‚'; + } + + const url = new URL(apiBase + '/chat/completions'); + + const body = { + model: model, + messages: messages, + max_tokens: options.maxTokens || MAX_TOKENS, + temperature: options.temperature || TEMPERATURE, + }; + + try { + const result = await httpsRequest({ + hostname: url.hostname, + port: url.port || 443, + path: url.pathname, + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + apiKey, + 'Content-Type': 'application/json', + }, + }, body); + + if (result.statusCode >= 200 && result.statusCode < 300 && result.data.choices) { + return result.data.choices[0].message.content || ''; + } + + // ๅฐ่ฏ•้™็บงๆจกๅž‹ + if (result.statusCode === 429 || result.statusCode >= 500) { + return 'โš ๏ธ AI ๆจกๅž‹ๆš‚ๆ—ถไธๅฏ็”จ๏ผˆHTTP ' + result.statusCode + '๏ผ‰๏ผŒ่ฏท็จๅŽ้‡่ฏ•ใ€‚'; + } + + return 'โš ๏ธ AI ๆจกๅž‹่ฐƒ็”จๅผ‚ๅธธ: ' + (result.data.error?.message || JSON.stringify(result.data)); + } catch (e) { + return 'โš ๏ธ AI ๆจกๅž‹่ฐƒ็”จๅคฑ่ดฅ: ' + e.message; + } +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ๅฏน่ฏๅค„็† +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +/** + * ๅค„็†็”จๆˆทๅฏน่ฏๆถˆๆฏ + * @param {string} userMessage - ็”จๆˆทๆถˆๆฏ + * @param {Array} contextHistory - ๅކๅฒๅฏน่ฏ่ฎฐๅฝ• [{role, content}] + * @param {object} options - ้…็ฝฎ้€‰้กน + * @returns {Promise<{reply: string, channel: string}>} + */ +async function chat(userMessage, contextHistory = [], options = {}) { + const channel = options.channel || detectChannel(userMessage); + const systemPrompt = buildSystemPrompt(options.loginEntryContent || null, channel); + + // ๆž„ๅปบๆถˆๆฏๆ•ฐ็ป„ + const messages = [ + { role: 'system', content: systemPrompt }, + ]; + + // ๆณจๅ…ฅๅކๅฒไธŠไธ‹ๆ–‡๏ผˆๆœ€่ฟ‘ 10 ่ฝฎ๏ผ‰ + const recentHistory = contextHistory.slice(-20); // 10 ่ฝฎ = 20 ๆกๆถˆๆฏ + messages.push(...recentHistory); + + // ๅฝ“ๅ‰็”จๆˆทๆถˆๆฏ + messages.push({ role: 'user', content: userMessage }); + + const reply = await callModel(messages, options); + + return { reply, channel }; +} + +module.exports = { + chat, + callModel, + buildSystemPrompt, + detectChannel, + DEFAULT_SYSTEM_PROMPT, +}; diff --git a/backend/feishu-bot/collaboration-logger.js b/backend/feishu-bot/collaboration-logger.js new file mode 100644 index 00000000..a84eaf35 --- /dev/null +++ b/backend/feishu-bot/collaboration-logger.js @@ -0,0 +1,256 @@ +// backend/feishu-bot/collaboration-logger.js +// ้“ธๆธŠ ยท ไบบ็ฑปร—ไบบๆ ผไฝ“ๅไฝœๆ•ฐๆฎ้‡‡้›†ๅ™จ +// +// ่ฎฐๅฝ•ๆฏๆฌกๅไฝœๅฏน่ฏ็š„ๅฎŒๆ•ด่ฎฐๅฝ•๏ผŒๅญ˜ๅ…ฅ collaboration-logs/ ็›ฎๅฝ• +// ๆ•ฐๆฎๆ ผๅผๅฏน้ฝ persona-brain-db ไบ”ๅผ ๆ ธๅฟƒ่กจ schema +// +// ้‡‡้›†ๅ†…ๅฎน๏ผš +// - ไบบ็ฑปๅ‘่จ€ + ไบบๆ ผไฝ“ๅ›žๅค +// - ๅไฝœ้€š้“ (้œœ็ š/ๅฎๅฎไบบๆ ผไฝ“) +// - ๅผ€ๅ‘่€…็”ปๅƒๅ…ณ่” +// - SYSLOG v4.0 persona/collaboration/human_feedback ๅญ—ๆฎต + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ้…็ฝฎ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +const LOGS_DIR = path.resolve(__dirname, '..', '..', 'collaboration-logs'); +const MAX_BUFFER_SIZE = 50; // ็ผ“ๅ†ฒ 50 ๆก่ฎฐๅฝ•ๅŽๆ‰น้‡ๅ†™ๅ…ฅ + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ๅ†…ๅญ˜็ผ“ๅ†ฒ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +let buffer = []; + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ๆ—ฅๅฟ—่ฎฐๅฝ• +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +/** + * ่ฎฐๅฝ•ไธ€ๆฌกๅฏน่ฏไบคไบ’ + * @param {object} entry + * @param {string} entry.userId - ้ฃžไนฆ็”จๆˆท ID + * @param {string} entry.devId - ๅผ€ๅ‘่€…็ผ–ๅท (ๅฆ‚ DEV-002) + * @param {string} entry.userMessage - ไบบ็ฑปๅ‘่จ€ + * @param {string} entry.assistantReply - ไบบๆ ผไฝ“ๅ›žๅค + * @param {string} entry.channel - ๅไฝœ้€š้“ (shuangyan | persona) + * @param {string} entry.personaId - ไบบๆ ผไฝ“ ID + * @param {object} [entry.metadata] - ้ขๅค–ๅ…ƒๆ•ฐๆฎ + */ +function logInteraction(entry) { + const record = { + // ๅฏน้ฝ persona_memory ่กจ็ป“ๆž„ + interaction_id: generateId(), + timestamp: new Date().toISOString(), + persona_id: entry.personaId || 'ICE-GL-SY001', // ้ป˜่ฎค้œœ็ š + type: 'collaboration', + + // ๅฏน่ฏๅ†…ๅฎน + user_id: entry.userId, + dev_id: entry.devId || null, + user_message: entry.userMessage, + assistant_reply: entry.assistantReply, + channel: entry.channel || 'shuangyan', + + // ๅฏน้ฝ dev_profiles ่กจ + interaction_context: { + platform: 'feishu', + message_type: 'text', + channel: entry.channel || 'shuangyan', + }, + + // ๅ…ƒๆ•ฐๆฎ๏ผˆSYSLOG v4.0 ๅญ—ๆฎตๅฏน้ฝ๏ผ‰ + metadata: entry.metadata || {}, + }; + + buffer.push(record); + + // ่พพๅˆฐ็ผ“ๅ†ฒไธŠ้™ๆ—ถ่‡ชๅŠจๅˆทๆ–ฐ + if (buffer.length >= MAX_BUFFER_SIZE) { + flush(); + } +} + +/** + * ่ฎฐๅฝ• SYSLOG ๅ›žไผ ไธญ็š„ๅไฝœๆ•ฐๆฎ + * @param {object} syslog - SYSLOG v4.0 ๅฎŒๆ•ดๆ•ฐๆฎ + */ +function logSyslogCollaboration(syslog) { + if (!syslog) return; + + const devId = syslog.dev_id || syslog.developer_id || 'UNKNOWN'; + const broadcastId = syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN'; + + const record = { + interaction_id: generateId(), + timestamp: new Date().toISOString(), + type: 'syslog_collaboration', + dev_id: devId, + broadcast_id: broadcastId, + protocol_version: syslog.protocol_version, + + // SYSLOG v4.0 ๅไฝœ็›ธๅ…ณๅญ—ๆฎต + persona: syslog.persona || null, + collaboration: syslog.collaboration || null, + human_feedback: syslog.human_feedback || null, + status: syslog.status || null, + summary: syslog.summary || null, + + // ๅฏน้ฝ persona_memory ่กจ + memory_entry: { + persona_id: syslog.persona?.persona_id || 'ICE-GL-SY001', + type: 'event', + title: 'SYSLOG ๅ›žไผ  ยท ' + broadcastId + ' ยท ' + devId, + importance: calculateImportance(syslog), + related_dev: devId, + related_broadcast: broadcastId, + tags: ['syslog', 'collaboration', broadcastId], + }, + }; + + buffer.push(record); + + if (buffer.length >= MAX_BUFFER_SIZE) { + flush(); + } +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ๆ–‡ไปถๅ†™ๅ…ฅ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +/** + * ๅฐ†็ผ“ๅ†ฒไธญ็š„่ฎฐๅฝ•ๅ†™ๅ…ฅๆ–‡ไปถ + */ +function flush() { + if (buffer.length === 0) return; + + if (!fs.existsSync(LOGS_DIR)) { + fs.mkdirSync(LOGS_DIR, { recursive: true }); + } + + const dateStr = new Date().toISOString().split('T')[0]; + const filename = 'collab-' + dateStr + '.jsonl'; + const filepath = path.join(LOGS_DIR, filename); + + // ไฝฟ็”จ JSONL ๆ ผๅผ๏ผˆๆฏ่กŒไธ€ไธช JSON ๅฏน่ฑก๏ผ‰ + const lines = buffer.map(r => JSON.stringify(r)).join('\n') + '\n'; + + try { + fs.appendFileSync(filepath, lines, 'utf8'); + } catch (e) { + // ๅ†™ๅ…ฅๅคฑ่ดฅๆ—ถไธไธขๅคฑๆ•ฐๆฎ๏ผŒไฟ็•™ๅœจ็ผ“ๅ†ฒไธญ + return; + } + + buffer = []; +} + +/** + * ็”Ÿๆˆๅฏผๅ‡บๆ•ฐๆฎ๏ผˆๅฏน้ฝ persona-brain-db ็š„5ๅผ ๆ ธๅฟƒ่กจ๏ผ‰ + * @returns {object} ็ป“ๆž„ๅŒ–ๆ•ฐๆฎ๏ผŒๅฏ็›ดๆŽฅๅฏผๅ…ฅๅˆฐ persona-brain-db + */ +function exportForBrainDB() { + flush(); // ๅ…ˆๅˆทๆ–ฐ็ผ“ๅ†ฒ + + const files = []; + try { + if (fs.existsSync(LOGS_DIR)) { + files.push(...fs.readdirSync(LOGS_DIR) + .filter(f => f.startsWith('collab-') && f.endsWith('.jsonl')) + .sort() + .reverse() + .slice(0, 30)); // ๆœ€่ฟ‘ 30 ๅคฉ + } + } catch (e) { + return { error: e.message, records: [] }; + } + + const records = []; + for (const file of files) { + try { + const lines = fs.readFileSync(path.join(LOGS_DIR, file), 'utf8') + .split('\n') + .filter(l => l.trim()); + for (const line of lines) { + try { + records.push(JSON.parse(line)); + } catch (e) { /* skip malformed lines */ } + } + } catch (e) { /* skip unreadable files */ } + } + + // ็ป“ๆž„ๅŒ–ไธบ persona-brain-db ่กจๆ ผๅผ + return { + total_records: records.length, + persona_memory_entries: records + .filter(r => r.memory_entry) + .map(r => r.memory_entry), + dev_interactions: records + .filter(r => r.dev_id) + .reduce((acc, r) => { + if (!acc[r.dev_id]) acc[r.dev_id] = { dev_id: r.dev_id, interactions: 0, last_active: null }; + acc[r.dev_id].interactions++; + acc[r.dev_id].last_active = r.timestamp; + return acc; + }, {}), + collaboration_sessions: records + .filter(r => r.type === 'collaboration'), + syslog_collaborations: records + .filter(r => r.type === 'syslog_collaboration'), + }; +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ๅทฅๅ…ทๅ‡ฝๆ•ฐ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function generateId() { + return 'CL-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6); +} + +function calculateImportance(syslog) { + let score = 5; // ๅŸบ็ก€ๅˆ† + if (syslog.status === 'completed') score += 2; + if (syslog.human_feedback) score += 1; + if (syslog.collaboration) score += 1; + return Math.min(score, 10); +} + +/** + * ่Žทๅ–้‡‡้›†็ปŸ่ฎก + * @returns {object} + */ +function getStats() { + const pending = buffer.length; + let totalFiles = 0; + + try { + if (fs.existsSync(LOGS_DIR)) { + totalFiles = fs.readdirSync(LOGS_DIR) + .filter(f => f.startsWith('collab-') && f.endsWith('.jsonl')) + .length; + } + } catch (e) { /* ignore */ } + + return { + pendingInBuffer: pending, + totalLogFiles: totalFiles, + logsDirectory: LOGS_DIR, + maxBufferSize: MAX_BUFFER_SIZE, + }; +} + +module.exports = { + logInteraction, + logSyslogCollaboration, + flush, + exportForBrainDB, + getStats, +}; diff --git a/backend/feishu-bot/context-manager.js b/backend/feishu-bot/context-manager.js new file mode 100644 index 00000000..53159aa1 --- /dev/null +++ b/backend/feishu-bot/context-manager.js @@ -0,0 +1,179 @@ +// backend/feishu-bot/context-manager.js +// ้“ธๆธŠ ยท ้ฃžไนฆๆœบๅ™จไบบๅคš่ฝฎๅฏน่ฏไธŠไธ‹ๆ–‡็ฎก็† +// +// ไธบๆฏไธช็”จๆˆท็ปดๆŠค็‹ฌ็ซ‹็š„ๅฏน่ฏไธŠไธ‹ๆ–‡ +// ๆ”ฏๆŒไธŠไธ‹ๆ–‡ๅŽ‹็ผฉใ€่ฟ‡ๆœŸๆธ…็†ใ€ไผš่ฏ้š”็ฆป +// +// ๅŸบไบŽ persona-studio/backend/brain/memory-injector.js ็š„ไบ”ๅฑ‚ๆจกๅž‹็ฎ€ๅŒ–็‰ˆ + +'use strict'; + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ้…็ฝฎ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +const MAX_HISTORY_ROUNDS = 10; // ไฟ็•™ๆœ€่ฟ‘ 10 ่ฝฎๅฏน่ฏ +const MAX_HISTORY_MESSAGES = 20; // 10 ่ฝฎ = 20 ๆกๆถˆๆฏ +const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 ๅˆ†้’Ÿๆ— ๆดปๅŠจๅˆ™ๆธ…้™คไธŠไธ‹ๆ–‡ +const MAX_SESSIONS = 500; // ๅ†…ๅญ˜ไธญๆœ€ๅคšไฟ็•™็š„ไผš่ฏๆ•ฐ + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ไผš่ฏๅญ˜ๅ‚จ๏ผˆๅ†…ๅญ˜็บง๏ผŒ้‡ๅฏๅŽๆธ…็ฉบ๏ผ‰ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +// Map +const sessions = new Map(); + +/** + * @typedef {object} SessionData + * @property {string} userId + * @property {Array<{role: string, content: string}>} history + * @property {number} lastActiveAt + * @property {string} channel + * @property {number} totalRounds + */ + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ไผš่ฏ็ฎก็† +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +/** + * ่Žทๅ–ๆˆ–ๅˆ›ๅปบ็”จๆˆทไผš่ฏ + * @param {string} userId - ้ฃžไนฆ็”จๆˆท ID + * @returns {SessionData} + */ +function getSession(userId) { + cleanExpiredSessions(); + + if (sessions.has(userId)) { + const session = sessions.get(userId); + session.lastActiveAt = Date.now(); + return session; + } + + const session = { + userId, + history: [], + lastActiveAt: Date.now(), + channel: 'shuangyan', + totalRounds: 0, + }; + + // ๅฆ‚ๆžœ่ถ…่ฟ‡ๆœ€ๅคงไผš่ฏๆ•ฐ๏ผŒๆธ…้™คๆœ€ๆ—ง็š„ไผš่ฏ + if (sessions.size >= MAX_SESSIONS) { + evictOldestSession(); + } + + sessions.set(userId, session); + return session; +} + +/** + * ๆทปๅŠ ไธ€่ฝฎๅฏน่ฏๅˆฐไธŠไธ‹ๆ–‡ + * @param {string} userId - ้ฃžไนฆ็”จๆˆท ID + * @param {string} userMessage - ็”จๆˆทๆถˆๆฏ + * @param {string} assistantReply - ๅŠฉๆ‰‹ๅ›žๅค + * @param {string} channel - ้€š้“ๆ ‡่ฏ† + */ +function addRound(userId, userMessage, assistantReply, channel) { + const session = getSession(userId); + + session.history.push( + { role: 'user', content: userMessage }, + { role: 'assistant', content: assistantReply } + ); + + session.channel = channel || session.channel; + session.totalRounds += 1; + + // ไฟ็•™ๆœ€่ฟ‘ N ่ฝฎ + if (session.history.length > MAX_HISTORY_MESSAGES) { + session.history = session.history.slice(-MAX_HISTORY_MESSAGES); + } +} + +/** + * ่Žทๅ–็”จๆˆท็š„ๅฏน่ฏๅކๅฒ + * @param {string} userId - ้ฃžไนฆ็”จๆˆท ID + * @returns {Array<{role: string, content: string}>} + */ +function getHistory(userId) { + const session = getSession(userId); + return session.history; +} + +/** + * ๆธ…้™ค็”จๆˆท็š„ๅฏน่ฏไธŠไธ‹ๆ–‡ + * @param {string} userId - ้ฃžไนฆ็”จๆˆท ID + */ +function clearSession(userId) { + sessions.delete(userId); +} + +/** + * ่Žทๅ–ไผš่ฏ็ปŸ่ฎกไฟกๆฏ + * @param {string} userId - ้ฃžไนฆ็”จๆˆท ID + * @returns {object} + */ +function getSessionInfo(userId) { + const session = sessions.get(userId); + if (!session) { + return { exists: false, totalRounds: 0, historyLength: 0 }; + } + return { + exists: true, + totalRounds: session.totalRounds, + historyLength: session.history.length, + channel: session.channel, + lastActiveAt: new Date(session.lastActiveAt).toISOString(), + idleMinutes: Math.floor((Date.now() - session.lastActiveAt) / 60000), + }; +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ๅ†…้ƒจ็ปดๆŠค +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function cleanExpiredSessions() { + const now = Date.now(); + for (const [userId, session] of sessions) { + if (now - session.lastActiveAt > SESSION_TIMEOUT_MS) { + sessions.delete(userId); + } + } +} + +function evictOldestSession() { + let oldestKey = null; + let oldestTime = Infinity; + for (const [userId, session] of sessions) { + if (session.lastActiveAt < oldestTime) { + oldestTime = session.lastActiveAt; + oldestKey = userId; + } + } + if (oldestKey) sessions.delete(oldestKey); +} + +/** + * ่Žทๅ–ๅ…จๅฑ€็ปŸ่ฎก + * @returns {object} + */ +function getStats() { + cleanExpiredSessions(); + return { + activeSessions: sessions.size, + maxSessions: MAX_SESSIONS, + sessionTimeoutMinutes: SESSION_TIMEOUT_MS / 60000, + maxHistoryRounds: MAX_HISTORY_ROUNDS, + }; +} + +module.exports = { + getSession, + addRound, + getHistory, + clearSession, + getSessionInfo, + getStats, +}; diff --git a/backend/routes/feishu-bot.js b/backend/routes/feishu-bot.js index b173dcbc..5936123a 100644 --- a/backend/routes/feishu-bot.js +++ b/backend/routes/feishu-bot.js @@ -1,16 +1,22 @@ // backend/routes/feishu-bot.js // ้“ธๆธŠ ยท ้ฃžไนฆๆœบๅ™จไบบไบ‹ไปถๅ›ž่ฐƒๅค„็† // -// SYSLOG ๅ›žไผ ๆกฅ๏ผšๅผ€ๅ‘่€…ๅ‘ SYSLOG โ†’ ้ฃžไนฆๆœบๅ™จไบบ โ†’ GitHub repository_dispatch +// Phase 1-3: SYSLOG ๅ›žไผ ๆกฅ๏ผˆๅผ€ๅ‘่€…ๅ‘ SYSLOG โ†’ GitHub โ†’ Notion๏ผ‰ +// Phase 4A: AI ๅฏน่ฏๅผ•ๆ“Ž๏ผˆ้œœ็ š/ไบบๆ ผไฝ“ไธŠ็บฟ้ฃžไนฆ๏ผ‰ +// Phase 4C: ๅไฝœๆ•ฐๆฎ้‡‡้›†๏ผˆๅฏน่ฏ่ฎฐๅฝ• โ†’ collaboration-logs/๏ผ‰ // // ้ฃžไนฆไบ‹ไปถ่ฎข้˜…๏ผšim.message.receive_v1 // ้ƒจ็ฝฒ๏ผš้›†ๆˆๅˆฐ็Žฐๆœ‰ M-BRIDGE ๅŽ็ซฏๆœๅŠก // // ็Žฏๅขƒๅ˜้‡๏ผš -// FEISHU_APP_ID ้ฃžไนฆๅบ”็”จ App ID -// FEISHU_APP_SECRET ้ฃžไนฆๅบ”็”จ App Secret -// GITHUB_TOKEN GitHub Token๏ผˆ้œ€่ฆ repo scope๏ผ‰ -// FEISHU_VERIFICATION_TOKEN ้ฃžไนฆไบ‹ไปถ้ชŒ่ฏ Token๏ผˆๅฏ้€‰๏ผŒ็”จไบŽๅฎ‰ๅ…จๆ ก้ชŒ๏ผ‰ +// FEISHU_APP_ID ้ฃžไนฆๅบ”็”จ App ID +// FEISHU_APP_SECRET ้ฃžไนฆๅบ”็”จ App Secret +// GITHUB_TOKEN GitHub Token๏ผˆ้œ€่ฆ repo scope๏ผ‰ +// FEISHU_VERIFICATION_TOKEN ้ฃžไนฆไบ‹ไปถ้ชŒ่ฏ Token๏ผˆๅฏ้€‰๏ผ‰ +// MODEL_API_KEY AI ๆจกๅž‹ API Key๏ผˆPhase 4A๏ผ‰ +// MODEL_API_BASE AI ๆจกๅž‹ API Base URL๏ผˆPhase 4A๏ผ‰ +// MODEL_NAME AI ๆจกๅž‹ๅ็งฐ๏ผˆPhase 4A๏ผ‰ +// FEISHU_ALERT_CHAT_ID ๅคฑ่ดฅๅ‘Š่ญฆๆŽจ้€็š„้ฃžไนฆ็พค chat_id๏ผˆPhase 4D๏ผ‰ 'use strict'; @@ -18,16 +24,25 @@ const express = require('express'); const https = require('https'); const router = express.Router(); -const FEISHU_APP_ID = process.env.FEISHU_APP_ID; -const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET; -const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const aiChat = require('../feishu-bot/ai-chat'); +const contextManager = require('../feishu-bot/context-manager'); +const collaborationLogger = require('../feishu-bot/collaboration-logger'); + +const FEISHU_APP_ID = process.env.FEISHU_APP_ID; +const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const VERIFICATION_TOKEN = process.env.FEISHU_VERIFICATION_TOKEN; +const ALERT_CHAT_ID = process.env.FEISHU_ALERT_CHAT_ID; const GITHUB_REPO_OWNER = process.env.GITHUB_REPO_OWNER || 'qinfendebingshuo'; const GITHUB_REPO_NAME = process.env.GITHUB_REPO_NAME || 'guanghulab'; const VALID_PROTOCOL_VERSIONS = ['4.0', 'v4.0', '4.0.0']; +// ๅทฒๅค„็†็š„ไบ‹ไปถ ID ๅŽป้‡๏ผˆ้ฃžไนฆๅฏ่ƒฝ้‡ๅ‘ไบ‹ไปถ๏ผ‰ +const processedEvents = new Set(); +const MAX_PROCESSED_EVENTS = 2000; + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ๅทฅๅ…ทๅ‡ฝๆ•ฐ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• @@ -52,6 +67,9 @@ function httpsRequest(options, body) { }); }); req.on('error', reject); + req.setTimeout(60000, () => { + req.destroy(new Error('Request timeout')); + }); if (payload) req.write(payload); req.end(); }); @@ -84,7 +102,24 @@ async function replyFeishuMessage(token, messageId, content) { }); } -async function triggerGitHubDispatch(syslog) { +async function sendFeishuGroupMessage(token, chatId, text) { + return httpsRequest({ + hostname: 'open.feishu.cn', + port: 443, + path: '/open-apis/im/v1/messages?receive_id_type=chat_id', + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + }, + }, { + receive_id: chatId, + msg_type: 'text', + content: JSON.stringify({ text }), + }); +} + +async function triggerGitHubDispatch(eventType, payload) { return httpsRequest({ hostname: 'api.github.com', port: 443, @@ -97,22 +132,36 @@ async function triggerGitHubDispatch(syslog) { 'Content-Type': 'application/json', }, }, { - event_type: 'receive-syslog', - client_payload: { - syslog: syslog, - dev_id: syslog.dev_id || syslog.developer_id || 'UNKNOWN', - broadcast_id: syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN', - }, + event_type: eventType, + client_payload: payload, }); } +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ไบ‹ไปถๅŽป้‡ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function isEventProcessed(eventId) { + if (!eventId) return false; + if (processedEvents.has(eventId)) return true; + + // ๆธ…็†่ฟ‡ๅคš็š„่ฎฐๅฝ• + if (processedEvents.size >= MAX_PROCESSED_EVENTS) { + const iterator = processedEvents.values(); + for (let i = 0; i < MAX_PROCESSED_EVENTS / 2; i++) { + processedEvents.delete(iterator.next().value); + } + } + + processedEvents.add(eventId); + return false; +} + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // SYSLOG ๆๅ–ไธŽ้ชŒ่ฏ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function extractSyslogFromMessage(text) { - // ๅฐ่ฏ•ไปŽๆถˆๆฏไธญๆๅ– JSON - // ๆ”ฏๆŒ๏ผš็บฏ JSONใ€ไปฃ็ ๅ—ๅŒ…่ฃน็š„ JSONใ€ๆททๅˆๆ–‡ๆœฌไธญ็š„ JSON if (!text || typeof text !== 'string') return null; // 1. ๅฐ่ฏ•็›ดๆŽฅ่งฃๆžๆ•ดไธชๆ–‡ๆœฌ @@ -161,14 +210,14 @@ function validateSyslog(syslog) { if (!syslog.dev_id && !syslog.developer_id) { return '็ผบๅฐ‘ dev_id ๆˆ– developer_id ๅญ—ๆฎต'; } - return null; // ้ชŒ่ฏ้€š่ฟ‡ + return null; } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ่ทฏ็”ฑๅค„็† // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -// ้ฃžไนฆไบ‹ไปถๅ›ž่ฐƒ URL ้ชŒ่ฏ๏ผˆ้ฆ–ๆฌก้…็ฝฎๆ—ถ้ฃžไนฆไผšๅ‘้€ challenge๏ผ‰ +// ้ฃžไนฆไบ‹ไปถๅ›ž่ฐƒ URL ้ชŒ่ฏ + ๆถˆๆฏๅค„็† router.post('/event', async (req, res) => { const body = req.body; @@ -177,7 +226,7 @@ router.post('/event', async (req, res) => { return res.json({ challenge: body.challenge }); } - // 2. ้ชŒ่ฏ token๏ผˆๅฆ‚ๆžœ้…็ฝฎไบ† VERIFICATION_TOKEN๏ผ‰ + // 2. ้ชŒ่ฏ token if (VERIFICATION_TOKEN && body.token && body.token !== VERIFICATION_TOKEN) { return res.status(403).json({ error: true, message: '้ชŒ่ฏ token ไธๅŒน้…' }); } @@ -186,26 +235,30 @@ router.post('/event', async (req, res) => { const header = body.header || {}; const event = body.event || {}; + // ไบ‹ไปถๅŽป้‡๏ผˆ้ฃžไนฆๅฏ่ƒฝๅœจ่ถ…ๆ—ถๅŽ้‡ๅ‘๏ผ‰ + if (isEventProcessed(header.event_id)) { + return res.json({ code: 0, message: 'duplicate event' }); + } + // ๅค„็† im.message.receive_v1 ไบ‹ไปถ if (header.event_type === 'im.message.receive_v1') { // ็ซ‹ๅณๅ“ๅบ”้ฃžไนฆ๏ผˆ้ฟๅ…่ถ…ๆ—ถ้‡ๅ‘๏ผ‰ res.json({ code: 0 }); // ๅผ‚ๆญฅๅค„็†ๆถˆๆฏ - processMessage(event).catch(err => { - console.error('โŒ ๆถˆๆฏๅค„็†ๅคฑ่ดฅ:', err.message); - }); + processMessage(event).catch(() => {}); return; } - // ๅ…ถไป–ไบ‹ไปถ็ฑปๅž‹ res.json({ code: 0, message: 'event received' }); }); async function processMessage(event) { const message = event.message || {}; + const sender = event.sender || {}; const messageId = message.message_id; const msgType = message.message_type; + const userId = sender.sender_id?.open_id || sender.sender_id?.user_id || 'unknown'; // ๅชๅค„็†ๆ–‡ๆœฌๆถˆๆฏ if (msgType !== 'text') return; @@ -218,37 +271,74 @@ async function processMessage(event) { return; } - // ๆๅ– SYSLOG - const syslog = extractSyslogFromMessage(textContent); - if (!syslog) return; // ไธๆ˜ฏ SYSLOG ๆถˆๆฏ๏ผŒๅฟฝ็•ฅ - - // ้ชŒ่ฏ SYSLOG - const validationError = validateSyslog(syslog); + if (!textContent.trim()) return; // ่Žทๅ–้ฃžไนฆ token ็”จไบŽๅ›žๅค let feishuToken; try { feishuToken = await getFeishuToken(); } catch (e) { - console.error('โŒ ่Žทๅ–้ฃžไนฆ token ๅคฑ่ดฅ:', e.message); return; } + // โ”€โ”€ ่ทฏ็”ฑๅˆคๆ–ญ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // 1. ๆฃ€ๆŸฅๆ˜ฏๅฆไธบ SYSLOG ๅ›žไผ  + const syslog = extractSyslogFromMessage(textContent); + if (syslog) { + await handleSyslogMessage(syslog, feishuToken, messageId); + return; + } + + // 2. ๆฃ€ๆŸฅๆ˜ฏๅฆไธบ็‰นๆฎŠๆŒ‡ไปค + const trimmed = textContent.trim(); + if (trimmed === '/clear' || trimmed === '/reset') { + contextManager.clearSession(userId); + await replyFeishuMessage(feishuToken, messageId, '๐Ÿ”„ ๅฏน่ฏไธŠไธ‹ๆ–‡ๅทฒๆธ…้™คใ€‚'); + return; + } + if (trimmed === '/status') { + const info = contextManager.getSessionInfo(userId); + const stats = collaborationLogger.getStats(); + await replyFeishuMessage(feishuToken, messageId, + '๐Ÿ“Š ไผš่ฏ็Šถๆ€\n' + + 'ๅฏน่ฏ่ฝฎๆ•ฐ: ' + info.totalRounds + '\n' + + 'ๅฝ“ๅ‰้€š้“: ' + (info.channel || 'ๆœชๅผ€ๅง‹') + '\n' + + '็ฉบ้—ฒๆ—ถ้—ด: ' + (info.idleMinutes || 0) + ' ๅˆ†้’Ÿ\n' + + 'ๅไฝœๆ—ฅๅฟ—: ' + stats.totalLogFiles + ' ไธชๆ–‡ไปถ'); + return; + } + + // 3. AI ๅฏน่ฏ๏ผˆPhase 4A๏ผ‰ + await handleAIChatMessage(textContent, userId, feishuToken, messageId); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// SYSLOG ๅค„็†๏ผˆPhase 1-3๏ผ‰ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +async function handleSyslogMessage(syslog, feishuToken, messageId) { + const validationError = validateSyslog(syslog); if (validationError) { await replyFeishuMessage(feishuToken, messageId, 'โŒ SYSLOG ้ชŒ่ฏๅคฑ่ดฅ: ' + validationError + '\n\n่ฏทๆฃ€ๆŸฅ JSON ๆ ผๅผๅŽ้‡ๆ–ฐๅ‘้€ใ€‚'); return; } - // ่งฆๅ‘ GitHub repository_dispatch if (!GITHUB_TOKEN) { await replyFeishuMessage(feishuToken, messageId, 'โš ๏ธ ็ณป็ปŸ้…็ฝฎ็ผบๅคฑ (GITHUB_TOKEN)๏ผŒ่ฏท่”็ณป็ฎก็†ๅ‘˜ใ€‚'); return; } + // Phase 4C: ่ฎฐๅฝ• SYSLOG ๅไฝœๆ•ฐๆฎ + collaborationLogger.logSyslogCollaboration(syslog); + try { - const result = await triggerGitHubDispatch(syslog); + const result = await triggerGitHubDispatch('receive-syslog', { + syslog: syslog, + dev_id: syslog.dev_id || syslog.developer_id || 'UNKNOWN', + broadcast_id: syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN', + }); if (result.statusCode === 204 || result.statusCode === 200) { const devId = syslog.dev_id || syslog.developer_id; const broadcastId = syslog.broadcast_id || syslog.broadcastId || 'ๆ— '; @@ -267,14 +357,89 @@ async function processMessage(event) { } } -// ๅฅๅบทๆฃ€ๆŸฅ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// AI ๅฏน่ฏๅค„็†๏ผˆPhase 4A๏ผ‰ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +async function handleAIChatMessage(textContent, userId, feishuToken, messageId) { + // ่Žทๅ–ๅฏน่ฏๅކๅฒ + const history = contextManager.getHistory(userId); + + // ่ฐƒ็”จ AI ๆจกๅž‹ + const { reply, channel } = await aiChat.chat(textContent, history, { + loginEntryContent: null, // TODO: ไปŽ้ฃžไนฆๆ–‡ๆกฃA็ผ“ๅญ˜ไธญ่Žทๅ– + }); + + // ๆ›ดๆ–ฐไธŠไธ‹ๆ–‡ + contextManager.addRound(userId, textContent, reply, channel); + + // Phase 4C: ่ฎฐๅฝ•ๅไฝœๆ•ฐๆฎ + collaborationLogger.logInteraction({ + userId, + userMessage: textContent, + assistantReply: reply, + channel, + personaId: channel === 'persona' ? 'ICE-GL-ZQ001' : 'ICE-GL-SY001', + }); + + // ๅ›žๅค็”จๆˆท + await replyFeishuMessage(feishuToken, messageId, reply); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ๅคฑ่ดฅๅ‘Š่ญฆ๏ผˆPhase 4D๏ผ‰ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +/** + * ๅ‘้€ๅคฑ่ดฅๅ‘Š่ญฆๅˆฐ้ฃžไนฆ็พค๏ผˆไพ›ๅค–้ƒจ workflow ่ฐƒ็”จ๏ผ‰ + */ +router.post('/alert', async (req, res) => { + const { title, content, level } = req.body; + if (!ALERT_CHAT_ID) { + return res.status(400).json({ error: true, message: 'ๆœช้…็ฝฎ FEISHU_ALERT_CHAT_ID' }); + } + + try { + const feishuToken = await getFeishuToken(); + const emoji = level === 'error' ? '๐Ÿ”ด' : level === 'warning' ? '๐ŸŸก' : 'โ„น๏ธ'; + const text = emoji + ' ' + (title || '็ณป็ปŸๅ‘Š่ญฆ') + '\n\n' + (content || 'ๆ— ่ฏฆ็ป†ไฟกๆฏ'); + await sendFeishuGroupMessage(feishuToken, ALERT_CHAT_ID, text); + res.json({ success: true }); + } catch (e) { + res.status(500).json({ error: true, message: e.message }); + } +}); + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ๅฅๅบทๆฃ€ๆŸฅ + ็ปŸ่ฎก +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + router.get('/health', (req, res) => { res.json({ status: 'ok', service: 'feishu-bot-syslog-bridge', + version: '2.0.0', + features: { + syslog_bridge: true, + ai_chat: !!process.env.MODEL_API_KEY, + collaboration_logging: true, + failure_alerting: !!ALERT_CHAT_ID, + }, github_token_configured: !!GITHUB_TOKEN, feishu_app_configured: !!(FEISHU_APP_ID && FEISHU_APP_SECRET), verification_token_configured: !!VERIFICATION_TOKEN, + model_api_configured: !!process.env.MODEL_API_KEY, + context_stats: contextManager.getStats(), + collaboration_stats: collaborationLogger.getStats(), + }); +}); + +// ๅฏผๅ‡บๅไฝœๆ•ฐๆฎ๏ผˆๅฏน้ฝ persona-brain-db ๆ ผๅผ๏ผ‰ +router.get('/collaboration-export', (req, res) => { + const data = collaborationLogger.exportForBrainDB(); + res.json({ + hli_id: 'HLI-FEISHU-COLLAB-EXPORT', + ...data, }); }); diff --git a/collaboration-logs/.gitkeep b/collaboration-logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scripts/receive-syslog.js b/scripts/receive-syslog.js index 95e5b87e..dd136852 100644 --- a/scripts/receive-syslog.js +++ b/scripts/receive-syslog.js @@ -135,9 +135,15 @@ function saveSyslogToFile(syslog) { const filename = dateStr + '_' + broadcastId + '_' + devId + '.json'; const filepath = path.join(SYSLOG_DIR, filename); + // ๅน‚็ญ‰ๆ€งๆฃ€ๆŸฅ๏ผšๅŒไธ€ broadcast_id + dev_id + date ไธ้‡ๅคๅˆ›ๅปบ + if (fs.existsSync(filepath)) { + console.log(' โš ๏ธ ๅน‚็ญ‰่ทณ่ฟ‡: syslog/' + filename + ' ๅทฒๅญ˜ๅœจ'); + return { filepath, dateStr, broadcastId, devId, duplicate: true }; + } + fs.writeFileSync(filepath, JSON.stringify(syslog, null, 2), 'utf8'); console.log(' โ†’ ๅทฒไฟๅญ˜: syslog/' + filename); - return { filepath, dateStr, broadcastId, devId }; + return { filepath, dateStr, broadcastId, devId, duplicate: false }; } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• @@ -268,7 +274,14 @@ async function main() { // ๆญฅ้ชคโ‘ ๏ผšๅญ˜ๅ…ฅไป“ๅบ“ console.log('๐Ÿ’พ ๆญฅ้ชคโ‘ : ๅญ˜ๅ…ฅไป“ๅบ“ syslog/ ็›ฎๅฝ•...'); - const { dateStr } = saveSyslogToFile(syslog); + const { dateStr, duplicate } = saveSyslogToFile(syslog); + + // ๅน‚็ญ‰ๆ€ง๏ผš้‡ๅค็š„ SYSLOG ไธๅ†ๅˆ›ๅปบ Notion ๅทฅๅ• + if (duplicate) { + console.log('โš ๏ธ ็›ธๅŒ SYSLOG ๅทฒๅญ˜ๅœจ๏ผŒ่ทณ่ฟ‡ Notion ๆ“ไฝœ๏ผˆๅน‚็ญ‰ไฟๆŠค๏ผ‰'); + console.log('โœ… SYSLOG ๆŽฅๆ”ถๅค„็†ๅฎŒๆˆ๏ผˆๅน‚็ญ‰่ทณ่ฟ‡๏ผ‰'); + return; + } // ๆญฅ้ชคโ‘ก๏ผšๅˆ›ๅปบ Notion SYSLOG ๆ”ถไปถ็ฎฑๆก็›ฎ if (notionToken && syslogDbId) { diff --git a/scripts/save-collaboration-log.js b/scripts/save-collaboration-log.js new file mode 100644 index 00000000..d7b4e9aa --- /dev/null +++ b/scripts/save-collaboration-log.js @@ -0,0 +1,148 @@ +// scripts/save-collaboration-log.js +// ้“ธๆธŠ ยท ๅไฝœๆ•ฐๆฎๅฝ’ๆกฃ่„šๆœฌ +// +// ๅฐ† collaboration-logs/ ็›ฎๅฝ•ไธญ็š„ JSONL ๆ•ฐๆฎ +// ็ป“ๆž„ๅŒ–ๅฏน้ฝ persona-brain-db ไบ”ๅผ ๆ ธๅฟƒ่กจ schema +// ็”Ÿๆˆๅฏ็›ดๆŽฅๅฏผๅ…ฅ็š„ JSON ๆ–‡ไปถ +// +// ็”จๆณ•: node scripts/save-collaboration-log.js +// +// ่พ“ๅ‡บ: +// collaboration-logs/exports/persona-memory-import.json +// collaboration-logs/exports/dev-interactions-import.json + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const LOGS_DIR = path.resolve(__dirname, '..', 'collaboration-logs'); +const EXPORT_DIR = path.join(LOGS_DIR, 'exports'); + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ไธปๆต็จ‹ +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function main() { + console.log('๐Ÿ“Š ๅไฝœๆ•ฐๆฎๅฝ’ๆกฃๅผ€ๅง‹...'); + + // 1. ๆ‰ซๆ JSONL ๆ–‡ไปถ + if (!fs.existsSync(LOGS_DIR)) { + console.log('๐Ÿ“ญ ๆ— ๅไฝœๆ—ฅๅฟ—็›ฎๅฝ•'); + process.exit(0); + } + + const files = fs.readdirSync(LOGS_DIR) + .filter(f => f.startsWith('collab-') && f.endsWith('.jsonl')) + .sort(); + + if (files.length === 0) { + console.log('๐Ÿ“ญ ๆ— ๅไฝœๆ—ฅๅฟ—ๆ–‡ไปถ'); + process.exit(0); + } + + console.log(' โ†’ ๅ‘็Žฐ ' + files.length + ' ไธชๆ—ฅๅฟ—ๆ–‡ไปถ'); + + // 2. ่ฏปๅ–ๆ‰€ๆœ‰่ฎฐๅฝ• + const records = []; + for (const file of files) { + const lines = fs.readFileSync(path.join(LOGS_DIR, file), 'utf8') + .split('\n') + .filter(l => l.trim()); + for (const line of lines) { + try { + records.push(JSON.parse(line)); + } catch (e) { /* skip malformed */ } + } + } + + console.log(' โ†’ ๅ…ฑ ' + records.length + ' ๆก่ฎฐๅฝ•'); + + // 3. ็ป“ๆž„ๅŒ–ๅฏผๅ‡บ + + // 3a. persona_memory ่กจๅฏผๅ…ฅๆ•ฐๆฎ + const memoryEntries = records + .filter(r => r.memory_entry || r.type === 'collaboration') + .map(r => { + if (r.memory_entry) { + return { + memory_id: r.interaction_id, + ...r.memory_entry, + content: r.type === 'collaboration' + ? 'User: ' + (r.user_message || '').slice(0, 500) + '\nAssistant: ' + (r.assistant_reply || '').slice(0, 500) + : r.summary || r.memory_entry.title, + timestamp: r.timestamp, + }; + } + return { + memory_id: r.interaction_id, + persona_id: r.persona_id || r.personaId || 'ICE-GL-SY001', + type: 'learning', + title: '้ฃžไนฆๅฏน่ฏ ยท ' + (r.channel || 'shuangyan'), + content: 'User: ' + (r.user_message || '').slice(0, 500) + '\nAssistant: ' + (r.assistant_reply || '').slice(0, 500), + importance: 3, + related_dev: r.dev_id || null, + tags: ['feishu', 'collaboration', r.channel || 'shuangyan'], + timestamp: r.timestamp, + }; + }); + + // 3b. dev_profiles ไบคไบ’็ปŸ่ฎก + const devInteractions = {}; + for (const r of records) { + const devId = r.dev_id; + if (!devId) continue; + if (!devInteractions[devId]) { + devInteractions[devId] = { + dev_id: devId, + total_interactions: 0, + syslog_count: 0, + chat_count: 0, + last_active_at: null, + channels_used: new Set(), + broadcasts_referenced: new Set(), + }; + } + const d = devInteractions[devId]; + d.total_interactions++; + if (r.type === 'syslog_collaboration') d.syslog_count++; + if (r.type === 'collaboration') d.chat_count++; + d.last_active_at = r.timestamp; + if (r.channel) d.channels_used.add(r.channel); + if (r.broadcast_id && r.broadcast_id !== 'UNKNOWN') d.broadcasts_referenced.add(r.broadcast_id); + } + + // Set ่ฝฌ Array ็”จไบŽ JSON ๅบๅˆ—ๅŒ– + for (const key of Object.keys(devInteractions)) { + devInteractions[key].channels_used = Array.from(devInteractions[key].channels_used); + devInteractions[key].broadcasts_referenced = Array.from(devInteractions[key].broadcasts_referenced); + } + + // 4. ๅ†™ๅ…ฅๅฏผๅ‡บๆ–‡ไปถ + if (!fs.existsSync(EXPORT_DIR)) { + fs.mkdirSync(EXPORT_DIR, { recursive: true }); + } + + const memoryFile = path.join(EXPORT_DIR, 'persona-memory-import.json'); + fs.writeFileSync(memoryFile, JSON.stringify({ + table: 'persona_memory', + exported_at: new Date().toISOString(), + total: memoryEntries.length, + entries: memoryEntries, + }, null, 2)); + console.log(' โ†’ ๅฏผๅ‡บ persona_memory: ' + memoryEntries.length + ' ๆก'); + + const devFile = path.join(EXPORT_DIR, 'dev-interactions-import.json'); + fs.writeFileSync(devFile, JSON.stringify({ + table: 'dev_profiles', + exported_at: new Date().toISOString(), + total: Object.keys(devInteractions).length, + profiles: devInteractions, + }, null, 2)); + console.log(' โ†’ ๅฏผๅ‡บ dev_profiles: ' + Object.keys(devInteractions).length + ' ไธชๅผ€ๅ‘่€…'); + + console.log('โœ… ๅไฝœๆ•ฐๆฎๅฝ’ๆกฃๅฎŒๆˆ'); + console.log(' ๅฏผๅ‡บ็›ฎๅฝ•: ' + EXPORT_DIR); +} + +main();