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