diff --git a/.github/workflows/auto-submit.yml b/.github/workflows/auto-submit.yml new file mode 100644 index 0000000..2191dcd --- /dev/null +++ b/.github/workflows/auto-submit.yml @@ -0,0 +1,24 @@ +name: Auto Submit + +on: + issues: + types: [labeled] + +jobs: + autoflow: + runs-on: ubuntu-latest + if: github.event.label.name == '🤖 Agent PR' + steps: + - uses: actions/checkout@v4 + + - name: Install bun + uses: oven-sh/setup-bun@v1 + + - name: Install deps + run: bun i + + - name: Auto submit + run: bun run submit + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} diff --git a/.github/workflows/issue-auto-comments.yml b/.github/workflows/issue-auto-comments.yml new file mode 100644 index 0000000..1762115 --- /dev/null +++ b/.github/workflows/issue-auto-comments.yml @@ -0,0 +1,73 @@ +name: Issue Auto Comment +on: + issues: + types: + - opened + - closed + - assigned + pull_request_target: + types: + - opened + - closed + +permissions: + contents: read + +jobs: + run: + permissions: + issues: write # for actions-cool/issues-helper to update issues + pull-requests: write # for actions-cool/issues-helper to update PRs + runs-on: ubuntu-latest + steps: + - name: Auto Comment on Issues Opened + uses: wow-actions/auto-comment@v1 + with: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} + issuesOpened: | + 👀 @{{ author }} + + Thank you for raising an issue. We will investigate into the matter and get back to you as soon as possible. + Please make sure you have given us as much context as possible.\ + 非常感谢您提交 issue。我们会尽快调查此事,并尽快回复您。 请确保您已经提供了尽可能多的背景信息。 + - name: Auto Comment on Issues Closed + uses: wow-actions/auto-comment@v1 + with: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} + issuesClosed: | + ✅ @{{ author }} + + This issue is closed, If you have any questions, you can comment and reply.\ + 此问题已经关闭。如果您有任何问题,可以留言并回复。 + - name: Auto Comment on Pull Request Opened + uses: wow-actions/auto-comment@v1 + with: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} + pullRequestOpened: | + 👍 @{{ author }} + + Thank you for raising your pull request and contributing to our Community + Please make sure you have followed our contributing guidelines. We will review it as soon as possible. + If you encounter any problems, please feel free to connect with us.\ + 非常感谢您提出拉取请求并为我们的社区做出贡献,请确保您已经遵循了我们的贡献指南,我们会尽快审查它。 + 如果您遇到任何问题,请随时与我们联系。 + - name: Auto Comment on Pull Request Merged + uses: actions-cool/pr-welcome@main + if: github.event.pull_request.merged == true + with: + token: ${{ secrets.GH_TOKEN }} + comment: | + ❤️ Great PR @${{ github.event.pull_request.user.login }} ❤️ + + The growth of project is inseparable from user feedback and contribution, thanks for your contribution!\ + 项目的成长离不开用户反馈和贡献,感谢您的贡献! + emoji: 'hooray' + pr-emoji: '+1, heart' + - name: Remove inactive + if: github.event.issue.state == 'open' && github.actor == github.event.issue.user.login + uses: actions-cool/issues-helper@v3 + with: + actions: 'remove-labels' + token: ${{ secrets.GH_TOKEN }} + issue-number: ${{ github.event.issue.number }} + labels: 'Inactive' diff --git a/.github/workflows/issue-close-require.yml b/.github/workflows/issue-close-require.yml new file mode 100644 index 0000000..f0c3a87 --- /dev/null +++ b/.github/workflows/issue-close-require.yml @@ -0,0 +1,67 @@ +name: Issue Close Require + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' + +permissions: + contents: read + +jobs: + issue-check-inactive: + permissions: + issues: write # for actions-cool/issues-helper to update issues + pull-requests: write # for actions-cool/issues-helper to update PRs + runs-on: ubuntu-latest + steps: + - name: check-inactive + uses: actions-cool/issues-helper@v3 + with: + actions: 'check-inactive' + token: ${{ secrets.GH_TOKEN }} + inactive-label: 'Inactive' + inactive-day: 30 + + issue-close-require: + permissions: + issues: write # for actions-cool/issues-helper to update issues + pull-requests: write # for actions-cool/issues-helper to update PRs + runs-on: ubuntu-latest + steps: + - name: need reproduce + uses: actions-cool/issues-helper@v3 + with: + actions: 'close-issues' + token: ${{ secrets.GH_TOKEN }} + labels: '✅ Fixed' + inactive-day: 3 + body: | + 👋 @{{ github.event.issue.user.login }} +
+ Since the issue was labeled with `✅ Fixed`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.\ + 由于该 issue 被标记为已修复,同时 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 + - name: need reproduce + uses: actions-cool/issues-helper@v3 + with: + actions: 'close-issues' + token: ${{ secrets.GH_TOKEN }} + labels: '🤔 Need Reproduce' + inactive-day: 3 + body: | + 👋 @{{ github.event.issue.user.login }} +
+ Since the issue was labeled with `🤔 Need Reproduce`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.\ + 由于该 issue 被标记为需要更多信息,却 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 + - name: need reproduce + uses: actions-cool/issues-helper@v3 + with: + actions: 'close-issues' + token: ${{ secrets.GH_TOKEN }} + labels: "🙅🏻‍♀️ WON'T DO" + inactive-day: 3 + body: | + 👋 @{{ github.event.issue.user.login }} +
+ Since the issue was labeled with `🙅🏻‍♀️ WON'T DO`, and no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.\ + 由于该 issue 被标记为暂不处理,同时 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 diff --git a/package.json b/package.json index 8d7135d..e9984f5 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,14 @@ "type": "module", "scripts": { "build": "bun scripts/build.ts", - "test": "bun scripts/test.ts" + "test": "bun scripts/test.ts", + "submit": "bun scripts/autoSubmit.ts" }, "devDependencies": { "@types/node": "^20.12.8", "consola": "^3.2.3", + "dotenv": "^16", + "@octokit/rest": "^20", "dayjs": "^1.11.11", "fs-extra": "^11.2.0", "lodash-es": "^4.17.21", diff --git a/scripts/autoSubmit.ts b/scripts/autoSubmit.ts new file mode 100644 index 0000000..9eacc05 --- /dev/null +++ b/scripts/autoSubmit.ts @@ -0,0 +1,258 @@ +import { Octokit } from '@octokit/rest'; +import { consola } from 'consola'; +import 'dotenv/config'; +import { kebabCase } from 'lodash-es'; +import { execSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { formatAgentSchema } from './check'; +import { agentsDir, githubHomepage } from './const'; +import { checkHeader, writeJSON } from './utils'; + +const GENERATE_LABEL = '🤖 Agent PR'; +const SUCCESS_LABEL = '✅ Auto Check Pass'; +const ERROR_LABEL = '🚨 Auto Check Fail'; + +class AutoSubmit { + owner = 'lobehub'; + repo = 'lobe-vidol-market'; + issueNumber = Number(process.env.ISSUE_NUMBER); + private octokit: Octokit; + + constructor() { + this.octokit = new Octokit({ auth: `token ${process.env.GH_TOKEN}` }); + } + + async run() { + try { + await this.submit(); + } catch (error) { + await this.removeLabels(GENERATE_LABEL); + await this.removeLabels(SUCCESS_LABEL); + await this.addLabels(ERROR_LABEL); + await this.createComment( + [ + '**🚨 Auto Check Fail:**', + '- Fix error below', + `- Add issue label \`${GENERATE_LABEL}\` to the current issue`, + '- Wait for automation to regenerate', + '```bash', + error?.message, + '```', + ].join('\n'), + ); + consola.error(error); + } + } + + async submit() { + const issue = await this.getIssue(); + if (!issue) return; + consola.info(`Get issues #${this.issueNumber}`); + + const { agent } = await this.formatIssue(issue); + const comment = this.genCommentMessage(agent); + const agentName = agent.agentId; + + const fileName = `${agentName}.json`; + const filePath = resolve(agentsDir, fileName); + + // check same name + if (existsSync(filePath)) { + await this.createComment( + [ + `**🚨 Auto Check Fail:** Same name exist <${`${githubHomepage}/blob/main/agents/${fileName}`}>`, + '- Rename your agent identifier', + `- Add issue label \`${GENERATE_LABEL}\` to the current issue`, + '- Wait for automation to regenerate', + '---', + comment, + ].join('\n'), + ); + await this.removeLabels(GENERATE_LABEL); + await this.addLabels(ERROR_LABEL); + consola.error('Auto Check Fail'); + return; + } + + // comment in issues + await this.createComment(comment); + consola.info(`Auto Check Pass`); + + // commit and pull request + this.gitCommit(filePath, agent, agentName); + consola.info('Commit to', `agent/${agentName}`); + + await this.createPullRequest( + agentName, + agent.author, + [comment, `[@${agent.author}](${agent.homepage}) (resolve #${this.issueNumber})`].join('\n'), + ); + consola.success('Create PR'); + + await this.addLabels(SUCCESS_LABEL); + } + + gitCommit(filePath, agent, agentName) { + execSync('git diff'); + execSync('git config --global user.name "lobehubbot"'); + execSync('git config --global user.email "i@lobehub.com"'); + execSync('git pull'); + execSync(`git checkout -b agent/${agentName}`); + consola.info('Checkout branch'); + + // generate file + writeJSON(filePath, agent); + consola.info('Generate file', filePath); + + // commit + execSync('git add -A'); + execSync(`git commit -m "🤖 chore(auto-submit): Add ${agentName} (#${this.issueNumber})"`); + execSync(`git push origin agent/${agentName}`); + consola.info('Push agent'); + + // i18n + // execSync('bun run format'); + // consola.info('Generate i18n file'); + + // prettier + // execSync(`echo "module.exports = require('@lobehub/lint').prettier;" >> .prettierrc.cjs`); + // execSync('bun run prettier'); + // consola.info('Prettier'); + // + // // commit + // execSync('git add -A'); + // execSync( + // `git commit -m "🤖 chore(auto-submit): Generate i18n for ${agentName} (#${this.issueNumber})"`, + // ); + // execSync(`git push origin agent/${agentName}`); + // consola.info('Push i18n'); + } + + genCommentMessage(json) { + return [ + '🤖 Automatic generated agent config file', + '```json', + JSON.stringify(json, null, 2), + '```', + ].join('\n'); + } + + async createPullRequest(agentName, author, body) { + const { owner, repo, octokit } = this; + await octokit.pulls.create({ + base: 'main', + body, + head: `agent/${agentName}`, + owner: owner, + repo: repo, + title: `[AgentSubmit] ${agentName} @${author}`, + }); + } + async getIssue() { + const { owner, repo, octokit, issueNumber } = this; + const issue = await octokit.issues.get({ + issue_number: issueNumber, + owner, + repo, + }); + return issue.data; + } + + async addLabels(label) { + const { owner, repo, octokit, issueNumber } = this; + await octokit.issues.addLabels({ + issue_number: issueNumber, + labels: [label], + owner, + repo, + }); + } + + async removeLabels(label) { + const { owner, repo, octokit, issueNumber } = this; + const issue = await this.getIssue(); + + const baseLabels = issue.labels.map((l) => (typeof l === 'string' ? l : l.name)); + const removeLabels = baseLabels.filter((name) => name === label); + + for (const label of removeLabels) { + await octokit.issues.removeLabel({ + issue_number: issueNumber, + name: label, + owner, + repo, + }); + } + } + + async createComment(body) { + const { owner, repo, octokit, issueNumber } = this; + const { data } = await octokit.issues.createComment({ + body, + issue_number: issueNumber, + owner, + repo, + }); + return data.id; + } + + markdownToJson(markdown) { + const lines = markdown.split('\n'); + const json = {}; + + let currentKey = ''; + let currentValue = ''; + + for (const line of lines) { + if (checkHeader(line)) { + if (currentKey && currentValue) { + json[currentKey] = currentValue.trim(); + currentValue = ''; + } + currentKey = line.replace('###', '').trim(); + } else { + currentValue += line + '\n'; + } + } + + if (currentKey && currentValue) { + json[currentKey] = currentValue.trim(); + } + + // @ts-ignore + json.tags = json.tags.replaceAll(',', ',').replaceAll(', ', ',').split(',').filter(Boolean); + + return json; + } + + async formatIssue(data) { + const json = this.markdownToJson(data.body) as any; + const agent = { + author: data.user.login, + systemRole: json.systemRole, + homepage: data.user.html_url, + agentId: kebabCase(json.identifier), + meta: { + name: json.name, + avatar: json.avatar, + cover: json.cover, + description: json.description, + gender: json.gender, + model: json.modelUrl, + category: json.category, + }, + tts: JSON.parse(json.tts), + touch: JSON.parse(json.touch), + model: json.model, + params: JSON.parse(json.params), + }; + + return { agent: await formatAgentSchema(agent ) }; + } +} + +const autoSubmit = new AutoSubmit(); + +await autoSubmit.run(); diff --git a/scripts/const.ts b/scripts/const.ts index 92b0889..cd5e3bf 100644 --- a/scripts/const.ts +++ b/scripts/const.ts @@ -22,3 +22,6 @@ export const indexPath = resolve(publicDir, "index.json"); export const metaPath = resolve(root, "meta.json"); export const meta = readJSONSync(metaPath); + +export const githubHomepage = 'https://github.com/lobehub/lobe-vidol-market'; + diff --git a/scripts/schema/agent.ts b/scripts/schema/agent.ts index c49e671..5c3cc5d 100644 --- a/scripts/schema/agent.ts +++ b/scripts/schema/agent.ts @@ -54,10 +54,6 @@ export const MetaSchema = z.object({ * 角色性别 */ gender: GenderEnum, - /** - * 角色主页,比如 Vroid Hub 链接 - */ - homepage: z.string().optional(), /** * 模型地址 */ @@ -80,6 +76,14 @@ export const MetaSchema = z.object({ readme: z.string().optional(), }); +export const LLMParams = z.object({ + frequency_penalty: z.number().default(0), + max_tokens: z.number(), + presence_penalty: z.number().default(0), + temperature: z.number().default(0.6), + top_p: z.number().default(1), +}) + /** * Agent Schema */ @@ -116,6 +120,14 @@ export const VidolAgentSchema = z.object({ * 问候语,角色在每次聊天开始时说的第一句话 */ greeting: z.string(), + /** + * 模型 + */ + model: z.string().optional(), + /** + * 模型参数 + */ + params: LLMParams.optional(), /** * 创建时间 */ diff --git a/scripts/utils.ts b/scripts/utils.ts index 1d8ecdc..6a1cc72 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -11,3 +11,23 @@ export const checkDir = (dirpath) => { export const checkJSON = (file: Dirent) => file.isFile() && file.name?.endsWith(".json"); + + +export const checkHeader = (line: string) => { + const header = [ + '### systemRole', + '### agentId', + '### avatar', + '### cover', + '### name', + '### description', + '### model', + '### tags', + '### locale', + ]; + let check = false; + header.forEach((item) => { + if (line.startsWith(item)) check = true; + }); + return check; +}; \ No newline at end of file