commit e6402c600ba7537c85c8df06945f9617fe9d4090 Author: wehub-resource-sync Date: Mon Jul 13 13:01:53 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..d6af5e2 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "aitoearn-web", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["run", "dev"], + "port": 6060, + "cwd": "project/aitoearn-web" + }, + { + "name": "aitoearn-server", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["nx", "serve", "aitoearn-server"], + "port": 3002, + "cwd": "project/aitoearn-backend" + }, + { + "name": "aitoearn-ai", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["nx", "serve", "aitoearn-ai"], + "port": 3010, + "cwd": "project/aitoearn-backend" + } + ] +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..335f236 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,62 @@ +name: "🕷️ Bug report" +description: Report errors or unexpected behavior +labels: + - bug +body: + - type: checkboxes + attributes: + label: Self Checks + description: "Please review the following before submitting. We recommend completing as many items as possible." + options: + - label: I have read the [Contributing Guide](https://github.com/yikart/AiToEarn/blob/main/CONTRIBUTING.md). + required: false + - label: I have searched for existing issues [search for existing issues](https://github.com/yikart/AiToEarn/issues), including closed ones. + required: false + - label: I can provide the report in English if needed to help more contributors participate in the discussion. + required: false + - label: "Please do not modify this template :) and fill in all the required fields." + required: true + + - type: input + attributes: + label: Aitoearn version + description: + validations: + required: true + + - type: dropdown + attributes: + label: Please select your platform + description: + multiple: true + options: + - Android + - Windows + - Mac + - Web + validations: + required: true + + - type: textarea + attributes: + label: Steps to reproduce + description: We highly suggest including screenshots and a bug report log. Please use the right markdown syntax for code blocks. + placeholder: Having detailed steps helps us reproduce the bug. If you have logs, please use fenced code blocks (triple backticks ```) to format them. + validations: + required: true + + - type: textarea + attributes: + label: ✔️ Expected Behavior + description: Describe what you expected to happen. + placeholder: What were you expecting? Please do not copy and paste the steps to reproduce here. + validations: + required: true + + - type: textarea + attributes: + label: ❌ Actual Behavior + description: Describe what actually happened. + placeholder: What happened instead? Please do not copy and paste the steps to reproduce here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/draft.yml b/.github/ISSUE_TEMPLATE/draft.yml new file mode 100644 index 0000000..c9ad7b7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/draft.yml @@ -0,0 +1,40 @@ +name: General Issue +description: Report a bug, request a feature, or ask a question +title: "[Issue] " +labels: [] +body: + - type: markdown + attributes: + value: | + ### What happened? + (One short sentence) + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Describe the issue in one short sentence. + placeholder: Something happened... + validations: + required: true + + - type: checkboxes + id: type + attributes: + label: Type + options: + - label: Bug + - label: Idea + - label: Improvement + - label: Question + validations: + required: true + + - type: textarea + id: extra + attributes: + label: Extra (optional) + description: Screenshot / steps to reproduce / environment + placeholder: Attach screenshot / describe steps... + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..48d4a09 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,40 @@ +name: "⭐ Feature or enhancement request" +description: Propose something new. +labels: + - enhancement +body: + - type: checkboxes + attributes: + label: Self Checks + description: "Please review the following before submitting. We recommend completing as many items as possible." + options: + - label: I have read the [Contributing Guide](https://github.com/yikart/AiToEarn/blob/main/CONTRIBUTING.md). + required: false + - label: I have searched for existing issues [search for existing issues](https://github.com/yikart/AiToEarn/issues), including closed ones. + required: false + - label: I can provide the request in English if needed to help more contributors participate in the discussion. + required: false + - label: "Please do not modify this template :) and fill in all the required fields." + required: true + - type: textarea + attributes: + label: 1. Is this request related to a challenge you're experiencing? Tell me about your story. + placeholder: Please describe the specific scenario or problem you're facing as clearly as possible. For instance "I was trying to use [feature] for [specific task], and [what happened]... It was frustrating because...." + validations: + required: true + - type: textarea + attributes: + label: 2. Additional context or comments + placeholder: (Any other information, comments, documentations, links, or screenshots that would provide more clarity. This is the place to add anything else not covered above.) + validations: + required: false + - type: checkboxes + attributes: + label: 3. Can you help us with this feature? + description: Let us know! This is not a commitment, but a starting point for collaboration. + options: + - label: I am interested in contributing to this feature. + required: false + - type: markdown + attributes: + value: You may include related requests in one issue when they belong to the same use case or problem context. diff --git a/.github/ISSUE_TEMPLATE/refactor.yml b/.github/ISSUE_TEMPLATE/refactor.yml new file mode 100644 index 0000000..f902e54 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor.yml @@ -0,0 +1,39 @@ +name: "✨ Refactor" +description: Refactor existing code for improved readability and maintainability. +labels: + - refactor +body: + - type: checkboxes + attributes: + label: Self Checks + description: "Please review the following before submitting. We recommend completing as many items as possible." + options: + - label: I have read the [Contributing Guide](https://github.com/yikart/AiToEarn/blob/main/CONTRIBUTING.md). + required: false + - label: I have searched for existing issues [search for existing issues](https://github.com/yikart/AiToEarn/issues), including closed ones. + required: false + - label: I can provide the request in English if needed to help more contributors participate in the discussion. + required: false + - label: "Please do not modify this template :) and fill in all the required fields." + required: true + - type: textarea + id: description + attributes: + label: Description + placeholder: "Describe the refactor you are proposing." + validations: + required: true + - type: textarea + id: motivation + attributes: + label: Motivation + placeholder: "Explain why this refactor is necessary." + validations: + required: false + - type: textarea + id: additional-context + attributes: + label: Additional Context + placeholder: "Add any other context or screenshots about the request here." + validations: + required: false diff --git a/.github/languages.yml b/.github/languages.yml new file mode 100644 index 0000000..841c9fe --- /dev/null +++ b/.github/languages.yml @@ -0,0 +1,2 @@ +l10n: + default_locale: zh-CN diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..5ff2f58 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +# Pull Request + +## 🧩 Issue Link + + + +Closes # + +## 📝 Description + + + + diff --git a/.github/workflows/backen-check.yml b/.github/workflows/backen-check.yml new file mode 100644 index 0000000..4d0862b --- /dev/null +++ b/.github/workflows/backen-check.yml @@ -0,0 +1,73 @@ +name: AitoEarn Backend Check + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: read + +env: + NX_NO_CLOUD: true + MONOREPO_DIR: project/aitoearn-backend + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-and-build: + name: Lint & Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check if backend files changed + id: check-changes + run: | + if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -q "^project/aitoearn-backend/"; then + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "changed=false" >> $GITHUB_OUTPUT + fi + + - name: Skip if no changes + if: steps.check-changes.outputs.changed == 'false' + run: | + echo "No changes in project/aitoearn-backend/, skipping checks" + exit 0 + + - name: Install pnpm + if: steps.check-changes.outputs.changed == 'true' + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + if: steps.check-changes.outputs.changed == 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: ${{ env.MONOREPO_DIR }}/pnpm-lock.yaml + + - name: Install dependencies + if: steps.check-changes.outputs.changed == 'true' + working-directory: ${{ env.MONOREPO_DIR }} + run: pnpm install --frozen-lockfile + + - name: Run ESLint + if: steps.check-changes.outputs.changed == 'true' + working-directory: ${{ env.MONOREPO_DIR }} + run: pnpm nx run-many --target=lint --no-fix --nxBail --outputStyle=static + + - name: Run build + if: steps.check-changes.outputs.changed == 'true' + working-directory: ${{ env.MONOREPO_DIR }} + run: pnpm nx run-many --target=build --nxBail --outputStyle=static + diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml new file mode 100644 index 0000000..c84935d --- /dev/null +++ b/.github/workflows/backend-build.yml @@ -0,0 +1,352 @@ +name: Build Backend Images + +on: + push: + branches: + - main + paths: + - 'project/aitoearn-backend/**' + - '.github/workflows/backend-build.yml' + workflow_dispatch: + inputs: + build_aitoearn_ai: + description: 'aitoearn-ai' + required: false + type: boolean + default: false + build_aitoearn_server: + description: 'aitoearn-server' + required: false + type: boolean + default: false + push_latest: + description: 'Push as latest tag' + required: false + type: boolean + default: false + workflow_call: + inputs: + base_ref: + description: 'Base branch for affected detection (for PR preview)' + required: false + type: string + default: '' + manual_apps: + description: 'Comma-separated list of apps to build (for manual deployment)' + required: false + type: string + default: '' + outputs: + has_changes: + description: 'Whether any apps were affected' + value: ${{ jobs.generate-matrix.outputs.has_changes }} + +permissions: + contents: read + actions: write + +env: + DOCKER_HUB_USERNAME: aitoearn + NX_NO_CLOUD: true + MONOREPO_DIR: project/aitoearn-backend + +jobs: + generate-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + has_changes: ${{ steps.set-matrix.outputs.has_changes }} + version: ${{ steps.version.outputs.version }} + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: ${{ env.MONOREPO_DIR }}/pnpm-lock.yaml + + - name: Install dependencies + working-directory: ${{ env.MONOREPO_DIR }} + run: pnpm install --frozen-lockfile + + - name: Parse manual app selection + id: parse-manual + run: | + MANUAL_APPS="" + + # If manual_apps passed via workflow_call, use directly + if [ -n "${{ inputs.manual_apps }}" ]; then + MANUAL_APPS="${{ inputs.manual_apps }}" + echo "✅ Using workflow_call manual apps: $MANUAL_APPS" + else + # Parse workflow_dispatch boolean inputs + if [ "${{ inputs.build_aitoearn_ai }}" = "true" ]; then + MANUAL_APPS="aitoearn-ai" + fi + + if [ "${{ inputs.build_aitoearn_server }}" = "true" ]; then + if [ -n "$MANUAL_APPS" ]; then + MANUAL_APPS="$MANUAL_APPS,aitoearn-server" + else + MANUAL_APPS="aitoearn-server" + fi + fi + + if [ -n "$MANUAL_APPS" ]; then + echo "✅ Manually selected apps: $MANUAL_APPS" + else + echo "⚠️ No apps manually selected" + fi + fi + + echo "manual_apps=$MANUAL_APPS" >> $GITHUB_OUTPUT + + - name: Detect affected apps + id: detect-changes + working-directory: ${{ env.MONOREPO_DIR }} + run: | + MANUAL_APPS="${{ steps.parse-manual.outputs.manual_apps }}" + + # Check if manual mode + if [ -n "$MANUAL_APPS" ]; then + echo "🎯 Manual build mode: Using specified app list" + AFFECTED_APPS="$MANUAL_APPS" + echo "✅ Manually selected apps: $AFFECTED_APPS" + echo "affected_apps=$AFFECTED_APPS" >> $GITHUB_OUTPUT + + echo "" + echo "Details:" + for APP in $(echo "$AFFECTED_APPS" | tr ',' ' '); do + echo " - $APP" + done + else + # Set base branch based on trigger type + if [ -n "${{ inputs.base_ref }}" ]; then + BASE_REF="origin/${{ inputs.base_ref }}" + echo "📋 PR mode: Detecting changes from $BASE_REF to HEAD" + else + BASE_REF="HEAD~1" + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "📋 Manual trigger mode: Detecting changes from last commit" + else + echo "📋 Push mode: Detecting changes from last commit" + fi + fi + + echo "Base: $BASE_REF" + echo "Head: HEAD" + + echo "" + echo "📊 Diagnostic:" + git log --oneline -3 + echo "" + echo "Changed files:" + git diff --name-only $BASE_REF HEAD 2>/dev/null || true + echo "" + + NX_OUTPUT=$(pnpm nx show projects --affected --type=app --sep "," --exclude="@yikart/source" --base=$BASE_REF --head=HEAD 2>&1) + NX_EXIT_CODE=$? + + echo "Nx exit code: $NX_EXIT_CODE" + echo "Nx output: $NX_OUTPUT" + + if [ $NX_EXIT_CODE -ne 0 ]; then + echo "⚠️ Nx affected detection failed (exit code: $NX_EXIT_CODE)" + echo "Falling back to building ALL apps" + AFFECTED_APPS="aitoearn-ai,aitoearn-server" + else + AFFECTED_APPS="$NX_OUTPUT" + fi + + if [ -z "$AFFECTED_APPS" ]; then + echo "ℹ️ No affected apps detected" + echo "affected_apps=" >> $GITHUB_OUTPUT + else + echo "✅ Affected apps: $AFFECTED_APPS" + echo "affected_apps=$AFFECTED_APPS" >> $GITHUB_OUTPUT + + echo "" + echo "Details:" + for APP in $(echo "$AFFECTED_APPS" | tr ',' ' '); do + echo " - $APP" + done + fi + fi + + - name: Generate version + id: version + run: | + VERSION="$(date +%Y%m%d)-$(git rev-parse --short HEAD)" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "📦 Version: $VERSION" + + - name: Generate matrix + id: set-matrix + working-directory: ${{ env.MONOREPO_DIR }} + run: | + AFFECTED_APPS="${{ steps.detect-changes.outputs.affected_apps }}" + VERSION="${{ steps.version.outputs.version }}" + + if [ -z "$AFFECTED_APPS" ]; then + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "matrix={\"include\":[]}" >> $GITHUB_OUTPUT + echo "⚠️ No changes, skipping build" + exit 0 + fi + + echo "has_changes=true" >> $GITHUB_OUTPUT + + # Determine if should push latest tag + PUSH_LATEST=false + if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then + PUSH_LATEST=true + echo "📌 Main branch push: Will push latest tag" + elif [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.push_latest }}" = "true" ]; then + PUSH_LATEST=true + echo "📌 Manual trigger with push_latest: Will push latest tag" + fi + + echo "push_latest=$PUSH_LATEST" >> $GITHUB_OUTPUT + + # Build matrix JSON array + MATRIX_JSON="{" + MATRIX_JSON="$MATRIX_JSON\"include\":[" + + FIRST=true + IFS=',' read -ra APPS <<< "$AFFECTED_APPS" + + for APP in "${APPS[@]}"; do + IMAGE_TAG="${{ env.DOCKER_HUB_USERNAME }}/$APP:$VERSION" + + # Add to matrix + if [ "$FIRST" = true ]; then + FIRST=false + else + MATRIX_JSON="$MATRIX_JSON," + fi + + MATRIX_JSON="$MATRIX_JSON{\"app\":\"$APP\",\"image\":\"$IMAGE_TAG\",\"push_latest\":$PUSH_LATEST}" + done + + MATRIX_JSON="$MATRIX_JSON]}" + + echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT + + echo "" + echo "🎯 Matrix configuration:" + echo "$MATRIX_JSON" | jq . + + build-and-push: + needs: generate-matrix + if: needs.generate-matrix.outputs.has_changes == 'true' + runs-on: ubuntu-latest + strategy: + matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} + fail-fast: false + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: ${{ env.MONOREPO_DIR }}/pnpm-lock.yaml + + - name: Install dependencies + working-directory: ${{ env.MONOREPO_DIR }} + run: pnpm install --frozen-lockfile + + - name: Restore Nx cache + uses: actions/cache@v4 + with: + path: ${{ env.MONOREPO_DIR }}/.nx/cache + key: nx-${{ runner.os }}-${{ hashFiles('project/aitoearn-backend/pnpm-lock.yaml') }}-${{ matrix.app }}-${{ github.sha }} + restore-keys: | + nx-${{ runner.os }}-${{ hashFiles('project/aitoearn-backend/pnpm-lock.yaml') }}-${{ matrix.app }}- + nx-${{ runner.os }}-${{ hashFiles('project/aitoearn-backend/pnpm-lock.yaml') }}- + nx-${{ runner.os }}- + + - name: Build application + working-directory: ${{ env.MONOREPO_DIR }} + run: | + echo "🔨 Building app: ${{ matrix.app }}" + pnpm nx run ${{ matrix.app }}:build + + - name: Prepare Docker context + working-directory: ${{ env.MONOREPO_DIR }} + run: pnpm nx run ${{ matrix.app }}:docker-context + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: | + network=host + image=moby/buildkit:latest + buildkitd-flags: --debug + + - name: Prepare tags + id: tags + run: | + TAGS="${{ matrix.image }}" + if [ "${{ matrix.push_latest }}" = "true" ]; then + TAGS="$TAGS"$'\n'"${{ env.DOCKER_HUB_USERNAME }}/${{ matrix.app }}:latest" + fi + echo "tags<> $GITHUB_OUTPUT + echo "$TAGS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: ${{ env.MONOREPO_DIR }}/tmp/docker-context + file: ${{ env.MONOREPO_DIR }}/tmp/docker-context/Dockerfile + platforms: linux/amd64,linux/arm64 + build-args: | + APP_NAME=${{ matrix.app }} + tags: ${{ steps.tags.outputs.tags }} + push: true + cache-from: | + type=gha,scope=${{ matrix.app }} + type=registry,ref=${{ env.DOCKER_HUB_USERNAME }}/${{ matrix.app }}:buildcache + cache-to: | + type=gha,mode=max,scope=${{ matrix.app }} + type=inline + provenance: false + sbom: false + + - name: Build summary + run: | + echo "### ✅ Build successful: ${{ matrix.app }}" >> $GITHUB_STEP_SUMMARY + echo "- **Image**: \`${{ matrix.image }}\`" >> $GITHUB_STEP_SUMMARY + if [ "${{ matrix.push_latest }}" = "true" ]; then + echo "- **Latest**: \`${{ env.DOCKER_HUB_USERNAME }}/${{ matrix.app }}:latest\`" >> $GITHUB_STEP_SUMMARY + fi + diff --git a/.github/workflows/pr-issue-check.yml b/.github/workflows/pr-issue-check.yml new file mode 100644 index 0000000..197d771 --- /dev/null +++ b/.github/workflows/pr-issue-check.yml @@ -0,0 +1,54 @@ +name: PR Issue Check + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: read + issues: read + +jobs: + check-issue-linked: + name: Check Issue Linked + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check if PR is linked to an issue + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + let prBody = context.payload.pull_request.body || ''; + const prTitle = context.payload.pull_request.title || ''; + + const conventionalCommitsPattern = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|build)(\(.+\))?: .+$/; + const validTypes = ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'ci', 'build']; + + if (!conventionalCommitsPattern.test(prTitle)) { + const errorMessage = + '❌ PR title does not follow Conventional Commits format.\n\n' + + 'Required format: (): \n\n' + + 'Valid types:\n' + + validTypes.map(t => ` - ${t}`).join('\n') + '\n\n' + + 'Examples:\n' + + ' ✅ feat: add user authentication\n' + + ' ✅ fix(web): resolve login button issue\n' + + ' ✅ docs: update API documentation\n' + + ' ✅ refactor(backend): improve error handling\n' + + ' ✅ chore: update dependencies\n\n' + + 'Common mistakes:\n' + + ' ❌ "Add new feature" (missing type and colon)\n' + + ' ❌ "feat add feature" (missing colon)\n' + + ' ❌ "feat: " (missing description)\n' + + ' ❌ "feature: add something" (invalid type)\n\n' + + `Current title: "${prTitle}"\n\n` + + 'Please update your PR title to follow the Conventional Commits specification.'; + + core.setFailed(errorMessage); + return; + } \ No newline at end of file diff --git a/.github/workflows/pr-to-feishu.yml b/.github/workflows/pr-to-feishu.yml new file mode 100644 index 0000000..b32b3e7 --- /dev/null +++ b/.github/workflows/pr-to-feishu.yml @@ -0,0 +1,17 @@ +name: Feishu Notify (Org-wide, No Relay) +on: + pull_request: + types: [ready_for_review, closed] + issues: + types: [opened, closed, reopened] + +jobs: + notify: + uses: yikart/.github/.github/workflows/feishu-notify-template.yml@main + secrets: inherit + with: + important_labels: "urgent,security,release-blocker" + important_paths_regex: "^(apps/web/|infra/|payment/|security/)" + pr_min_changed_files: "30" + title_keywords_regex: "(hotfix|rollback|security|incident)" + notify_issue_on_open_close_without_labels: "false" diff --git a/.github/workflows/web-build.yml b/.github/workflows/web-build.yml new file mode 100644 index 0000000..af2db96 --- /dev/null +++ b/.github/workflows/web-build.yml @@ -0,0 +1,108 @@ +name: Build Web Image + +on: + push: + branches: + - main + paths: + - 'project/aitoearn-web/**' + - '.github/workflows/web-build.yml' + workflow_dispatch: + inputs: + version: + description: Image version (leave empty to use default generation rule) + required: false + type: string + push_latest: + description: 'Push as latest tag' + required: false + type: boolean + default: false + +env: + APP_NAME: aitoearn-web + MONOREPO_DIR: project/aitoearn-web + DOCKER_HUB_USERNAME: aitoearn + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Generate version + id: version + run: | + if [ -n "${{ github.event.inputs.version }}" ]; then + VERSION="${{ github.event.inputs.version }}" + else + VERSION=$(date +%Y%m%d)-$(git rev-parse --short HEAD) + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Determine push latest + id: push-latest + run: | + PUSH_LATEST=false + if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then + PUSH_LATEST=true + echo "📌 Main branch push: Will push latest tag" + elif [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.push_latest }}" = "true" ]; then + PUSH_LATEST=true + echo "📌 Manual trigger with push_latest: Will push latest tag" + fi + echo "push_latest=$PUSH_LATEST" >> $GITHUB_OUTPUT + + - name: Prepare tags + id: tags + run: | + TAGS="${{ env.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}:${{ steps.version.outputs.version }}" + if [ "${{ steps.push-latest.outputs.push_latest }}" = "true" ]; then + TAGS="$TAGS"$'\n'"${{ env.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}:latest" + fi + echo "tags<> $GITHUB_OUTPUT + echo "$TAGS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Build and push application + uses: docker/build-push-action@v5 + with: + context: ${{ env.MONOREPO_DIR }}/ + file: ${{ env.MONOREPO_DIR }}/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.tags.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Summary + if: always() + run: | + echo "## 📦 Build Summary" >> $GITHUB_STEP_SUMMARY + echo "- **App**: ${{ env.APP_NAME }}" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Platform**: linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🐋 Docker Hub Images" >> $GITHUB_STEP_SUMMARY + echo "- **Version Image**: \`${{ env.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}:${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.push-latest.outputs.push_latest }}" = "true" ]; then + echo "- **Latest**: \`${{ env.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}:latest\`" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔗 Quick Links" >> $GITHUB_STEP_SUMMARY + echo "- [Source Commit](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})" >> $GITHUB_STEP_SUMMARY + echo "- [Build Log](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/web-check.yml b/.github/workflows/web-check.yml new file mode 100644 index 0000000..87bc2fb --- /dev/null +++ b/.github/workflows/web-check.yml @@ -0,0 +1,73 @@ +name: AitoEarn Web Check + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: read + +env: + MONOREPO_DIR: project/aitoearn-web + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-and-build: + name: Lint & Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check if web files changed + id: check-changes + run: | + if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -q "^project/aitoearn-web/"; then + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "changed=false" >> $GITHUB_OUTPUT + fi + + - name: Skip if no changes + if: steps.check-changes.outputs.changed == 'false' + run: | + echo "No changes in project/aitoearn-web/, skipping checks" + exit 0 + + - name: Install pnpm + if: steps.check-changes.outputs.changed == 'true' + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + if: steps.check-changes.outputs.changed == 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: ${{ env.MONOREPO_DIR }}/pnpm-lock.yaml + + - name: Install dependencies + if: steps.check-changes.outputs.changed == 'true' + working-directory: ${{ env.MONOREPO_DIR }} + run: pnpm install --frozen-lockfile + +# - name: Run ESLint +# if: steps.check-changes.outputs.changed == 'true' +# working-directory: ${{ env.MONOREPO_DIR }} +# run: pnpm lint + + - name: Run build + if: steps.check-changes.outputs.changed == 'true' + working-directory: ${{ env.MONOREPO_DIR }} + run: pnpm build + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dfe997 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +dist-electron +release +*.local +.vscode +# Editor directories and files +.vscode/.debug.env +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +# .env + +#lockfile +# package-lock.json +# pnpm-lock.yaml +# yarn.lock +/test-results/ +/playwright-report/ +/playwright/.cache/ + +# 忽略所有.db文件 +*.db + +# 忽略所有.exe文件 +*.exe + +# 忽略/public/bin目录下的非.md文件 +public/bin/* +!public/bin/webkit +!public/bin/*.md + +sh/* +project/web/.cursor/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..12dfff8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ +# AGENTS.md + +本文件定义 Codex 在 `AiToEarn` 仓库内的默认工作规则。 + +## Communication + +- 默认使用简体中文回复。 + +## Project Layout + +- `project/aitoearn-backend` 是 Nx + pnpm 后端工作区。 +- `project/aitoearn-web` 是 Next.js + pnpm 前端项目。 +- 根目录主要维护 README、Docker 部署文档、`docker-compose.yml` 和展示资源。 + +## Package & Command Rules + +- backend/web 使用 `pnpm`。 +- 根目录没有统一 package,不要在根目录随手执行 install/build。 +- backend 改动优先在 `project/aitoearn-backend` 用 `pnpm nx ...` 验证,并遵循 `project/aitoearn-backend/CLAUDE.md`。 +- web 改动在 `project/aitoearn-web` 验证,优先使用 `pnpm run type-check` 和 `pnpm build`。 +- 纯文档改动至少运行 `git diff --check`。 + +## Documentation Rules + +- 根 README 对外文档包含 `README.md`、`README_EN.md`、`README_JA.md`;涉及用户可见能力、安装、OpenClaw、MCP、Relay、API Key 或环境地址时默认三语同步。 +- Docker 部署说明涉及生产部署、环境变量或 `docker compose` 时,同步检查 `DOCKER_DEPLOYMENT_CN.md` 和 `DOCKER_DEPLOYMENT_EN.md`。 +- README 类改动保持最小可用改写,不要把参考文档整段复制进来。 +- 用户可见 README、skill、capability reference 只写当前能力与环境规则,不写 `dev`、测试环境、验证日期等来源说明。 + +## Environment Rules + +- OpenClaw、MCP、Relay 都必须明确区分中国版和国际版环境:`*.aitoearn.cn` 属于中国版,`*.aitoearn.ai` 属于国际版。 +- 中国版 API Key 只能搭配 `aitoearn.cn` 相关 URL;国际版 API Key 只能搭配 `aitoearn.ai` 相关 URL。环境和 Key 不匹配会导致 401。 +- MCP 示例需要按环境区分 `https://aitoearn.cn/api/unified/mcp` / `https://aitoearn.ai/api/unified/mcp`,SSE 示例同理区分 `/api/unified/sse`。 +- Relay 示例需要按 `RELAY_API_KEY` 来源选择 `RELAY_SERVER_URL`:中国版使用 `https://aitoearn.cn/api`,国际版使用 `https://aitoearn.ai/api`。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3981cd1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,102 @@ +# CONTRIBUTING + +So you're looking to contribute to AiToEarn - that's awesome! We can't wait to see what you do. We have grand ambitions to build the best platform for AI-driven earning. Any help from the community counts, truly. + +We need to be nimble and ship fast, but we also want to make sure that contributors like you get as smooth an experience as possible. We've assembled this contribution guide for that purpose, aiming at getting you familiarized with the codebase & how we work with contributors, so you could quickly jump to the fun part. + +This guide is a constant work in progress. We highly appreciate your understanding if at times it lags behind the actual project, and welcome any feedback for us to improve. + +## Before you jump in + +Looking for something to tackle? Browse our [issues](https://github.com/AiToEarn/AiToEarn/issues) and pick one to get started! + +### Good first issue + +Issues with titles containing **【good frist】** are issues we provide for new contributors. If you want to join our team, please submit a PR linked to such an issue. + +Join us, contribute code, and let's build something awesome together! 💡✨ + +Don't forget to link an existing issue in the PR's description. + +### Bug reports + +> [!IMPORTANT] +> Please make sure to include the following information when submitting a bug report: + +- A clear and descriptive title +- A detailed description of the bug, including any error messages +- Steps to reproduce the bug +- Expected behavior +- **Logs**, if available (really important for backend issues) +- Screenshots or videos, if applicable + +How we prioritize: + +| Issue Type | Priority | +| ------------------------------------------------------------ | --------------- | +| Bugs in core functions (cannot login, applications not working, security loopholes) | Critical | +| Non-critical bugs, performance boosts | Medium Priority | +| Minor fixes (typos, confusing but working UI) | Low Priority | + +### Feature requests + +> [!NOTE] +> Please make sure to include the following information when submitting a feature request: + +- A clear and descriptive title +- A detailed description of the feature +- A use case for the feature +- Any other context or screenshots about the feature request + +How we prioritize: + +| Feature Type | Priority | +| ------------------------------------------------------------ | --------------- | +| High-Priority Features as being labeled by a team member | High Priority | +| Popular feature requests from our community | Medium Priority | +| Non-core features and minor enhancements | Low Priority | +| Valuable but not immediate | Future-Feature | + +## Development Environment Setup + +1. Fork the project to your own GitHub account +2. Create a feature branch: + ```bash + git checkout -b feature/your-feature-name + ``` +3. Commit your changes: + ```bash + git add . + git commit -m "feat: add new feature" + ``` +4. Push to your forked repository: + ```bash + git push origin feature/your-feature-name + ``` +5. Create a Pull Request on GitHub + +## Submitting your PR + +### Pull Request Process + +1. Fork the repository +2. Before you draft a PR, please create an issue to discuss the changes you want to make +3. Create a new branch for your changes +4. Please add tests for your changes accordingly +5. Ensure your code passes the existing tests +6. Please link the issue in the PR description, `fixes #` +7. Get merged! + +### Setup the project + +#### Backend + +For setting up the backend service, kindly refer to our detailed instructions in [project/backend/DEVELOPER_GUIDE.md](./project/backend/DEVELOPER_GUIDE.md). This document contains step-by-step guidance to help you get the backend up and running smoothly. + +#### Other things to note + +We recommend reviewing these documents carefully before proceeding with the setup. + +## Getting Help + +If you ever get stuck or get a burning question while contributing, simply shoot your queries our way via the related GitHub issue. diff --git a/CONTRIBUTING_CN.md b/CONTRIBUTING_CN.md new file mode 100644 index 0000000..dc62980 --- /dev/null +++ b/CONTRIBUTING_CN.md @@ -0,0 +1,102 @@ +# 贡献指南 + +很高兴你有兴趣为 AiToEarn 做出贡献——这太棒了!我们迫不及待想看到你的成果。我们致力于构建最佳的 AI 赚钱平台。来自社区的任何帮助都非常宝贵。 + +我们需要快速迭代,但也希望确保像你一样的贡献者能获得尽可能顺畅的体验。我们编写这份贡献指南正是为了这个目的,旨在让你熟悉代码库以及我们与贡献者的合作方式,以便你可以快速进入正题。 + +本指南是一个持续完善的过程。如果有时它滞后于实际项目,我们非常感谢你的理解,并欢迎提出任何改进反馈。 + +## 在开始之前 + +正在寻找可以解决的问题?浏览我们的 [issues](https://github.com/AiToEarn/AiToEarn/issues) 并挑选一个开始吧! + +### 友好的第一次开始 + +标题包含 **【good frist】** 的 issues,是我们为新的贡献者提供的issues。如果想加入我们的团队,请提交关联该 issues 的 PR。 + +加入我们,通过贡献代码,让我们一起构建一些很棒的东西!💡✨ + +别忘了在 PR 描述中链接现有的 issue + +### 错误报告 (Bug Reports) + +> [!IMPORTANT] +> 提交错误报告时,请务必包含以下信息: + +- 清晰且描述性的标题 +- 错误的详细描述,包括任何错误消息 +- 复现错误的步骤 +- 预期行为 +- **日志**(如果有),对于后端问题,这非常重要 +- 截图或视频(如果适用) + +我们的优先级排序方式: + +| 问题类型 | 优先级 | +| ------------------------------------------------------------ | --------------- | +| 核心功能错误(无法登录、应用无法工作、安全漏洞) | 严重 (Critical) | +| 非严重错误、性能提升 | 中等优先级 (Medium Priority) | +| 小修补(拼写错误、UI 令人困惑但可工作) | 低优先级 (Low Priority) | + +### 功能请求 (Feature Requests) + +> [!NOTE] +> 提交功能请求时,请务必包含以下信息: + +- 清晰且描述性的标题 +- 功能的详细描述 +- 功能的使用场景 +- 关于功能请求的任何其他背景信息或截图 + +我们的优先级排序方式: + +| 功能类型 | 优先级 | +| ------------------------------------------------------------ | --------------- | +| 团队成员标记为高优先级的功能 | 高优先级 (High Priority) | +| 社区反馈中受欢迎的功能请求 | 中等优先级 (Medium Priority) | +| 非核心功能和小幅增强 | 低优先级 (Low Priority) | +| 有价值但不紧急 | 未来特性 (Future-Feature) | + +## 开发环境设置 + +1. Fork 项目到你的 GitHub 账号 +2. 创建特性分支: + ```bash + git checkout -b feature/your-feature-name + ``` +3. 提交你的更改: + ```bash + git add . + git commit -m "feat: add new feature" + ``` +4. 推送到你的 Fork 仓库: + ```bash + git push origin feature/your-feature-name + ``` +5. 在 GitHub 上创建 Pull Request + +## 提交你的 PR + +### Pull Request 流程 + +1. Fork 本仓库 +2. 在起草 PR 之前,请创建一个 issue 来讨论你想要进行的更改 +3. 为你的更改创建一个新分支 +4. 请相应地为你的更改添加测试 +5. 确保你的代码通过现有的测试 +6. 请在 PR 描述中链接该 issue,例如 `fixes #` +7. 等待合并! + +### 项目设置 + +#### 后端 + +关于设置后端服务,请参考 [project/backend/README.md](./project/backend/README.md) 中的详细说明。该文档包含逐步指导,帮助你顺利运行后端。 + +#### 其他注意事项 + +我们建议在继续设置之前仔细阅读这些文档。 + +## 获取帮助 + +如果你在贡献过程中遇到困难或有紧急问题,只需通过相关的 GitHub issue 向我们提问即可。 diff --git a/DOCKER_DEPLOYMENT_CN.md b/DOCKER_DEPLOYMENT_CN.md new file mode 100644 index 0000000..b4f38e1 --- /dev/null +++ b/DOCKER_DEPLOYMENT_CN.md @@ -0,0 +1,137 @@ +# AiToEarn Docker 部署指南 + +本指南帮助你使用 Docker Compose 快速部署完整的 AiToEarn 应用。 + +## 服务架构 + +``` + ┌──────────┐ + │ Nginx │ + │ :8080 │ + └────┬─────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ┌─────┴─────┐ ┌─────┴──────┐ ┌─────┴─────┐ + │ Web (FE) │ │ Server │ │ AI │ + │ :3000 │ │ :3002 │ │ :3010 │ + └────────────┘ └──────┬─────┘ └─────┬─────┘ + │ │ + ┌────────────┼──────────────┤ + │ │ │ + ┌────┴─────┐ ┌───┴────┐ ┌──────┴───┐ + │ MongoDB │ │ Redis │ │ RustFS │ + │ :27017 │ │ :6379 │ │ :9000/01 │ + └──────────┘ └────────┘ └──────────┘ +``` + +| 服务 | 说明 | 端口 | +|------|------|------| +| **Nginx** | 反向代理,统一入口 | 8080 (对外) | +| **aitoearn-web** | Next.js 前端 | 3000 (内部) | +| **aitoearn-server** | NestJS 主后端 API | 3002 (内部) | +| **aitoearn-ai** | NestJS AI 服务 | 3010 (内部) | +| **MongoDB** | 数据库 | 27017 | +| **Redis** | 缓存/队列 | 6379 | +| **RustFS** | S3 兼容对象存储 | 9000 (API) / 9001 (控制台) | + +## 前置要求 + +- **Docker**: 20.10+ +- **Docker Compose**: 2.0+ +- **系统内存**: 建议 4GB+ +- **磁盘空间**: 建议 20GB+ + +验证安装: + +```bash +docker --version +docker compose version +``` + +--- + +## 🚀 3 分钟快速启动 + +只需 3 步,即可在本地跑起完整的 AiToEarn。 + +### 第 1 步:克隆并启动 + +```bash +git clone https://github.com/yikart/AiToEarn.git +cd AiToEarn +docker compose up -d +``` + +首次启动会拉取镜像,可能需要几分钟。运行 `docker compose ps` 确认所有服务为 `healthy` 或 `running`。 + +### 第 2 步:打开界面 + +启动成功后,打开浏览器访问:**[http://localhost:8080](http://localhost:8080)** + +> 首次启动会自动创建管理员账号并自动登录,无需手动注册。 + +### 第 3 步:配置 Relay 中继(强烈推荐) + +> **为什么要配 Relay?** +> +> AiToEarn 需要登录你的社交媒体账号(抖音、小红书、TikTok 等)才能发布内容。这些平台要求 OAuth 开发者凭据才能授权登录。 +> +> - **不配 Relay**:你需要自己去十几个平台逐一申请开发者账号,获取 client_id/secret,非常麻烦。 +> - **配了 Relay**:借用官方 aitoearn.ai 的凭据完成授权,**一个 API Key 搞定所有平台**。 + +**配置方法**: + +1. 在 [aitoearn.ai](https://aitoearn.ai)(国际)或 [aitoearn.cn](https://aitoearn.cn)(中国)注册登录,进入 **设置 → API Key**,创建一个 API Key +2. 在浏览器打开部署后的界面,进入 **配置管理** +3. 选择 **Server → Relay 中转**,配置发布平台授权使用的 Relay +4. 如需使用平台提供的 AI 模型,选择 **AI → Relay 中转** 并配置 AI Relay +5. OpenAI、Gemini、Anthropic 等模型服务商也可以在 **AI → 模型服务商** 中填写平台提供的 API Key 和 API 地址 +6. 保存后点击 **保存并重启**,让对应服务重新加载配置 + +中国版 Key 搭配 `https://aitoearn.cn/api`,国际版 Key 搭配 `https://aitoearn.ai/api`;环境和 Key 不匹配会导致 401。 + +**到这里,你已经可以正常使用 AiToEarn 了!** 🎉 + +## 运维参考 + +### 自动登录 + +自动登录默认已启用。首次启动时,`aitoearn-init` 服务会生成管理员 token 并保存到共享卷中,`aitoearn-web` 服务自动读取该 token 完成登录。 + +### 镜像拉取策略 + +所有应用服务镜像使用 `pull_policy: always`,确保每次 `docker compose up` 都拉取最新镜像。 + +### 内部服务通信 + +以下配置用于服务间通信,使用 Docker 内部网络,通常无需修改: + +| 配置项 | 所属服务 | 默认值 | +|------|----------|--------| +| `serverClient.baseUrl` | aitoearn-ai | `http://aitoearn-server:3002` | +| `aiClient.baseUrl` | aitoearn-server | `http://aitoearn-ai:3010` | + +### 配置文件 + +配置文件以可写卷挂载到容器中,可通过配置管理界面修改,保存后需重启对应服务: + +| 文件 | 挂载到 | 说明 | +|------|--------|------| +| `project/aitoearn-backend/apps/aitoearn-ai/config/config.yaml` | aitoearn-ai:/app/config.yaml | AI 服务配置 | +| `project/aitoearn-backend/apps/aitoearn-server/config/config.yaml` | aitoearn-server:/app/config.yaml | 后端配置 | + +--- + +## 配置速查表 + +`aitoearn-ai` 和 `aitoearn-server` 不再通过 `docker-compose.yml` 的 `environment` 配置运行参数,统一读取挂载的 `config.yaml`,并通过配置管理界面修改。 + +### Compose 中仍保留的环境变量 + +| 变量 | 所属服务 | 说明 | 默认值 | +|------|----------|------|--------| +| `MONGO_INITDB_ROOT_PASSWORD` | mongodb | MongoDB root 密码 | `password` | +| `RUSTFS_ACCESS_KEY` / `RUSTFS_SECRET_KEY` | rustfs | RustFS 访问凭证 | `rustfsadmin` | +| `MONGO_URI` / `JWT_SECRET` / `DB_NAME` / `AUTO_LOGIN_TOKEN_PATH` | aitoearn-init | 首次启动初始化管理员与自动登录 token | 内置默认值 | +| `NODE_ENV` / `NEXT_TELEMETRY_DISABLED` | aitoearn-web | Web 运行环境与 Next.js telemetry 设置 | `production` / `1` | diff --git a/DOCKER_DEPLOYMENT_EN.md b/DOCKER_DEPLOYMENT_EN.md new file mode 100644 index 0000000..29e6939 --- /dev/null +++ b/DOCKER_DEPLOYMENT_EN.md @@ -0,0 +1,137 @@ +# AiToEarn Docker Deployment Guide + +This guide helps you quickly deploy the complete AiToEarn application using Docker Compose. + +## Architecture + +``` + ┌──────────┐ + │ Nginx │ + │ :8080 │ + └────┬─────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ┌─────┴─────┐ ┌─────┴──────┐ ┌─────┴─────┐ + │ Web (FE) │ │ Server │ │ AI │ + │ :3000 │ │ :3002 │ │ :3010 │ + └────────────┘ └──────┬─────┘ └─────┬─────┘ + │ │ + ┌────────────┼──────────────┤ + │ │ │ + ┌────┴─────┐ ┌───┴────┐ ┌──────┴───┐ + │ MongoDB │ │ Redis │ │ RustFS │ + │ :27017 │ │ :6379 │ │ :9000/01 │ + └──────────┘ └────────┘ └──────────┘ +``` + +| Service | Description | Port | +|---------|-------------|------| +| **Nginx** | Reverse proxy, unified entry | 8080 (public) | +| **aitoearn-web** | Next.js frontend | 3000 (internal) | +| **aitoearn-server** | NestJS main backend API | 3002 (internal) | +| **aitoearn-ai** | NestJS AI service | 3010 (internal) | +| **MongoDB** | Database | 27017 | +| **Redis** | Cache / Queue | 6379 | +| **RustFS** | S3-compatible object storage | 9000 (API) / 9001 (Console) | + +## Prerequisites + +- **Docker**: 20.10+ +- **Docker Compose**: 2.0+ +- **System RAM**: 4GB+ recommended +- **Disk Space**: 20GB+ recommended + +Verify installation: + +```bash +docker --version +docker compose version +``` + +--- + +## 🚀 Get Running in 3 Minutes + +Just 3 steps to run the complete AiToEarn on your machine. + +### Step 1: Clone and Start + +```bash +git clone https://github.com/yikart/AiToEarn.git +cd AiToEarn +docker compose up -d +``` + +First startup pulls images — may take a few minutes. Run `docker compose ps` to confirm all services are `healthy` or `running`. + +### Step 2: Open the UI + +Visit: **[http://localhost:8080](http://localhost:8080)** + +> First startup auto-creates an admin account and logs you in automatically. + +### Step 3: Configure Relay (Strongly Recommended) + +> **Why configure Relay?** +> +> AiToEarn needs to log into your social media accounts (TikTok, Instagram, YouTube, etc.) to publish content. These platforms require OAuth developer credentials for authorization. +> +> - **Without Relay**: You'd need to register as a developer on each platform and obtain client_id/secret — extremely tedious. +> - **With Relay**: Use the official aitoearn.ai credentials to authorize all platforms with **just one API Key**. + +**How to configure**: + +1. Sign up at [aitoearn.ai](https://aitoearn.ai) (international) or [aitoearn.cn](https://aitoearn.cn) (China), go to **Settings → API Key**, and create an API Key +2. Open the deployed UI in your browser and go to **Configuration** +3. Select **Server → Relay** and configure Relay for publishing platform authorization +4. To use AI models provided by the platform, select **AI → Relay** and configure AI Relay +5. For OpenAI, Gemini, Anthropic, and other model providers, you can also fill in the platform-provided API key and API URL under **AI → Model providers** +6. After saving, click **Save and restart** so the corresponding service reloads the configuration + +China keys must use `https://aitoearn.cn/api`, and international keys must use `https://aitoearn.ai/api`; mismatched environments return 401. + +**You're all set!** 🎉 + +## Operations Reference + +### Auto-Login + +Enabled by default. On first startup, `aitoearn-init` generates an admin token saved to a shared volume. `aitoearn-web` reads it automatically. + +### Image Pull Policy + +All app images use `pull_policy: always` to pull the latest on every `docker compose up`. + +### Internal Service Communication + +These settings handle inter-service communication via Docker networking. Usually no changes needed: + +| Setting | Service | Default | +|----------|---------|---------| +| `serverClient.baseUrl` | aitoearn-ai | `http://aitoearn-server:3002` | +| `aiClient.baseUrl` | aitoearn-server | `http://aitoearn-ai:3010` | + +### Config Files + +Mounted as writable volumes and editable from the Configuration UI. Restart the corresponding service after changes: + +| File | Mounted to | Description | +|------|------------|-------------| +| `project/aitoearn-backend/apps/aitoearn-ai/config/config.yaml` | aitoearn-ai:/app/config.yaml | AI service config | +| `project/aitoearn-backend/apps/aitoearn-server/config/config.yaml` | aitoearn-server:/app/config.yaml | Backend config | + +--- + +## Configuration Quick Reference + +`aitoearn-ai` and `aitoearn-server` no longer use `docker-compose.yml` `environment` entries for runtime settings. They read the mounted `config.yaml`, which you can edit from the Configuration UI. + +### Environment Variables Still Kept in Compose + +| Variable | Service(s) | Description | Default | +|----------|------------|-------------|---------| +| `MONGO_INITDB_ROOT_PASSWORD` | mongodb | MongoDB root password | `password` | +| `RUSTFS_ACCESS_KEY` / `RUSTFS_SECRET_KEY` | rustfs | RustFS credentials | `rustfsadmin` | +| `MONGO_URI` / `JWT_SECRET` / `DB_NAME` / `AUTO_LOGIN_TOKEN_PATH` | aitoearn-init | First-start admin initialization and auto-login token | Built-in defaults | +| `NODE_ENV` / `NEXT_TELEMETRY_DISABLED` | aitoearn-web | Web runtime mode and Next.js telemetry setting | `production` / `1` | diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..63cbe86 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AiToEarn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..98d519b --- /dev/null +++ b/README.md @@ -0,0 +1,353 @@ +# [Aitoearn:#1 AI内容营销智能体](https://aitoearn.ai) + +yikart%2FAiToEarn | Trendshift + +[![GitHub stars](https://img.shields.io/github/stars/yikart/AiToEarn?color=fa6470)](https://github.com/yikart/AiToEarn/stargazers) +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Required Node.JS 20.18.x](https://img.shields.io/static/v1?label=node&message=20.18.x&logo=node.js&color=3f893e)](https://nodejs.org/about/releases) + +简体中文 | [English](README_EN.md) | [日本語](README_JA.md) + +**Monetize · Publish · Engage · Create —— 一站式平台。** + +AiToEarn 通过 **AI Agent自动化**,帮助 OPC(一人公司)、创作者、品牌与企业在全球主流平台上构建、分发并变现内容。 + +支持渠道: +抖音、小红书(Rednote)、快手、哔哩哔哩、视频号、TikTok、YouTube、Facebook、Instagram、Threads、Twitter(X)、Pinterest、LinkedIn + +## 🚀 快速使用 AiToEarn(5 种方式) + +| 方式 | 适合谁 | 需要部署吗 | +|------|--------|-----------| +| [① 打开网站直接用](#use-web) | 所有用户 | ❌ 不需要 | +| [② 在龙虾 OpenClaw 中用](#use-in-openclaw) | 龙虾用户 | ❌ 不需要 | +| [③ 在 Claude / Cursor 等 AI 助手中用](#use-in-claude) | AI 工具用户 | ❌ 不需要 | +| [④ Docker 一键部署](#use-docker) | 想私有化部署的团队 | ✅ 需要服务器 | +| [⑤ 源码开发](#use-source) | 开发者 | ✅ 需要开发环境 | + +> 💡 **方式 ②③④ 都需要先获取 API Key**,请先看 [如何获取 API Key](#get-api-key)。 + +## 最新动态 + +- **2026-06-23**: [2.5 version](https://github.com/yikart/AiToEarn/releases/tag/v2.5.0) — Relay 配置改为在配置管理界面中操作,并拆分为 Server Relay 与 AI Relay:Server Relay 用于发布平台授权,AI Relay 用于使用平台提供的 AI 模型,新增[开放平台](https://docs.aitoearn.cn/)。 +- **2026-05-21**: [2.4 version](https://github.com/yikart/AiToEarn/releases/tag/v2.4.0) — 草稿生成新增支持 HappyHorse 1.0 和 Seedance 2.0,增强视频/图文草稿批量生成、多模型选择、参考图片/视频、目标平台限制与文案提示词;带来全新界面风格,并增强 Twitter/X 探索与互动能力。 +- **2026-04-20**: OpenClaw(龙虾)新增 AiToEarn 赚钱支持,可在龙虾中直接接收并执行内容变现任务。 +- **2026-03-26**: [2.1 version](https://www.aitoearn.ai/) — 内容交易市场上线;新增 OpenClaw(龙虾)支持,可在龙虾中直接使用 AiToEarn;新增 MCP 协议支持,可在 Claude、Cursor 等任何支持 MCP 的 Agent 或大模型中使用 AiToEarn。 +- **2026-02-07**: [1.8.0 version](https://www.aitoearn.ai/),新增线下商户推广解决方案,支持餐厅、零售店、民宿、美容美发、健身房等多种线下业态,将线下推广活动转化为可执行的线上传播任务,通过内容发布与用户参与机制,帮助门店获取更多线上曝光和到店流量。 +- **2025-12-15**: "All In Agent" 的开始!我们加入了能够自动内容生成和发布以及一些帮助你操作 Aitoearn 的超级 AI 智能 Agent。[v1.4.3](https://github.com/yikart/AiToEarn/releases/tag/v1.4.3) +- **2025-11-28**: 支持应用内自动更新。在创作界面新增大量 AI 功能,例如:缩写、扩写、图片生成、视频生成、标签生成等,并支持 Nano Banana Pro。[v1.4.0](https://github.com/yikart/AiToEarn/releases/tag/v1.4.0) +- **2025-11-12**: 首个开源且可完全使用的版本。[v1.3.2](https://github.com/yikart/AiToEarn/releases/tag/v1.3.2) +- **2025-09-16**: 首个出海版本,新增支持 Facebook、Instagram、Threads、Twitter、YouTube、TikTok、Pinterest。[v1.0.18](https://github.com/yikart/AiToEarn/releases/tag/v1.0.18) +- **2025-02-26**: 首个开源版本,初步实现小红书、抖音、快手、视频号视频一键发布。[v0.1.1](https://github.com/yikart/AiToEarn/releases/tag/v0.1.1) + +
+

目录

+ +
+ + 1. [快速使用 AiToEarn(5 种方式)](#-快速使用-aitoearn5-种方式) + 2. [最新动态](#最新动态) + 3. [核心功能](#核心功能) + 4. [如何获取 API Key](#get-api-key) + 5. [贡献指南](#贡献指南) + 6. [联系](#联系) + 7. [推荐](#推荐) +
+ +## 核心功能 + +AiToEarn 围绕内容创作者的完整变现链路,提供四大 Agent 能力: + +> **Monetize · Publish · Engage · Create** + +--- + +### 💰 Monetize —— 内容赚钱 + +AiToEarn 最核心的目标:**帮助每一位创作者赚钱**。 + +创作者可以在平台出售内容以完成商家的推广任务。所有结算以结果为导向,我们提供三种结算模式: + +| 结算模式 | 全称 | 含义 | +|---------|------|------| +| **CPS** | Cost Per Sale | 按成交额结算 | +| **CPE** | Cost Per Engagement | 按互动量结算 | +| **CPM** | Cost Per Mille | 按播放量结算 | + + + +--- + +### 📢 Publish —— 内容发布 Agent + +一键将内容分发到全球 10+ 主流平台,告别逐个平台手动发布。 + +- **全网分发**:覆盖抖音、快手、B站、小红书、视频号、微信公众号、TikTok、YouTube、Facebook、Instagram、Threads、X(Twitter)、Pinterest、LinkedIn +- **日历排期**:像排日程一样统一规划所有平台的内容发布时间 + + + +> ▶ 观看演示视频 + + + Publish 演示视频 + + +--- + +### 💬 Engage —— 内容互动 Agent + +通过 AiToEarn 浏览器插件,在上述所有平台上实现自动化互动运营。 + +- **自动化操作**:自动点赞、收藏、关注,批量高效运营 +- **AI 智能回复**:调用大模型为每条评论生成针对性回复,精准互动 +- **评论挖掘**:识别"求链接""怎么购买"等高转化信号,快速响应 +- **品牌监测**:实时追踪关于你品牌的讨论,主动参与热点话题 + +> ▶ 观看演示视频 + + + Engage 演示视频 + + +--- + +### 🎨 Create —— 内容创作 Agent + +我们用 Agent 的方式重构了内容制作流程。只需告诉 Agent 你的内容需求,它会自动完成从创意到成品的全部工作。 + +**视频内容**:Agent 自动调用视频生成模型(Grok、Veo、Seedance 等)、视频翻译模块、视频剪辑模块,一站式完成视频制作。 + +**图文内容**:支持调用 Nano Banana 等顶级图片模型,自动生成高质量图文内容。 + +**批量生成**:支持批量下发创作任务,Agent 可并行生成多条内容,快速铺量,适合矩阵账号运营和大规模内容分发场景。 + +> ▶ 观看演示视频 + + + Create 演示视频 + + +--- + +

① 打开网站直接用

+ +最简单的方式,打开浏览器即可使用,无需任何配置: + +- 🇨🇳 中国用户访问:**[aitoearn.cn](https://aitoearn.cn/)** +- 🌍 国际用户访问:**[aitoearn.ai](https://aitoearn.ai/)** + +--- + +

🔑 如何获取 API Key(后续步骤的前置条件)

+ +> 以下的方式 ②③④ 都需要 API Key。只需获取一次,所有方式通用。 + +**3 步获取**: + +1. 打开 [aitoearn.cn](https://aitoearn.cn/)(中国用户)或 [aitoearn.ai](https://aitoearn.ai/)(国际用户),注册并登录 +2. 点击左侧菜单 **设置** +3. 在 **API Key** 中点击创建,复制生成的 Key + +获取 API Key + +--- + +

② 在龙虾 OpenClaw 中使用

+ +> 前置条件:已 [获取 API Key](#get-api-key) + + +**请在服务器终端输入以下命令!请在服务器终端输入以下命令!请在服务器终端输入以下命令!** + +**安装插件** + +```bash +npx -y @aitoearn/openclaw-plugin-cli +``` + +首次运行后会先让你选择环境并输入 API Key。请确保环境与 Key 匹配: + +- 中国版:使用 `aitoearn.cn` 获取的 API Key +- 国际版:使用 `aitoearn.ai` 获取的 API Key + +环境和 Key 不匹配会导致 401。 + +安装完成后,你就可以在 OpenClaw 中直接接收并执行 AiToEarn 的赚钱任务: + +在 OpenClaw 中执行 AiToEarn 赚钱任务 + +--- + +

③ 在 Claude / Cursor / 其他 AI 助手中使用

+ +> 前置条件:已 [获取 API Key](#get-api-key) + +AiToEarn 支持所有兼容 MCP 协议的 AI 助手。以下是常见工具的配置方式: + +请根据 API Key 来源选择地址,环境和 Key 不匹配会导致 401: + +| 环境 | MCP 地址 | SSE 地址 | +|------|---------|---------| +| 中国版 | `https://aitoearn.cn/api/unified/mcp` | `https://aitoearn.cn/api/unified/sse` | +| 国际版 | `https://aitoearn.ai/api/unified/mcp` | `https://aitoearn.ai/api/unified/sse` | + +
+Claude Desktop + +找到并编辑 `claude_desktop_config.json`,添加: + +```json +{ + "mcpServers": { + "aitoearn": { + "type": "http", + "url": "https://aitoearn.ai/api/unified/mcp", + "headers": { + "x-api-key": "你的API-Key" + } + } + } +} +``` + +
+ +
+Cursor + +在 Cursor 的 MCP 设置中添加: + +``` +MCP 地址:https://aitoearn.ai/api/unified/mcp +认证 Header:x-api-key: 你的API-Key +``` + +
+ +
+其他 AI 助手(通用配置) + +任何支持 MCP 协议的工具,只需要两个信息: + +| 配置项 | 值 | +|--------|------| +| **MCP 地址** | `https://aitoearn.ai/api/unified/mcp` | +| **认证 Header** | `x-api-key: 你的API-Key` | + +也支持 SSE 长连接方式:`https://aitoearn.ai/api/unified/sse` + +
+ +> 💡 如果你是自部署用户,将 `aitoearn.ai` 替换为你自己的地址(如 `localhost:8080`)。 + +--- + +

④ Docker 一键部署

+ +> 前置条件:已安装 [Docker](https://docs.docker.com/get-docker/) + +适合想把 AiToEarn 部署在自己服务器上的团队。3 条命令搞定,无需手动安装数据库: + +```bash +git clone https://github.com/yikart/AiToEarn.git +cd AiToEarn +docker compose up -d +``` + +启动后打开 **[http://localhost:8080](http://localhost:8080)** 即可使用。 + +#### 配置 Relay(强烈推荐) + +> **为什么要配 Relay?** 发布内容需要登录社交媒体账号(抖音、小红书、TikTok 等),而这些平台的 OAuth 登录需要开发者凭据。配置 Relay 后,你可以直接借用官方 aitoearn.ai 的凭据完成授权,**不需要自己去各平台申请开发者账号**。 + +在浏览器打开部署后的界面,进入 **配置管理**,按需分别配置: + +- **Server → Relay 中转**:用于内容发布和社交平台 OAuth 授权。 +- **AI → Relay 中转**:用于使用平台提供的 AI 模型。 + +OpenAI、Gemini、Anthropic 等模型服务商也可以在 **AI → 模型服务商** 中填写平台提供的 API Key 和 API 地址。 + +API Key 获取方式见 [上方说明](#get-api-key)。中国版 Key 搭配 `https://aitoearn.cn/api`,国际版 Key 搭配 `https://aitoearn.ai/api`;环境和 Key 不匹配会导致 401。 + +保存后点击 **保存并重启**,让对应服务重新加载配置。 + +> 📖 完整部署指南(生产环境配置、AI 服务、OAuth、存储等)请参阅 [DOCKER_DEPLOYMENT_CN.md](DOCKER_DEPLOYMENT_CN.md)。 + +--- + +

⑤ 源码开发

+ +
+🧪 手动运行后端和前端(开发模式) + +此模式主要用于本地开发和调试。 +您仍然可以使用 Docker 运行 MongoDB/Redis,或在配置文件中指向您自己的服务。 + +#### 1. 启动后端服务 + +```bash +cd project/aitoearn-backend +pnpm install +# 复制配置文件用于本地开发 +cp apps/aitoearn-ai/config/config.yaml apps/aitoearn-ai/config/local.config.yaml +cp apps/aitoearn-server/config/config.yaml apps/aitoearn-server/config/local.config.yaml +pnpm nx serve aitoearn-ai +# 在另一个终端 +pnpm nx serve aitoearn-server +``` + +#### 2. 启动前端 `aitoearn-web` + +```bash +pnpm install +pnpm run dev +``` + +
+ +
+🖥️ 启动 Electron 桌面项目 + +```bash +# 克隆仓库 +git clone https://github.com/yikart/AttAiToEarn.git + +# 进入目录 +cd AttAiToEarn + +# 安装依赖 +npm install + +# 编译 sqlite(better-sqlite3 需要 node-gyp 和本地 Python) +npm run rebuild + +# 启动开发 +npm run dev +``` + +Electron 项目为 AiToEarn 提供桌面客户端。 + +
+ +## 贡献指南 + +请查看 [贡献指南](./CONTRIBUTING.md) 开始参与。 + +## 联系 + +如果你在使用过程中遇到困难、使用问题或异常情况,优先通过 [GitHub Issues](https://github.com/yikart/AiToEarn/issues) 提交反馈,方便我们统一跟进和处理。 + +- Telegram: [https://t.me/harryyyy2025](https://t.me/harryyyy2025) +- 微信:扫码添加 + +微信二维码 + +## 推荐 +- [AtomGit托管](https://atomgit.com/yikart/AitoEarn) +- [MuseTalk](https://github.com/TMElyralab/MuseTalk) +- [video_spider](https://github.com/5ime/video_spider) +- [CosyVoice](https://github.com/FunAudioLLM/CosyVoice?tab=readme-ov-file) +- [facefusion](https://github.com/facefusion/facefusion) +- [NarratoAI](https://github.com/linyqh/NarratoAI) +- [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo) diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..ee79678 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`yikart/AiToEarn` +- 原始仓库:https://github.com/yikart/AiToEarn +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..39241b8 --- /dev/null +++ b/README_EN.md @@ -0,0 +1,346 @@ +# [Aitoearn: The Best Open-Source AI Agent for Content Marketing](https://aitoearn.ai) + +yikart%2FAiToEarn | Trendshift + +[![GitHub stars](https://img.shields.io/github/stars/yikart/AiToEarn?color=fa6470)](https://github.com/yikart/AiToEarn/stargazers) +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Required Node.JS 20.18.x](https://img.shields.io/static/v1?label=node&message=20.18.x&logo=node.js&color=3f893e)](https://nodejs.org/about/releases) + +English | [简体中文](README.md) | [日本語](README_JA.md) + +**Monetize · Publish · Engage · Create — all in one platform.** + +AiToEarn helps OPCs (One-Person Companies), creators, brands, and businesses build, distribute, and monetize content with **AI-powered automation** across the world's most popular platforms. + +Supported Channels: +Douyin, Xiaohongshu (Rednote), Kuaishou, Bilibili, WeChat Channels, WeChat Official Accounts, TikTok, YouTube, Facebook, Instagram, Threads, Twitter (X), Pinterest, LinkedIn + +## 🚀 Quick Start with AiToEarn (5 Ways) + +| Option | Best for | Deployment needed? | +|--------|----------|-------------------| +| [① Use the Website](#use-web) | Everyone | ❌ No | +| [② Use in OpenClaw](#use-in-openclaw) | OpenClaw users | ❌ No | +| [③ Use in Claude / Cursor / Other AI Assistants](#use-in-claude) | AI tool users | ❌ No | +| [④ Docker One-Click Deploy](#use-docker) | Teams wanting self-hosted | ✅ Server needed | +| [⑤ Build from Source](#use-source) | Developers | ✅ Dev environment needed | + +> 💡 **Options ②③④ require an API Key first.** See [How to Get an API Key](#get-api-key). + +## What's New + +- **2026-06-23**: [2.5 version](https://github.com/yikart/AiToEarn/releases/tag/v2.5.0) — Relay configuration now happens in the Configuration UI and is split into Server Relay and AI Relay: Server Relay handles publishing platform authorization, while AI Relay lets users use AI models provided by the platform. +- **2026-05-21**: [2.4 version](https://github.com/yikart/AiToEarn/releases/tag/v2.4.0) — Draft generation now supports HappyHorse 1.0 and Seedance 2.0, with improved batch video/image-text draft generation, multi-model selection, reference images/videos, target-platform limits, and caption prompts; refreshed interface style and enhanced Twitter/X exploration and engagement. +- **2026-04-20**: OpenClaw now supports AiToEarn earning workflows, so you can receive and execute monetization tasks directly inside OpenClaw. +- **2026-03-26**: [2.1 version](https://www.aitoearn.ai/) — Content marketplace launched; added OpenClaw support for using AiToEarn directly within OpenClaw; added MCP protocol support for using AiToEarn in Claude, Cursor, and any MCP-compatible Agent or LLM. +- **2026-02-07**: [1.8.0 version](https://www.aitoearn.ai/) — Added offline business promotion solutions for restaurants, retail stores, hotels, beauty salons, gyms, and more. +- **2025-12-15**: "All In Agent" arrives! We've introduced a super AI agent that can automatically generate and publish content. [v1.4.3](https://github.com/yikart/AiToEarn/releases/tag/v1.4.3) +- **2025-11-28**: Support automatic updates within the application. Added AI functions: abbreviation, expansion, image creation, video creation, tag generation, etc. [v1.4.0](https://github.com/yikart/AiToEarn/releases/tag/v1.4.0) +- **2025-11-12**: The first open-source, fully usable version. [v1.3.2](https://github.com/yikart/AiToEarn/releases/tag/v1.3.2) +- **2025-09-16**: First international version, added support for Facebook, Instagram, Threads, Twitter, YouTube, TikTok, Pinterest. [v1.0.18](https://github.com/yikart/AiToEarn/releases/tag/v1.0.18) +- **2025-02-26**: First open-source release, initial support for one-click publishing to Xiaohongshu, Douyin, Kuaishou, and WeChat Channels. [v0.1.1](https://github.com/yikart/AiToEarn/releases/tag/v0.1.1) + +
+

Table of Contents

+ +
+ + 1. [Quick Start with AiToEarn (5 Ways)](#-quick-start-with-aitoearn-5-ways) + 2. [What's New](#whats-new) + 3. [Key Features](#key-features) + 4. [How to Get an API Key](#get-api-key) + 5. [Contributing](#contributing) + 6. [Contact](#contact) + 7. [Recommended](#recommended) +
+ +## Key Features + +AiToEarn provides four core Agent capabilities around the creator's full monetization pipeline: + +> **Monetize · Publish · Engage · Create** + +--- + +### 💰 Monetize — Earn from Your Content + +The core mission of AiToEarn: **help every creator earn money**. + +Creators can sell content on the platform to complete brand promotion tasks. All settlements are results-driven, with three models: + +| Model | Full Name | Meaning | +|-------|-----------|---------| +| **CPS** | Cost Per Sale | Settle by transaction amount | +| **CPE** | Cost Per Engagement | Settle by engagement count | +| **CPM** | Cost Per Mille | Settle by view count | + + + +--- + +### 📢 Publish — Content Publishing Agent + +Distribute content to 10+ major platforms worldwide with one click — no more manual posting on each platform. + +- **Multi-Platform Distribution**: Douyin, Kwai, Bilibili, Rednote, WeChat Channels, WeChat Official Accounts, TikTok, YouTube, Facebook, Instagram, Threads, X (Twitter), Pinterest, LinkedIn +- **Calendar Scheduler**: Plan and coordinate content publishing across all platforms like a calendar + + + +> ▶ Watch Demo Video + + + Publish Demo Video + + +--- + +### 💬 Engage — Content Engagement Agent + +Automate engagement operations across all supported platforms via the AiToEarn browser extension. + +- **Automated Actions**: Auto-like, bookmark, and follow — batch operations at scale +- **AI Smart Replies**: Use LLMs to generate targeted replies for each comment +- **Comment Mining**: Detect high-conversion signals like "link please" or "how to buy" and respond instantly +- **Brand Monitoring**: Track brand mentions in real-time and proactively join trending conversations + +> ▶ Watch Demo Video + + + Engage Demo Video + + +--- + +### 🎨 Create — Content Creation Agent + +We've rebuilt the content creation workflow with Agents. Just tell the Agent what you need — it handles everything from idea to finished product. + +**Video Content**: The Agent automatically invokes video generation models (Grok, Veo, Seedance, etc.), video translation modules, and video editing modules to produce a complete video. + +**Image & Text Content**: Supports top-tier image models like Nano Banana to create high-quality visual content automatically. + +**Batch Generation**: Submit creation tasks in bulk — the Agent generates multiple pieces of content in parallel, perfect for matrix account operations and large-scale content distribution. + +> ▶ Watch Demo Video + + + Create Demo Video + + +--- + +

① Use the Website

+ +The simplest way — just open your browser: + +- 🇨🇳 China users: **[aitoearn.cn](https://aitoearn.cn/)** +- 🌍 International users: **[aitoearn.ai](https://aitoearn.ai/)** + +--- + +

🔑 How to Get an API Key (Required for Steps Below)

+ +> Options ②③④ all need an API Key. You only need to get it once. + +**3 steps**: + +1. Open [aitoearn.cn](https://aitoearn.cn/) (China) or [aitoearn.ai](https://aitoearn.ai/) (international), sign up and log in +2. Click **Settings** in the left menu +3. Go to **API Key**, click Create, and copy the generated key + +Get API Key + +> ⚠️ Keep your API Key safe. Do not share it with others. + +--- + +

② Use in OpenClaw

+ +> Prerequisite: [Get an API Key](#get-api-key) first + +**Install the plugin** + +```bash +npx -y @aitoearn/openclaw-plugin-cli +``` + +On first run, select the environment and enter your API Key. Make sure they match: China uses an API Key from `aitoearn.cn`, and international uses one from `aitoearn.ai`. A mismatch returns 401. + +After setup, you can receive and execute AiToEarn earning tasks directly inside OpenClaw: + +Run AiToEarn earning tasks in OpenClaw + +--- + +

③ Use in Claude / Cursor / Other AI Assistants

+ +> Prerequisite: [Get an API Key](#get-api-key) first + +AiToEarn works with any MCP-compatible AI assistant. Here's how to configure the most popular ones: + +Choose the URL that matches your API Key. A mismatched environment and key returns 401: + +| Environment | MCP URL | SSE URL | +|-------------|---------|---------| +| China | `https://aitoearn.cn/api/unified/mcp` | `https://aitoearn.cn/api/unified/sse` | +| International | `https://aitoearn.ai/api/unified/mcp` | `https://aitoearn.ai/api/unified/sse` | + +
+Claude Desktop + +Find and edit `claude_desktop_config.json`, add: + +```json +{ + "mcpServers": { + "aitoearn": { + "type": "http", + "url": "https://aitoearn.ai/api/unified/mcp", + "headers": { + "x-api-key": "your-api-key" + } + } + } +} +``` + +
+ +
+Cursor + +In Cursor's MCP settings, add: + +``` +MCP URL: https://aitoearn.ai/api/unified/mcp +Auth Header: x-api-key: your-api-key +``` + +
+ +
+Other AI Assistants (Generic Config) + +Any MCP-compatible tool just needs two pieces of info: + +| Setting | Value | +|---------|-------| +| **MCP URL** | `https://aitoearn.ai/api/unified/mcp` | +| **Auth Header** | `x-api-key: your-api-key` | + +SSE transport is also available: `https://aitoearn.ai/api/unified/sse` + +
+ +> 💡 For self-hosted instances, replace `aitoearn.ai` with your own address (e.g., `localhost:8080`). + +--- + +

④ Docker One-Click Deploy

+ +> Prerequisite: [Docker](https://docs.docker.com/get-docker/) installed + +For teams wanting to run AiToEarn on their own server. 3 commands, no manual database setup: + +```bash +git clone https://github.com/yikart/AiToEarn.git +cd AiToEarn +docker compose up -d +``` + +Open **[http://localhost:8080](http://localhost:8080)** and you're ready to go. + +#### Configure Relay (Strongly Recommended) + +> **Why Relay?** Publishing content requires logging into social media accounts (TikTok, Instagram, YouTube, etc.), which need OAuth developer credentials. With Relay, you can use the official aitoearn.ai credentials — **no need to register as a developer on each platform**. + +Open the deployed UI in your browser, go to **Configuration**, and configure these sections as needed: + +- **Server → Relay**: for content publishing and social platform OAuth authorization. +- **AI → Relay**: for using AI models provided by the platform. + +For OpenAI, Gemini, Anthropic, and other model providers, you can also fill in the platform-provided API key and API URL under **AI → Model providers**. + +See [How to Get an API Key](#get-api-key). China keys must use `https://aitoearn.cn/api`, and international keys must use `https://aitoearn.ai/api`; mismatched environments return 401. + +After saving, click **Save and restart** so the corresponding service reloads the configuration. + +> 📖 Full deployment guide (production config, AI services, OAuth, storage, etc.): [DOCKER_DEPLOYMENT_EN.md](DOCKER_DEPLOYMENT_EN.md). + +--- + +

⑤ Build from Source

+ +
+🧪 Run backend & frontend manually (dev mode) + +For local development and debugging. You can use Docker for MongoDB/Redis, or point to your own services. + +#### 1. Start the backend services + +```bash +cd project/aitoearn-backend +pnpm install +# Copy config files for local development +cp apps/aitoearn-ai/config/config.yaml apps/aitoearn-ai/config/local.config.yaml +cp apps/aitoearn-server/config/config.yaml apps/aitoearn-server/config/local.config.yaml +pnpm nx serve aitoearn-ai +# in another terminal +pnpm nx serve aitoearn-server +``` + +#### 2. Start the frontend `aitoearn-web` + +```bash +pnpm install +pnpm run dev +``` + +
+ +
+🖥️ Start Electron desktop project + +```bash +# Clone the repo +git clone https://github.com/yikart/AttAiToEarn.git + +# Enter directory +cd AttAiToEarn + +# Install dependencies +npm install + +# Compile sqlite (better-sqlite3 requires node-gyp and local Python) +npm run rebuild + +# Start development +npm run dev +``` + +The Electron project provides a desktop client for AiToEarn. + +
+ +## Contributing + +Please see [Contributing Guide](./CONTRIBUTING.md) to get started. + +## Contact + +If you run into usage difficulties, questions, or unexpected behavior, please open a [GitHub Issue](https://github.com/yikart/AiToEarn/issues) first so we can track and respond in one place. + +- Telegram: [https://t.me/harryyyy2025](https://t.me/harryyyy2025) +- WeChat: Scan to add + +WeChat QR Code + +## Recommended + +- [MuseTalk](https://github.com/TMElyralab/MuseTalk) +- [video_spider](https://github.com/5ime/video_spider) +- [CosyVoice](https://github.com/FunAudioLLM/CosyVoice?tab=readme-ov-file) +- [facefusion](https://github.com/facefusion/facefusion) +- [NarratoAI](https://github.com/linyqh/NarratoAI) +- [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo) diff --git a/README_JA.md b/README_JA.md new file mode 100644 index 0000000..8cbc540 --- /dev/null +++ b/README_JA.md @@ -0,0 +1,352 @@ +# [Aitoearn:個人ビジネス向けAIコンテンツマーケティングエージェント](https://aitoearn.ai) + +yikart%2FAiToEarn | Trendshift + +[![GitHub stars](https://img.shields.io/github/stars/yikart/AiToEarn?color=fa6470)](https://github.com/yikart/AiToEarn/stargazers) +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Required Node.JS 20.18.x](https://img.shields.io/static/v1?label=node&message=20.18.x&logo=node.js&color=3f893e)](https://nodejs.org/about/releases) + +日本語 | [简体中文](README.md) | [English](README_EN.md) + +**収益化 · 公開 · エンゲージメント · クリエイト —— オールインワンプラットフォーム。** + +AiToEarnは**AI自動化**を通じて、クリエイター、ブランド、企業が世界中の主要プラットフォームでコンテンツを構築、配信、収益化するのを支援します。 + +対応チャンネル: +抖音(Douyin)、小紅書(Rednote)、快手(Kuaishou)、bilibili、視頻号(WeChat Channels)、微信公式アカウント(WeChat Official Accounts)、TikTok、YouTube、Facebook、Instagram、Threads、Twitter(X)、Pinterest、LinkedIn + +## 🚀 AiToEarnをすぐに使う(5つの方法) + +| 方法 | 対象 | デプロイ必要? | +|------|------|--------------| +| [① ウェブサイトで直接使う](#use-web) | すべてのユーザー | ❌ 不要 | +| [② OpenClaw(ロブスター)で使う](#use-in-openclaw) | OpenClawユーザー | ❌ 不要 | +| [③ Claude / Cursor などのAIアシスタントで使う](#use-in-claude) | AIツールユーザー | ❌ 不要 | +| [④ Dockerワンクリックデプロイ](#use-docker) | 自己デプロイしたいチーム | ✅ サーバー必要 | +| [⑤ ソースコードから開発](#use-source) | 開発者 | ✅ 開発環境必要 | + +> 💡 **方法②③④は事前にAPI Keyの取得が必要です**。[API Keyの取得方法](#get-api-key)を先にご覧ください。 + +## 最新情報 + +- **2026-06-23**: [2.5バージョン](https://github.com/yikart/AiToEarn/releases/tag/v2.5.0) — Relay 設定は設定管理画面で操作する方式になり、Server Relay と AI Relay に分かれました。Server Relay は公開プラットフォームの認証に、AI Relay はプラットフォーム提供の AI モデル利用に使用します。 +- **2026-05-21**: [2.4バージョン](https://github.com/yikart/AiToEarn/releases/tag/v2.4.0) — 草稿生成が HappyHorse 1.0 と Seedance 2.0 に新対応。動画・画像/テキスト草稿の一括生成、複数モデル選択、参照画像/動画、対象プラットフォーム制限、文案プロンプトを強化し、新しいUIスタイルと Twitter/X の探索・エンゲージメント機能も強化しました。 +- **2026-04-20**: OpenClaw(ロブスター)で AiToEarn の収益化タスクに新対応し、OpenClaw 内で直接受け取り実行できるようになりました。 +- **2026-03-26**: [2.1バージョン](https://www.aitoearn.ai/) — コンテンツ取引マーケットプレイスをリリース。OpenClaw(ロブスター)対応を追加し、OpenClaw内で直接AiToEarnを使用可能に。MCPプロトコル対応を追加し、Claude、CursorなどMCP対応のエージェントやLLMでAiToEarnを使用可能に。 +- **2026-02-07**: [1.8.0バージョン](https://www.aitoearn.ai/) — オフライン店舗プロモーションソリューションを追加。レストラン、小売店、民宿、美容室、ジムなど多様なオフラインビジネスに対応。オフラインのプロモーション活動を実行可能なオンライン拡散タスクに変換し、コンテンツ公開とユーザー参加メカニズムを通じて店舗のオンライン露出と来店トラフィックの増加を支援。 +- **2025-12-15**: 「All In Agent」の始まり!コンテンツの自動生成・公開、そしてAitoearnの操作を支援するスーパーAIエージェントを追加。[v1.4.3](https://github.com/yikart/AiToEarn/releases/tag/v1.4.3) +- **2025-11-28**: アプリ内自動更新に対応。作成画面に多くのAI機能を追加:要約、拡張、画像生成、動画生成、タグ生成など。Nano Banana Proにも対応。[v1.4.0](https://github.com/yikart/AiToEarn/releases/tag/v1.4.0) +- **2025-11-12**: 初のオープンソースで完全に使用可能なバージョン。[v1.3.2](https://github.com/yikart/AiToEarn/releases/tag/v1.3.2) +- **2025-09-16**: 初の海外展開バージョン、Facebook、Instagram、Threads、Twitter、YouTube、TikTok、Pinterestに対応。[v1.0.18](https://github.com/yikart/AiToEarn/releases/tag/v1.0.18) +- **2025-02-26**: 初のオープンソースバージョン、小紅書・抖音・快手・ビデオアカウントへのワンクリック動画投稿を実現。[v0.1.1](https://github.com/yikart/AiToEarn/releases/tag/v0.1.1) + +
+

目次

+ +
+ + 1. [AiToEarnをすぐに使う(5つの方法)](#-aitoearnをすぐに使う5つの方法) + 2. [最新情報](#最新情報) + 3. [主な機能](#主な機能) + 4. [API Keyの取得方法](#get-api-key) + 5. [貢献ガイド](#貢献ガイド) + 6. [お問い合わせ](#お問い合わせ) + 7. [推奨](#推奨) +
+ +## 主な機能 + +AiToEarnはコンテンツクリエイターの完全な収益化パイプラインを中心に、4つのエージェント機能を提供します: + +> **収益化 · 公開 · エンゲージメント · クリエイト** + +--- + +### 💰 収益化 —— コンテンツで稼ぐ + +AiToEarnの最も重要な目標:**すべてのクリエイターが稼げるようにする**。 + +クリエイターはプラットフォーム上でコンテンツを販売し、ブランドのプロモーションタスクを完了できます。すべての決済は成果報酬型で、3つの決済モードを提供: + +| 決済モード | 正式名称 | 意味 | +|---------|------|------| +| **CPS** | Cost Per Sale | 売上額に基づいて決済 | +| **CPE** | Cost Per Engagement | エンゲージメント数に基づいて決済 | +| **CPM** | Cost Per Mille | 再生数に基づいて決済 | + +
+ +
+ +--- + +### 📢 公開 —— コンテンツ公開エージェント + +ワンクリックで世界中の10以上の主要プラットフォームにコンテンツを配信。各プラットフォームで手動投稿する手間から解放されます。 + +- **マルチプラットフォーム配信**:抖音、快手、B站、小紅書、視頻号(WeChat Channels)、微信公式アカウント(WeChat Official Accounts)、TikTok、YouTube、Facebook、Instagram、Threads、X(Twitter)、Pinterest、LinkedInに対応 +- **カレンダースケジュール**:カレンダーのように全プラットフォームのコンテンツ公開時間を統一的に計画 + +
+ + +
+ +> ▶ デモ動画を見る + + + 公開 デモ動画 + + +--- + +### 💬 エンゲージメント —— コンテンツ交流エージェント + +AiToEarnブラウザ拡張機能を通じて、上記のすべてのプラットフォームで自動化された交流運用を実現。 + +- **自動化アクション**:自動いいね、保存、フォロー — 大規模な一括操作 +- **AIスマート返信**:LLMを使用して各コメントに的確な返信を生成 +- **コメントマイニング**:「リンクください」「購入方法は」などの高コンバージョンシグナルを検出し、即座に対応 +- **ブランドモニタリング**:ブランドに関する言及をリアルタイムで追跡し、トレンドの会話に積極的に参加 + +> ▶ デモ動画を見る + + + エンゲージメント デモ動画 + + +--- + +### 🎨 クリエイト —— コンテンツ作成エージェント + +エージェント方式でコンテンツ制作ワークフローを再構築しました。エージェントにコンテンツのニーズを伝えるだけで、アイデアから完成品まで全てを自動的に処理します。 + +**動画コンテンツ**:エージェントが自動的に動画生成モデル(Grok、Veo、Seedanceなど)、動画翻訳モジュール、動画編集モジュールを呼び出し、一貫して動画を制作。 + +**画像・テキストコンテンツ**:Nano Bananaなどのトップクラスの画像モデルをサポートし、高品質なビジュアルコンテンツを自動生成。 + +**一括生成**:作成タスクを一括で投入 — エージェントが複数のコンテンツを並列生成。マトリックスアカウント運用や大規模コンテンツ配信に最適。 + +> ▶ デモ動画を見る + + + クリエイト デモ動画 + + +--- + +

① ウェブサイトで直接使う

+ +最も簡単な方法 — ブラウザを開くだけで使用可能: + +- 🇨🇳 中国のユーザー:**[aitoearn.cn](https://aitoearn.cn/)** +- 🌍 その他のユーザー:**[aitoearn.ai](https://aitoearn.ai/)** + +--- + +

🔑 API Keyの取得方法(以下の手順に必要)

+ +> 以下の方法②③④にはAPI Keyが必要です。一度取得すれば、すべての方法で使用可能です。 + +**3ステップで取得**: + +1. [aitoearn.cn](https://aitoearn.cn/)(中国)または[aitoearn.ai](https://aitoearn.ai/)(その他)を開き、登録・ログイン +2. 左メニューの**設定**をクリック +3. **API Key**に移動し、作成をクリックして、生成されたキーをコピー + +API Key取得 + +> ⚠️ API Keyは安全に保管し、他人に共有しないでください。 + +--- + +

② OpenClaw(ロブスター)で使う

+ +> 前提条件:[API Keyを取得済み](#get-api-key) + +**プラグインをインストール** + +```bash +npx -y @aitoearn/openclaw-plugin-cli +``` + +初回実行時は環境を選択し、API Key を入力してください。中国版は `aitoearn.cn` の API Key、国際版は `aitoearn.ai` の API Key を使用します。環境と Key が一致しない場合は 401 になります。 + +設定後は、OpenClaw 内で AiToEarn の収益化タスクを直接受け取り実行できます。 + +OpenClaw で AiToEarn の収益化タスクを実行 + +--- + +

③ Claude / Cursor / その他のAIアシスタントで使う

+ +> 前提条件:[API Keyを取得済み](#get-api-key) + +AiToEarnはMCPプロトコルに対応するすべてのAIアシスタントで動作します。一般的なツールの設定方法: + +API Key の取得元に合わせて URL を選択してください。環境と Key が一致しない場合は 401 になります。 + +| 環境 | MCP URL | SSE URL | +|------|---------|---------| +| 中国版 | `https://aitoearn.cn/api/unified/mcp` | `https://aitoearn.cn/api/unified/sse` | +| 国際版 | `https://aitoearn.ai/api/unified/mcp` | `https://aitoearn.ai/api/unified/sse` | + +
+Claude Desktop + +`claude_desktop_config.json`を見つけて編集し、以下を追加: + +```json +{ + "mcpServers": { + "aitoearn": { + "type": "http", + "url": "https://aitoearn.ai/api/unified/mcp", + "headers": { + "x-api-key": "あなたのAPI-Key" + } + } + } +} +``` + +
+ +
+Cursor + +CursorのMCP設定で以下を追加: + +``` +MCP URL: https://aitoearn.ai/api/unified/mcp +認証ヘッダー: x-api-key: あなたのAPI-Key +``` + +
+ +
+その他のAIアシスタント(汎用設定) + +MCPプロトコル対応のツールなら、2つの情報だけでOK: + +| 設定項目 | 値 | +|--------|------| +| **MCP URL** | `https://aitoearn.ai/api/unified/mcp` | +| **認証ヘッダー** | `x-api-key: あなたのAPI-Key` | + +SSE接続もサポート:`https://aitoearn.ai/api/unified/sse` + +
+ +> 💡 自己デプロイの場合は、`aitoearn.ai`をご自身のアドレス(例:`localhost:8080`)に置き換えてください。 + +--- + +

④ Dockerワンクリックデプロイ

+ +> 前提条件:[Docker](https://docs.docker.com/get-docker/)がインストール済み + +自分のサーバーでAiToEarnを運用したいチーム向け。3つのコマンドで完了、データベースの手動インストール不要: + +```bash +git clone https://github.com/yikart/AiToEarn.git +cd AiToEarn +docker compose up -d +``` + +起動後、**[http://localhost:8080](http://localhost:8080)** を開けば使用可能。 + +#### Relayの設定(強く推奨) + +> **なぜRelayが必要?** コンテンツを公開するにはソーシャルメディアアカウント(TikTok、Instagram、YouTubeなど)へのログインが必要で、これらのプラットフォームのOAuthログインには開発者認証情報が必要です。Relayを設定すれば、公式aitoearn.aiの認証情報を直接借用して認証を完了できるため、**各プラットフォームで開発者アカウントを申請する必要がありません**。 + +ブラウザでデプロイ済みの画面を開き、**設定管理** に入り、必要に応じて次の項目を設定します: + +- **Server → Relay**:コンテンツ公開とソーシャルプラットフォームの OAuth 認証に使用します。 +- **AI → Relay**:プラットフォームが提供する AI モデルを利用するために使用します。 + +OpenAI、Gemini、Anthropic などのモデルプロバイダーも、**AI → モデルプロバイダー** でプラットフォーム提供の API Key と API URL を入力できます。 + +API Key の取得方法は [上記](#get-api-key) を参照してください。中国版 Key は `https://aitoearn.cn/api`、国際版 Key は `https://aitoearn.ai/api` と組み合わせて使用します。環境と Key が一致しない場合は 401 になります。 + +保存後、**保存して再起動** をクリックして、対象サービスに設定を再読み込みさせます。 + +> 📖 完全なデプロイガイド(本番環境設定、AIサービス、OAuth、ストレージなど):[DOCKER_DEPLOYMENT_EN.md](DOCKER_DEPLOYMENT_EN.md)を参照。 + +--- + +

⑤ ソースコードから開発

+ +
+🧪 バックエンドとフロントエンドを手動で実行(開発モード) + +このモードは主にローカル開発とデバッグ用です。 +Dockerを使用してMongoDB/Redisを実行するか、設定ファイルで独自のサービスを指定できます。 + +#### 1. バックエンドサービスを起動 + +```bash +cd project/aitoearn-backend +pnpm install +# ローカル開発用設定ファイルをコピー +cp apps/aitoearn-ai/config/config.yaml apps/aitoearn-ai/config/local.config.yaml +cp apps/aitoearn-server/config/config.yaml apps/aitoearn-server/config/local.config.yaml +pnpm nx serve aitoearn-ai +# 別のターミナルで +pnpm nx serve aitoearn-server +``` + +#### 2. フロントエンド `aitoearn-web` を起動 + +```bash +pnpm install +pnpm run dev +``` + +
+ +
+🖥️ Electronデスクトッププロジェクトを起動 + +```bash +# リポジトリをクローン +git clone https://github.com/yikart/AttAiToEarn.git + +# ディレクトリに移動 +cd AttAiToEarn + +# 依存関係をインストール +npm install + +# sqliteをコンパイル(better-sqlite3にはnode-gypとローカルPythonが必要) +npm run rebuild + +# 開発を起動 +npm run dev +``` + +ElectronプロジェクトはAiToEarnのデスクトップクライアントを提供します。 + +
+ +## 貢献ガイド + +参加するには[貢献ガイド](./CONTRIBUTING.md)をご覧ください。 + +## お問い合わせ + +利用中に困ったこと、使い方の質問、不具合がある場合は、まず [GitHub Issues](https://github.com/yikart/AiToEarn/issues) でご連絡ください。内容を一元管理し、順番に対応できます。 + +- Telegram: [https://t.me/harryyyy2025](https://t.me/harryyyy2025) +- WeChat:QRコードをスキャンして追加 + +WeChat QRコード + +## 推奨 + +- [MuseTalk](https://github.com/TMElyralab/MuseTalk) +- [video_spider](https://github.com/5ime/video_spider) +- [CosyVoice](https://github.com/FunAudioLLM/CosyVoice?tab=readme-ov-file) +- [facefusion](https://github.com/facefusion/facefusion) +- [NarratoAI](https://github.com/linyqh/NarratoAI) +- [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo) diff --git a/demo/kwai/index.html b/demo/kwai/index.html new file mode 100644 index 0000000..1e5b5b3 --- /dev/null +++ b/demo/kwai/index.html @@ -0,0 +1,18 @@ + + + + + Title + + + + + + \ No newline at end of file diff --git a/demo/xhs/index.html b/demo/xhs/index.html new file mode 100644 index 0000000..480732a --- /dev/null +++ b/demo/xhs/index.html @@ -0,0 +1,39 @@ + + + + + + + 小红书api demo + + + + + + + + \ No newline at end of file diff --git a/demo/xhs/signature.js b/demo/xhs/signature.js new file mode 100644 index 0000000..345d15e --- /dev/null +++ b/demo/xhs/signature.js @@ -0,0 +1,70 @@ +import axios from "axios"; +import crypto from "crypto-js" +const appKey = "red.gLvsVoksierVz0uF"; +const appSecret = "f13a2266d1e2c32a553cb7a42ea63c48"; +let cachedAccessToken = null; +let accessTokenExpiresAt = 0; // 记录 access_token 过期时间 + +// 生成小红书签名 +function generateSignature(appKey, nonce, timeStamp, appSecret) { + const params = { + appKey, + nonce, + timeStamp, + }; + const sortedParams = Object.keys(params) + .sort() + .map((key) => `${key}=${params[key]}`) + .join("&"); + const stringToSign = sortedParams + appSecret; + console.log(stringToSign); + return crypto.SHA256(stringToSign).toString(); +} + +// 获取小红书access_token +const getAccessToken = async (nonce, timestamp) => { + if (cachedAccessToken && Date.now() < accessTokenExpiresAt) { + // 如果 access_token 未过期,则直接返回缓存的 token + return cachedAccessToken; + } + + const signature = generateSignature(appKey, nonce, timestamp, appSecret); + console.log({ + app_key: appKey, + nonce: nonce, + timestamp: timestamp, + signature: signature, + }); + try { + const response = await axios.post("https://edith.xiaohongshu.com/api/sns/v1/ext/access/token", { + app_key: appKey, + nonce: nonce, + timestamp: timestamp, + signature: signature, + }, { + headers: { + "Content-Type": "application/json", + }, + }); + console.log(response.data); + const { access_token, expires_in } = response.data.data; + + // 缓存 access_token 和计算过期时间 + cachedAccessToken = access_token; + accessTokenExpiresAt = expires_in; + + return cachedAccessToken; + } catch (error) { + console.error('请求失败:', error); + throw error; // 处理错误 + } +}; + +const nonce = Math.random().toString(36).substring(2); +const timestamp = Date.now(); +const accessToken = await getAccessToken(nonce, timestamp); +const signature = generateSignature(appKey, nonce, timestamp, accessToken); +console.log("appKey:", appKey); +console.log("nonce:", nonce); +console.log("timestamp:", timestamp); +console.log("signature:", signature); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ee7fe43 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,222 @@ +services: + mongodb: + image: mongo:latest + container_name: aitoearn-mongodb + restart: unless-stopped + entrypoint: ["bash", "-c", "openssl rand -base64 756 > /data/mongodb-keyfile && chmod 400 /data/mongodb-keyfile && chown mongodb:mongodb /data/mongodb-keyfile && exec docker-entrypoint.sh mongod --replSet rs0 --keyFile /data/mongodb-keyfile"] + environment: + MONGO_INITDB_ROOT_USERNAME: admin + MONGO_INITDB_ROOT_PASSWORD: password + ports: + - "27017:27017" + volumes: + - mongodb-data:/data/db + - mongodb-config:/data/configdb + networks: + - aitoearn-network + healthcheck: + test: ["CMD-SHELL", "mongosh mongodb://admin:password@localhost:27017/?authSource=admin --quiet --eval \"try { quit(rs.status().ok === 1 ? 0 : 1) } catch(e) { quit(1) }\""] + interval: 10s + timeout: 5s + retries: 10 + start_period: 40s + + mongodb-rs-init: + image: mongo:latest + container_name: aitoearn-mongodb-rs-init + depends_on: + mongodb: + condition: service_started + entrypoint: ["/bin/sh", "-c", "until mongosh mongodb://admin:password@mongodb:27017/?authSource=admin --quiet --eval \"db.adminCommand('ping').ok\" >/dev/null 2>&1; do sleep 2; done; mongosh mongodb://admin:password@mongodb:27017/?authSource=admin --eval \"try { rs.initiate({_id:'rs0', members:[{_id:0, host:'mongodb:27017'}]}) } catch(e) { if (e.codeName!=='AlreadyInitialized') throw e }\""] + restart: "no" + networks: + - aitoearn-network + + redis: + image: redis:latest + container_name: aitoearn-redis + restart: unless-stopped + command: redis-server --requirepass password + ports: + - "6379:6379" + volumes: + - redis-data:/data + networks: + - aitoearn-network + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 30s + + rustfs: + image: rustfs/rustfs:latest + container_name: aitoearn-rustfs + hostname: rustfs.local + restart: unless-stopped + environment: + RUSTFS_ACCESS_KEY: rustfsadmin + RUSTFS_SECRET_KEY: rustfsadmin + ports: + - "9001:9001" + volumes: + - rustfs-data:/data + networks: + aitoearn-network: + aliases: + - rustfs.local + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + aitoearn-init: + image: node:lts-alpine + container_name: aitoearn-init + restart: "no" + depends_on: + mongodb: + condition: service_healthy + volumes: + - ./scripts/init.mjs:/app/init.mjs:ro + - ./scripts/init-package.json:/app/package.json:ro + - init-data:/data/init + working_dir: /app + environment: + MONGO_URI: mongodb://admin:password@mongodb:27017 + JWT_SECRET: change-this-jwt-secret + DB_NAME: aitoearn + AUTO_LOGIN_TOKEN_PATH: /data/init/token.txt + entrypoint: /bin/sh -c "npm install --omit=dev 2>/dev/null && node init.mjs" + networks: + - aitoearn-network + + rustfs-init: + image: minio/mc:latest + container_name: aitoearn-rustfs-init + depends_on: + rustfs: + condition: service_healthy + networks: + - aitoearn-network + entrypoint: > + /bin/sh -c " + mc alias set rustfs http://rustfs.local:9000 rustfsadmin rustfsadmin; + mc mb rustfs/aitoearn --ignore-existing; + mc anonymous set download rustfs/aitoearn; + exit 0; + " + + aitoearn-ai: + image: aitoearn/aitoearn-ai:latest + pull_policy: always + container_name: aitoearn-ai + restart: unless-stopped + depends_on: + mongodb: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./project/aitoearn-backend/apps/aitoearn-ai/config/config.yaml:/app/config.yaml:rw + networks: + - aitoearn-network + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3010/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + aitoearn-server: + image: aitoearn/aitoearn-server:latest + pull_policy: always + container_name: aitoearn-server + restart: unless-stopped + depends_on: + mongodb: + condition: service_healthy + redis: + condition: service_healthy + aitoearn-ai: + condition: service_healthy + volumes: + - ./project/aitoearn-backend/apps/aitoearn-server/config/config.yaml:/app/config.yaml:rw + networks: + - aitoearn-network + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3002/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + aitoearn-web: + image: aitoearn/aitoearn-web:latest + pull_policy: always + container_name: aitoearn-web + restart: unless-stopped + depends_on: + aitoearn-server: + condition: service_healthy + aitoearn-init: + condition: service_completed_successfully + volumes: + - init-data:/data/init:ro + networks: + - aitoearn-network + command: ["/bin/sh", "-c", "AUTO_LOGIN_TOKEN=$$(cat /data/init/token.txt 2>/dev/null || true); if [ -z \"$$AUTO_LOGIN_TOKEN\" ]; then echo 'AUTO_LOGIN_TOKEN is missing. Check the aitoearn-init container logs.' >&2; exit 1; fi; export AUTO_LOGIN_TOKEN; exec node server.js"] + environment: + NODE_ENV: production + NEXT_TELEMETRY_DISABLED: 1 + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000', (r) => { process.exit(r.statusCode < 500 ? 0 : 1) }).on('error', () => process.exit(1))"] + interval: 10s + timeout: 5s + retries: 6 + start_period: 30s + + nginx: + image: nginx:alpine + container_name: aitoearn-nginx + restart: unless-stopped + depends_on: + aitoearn-web: + condition: service_healthy + aitoearn-server: + condition: service_healthy + aitoearn-ai: + condition: service_healthy + ports: + - "8080:80" + - "9000:9000" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + networks: + - aitoearn-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/_nhealth"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + +networks: + aitoearn-network: + driver: bridge + name: aitoearn-network + +volumes: + mongodb-data: + driver: local + mongodb-config: + driver: local + redis-data: + driver: local + rustfs-data: + driver: local + init-data: + driver: local diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..47a3784 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,132 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; + + client_max_body_size 50m; + + map $http_origin $cors_origin { + default ""; + "~^https?://(.*\.)?(dev\.)?aitoearn\.ai$" $http_origin; + "~^https?://localhost(:[0-9]+)?$" $http_origin; + "~^https?://127\.0\.0\.1(:[0-9]+)?$" $http_origin; + "~^https?://0\.0\.0\.0(:[0-9]+)?$" $http_origin; + } + + proxy_set_header Host $proxy_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_http_version 1.1; + + # Hide backend CORS headers, managed by nginx + proxy_hide_header Access-Control-Allow-Origin; + proxy_hide_header Access-Control-Allow-Methods; + proxy_hide_header Access-Control-Allow-Headers; + proxy_hide_header Access-Control-Allow-Credentials; + proxy_hide_header Access-Control-Max-Age; + + proxy_connect_timeout 120s; + proxy_send_timeout 60s; + proxy_read_timeout 86400s; + + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS" always; + add_header Access-Control-Allow-Headers "Accept, Accept-Encoding, Accept-Language, Authorization, Cache-Control, Content-Type, Cookie, DNT, Origin, Pragma, Referer, User-Agent, X-Requested-With, X-Request-ID, X-CSRF-Token, X-Auth-Token, X-HTTP-Method-Override, Next-Router-Prefetch, Next-Router-State-Tree, Next-Url, RSC, Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform, Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site, Priority, If-None-Match, If-Modified-Since" always; + add_header Access-Control-Allow-Credentials true always; + add_header Access-Control-Max-Age 86400 always; + + server { + listen 80; + listen [::]:80; + server_name _; + + location = /_nhealth { + access_log off; + default_type text/plain; + return 200 "healthy\n"; + } + + if ($request_method = 'OPTIONS') { + return 204; + } + + location /api/ai/ { + rewrite ^/api/(.*)$ /$1 break; + proxy_pass http://aitoearn-ai:3010; + } + + location /api/agent/ { + rewrite ^/api/(.*)$ /$1 break; + proxy_pass http://aitoearn-ai:3010; + } + + location /api/ { + rewrite ^/api/(.*)$ /$1 break; + proxy_pass http://aitoearn-server:3002; + } + + location /oss/ { + proxy_pass http://aitoearn-rustfs:9000/aitoearn/; + } + + location / { + proxy_pass http://aitoearn-web:3000; + } + } + + # S3 代理:浏览器通过此端口上传文件到 rustfs + server { + listen 9000; + listen [::]:9000; + server_name _; + + client_max_body_size 500m; + + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, HEAD, PUT, POST, DELETE, OPTIONS" always; + add_header Access-Control-Allow-Headers * always; + add_header Access-Control-Expose-Headers "ETag" always; + add_header Access-Control-Max-Age 86400 always; + + if ($request_method = 'OPTIONS') { + return 204; + } + + location / { + proxy_pass http://aitoearn-rustfs:9000; + proxy_set_header Host $http_host; + proxy_http_version 1.1; + proxy_connect_timeout 120s; + proxy_send_timeout 120s; + proxy_read_timeout 120s; + } + } +} diff --git a/presentation/agent_0.png b/presentation/agent_0.png new file mode 100644 index 0000000..0f0ffb8 Binary files /dev/null and b/presentation/agent_0.png differ diff --git a/presentation/app-screenshot/0. api-key/api-key-settings.png b/presentation/app-screenshot/0. api-key/api-key-settings.png new file mode 100644 index 0000000..6ebeeb1 Binary files /dev/null and b/presentation/app-screenshot/0. api-key/api-key-settings.png differ diff --git a/presentation/app-screenshot/1. content publish/calendar.jpeg b/presentation/app-screenshot/1. content publish/calendar.jpeg new file mode 100644 index 0000000..b964f72 Binary files /dev/null and b/presentation/app-screenshot/1. content publish/calendar.jpeg differ diff --git a/presentation/app-screenshot/1. content publish/support_channels.jpeg b/presentation/app-screenshot/1. content publish/support_channels.jpeg new file mode 100644 index 0000000..42cc291 Binary files /dev/null and b/presentation/app-screenshot/1. content publish/support_channels.jpeg differ diff --git a/presentation/app-screenshot/2. content hotspot/hotspot.jpg b/presentation/app-screenshot/2. content hotspot/hotspot.jpg new file mode 100644 index 0000000..3520733 Binary files /dev/null and b/presentation/app-screenshot/2. content hotspot/hotspot.jpg differ diff --git a/presentation/app-screenshot/2. content hotspot/hotspot2.jpeg b/presentation/app-screenshot/2. content hotspot/hotspot2.jpeg new file mode 100644 index 0000000..e9b9486 Binary files /dev/null and b/presentation/app-screenshot/2. content hotspot/hotspot2.jpeg differ diff --git a/presentation/app-screenshot/2. content hotspot/hotspot3.jpeg b/presentation/app-screenshot/2. content hotspot/hotspot3.jpeg new file mode 100644 index 0000000..4ad538a Binary files /dev/null and b/presentation/app-screenshot/2. content hotspot/hotspot3.jpeg differ diff --git a/presentation/app-screenshot/2. content hotspot/hotspot4.jpeg b/presentation/app-screenshot/2. content hotspot/hotspot4.jpeg new file mode 100644 index 0000000..8ca16e4 Binary files /dev/null and b/presentation/app-screenshot/2. content hotspot/hotspot4.jpeg differ diff --git a/presentation/app-screenshot/3. content search/contentsearch.gif b/presentation/app-screenshot/3. content search/contentsearch.gif new file mode 100644 index 0000000..65939cf Binary files /dev/null and b/presentation/app-screenshot/3. content search/contentsearch.gif differ diff --git a/presentation/app-screenshot/3. content search/contentsearch0.mp4 b/presentation/app-screenshot/3. content search/contentsearch0.mp4 new file mode 100644 index 0000000..6343681 Binary files /dev/null and b/presentation/app-screenshot/3. content search/contentsearch0.mp4 differ diff --git a/presentation/app-screenshot/3. content search/contentsearch1.jpeg b/presentation/app-screenshot/3. content search/contentsearch1.jpeg new file mode 100644 index 0000000..6539f4b Binary files /dev/null and b/presentation/app-screenshot/3. content search/contentsearch1.jpeg differ diff --git a/presentation/app-screenshot/3. content search/contentsearch2.jpeg b/presentation/app-screenshot/3. content search/contentsearch2.jpeg new file mode 100644 index 0000000..360e619 Binary files /dev/null and b/presentation/app-screenshot/3. content search/contentsearch2.jpeg differ diff --git a/presentation/app-screenshot/3. content search/contentsearch4.jpeg b/presentation/app-screenshot/3. content search/contentsearch4.jpeg new file mode 100644 index 0000000..1e74068 Binary files /dev/null and b/presentation/app-screenshot/3. content search/contentsearch4.jpeg differ diff --git a/presentation/app-screenshot/4. comments search/commentfilter.jpeg b/presentation/app-screenshot/4. comments search/commentfilter.jpeg new file mode 100644 index 0000000..3d6307e Binary files /dev/null and b/presentation/app-screenshot/4. comments search/commentfilter.jpeg differ diff --git a/presentation/app-screenshot/4. comments search/commentsearch.gif b/presentation/app-screenshot/4. comments search/commentsearch.gif new file mode 100644 index 0000000..2c954ad Binary files /dev/null and b/presentation/app-screenshot/4. comments search/commentsearch.gif differ diff --git a/presentation/app-screenshot/4. comments search/untitled folder/commentfilter.mp4 b/presentation/app-screenshot/4. comments search/untitled folder/commentfilter.mp4 new file mode 100644 index 0000000..15c5ac3 Binary files /dev/null and b/presentation/app-screenshot/4. comments search/untitled folder/commentfilter.mp4 differ diff --git a/presentation/app-screenshot/4. comments search/untitled folder/commentfilter1.mp4 b/presentation/app-screenshot/4. comments search/untitled folder/commentfilter1.mp4 new file mode 100644 index 0000000..075f5d3 Binary files /dev/null and b/presentation/app-screenshot/4. comments search/untitled folder/commentfilter1.mp4 differ diff --git a/presentation/app-screenshot/5. content engagement/commentfilter2.jpeg b/presentation/app-screenshot/5. content engagement/commentfilter2.jpeg new file mode 100644 index 0000000..b28c546 Binary files /dev/null and b/presentation/app-screenshot/5. content engagement/commentfilter2.jpeg differ diff --git a/presentation/channel-cn.png b/presentation/channel-cn.png new file mode 100644 index 0000000..8d9998e Binary files /dev/null and b/presentation/channel-cn.png differ diff --git a/presentation/data_center.png b/presentation/data_center.png new file mode 100644 index 0000000..ddf91c2 Binary files /dev/null and b/presentation/data_center.png differ diff --git a/presentation/display-1.5.2png.png b/presentation/display-1.5.2png.png new file mode 100644 index 0000000..359fc4c Binary files /dev/null and b/presentation/display-1.5.2png.png differ diff --git a/presentation/engage-thumbnail-cn.png b/presentation/engage-thumbnail-cn.png new file mode 100644 index 0000000..6fbc2f4 Binary files /dev/null and b/presentation/engage-thumbnail-cn.png differ diff --git a/presentation/monetize-cn.png b/presentation/monetize-cn.png new file mode 100644 index 0000000..0d92e10 Binary files /dev/null and b/presentation/monetize-cn.png differ diff --git a/presentation/openclaw-earn-demo.png b/presentation/openclaw-earn-demo.png new file mode 100644 index 0000000..40f5534 Binary files /dev/null and b/presentation/openclaw-earn-demo.png differ diff --git a/presentation/publish-cn.png b/presentation/publish-cn.png new file mode 100644 index 0000000..d168e24 Binary files /dev/null and b/presentation/publish-cn.png differ diff --git a/presentation/wechat-0706.png b/presentation/wechat-0706.png new file mode 100644 index 0000000..54ea31e Binary files /dev/null and b/presentation/wechat-0706.png differ diff --git a/presentation/wechat.jpg b/presentation/wechat.jpg new file mode 100644 index 0000000..2c434b1 Binary files /dev/null and b/presentation/wechat.jpg differ diff --git a/project/aitoearn-backend/.agents/skills/caveman/SKILL.md b/project/aitoearn-backend/.agents/skills/caveman/SKILL.md new file mode 100644 index 0000000..85770a3 --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/caveman/SKILL.md @@ -0,0 +1,49 @@ +--- +name: caveman +description: > + Ultra-compressed communication mode. Cuts token usage ~75% by dropping + filler, articles, and pleasantries while keeping full technical accuracy. + Use when user says "caveman mode", "talk like caveman", "use caveman", + "less tokens", "be brief", or invokes /caveman. +--- + +Respond terse like smart caveman. All technical substance stay. Only fluff die. + +## Persistence + +ACTIVE EVERY RESPONSE once triggered. No revert after many turns. No filler drift. Still active if unsure. Off only when user says "stop caveman" or "normal mode". + +## Rules + +Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Abbreviate common terms (DB/auth/config/req/res/fn/impl). Strip conjunctions. Use arrows for causality (X -> Y). One word when one word enough. + +Technical terms stay exact. Code blocks unchanged. Errors quoted exact. + +Pattern: `[thing] [action] [reason]. [next step].` + +Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." +Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" + +### Examples + +**"Why React component re-render?"** + +> Inline obj prop -> new ref -> re-render. `useMemo`. + +**"Explain database connection pooling."** + +> Pool = reuse DB conn. Skip handshake -> fast under load. + +## Auto-Clarity Exception + +Drop caveman temporarily for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done. + +Example -- destructive op: + +> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. +> +> ```sql +> DROP TABLE users; +> ``` +> +> Caveman resume. Verify backup exist first. diff --git a/project/aitoearn-backend/.agents/skills/diagnose/SKILL.md b/project/aitoearn-backend/.agents/skills/diagnose/SKILL.md new file mode 100644 index 0000000..ed55bda --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/diagnose/SKILL.md @@ -0,0 +1,117 @@ +--- +name: diagnose +description: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says "diagnose this" / "debug this", reports a bug, says something is broken/throwing/failing, or describes a performance regression. +--- + +# Diagnose + +A discipline for hard bugs. Skip phases only when explicitly justified. + +When exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching. + +## Phase 1 — Build a feedback loop + +**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you. + +Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.** + +### Ways to construct one — try them in roughly this order + +1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e. +2. **Curl / HTTP script** against a running dev server. +3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot. +4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network. +5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation. +6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call. +7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode. +8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it. +9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs. +10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you. + +Build the right feedback loop, and the bug is 90% fixed. + +### Iterate on the loop itself + +Treat the loop as a product. Once you have _a_ loop, ask: + +- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.) +- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".) +- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.) + +A 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower. + +### Non-deterministic bugs + +The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable. + +### When you genuinely cannot build a loop + +Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop. + +Do not proceed to Phase 2 until you have a loop you believe in. + +## Phase 2 — Reproduce + +Run the loop. Watch the bug appear. + +Confirm: + +- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix. +- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against). +- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it. + +Do not proceed until you reproduce the bug. + +## Phase 3 — Hypothesise + +Generate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea. + +Each hypothesis must be **falsifiable**: state the prediction it makes. + +> Format: "If is the cause, then will make the bug disappear / will make it worse." + +If you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it. + +**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK. + +## Phase 4 — Instrument + +Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.** + +Tool preference: + +1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs. +2. **Targeted logs** at the boundaries that distinguish hypotheses. +3. Never "log everything and grep". + +**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die. + +**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second. + +## Phase 5 — Fix + regression test + +Write the regression test **before the fix** — but only if there is a **correct seam** for it. + +A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence. + +**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase. + +If a correct seam exists: + +1. Turn the minimised repro into a failing test at that seam. +2. Watch it fail. +3. Apply the fix. +4. Watch it pass. +5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario. + +## Phase 6 — Cleanup + post-mortem + +Required before declaring done: + +- [ ] Original repro no longer reproduces (re-run the Phase 1 loop) +- [ ] Regression test passes (or absence of seam is documented) +- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix) +- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location) +- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns + +**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started. diff --git a/project/aitoearn-backend/.agents/skills/diagnose/scripts/hitl-loop.template.sh b/project/aitoearn-backend/.agents/skills/diagnose/scripts/hitl-loop.template.sh new file mode 100644 index 0000000..40afc46 --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/diagnose/scripts/hitl-loop.template.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Human-in-the-loop reproduction loop. +# Copy this file, edit the steps below, and run it. +# The agent runs the script; the user follows prompts in their terminal. +# +# Usage: +# bash hitl-loop.template.sh +# +# Two helpers: +# step "" → show instruction, wait for Enter +# capture VAR "" → show question, read response into VAR +# +# At the end, captured values are printed as KEY=VALUE for the agent to parse. + +set -euo pipefail + +step() { + printf '\n>>> %s\n' "$1" + read -r -p " [Enter when done] " _ +} + +capture() { + local var="$1" question="$2" answer + printf '\n>>> %s\n' "$question" + read -r -p " > " answer + printf -v "$var" '%s' "$answer" +} + +# --- edit below --------------------------------------------------------- + +step "Open the app at http://localhost:3000 and sign in." + +capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)" + +capture ERROR_MSG "Paste the error message (or 'none'):" + +# --- edit above --------------------------------------------------------- + +printf '\n--- Captured ---\n' +printf 'ERRORED=%s\n' "$ERRORED" +printf 'ERROR_MSG=%s\n' "$ERROR_MSG" diff --git a/project/aitoearn-backend/.agents/skills/grill-me/SKILL.md b/project/aitoearn-backend/.agents/skills/grill-me/SKILL.md new file mode 100644 index 0000000..bd04394 --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/grill-me/SKILL.md @@ -0,0 +1,10 @@ +--- +name: grill-me +description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". +--- + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time. + +If a question can be answered by exploring the codebase, explore the codebase instead. diff --git a/project/aitoearn-backend/.agents/skills/grill-with-docs/ADR-FORMAT.md b/project/aitoearn-backend/.agents/skills/grill-with-docs/ADR-FORMAT.md new file mode 100644 index 0000000..da7e78e --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/grill-with-docs/ADR-FORMAT.md @@ -0,0 +1,47 @@ +# ADR Format + +ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. + +Create the `docs/adr/` directory lazily — only when the first ADR is needed. + +## Template + +```md +# {Short title of the decision} + +{1-3 sentences: what's the context, what did we decide, and why.} +``` + +That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most ADRs won't need them. + +- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited +- **Considered Options** — only when the rejected alternatives are worth remembering +- **Consequences** — only when non-obvious downstream effects need to be called out + +## Numbering + +Scan `docs/adr/` for the highest existing number and increment by one. + +## When to offer an ADR + +All three of these must be true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." + +### What qualifies + +- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." +- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." +- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. +- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. +- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. +- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." +- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/project/aitoearn-backend/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md b/project/aitoearn-backend/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md new file mode 100644 index 0000000..ddfa247 --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md @@ -0,0 +1,77 @@ +# CONTEXT.md Format + +## Structure + +```md +# {Context Name} + +{One or two sentence description of what this context is and why it exists.} + +## Language + +**Order**: +{A concise description of the term} +_Avoid_: Purchase, transaction + +**Invoice**: +A request for payment sent to a customer after delivery. +_Avoid_: Bill, payment request + +**Customer**: +A person or organization that places orders. +_Avoid_: Client, buyer, account + +## Relationships + +- An **Order** produces one or more **Invoices** +- An **Invoice** belongs to exactly one **Customer** + +## Example dialogue + +> **Dev:** "When a **Customer** places an **Order**, do we create the **Invoice** immediately?" +> **Domain expert:** "No — an **Invoice** is only generated once a **Fulfillment** is confirmed." + +## Flagged ambiguities + +- "account" was used to mean both **Customer** and **User** — resolved: these are distinct concepts. +``` + +## Rules + +- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid. +- **Flag conflicts explicitly.** If a term is used ambiguously, call it out in "Flagged ambiguities" with a clear resolution. +- **Keep definitions tight.** One sentence max. Define what it IS, not what it does. +- **Show relationships.** Use bold term names and express cardinality where obvious. +- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. +- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. +- **Write an example dialogue.** A conversation between a dev and a domain expert that demonstrates how the terms interact naturally and clarifies boundaries between related concepts. + +## Single vs multi-context repos + +**Single context (most repos):** One `CONTEXT.md` at the repo root. + +**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: + +```md +# Context Map + +## Contexts + +- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders +- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments +- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping + +## Relationships + +- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking +- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices +- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` +``` + +The skill infers which structure applies: + +- If `CONTEXT-MAP.md` exists, read it to find contexts +- If only a root `CONTEXT.md` exists, single context +- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved + +When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/project/aitoearn-backend/.agents/skills/grill-with-docs/SKILL.md b/project/aitoearn-backend/.agents/skills/grill-with-docs/SKILL.md new file mode 100644 index 0000000..6dad6ad --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/grill-with-docs/SKILL.md @@ -0,0 +1,88 @@ +--- +name: grill-with-docs +description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions. +--- + + + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time, waiting for feedback on each question before continuing. + +If a question can be answered by exploring the codebase, explore the codebase instead. + + + + + +## Domain awareness + +During codebase exploration, also look for existing documentation: + +### File structure + +Most repos have a single context: + +``` +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: + +``` +/ +├── CONTEXT-MAP.md +├── docs/ +│ └── adr/ ← system-wide decisions +├── src/ +│ ├── ordering/ +│ │ ├── CONTEXT.md +│ │ └── docs/adr/ ← context-specific decisions +│ └── billing/ +│ ├── CONTEXT.md +│ └── docs/adr/ +``` + +Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. + +## During the session + +### Challenge against the glossary + +When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" + +### Sharpen fuzzy language + +When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." + +### Discuss concrete scenarios + +When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. + +### Cross-reference with code + +When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" + +### Update CONTEXT.md inline + +When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). + +Don't couple `CONTEXT.md` to implementation details. Only include terms that are meaningful to domain experts. + +### Offer ADRs sparingly + +Only offer to create an ADR when all three are true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will wonder "why did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). + + diff --git a/project/aitoearn-backend/.agents/skills/handoff/SKILL.md b/project/aitoearn-backend/.agents/skills/handoff/SKILL.md new file mode 100644 index 0000000..28bfb3a --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/handoff/SKILL.md @@ -0,0 +1,13 @@ +--- +name: handoff +description: Compact the current conversation into a handoff document for another agent to pick up. +argument-hint: "What will the next session be used for?" +--- + +Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save it to a path produced by `mktemp -t handoff-XXXXXX.md` (read the file before you write to it). + +Suggest the skills to be used, if any, by the next session. + +Do not duplicate content already captured in other artifacts (PRDs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead. + +If the user passed arguments, treat them as a description of what the next session will focus on and tailor the doc accordingly. diff --git a/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/DEEPENING.md b/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/DEEPENING.md new file mode 100644 index 0000000..ecaf5d7 --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/DEEPENING.md @@ -0,0 +1,37 @@ +# Deepening + +How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**. + +## Dependency categories + +When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam. + +### 1. In-process + +Pure computation, in-memory state, no I/O. Always deepenable — merge the modules and test through the new interface directly. No adapter needed. + +### 2. Local-substitutable + +Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface. + +### 3. Remote but owned (Ports & Adapters) + +Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter. + +Recommendation shape: *"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."* + +### 4. True external (Mock) + +Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter. + +## Seam discipline + +- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection. +- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them. + +## Testing strategy: replace, don't layer + +- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist — delete them. +- Write new tests at the deepened module's interface. The **interface is the test surface**. +- Tests assert on observable outcomes through the interface, not internal state. +- Tests should survive internal refactors — they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface. diff --git a/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md b/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md new file mode 100644 index 0000000..3197723 --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md @@ -0,0 +1,44 @@ +# Interface Design + +When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best. + +Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**, **leverage**. + +## Process + +### 1. Frame the problem space + +Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate: + +- The constraints any new interface would need to satisfy +- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md)) +- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete + +Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel. + +### 2. Spawn sub-agents + +Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module. + +Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint: + +- Agent 1: "Minimize the interface — aim for 1–3 entry points max. Maximise leverage per entry point." +- Agent 2: "Maximise flexibility — support many use cases and extension." +- Agent 3: "Optimise for the most common caller — make the default case trivial." +- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies." + +Include both [LANGUAGE.md](LANGUAGE.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language. + +Each sub-agent outputs: + +1. Interface (types, methods, params — plus invariants, ordering, error modes) +2. Usage example showing how callers use it +3. What the implementation hides behind the seam +4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md)) +5. Trade-offs — where leverage is high, where it's thin + +### 3. Present and compare + +Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**. + +After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not a menu. diff --git a/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/LANGUAGE.md b/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/LANGUAGE.md new file mode 100644 index 0000000..530c276 --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/LANGUAGE.md @@ -0,0 +1,53 @@ +# Language + +Shared vocabulary for every suggestion this skill makes. Use these terms exactly — don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point. + +## Terms + +**Module** +Anything with an interface and an implementation. Deliberately scale-agnostic — applies equally to a function, class, package, or tier-spanning slice. +_Avoid_: unit, component, service. + +**Interface** +Everything a caller must know to use the module correctly. Includes the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. +_Avoid_: API, signature (too narrow — those refer only to the type-level surface). + +**Implementation** +What's inside a module — its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise. + +**Depth** +Leverage at the interface — the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface. A module is **shallow** when the interface is nearly as complex as the implementation. + +**Seam** _(from Michael Feathers)_ +A place where you can alter behaviour without editing in that place. The *location* at which a module's interface lives. Choosing where to put the seam is its own design decision, distinct from what goes behind it. +_Avoid_: boundary (overloaded with DDD's bounded context). + +**Adapter** +A concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside). + +**Leverage** +What callers get from depth. More capability per unit of interface they have to learn. One implementation pays back across N call sites and M tests. + +**Locality** +What maintainers get from depth. Change, bugs, knowledge, and verification concentrate at one place rather than spreading across callers. Fix once, fixed everywhere. + +## Principles + +- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface. +- **The deletion test.** Imagine deleting the module. If complexity vanishes, the module wasn't hiding anything (it was a pass-through). If complexity reappears across N callers, the module was earning its keep. +- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape. +- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it. + +## Relationships + +- A **Module** has exactly one **Interface** (the surface it presents to callers and tests). +- **Depth** is a property of a **Module**, measured against its **Interface**. +- A **Seam** is where a **Module**'s **Interface** lives. +- An **Adapter** sits at a **Seam** and satisfies the **Interface**. +- **Depth** produces **Leverage** for callers and **Locality** for maintainers. + +## Rejected framings + +- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead. +- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know. +- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**. diff --git a/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/SKILL.md b/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/SKILL.md new file mode 100644 index 0000000..05984a6 --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/improve-codebase-architecture/SKILL.md @@ -0,0 +1,71 @@ +--- +name: improve-codebase-architecture +description: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable. +--- + +# Improve Codebase Architecture + +Surface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability. + +## Glossary + +Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary." Full definitions in [LANGUAGE.md](LANGUAGE.md). + +- **Module** — anything with an interface and an implementation (function, class, package, slice). +- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature. +- **Implementation** — the code inside. +- **Depth** — leverage at the interface: a lot of behaviour behind a small interface. **Deep** = high leverage. **Shallow** = interface nearly as complex as the implementation. +- **Seam** — where an interface lives; a place behaviour can be altered without editing in place. (Use this, not "boundary.") +- **Adapter** — a concrete thing satisfying an interface at a seam. +- **Leverage** — what callers get from depth. +- **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place. + +Key principles (see [LANGUAGE.md](LANGUAGE.md) for the full list): + +- **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep. +- **The interface is the test surface.** +- **One adapter = hypothetical seam. Two adapters = real seam.** + +This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate. + +## Process + +### 1. Explore + +Read the project's domain glossary and any ADRs in the area you're touching first. + +Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction: + +- Where does understanding one concept require bouncing between many small modules? +- Where are modules **shallow** — interface nearly as complex as the implementation? +- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)? +- Where do tightly-coupled modules leak across their seams? +- Which parts of the codebase are untested, or hard to test through their current interface? + +Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want. + +### 2. Present candidates + +Present a numbered list of deepening opportunities. For each candidate: + +- **Files** — which files/modules are involved +- **Problem** — why the current architecture is causing friction +- **Solution** — plain English description of what would change +- **Benefits** — explained in terms of locality and leverage, and also in how tests would improve + +**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service." + +**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids. + +Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?" + +### 3. Grilling loop + +Once the user picks a candidate, drop into a grilling conversation. Walk the design tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive. + +Side effects happen inline as decisions crystallize: + +- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `/grill-with-docs` (see [CONTEXT-FORMAT.md](../grill-with-docs/CONTEXT-FORMAT.md)). Create the file lazily if it doesn't exist. +- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there. +- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See [ADR-FORMAT.md](../grill-with-docs/ADR-FORMAT.md). +- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md). diff --git a/project/aitoearn-backend/.agents/skills/prototype/LOGIC.md b/project/aitoearn-backend/.agents/skills/prototype/LOGIC.md new file mode 100644 index 0000000..526ecb1 --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/prototype/LOGIC.md @@ -0,0 +1,79 @@ +# Logic Prototype + +A tiny interactive terminal app that lets the user drive a state model by hand. Use this when the question is about **business logic, state transitions, or data shape** — the kind of thing that looks reasonable on paper but only feels wrong once you push it through real cases. + +## When this is the right shape + +- "I'm not sure if this state machine handles the edge case where X then Y." +- "Does this data model actually let me represent the case where..." +- "I want to feel out what the API should look like before writing it." +- Anything where the user wants to **press buttons and watch state change**. + +If the question is "what should this look like" — wrong branch. Use [UI.md](UI.md). + +## Process + +### 1. State the question + +Before writing code, write down what state model and what question you're prototyping. One paragraph, in the prototype's README or a comment at the top of the file. A logic prototype that answers the wrong question is pure waste — make the question explicit so it can be checked later, whether the user is watching now or returning to it AFK. + +### 2. Pick the language + +Use whatever the host project uses. If the project has no obvious runtime (e.g. a docs repo), ask. + +Match the project's existing conventions for tooling — don't add a new package manager or runtime just for the prototype. + +### 3. Isolate the logic in a portable module + +Put the actual logic — the bit that's answering the question — behind a small, pure interface that could be lifted out and dropped into the real codebase later. The TUI around it is throwaway; the logic module shouldn't be. + +The right shape depends on the question: + +- **A pure reducer** — `(state, action) => state`. Good when actions are discrete events and state is a single value. +- **A state machine** — explicit states and transitions. Good when "which actions are even legal right now" is part of the question. +- **A small set of pure functions** over a plain data type. Good when there's no implicit current state — just transformations. +- **A class or module with a clear method surface** when the logic genuinely owns ongoing internal state. + +Pick whichever shape best fits the question being asked, *not* whichever is easiest to wire to a TUI. Keep it pure: no I/O, no terminal code, no `console.log` for control flow. The TUI imports it and calls into it; nothing flows the other direction. + +This is what makes the prototype useful past its own lifetime. When the question's been answered, the validated reducer / machine / function set can be lifted into the real module — the TUI shell gets deleted. + +### 4. Build the smallest TUI that exposes the state + +Build it as a **lightweight TUI** — on every tick, clear the screen (`console.clear()` / `print("\033[2J\033[H")` / equivalent) and re-render the whole frame. The user should always see one stable view, not an ever-growing scrollback. + +Each frame has two parts, in this order: + +1. **Current state**, pretty-printed and diff-friendly (one field per line, or formatted JSON). Use **bold** for field names or section headers and **dim** for less important context (timestamps, IDs, derived values). Native ANSI escape codes are fine — `\x1b[1m` bold, `\x1b[2m` dim, `\x1b[0m` reset. No need to pull in a styling library unless one is already in the project. +2. **Keyboard shortcuts**, listed at the bottom: `[a] add user [d] delete user [t] tick clock [q] quit`. Bold the key, dim the description, or vice-versa — whatever reads cleanly. + +Behaviour: + +1. **Initialise state** — a single in-memory object/struct. Render the first frame on start. +2. **Read one keystroke (or one line)** at a time, dispatch to a handler that mutates state. +3. **Re-render** the full frame after every action — don't append, replace. +4. **Loop until quit.** + +The whole frame should fit on one screen. + +### 5. Make it runnable in one command + +Add a script to the project's existing task runner (`package.json` scripts, `Makefile`, `justfile`, `pyproject.toml`). The user should run `pnpm run ` or equivalent — never need to remember a path. + +If the host project has no task runner, just put the command at the top of the prototype's README. + +### 6. Hand it over + +Give the user the run command. They'll drive it themselves; the interesting moments are when they say "wait, that shouldn't be possible" or "huh, I assumed X would be different" — those are the bugs in the _idea_, which is the whole point. If they want new actions added, add them. Prototypes evolve. + +### 7. Capture the answer + +When the prototype has done its job, the answer to the question is the only thing worth keeping. If the user is around, ask what it taught them. If not, leave a `NOTES.md` next to the prototype so the answer can be filled in (or filled in by you, if you've watched the session) before the prototype gets deleted. + +## Anti-patterns + +- **Don't add tests.** A prototype that needs tests is no longer a prototype. +- **Don't wire it to the real database.** Use an in-memory store unless the question is specifically about persistence. +- **Don't generalise.** No "what if we wanted to support X later." The prototype answers one question. +- **Don't blur the logic and the TUI together.** If the reducer / state machine references `console.log`, prompts, or terminal escape codes, it's no longer portable. Keep the TUI as a thin shell over a pure module. +- **Don't ship the TUI shell into production.** The shell is optimised for being driven by hand from a terminal. The logic module behind it is the bit worth keeping. diff --git a/project/aitoearn-backend/.agents/skills/prototype/SKILL.md b/project/aitoearn-backend/.agents/skills/prototype/SKILL.md new file mode 100644 index 0000000..64f3e61 --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/prototype/SKILL.md @@ -0,0 +1,30 @@ +--- +name: prototype +description: Build a throwaway prototype to flesh out a design before committing to it. Routes between two branches — a runnable terminal app for state/business-logic questions, or several radically different UI variations toggleable from one route. Use when the user wants to prototype, sanity-check a data model or state machine, mock up a UI, explore design options, or says "prototype this", "let me play with it", "try a few designs". +--- + +# Prototype + +A prototype is **throwaway code that answers a question**. The question decides the shape. + +## Pick a branch + +Identify which question is being answered — from the user's prompt, the surrounding code, or by asking if the user is around: + +- **"Does this logic / state model feel right?"** → [LOGIC.md](LOGIC.md). Build a tiny interactive terminal app that pushes the state machine through cases that are hard to reason about on paper. +- **"What should this look like?"** → [UI.md](UI.md). Generate several radically different UI variations on a single route, switchable via a URL search param and a floating bottom bar. + +The two branches produce very different artifacts — getting this wrong wastes the whole prototype. If the question is genuinely ambiguous and the user isn't reachable, default to whichever branch better matches the surrounding code (a backend module → logic; a page or component → UI) and state the assumption at the top of the prototype. + +## Rules that apply to both + +1. **Throwaway from day one, and clearly marked as such.** Locate the prototype code close to where it will actually be used (next to the module or page it's prototyping for) so context is obvious — but name it so a casual reader can see it's a prototype, not production. For throwaway UI routes, obey whatever routing convention the project already uses; don't invent a new top-level structure. +2. **One command to run.** Whatever the project's existing task runner supports — `pnpm `, `python `, `bun `, etc. The user must be able to start it without thinking. +3. **No persistence by default.** State lives in memory. Persistence is the thing the prototype is _checking_, not something it should depend on. If the question explicitly involves a database, hit a scratch DB or a local file with a clear "PROTOTYPE — wipe me" name. +4. **Skip the polish.** No tests, no error handling beyond what makes the prototype _runnable_, no abstractions. The point is to learn something fast and then delete it. +5. **Surface the state.** After every action (logic) or on every variant switch (UI), print or render the full relevant state so the user can see what changed. +6. **Delete or absorb when done.** When the prototype has answered its question, either delete it or fold the validated decision into the real code — don't leave it rotting in the repo. + +## When done + +The _answer_ is the only thing worth keeping from a prototype. Capture it somewhere durable (commit message, ADR, issue, or a `NOTES.md` next to the prototype) along with the question it was answering. If the user is around, that capture is a quick conversation; if not, leave the placeholder so they (or you, on the next pass) can fill in the verdict before deleting the prototype. diff --git a/project/aitoearn-backend/.agents/skills/prototype/UI.md b/project/aitoearn-backend/.agents/skills/prototype/UI.md new file mode 100644 index 0000000..f3b6e64 --- /dev/null +++ b/project/aitoearn-backend/.agents/skills/prototype/UI.md @@ -0,0 +1,112 @@ +# UI Prototype + +Generate **several radically different UI variations** on a single route, switchable from a floating bottom bar. The user flips between variants in the browser, picks one (or steals bits from each), then throws the rest away. + +If the question is about logic/state rather than what something looks like — wrong branch. Use [LOGIC.md](LOGIC.md). + +## When this is the right shape + +- "What should this page look like?" +- "I want to see a few options for this dashboard before committing." +- "Try a different layout for the settings screen." +- Any time the user would otherwise spend a day picking between three vague mockups in their head. + +## Two sub-shapes — strongly prefer sub-shape A + +A UI prototype is much easier to judge when it's **butting up against the rest of the app** — real header, real sidebar, real data, real density. A throwaway route on its own is a vacuum: every variant looks fine in isolation. Default to sub-shape A whenever there's a plausible existing page to host the variants. Only reach for sub-shape B if the prototype genuinely has no nearby home. + +### Sub-shape A — adjustment to an existing page (preferred) + +The route already exists. Variants are rendered **on the same route**, gated by a `?variant=` URL search param. The existing data fetching, params, and auth all stay — only the rendering swaps. This is the default; pick it unless there's a specific reason not to. + +If the prototype is for something that doesn't yet have a page but *would naturally live inside one* (a new section of the dashboard, a new card on the settings screen, a new step in an existing flow) — that's still sub-shape A. Mount the variants inside the host page. + +### Sub-shape B — a new page (last resort) + +Only use this when the thing being prototyped genuinely has no existing page to live inside — e.g. an entirely new top-level surface, or a flow that can't be embedded anywhere sensible. + +Create a **throwaway route** following whatever routing convention the project already uses — don't invent a new top-level structure. Name it so it's obviously a prototype (e.g. include the word `prototype` in the path or filename). Same `?variant=` pattern. + +Before committing to sub-shape B, sanity-check: is there really no existing page this could be embedded in? An empty route hides design problems that a populated one would expose. + +In both sub-shapes the floating bottom bar is identical. + +## Process + +### 1. State the question and pick N + +Default to **3 variants**. More than 5 stops being radically different and starts being noise — cap there. + +Write down the plan in one line, in the prototype's location or a top-of-file comment: + +> "Three variants of the settings page, switchable via `?variant=`, on the existing `/settings` route." + +This works whether the user is here to push back or not. + +### 2. Generate radically different variants + +Draft each variant. Hold each one to: + +- The page's purpose and the data it has access to. +- The project's component library / styling system (TailwindCSS, shadcn, MUI, plain CSS, whatever). +- A clear exported component name, e.g. `VariantA`, `VariantB`, `VariantC`. + +Variants must be **structurally different** — different layout, different information hierarchy, different primary affordance, not just different colours. Three slightly-tweaked card grids isn't a UI prototype, it's wallpaper. If two drafts come out too similar, redo one with explicit "do not use a card grid" guidance. + +### 3. Wire them together + +Create a single switcher component on the route: + +```tsx +// pseudo-code — adapt to the project's framework +const variant = searchParams.get('variant') ?? 'A'; +return ( + <> + {variant === 'A' && } + {variant === 'B' && } + {variant === 'C' && } + + +); +``` + +For sub-shape A (existing page): keep all the existing data fetching above the switcher; only the rendered subtree changes per variant. + +For sub-shape B (new page): the throwaway route under `/prototype/` mounts the same switcher. + +### 4. Build the floating switcher + +A small fixed-position bar at the bottom-centre of the screen with three pieces: + +- **Left arrow** — cycles to the previous variant (wraps around). +- **Variant label** — shows the current variant key and, if the variant exports a name, that name too. e.g. `B — Sidebar layout`. +- **Right arrow** — cycles forward (wraps around). + +Behaviour: + +- Clicking an arrow updates the URL search param (use the framework's router — `router.replace` on Next, `navigate` on React Router, etc) so the variant is shareable and reload-stable. +- Keyboard: `←` and `→` arrow keys also cycle. Don't intercept arrow keys when an ``, `