diff --git a/.github/workflows/update-readme-bulletin.yml b/.github/workflows/update-readme-bulletin.yml index 219bfb68..04a41775 100644 --- a/.github/workflows/update-readme-bulletin.yml +++ b/.github/workflows/update-readme-bulletin.yml @@ -82,6 +82,17 @@ jobs: echo "📢 公告区无变化,跳过提交" else git commit -m "📢 自动更新系统公告区 [skip ci]" - git push - echo "✅ 公告区已更新并推送" + # 带重试的推送(避免并发推送导致 rejected) + for i in 1 2 3; do + if git push; then + echo "✅ 公告区已更新并推送" + break + fi + echo "⚠️ 推送失败(第 $i 次),拉取最新代码后重试..." + git pull --rebase origin main + if [ $i -eq 3 ]; then + echo "❌ 推送失败 3 次,放弃" + exit 1 + fi + done fi diff --git a/.github/workflows/zhuyuan-daily-agent.yml b/.github/workflows/zhuyuan-daily-agent.yml new file mode 100644 index 00000000..6b291011 --- /dev/null +++ b/.github/workflows/zhuyuan-daily-agent.yml @@ -0,0 +1,142 @@ +# 铸渊 · 每日巡检 Agent +# +# 每天定时巡检仓库健康状况,自动检查今天遗漏的任务, +# 发现问题后自动触发对应的修复工作流。 +# +# 触发条件: +# 1. 每日定时(北京时间 22:00 = UTC 14:00) +# 2. 手动触发 +# +# 巡检项: +# CHK-1: 公告栏是否已更新 +# CHK-2: 每日自检是否已执行 +# CHK-3: 大脑文件完整性 +# CHK-4: CI 最近状态 +# CHK-5: 关键模块 README +# CHK-6: 公告栏工作流健康 + +name: 🤖 铸渊巡检 Agent · 每日自动巡检与修复 + +on: + schedule: + # 北京时间 22:00 (UTC 14:00) — 每天收工前巡检 + - cron: '0 14 * * *' + workflow_dispatch: + +permissions: + contents: write + actions: write + +concurrency: + group: zhuyuan-daily-agent + cancel-in-progress: true + +jobs: + inspect: + name: 🔍 铸渊巡检 Agent + runs-on: ubuntu-latest + outputs: + has_issues: ${{ steps.agent.outputs.has_issues }} + need_bulletin_update: ${{ steps.agent.outputs.need_bulletin_update }} + need_selfcheck: ${{ steps.agent.outputs.need_selfcheck }} + summary: ${{ steps.agent.outputs.summary }} + + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + with: + fetch-depth: 50 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: 🟢 设置 Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 🔍 执行巡检 + id: agent + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: node scripts/zhuyuan-daily-agent.js + + - name: 📝 提交巡检结果 + run: | + git config user.name "铸渊 (ZhùYuān)" + git config user.email "zhuyuan@guanghulab.com" + git add .github/brain/memory.json + if git diff --cached --quiet; then + echo "📋 无变化,跳过提交" + else + git commit -m "🤖 铸渊巡检Agent · $(date +%Y-%m-%d) [skip ci]" + for i in 1 2 3; do + if git push; then + echo "✅ 巡检结果已推送" + break + fi + echo "⚠️ 推送失败(第 $i 次),拉取后重试..." + git pull --rebase origin main + if [ $i -eq 3 ]; then + echo "⚠️ 推送失败 3 次,跳过" + fi + done + fi + + # ── 自动修复:触发公告栏更新 ── + fix-bulletin: + name: 🔧 自动修复 · 公告栏更新 + needs: inspect + if: needs.inspect.outputs.need_bulletin_update == 'true' + runs-on: ubuntu-latest + steps: + - name: 🔄 触发公告栏更新工作流 + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'update-readme-bulletin.yml', + ref: 'main', + }); + console.log('✅ 已触发公告栏更新工作流'); + + # ── 自动修复:触发每日自检 ── + fix-selfcheck: + name: 🔧 自动修复 · 每日自检 + needs: inspect + if: needs.inspect.outputs.need_selfcheck == 'true' + runs-on: ubuntu-latest + steps: + - name: 🔄 触发每日自检工作流 + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'zhuyuan-daily-selfcheck.yml', + ref: 'main', + }); + console.log('✅ 已触发每日自检工作流'); + + # ── 巡检报告 ── + report: + name: 📊 巡检报告 + needs: [inspect, fix-bulletin, fix-selfcheck] + if: always() + runs-on: ubuntu-latest + steps: + - name: 📊 输出巡检摘要 + run: | + echo "═══════════════════════════════════════════" + echo "🤖 铸渊巡检 Agent · 每日报告" + echo "═══════════════════════════════════════════" + echo "" + echo "📋 巡检结果: ${{ needs.inspect.outputs.summary }}" + echo "🔧 公告栏修复: ${{ needs.fix-bulletin.result || '未触发' }}" + echo "🔧 自检修复: ${{ needs.fix-selfcheck.result || '未触发' }}" + echo "" + echo "✅ 巡检 Agent 流程结束" diff --git a/persona-studio/frontend/chat.js b/persona-studio/frontend/chat.js index a365219e..b9c23a43 100644 --- a/persona-studio/frontend/chat.js +++ b/persona-studio/frontend/chat.js @@ -140,87 +140,46 @@ async function sendMessage() { input.focus(); } -/* ---- API Key 流式响应(浏览器直连) ---- */ +/* ---- API Key 对话(通过后端代理,避免 CORS 问题) ---- */ async function streamApiKeyReply(text) { var apiMessages = conversationHistory.slice(-20).map(function (msg) { return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content }; }); - var chatUrl = USER_API_BASE + '/chat/completions'; - var reqBody = { - model: SELECTED_MODEL, - messages: apiMessages, - stream: true, - max_tokens: 2000, - temperature: 0.8 - }; - - var res = await fetch(chatUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + USER_API_KEY - }, - body: JSON.stringify(reqBody) - }); - - if (!res.ok) { - var errText = '请求失败 (HTTP ' + res.status + ')'; - try { - var errData = await res.json(); - errText = errData.error?.message || errText; - } catch (_e) { /* ignore parse error */ } - appendMessage('system', '⚠️ ' + errText); - return; - } - - // 流式读取响应 var streamEl = appendStreamMessage(); - var full = ''; - var reader = res.body.getReader(); - var decoder = new TextDecoder(); - var buf = ''; - while (true) { - var chunk = await reader.read(); - if (chunk.done) break; - buf += decoder.decode(chunk.value, { stream: true }); - var lines = buf.split('\n'); - buf = lines.pop(); + try { + var res = await fetch(API_BASE + '/api/ps/apikey/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + api_base: USER_API_BASE, + api_key: USER_API_KEY, + model: SELECTED_MODEL, + messages: apiMessages + }) + }); - for (var i = 0; i < lines.length; i++) { - var line = lines[i]; - if (!line.startsWith('data: ')) continue; - var d = line.slice(6); - if (d === '[DONE]') continue; + if (!res.ok) { + var errText = '请求失败 (HTTP ' + res.status + ')'; try { - var parsed = JSON.parse(d); - var delta = parsed.choices && parsed.choices[0] - && parsed.choices[0].delta - && parsed.choices[0].delta.content; - if (delta) { - full += delta; - streamEl.textContent = full; - } + var errData = await res.json(); + errText = errData.message || errText; } catch (_e) { /* ignore parse error */ } + streamEl.textContent = '⚠️ ' + errText; + return; } - } - // 如果没有流式内容,尝试非流式解析 - if (!full && buf) { - try { - var jsonRes = JSON.parse(buf); - if (jsonRes.choices && jsonRes.choices[0] && jsonRes.choices[0].message) { - full = jsonRes.choices[0].message.content; - streamEl.textContent = full; - } - } catch (_e) { /* ignore */ } - } + var data = await res.json(); - if (full) { - conversationHistory.push({ role: 'assistant', content: full }); - } else { - streamEl.textContent = '(未收到有效回复)'; + if (data.reply) { + streamEl.textContent = data.reply; + conversationHistory.push({ role: 'assistant', content: data.reply }); + } else { + streamEl.textContent = '(未收到有效回复)'; + } + } catch (err) { + streamEl.textContent = '⚠️ ' + (err.message || '请求失败,请检查网络连接'); } } diff --git a/scripts/zhuyuan-daily-agent.js b/scripts/zhuyuan-daily-agent.js new file mode 100644 index 00000000..fe801d01 --- /dev/null +++ b/scripts/zhuyuan-daily-agent.js @@ -0,0 +1,337 @@ +/** + * 铸渊 · 每日巡检 Agent + * + * 目的:每天自动巡检仓库健康状况,检查当天遗漏的任务, + * 生成巡检报告,并自动触发修复流程。 + * + * 检查项: + * 1. 公告栏是否已更新(README 中日期是否为今天) + * 2. 每日自检是否执行(memory.json 中 daily_selfcheck 日期) + * 3. PSP 巡检是否执行 + * 4. 大脑文件完整性 + * 5. CI/CD 最近状态 + * 6. 关键模块 README 是否存在 + * + * 输出: + * - 控制台巡检报告 + * - 更新 .github/brain/memory.json 写入巡检结果 + * - 设置 GitHub Actions 输出变量供后续步骤使用 + * + * 环境变量: + * GITHUB_TOKEN - GitHub API token + * GITHUB_REPOSITORY - owner/repo + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +const ROOT = path.join(__dirname, '..'); +const MEMORY_PATH = path.join(ROOT, '.github', 'brain', 'memory.json'); +const PERSONA_MEMORY_PATH = path.join(ROOT, '.github', 'persona-brain', 'memory.json'); +const README_PATH = path.join(ROOT, 'README.md'); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; +const REPO = process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab'; + +const now = new Date(); +const todayStr = now.toISOString().split('T')[0]; +const todayShort = (() => { + const fmt = new Intl.DateTimeFormat('zh-CN', { + timeZone: 'Asia/Shanghai', + month: '2-digit', + day: '2-digit', + }); + const parts = fmt.formatToParts(now); + const get = (type) => (parts.find(p => p.type === type) || {}).value || ''; + return `${get('month')}-${get('day')}`; +})(); + +/* ── 工具函数 ────────────────────────────── */ + +function loadJson(filePath) { + if (!fs.existsSync(filePath)) return null; + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch { + return null; + } +} + +function saveJson(filePath, data) { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); +} + +function githubApi(endpoint) { + return new Promise((resolve, reject) => { + if (!GITHUB_TOKEN) { + resolve(null); + return; + } + const url = `https://api.github.com/repos/${REPO}${endpoint}`; + const options = { + headers: { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'zhuyuan-agent', + }, + timeout: 15000, + }; + + https.get(url, options, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { + try { resolve(JSON.parse(data)); } + catch { resolve(null); } + }); + }).on('error', () => resolve(null)) + .on('timeout', function () { this.destroy(); resolve(null); }); + }); +} + +/* ── 巡检项 ──────────────────────────────── */ + +const checks = []; +const issues = []; +const actions = []; + +// CHK-1: 公告栏是否有今天的条目 +function checkBulletin() { + console.log('🔍 CHK-1: 检查公告栏更新...'); + const readme = fs.existsSync(README_PATH) ? fs.readFileSync(README_PATH, 'utf8') : ''; + const hasTodayEntry = readme.includes(todayShort); + + if (hasTodayEntry) { + checks.push({ id: 'CHK-1', name: '公告栏更新', status: '✅', detail: `今日 (${todayShort}) 有更新条目` }); + console.log(` ✅ 公告栏包含今日 (${todayShort}) 条目`); + } else { + checks.push({ id: 'CHK-1', name: '公告栏更新', status: '❌', detail: `今日 (${todayShort}) 无更新条目` }); + issues.push('公告栏今日未更新'); + actions.push('trigger_bulletin_update'); + console.log(` ❌ 公告栏缺少今日 (${todayShort}) 条目`); + } +} + +// CHK-2: 每日自检是否执行 +function checkDailySelfcheck() { + console.log('🔍 CHK-2: 检查每日自检...'); + const memory = loadJson(PERSONA_MEMORY_PATH); + const lastRun = memory?.daily_selfcheck?.last_run || ''; + const ranToday = lastRun.startsWith(todayStr); + + if (ranToday) { + checks.push({ id: 'CHK-2', name: '每日自检', status: '✅', detail: `今日已执行 (${lastRun})` }); + console.log(` ✅ 每日自检已执行 (${lastRun})`); + } else { + checks.push({ id: 'CHK-2', name: '每日自检', status: '⚠️', detail: `今日未执行,上次: ${lastRun || '无记录'}` }); + issues.push('每日自检今日未执行'); + actions.push('trigger_selfcheck'); + console.log(` ⚠️ 每日自检今日未执行,上次: ${lastRun || '无记录'}`); + } +} + +// CHK-3: 大脑文件完整性 +function checkBrainIntegrity() { + console.log('🔍 CHK-3: 检查大脑文件完整性...'); + const requiredFiles = [ + '.github/brain/memory.json', + '.github/persona-brain/memory.json', + ]; + const missing = []; + + for (const f of requiredFiles) { + const fullPath = path.join(ROOT, f); + if (!fs.existsSync(fullPath)) { + missing.push(f); + } + } + + if (missing.length === 0) { + checks.push({ id: 'CHK-3', name: '大脑文件完整性', status: '✅', detail: '所有核心文件完整' }); + console.log(' ✅ 所有核心文件完整'); + } else { + checks.push({ id: 'CHK-3', name: '大脑文件完整性', status: '❌', detail: `缺失: ${missing.join(', ')}` }); + issues.push(`大脑文件缺失: ${missing.join(', ')}`); + console.log(` ❌ 大脑文件缺失: ${missing.join(', ')}`); + } +} + +// CHK-4: CI 最近状态 +async function checkCiStatus() { + console.log('🔍 CHK-4: 检查 CI 状态...'); + const runs = await githubApi('/actions/runs?per_page=5&status=completed'); + + if (!runs || !runs.workflow_runs) { + checks.push({ id: 'CHK-4', name: 'CI 状态', status: '⚠️', detail: '无法获取 CI 状态' }); + console.log(' ⚠️ 无法获取 CI 状态'); + return; + } + + const recent = runs.workflow_runs; + const failures = recent.filter(r => r.conclusion === 'failure'); + + if (failures.length === 0) { + checks.push({ id: 'CHK-4', name: 'CI 状态', status: '✅', detail: `最近 ${recent.length} 次运行全部成功` }); + console.log(` ✅ 最近 ${recent.length} 次运行全部成功`); + } else { + const failNames = failures.map(f => f.name).join(', '); + checks.push({ id: 'CHK-4', name: 'CI 状态', status: '⚠️', detail: `${failures.length} 个失败: ${failNames}` }); + issues.push(`CI 有 ${failures.length} 个失败工作流`); + console.log(` ⚠️ CI 有 ${failures.length} 个失败: ${failNames}`); + } +} + +// CHK-5: 关键模块 README 检查 +function checkModuleReadmes() { + console.log('🔍 CHK-5: 检查关键模块 README...'); + const modules = [ + 'persona-studio', 'backend', 'backend-integration', + 'status-board', 'dingtalk-bot', 'notification', + ]; + const missingReadme = []; + + for (const mod of modules) { + const modDir = path.join(ROOT, mod); + if (!fs.existsSync(modDir)) continue; + const readmePath = path.join(modDir, 'README.md'); + if (!fs.existsSync(readmePath)) { + missingReadme.push(mod); + } + } + + if (missingReadme.length === 0) { + checks.push({ id: 'CHK-5', name: '模块 README', status: '✅', detail: '所有关键模块有 README' }); + console.log(' ✅ 所有关键模块有 README'); + } else { + checks.push({ id: 'CHK-5', name: '模块 README', status: '⚠️', detail: `缺失 README: ${missingReadme.join(', ')}` }); + console.log(` ⚠️ 缺失 README: ${missingReadme.join(', ')}`); + } +} + +// CHK-6: 公告栏更新工作流最近是否有失败 +async function checkBulletinWorkflow() { + console.log('🔍 CHK-6: 检查公告栏更新工作流...'); + const runs = await githubApi('/actions/workflows/update-readme-bulletin.yml/runs?per_page=3&status=completed'); + + if (!runs || !runs.workflow_runs) { + checks.push({ id: 'CHK-6', name: '公告栏工作流', status: '⚠️', detail: '无法获取工作流状态' }); + console.log(' ⚠️ 无法获取工作流状态'); + return; + } + + const latest = runs.workflow_runs[0]; + if (!latest) { + checks.push({ id: 'CHK-6', name: '公告栏工作流', status: '⚠️', detail: '无运行记录' }); + return; + } + + if (latest.conclusion === 'success') { + checks.push({ id: 'CHK-6', name: '公告栏工作流', status: '✅', detail: `最近一次成功 (${latest.created_at})` }); + console.log(` ✅ 最近一次成功 (${latest.created_at})`); + } else { + checks.push({ id: 'CHK-6', name: '公告栏工作流', status: '❌', detail: `最近一次 ${latest.conclusion} (${latest.created_at})` }); + issues.push(`公告栏工作流最近结论: ${latest.conclusion}`); + actions.push('trigger_bulletin_update'); + console.log(` ❌ 最近一次 ${latest.conclusion} (${latest.created_at})`); + } +} + +/* ── 主流程 ──────────────────────────────── */ + +async function main() { + console.log('═══════════════════════════════════════════'); + console.log('🤖 铸渊每日巡检 Agent · ' + todayStr); + console.log('═══════════════════════════════════════════\n'); + + // 执行所有检查 + checkBulletin(); + checkDailySelfcheck(); + checkBrainIntegrity(); + await checkCiStatus(); + checkModuleReadmes(); + await checkBulletinWorkflow(); + + // 汇总 + const passed = checks.filter(c => c.status === '✅').length; + const warnings = checks.filter(c => c.status === '⚠️').length; + const failed = checks.filter(c => c.status === '❌').length; + + console.log('\n═══════════════════════════════════════════'); + console.log('📊 巡检报告'); + console.log('═══════════════════════════════════════════'); + console.log(` ✅ 通过: ${passed} ⚠️ 警告: ${warnings} ❌ 失败: ${failed}`); + console.log(` 📋 总检查项: ${checks.length}`); + + if (issues.length > 0) { + console.log('\n🔴 待处理问题:'); + issues.forEach((issue, i) => console.log(` ${i + 1}. ${issue}`)); + } + + if (actions.length > 0) { + console.log('\n🔧 建议自动修复:'); + const uniqueActions = [...new Set(actions)]; + uniqueActions.forEach(a => console.log(` → ${a}`)); + } + + // 更新 memory.json + const memory = loadJson(MEMORY_PATH) || {}; + if (!memory.events) memory.events = []; + + const summaryText = `铸渊巡检Agent · ✅${passed} ⚠️${warnings} ❌${failed}` + + (issues.length > 0 ? ` · ${issues.length}个问题` : ' · 全部通过'); + + memory.events.push({ + type: 'daily_agent_inspection', + timestamp: now.toISOString(), + description: summaryText, + result: failed > 0 ? 'issues_found' : (warnings > 0 ? 'warnings' : 'passed'), + checks: checks.length, + passed, + warnings, + failed, + issues_detail: issues, + actions_suggested: [...new Set(actions)], + }); + + // 保留最近 50 条事件 + if (memory.events.length > 50) { + memory.events = memory.events.slice(-50); + } + + memory.last_agent_inspection = { + timestamp: now.toISOString(), + result: failed > 0 ? 'issues_found' : (warnings > 0 ? 'warnings' : 'passed'), + summary: summaryText, + }; + + saveJson(MEMORY_PATH, memory); + console.log('\n💾 巡检结果已写入 memory.json'); + + // 输出 GitHub Actions 变量 + const uniqueActions = [...new Set(actions)]; + const needBulletin = uniqueActions.includes('trigger_bulletin_update'); + const needSelfcheck = uniqueActions.includes('trigger_selfcheck'); + const outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + const outputs = [ + `has_issues=${issues.length > 0}`, + `issue_count=${issues.length}`, + `need_bulletin_update=${needBulletin}`, + `need_selfcheck=${needSelfcheck}`, + `summary=${summaryText}`, + ]; + fs.appendFileSync(outputFile, outputs.join('\n') + '\n'); + } + + console.log('\n✅ 铸渊巡检 Agent 完成'); +} + +main().catch(err => { + console.error('❌ 巡检 Agent 异常:', err.message); + process.exit(1); +});