kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: init
commit
8ef2c77a10
|
@ -0,0 +1,14 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This is an example .env file.
|
||||
#
|
||||
# All of these environment vars must be defined either in your environment or in
|
||||
# a local .env file in order to run this app.
|
||||
#
|
||||
# @see https://nextjs.org/docs/basic-features/environment-variables for details.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Replicate API
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
REPLICATE_API_TOKEN=
|
|
@ -0,0 +1 @@
|
|||
github: [transitive-bullshit]
|
|
@ -0,0 +1,51 @@
|
|||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test Node.js ${{ matrix.node-version }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version:
|
||||
- 18
|
||||
- 16
|
||||
- 14
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run test
|
||||
run: pnpm run test
|
|
@ -0,0 +1,49 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
*.swp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.build
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# data dumps
|
||||
out/
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run pre-commit
|
|
@ -0,0 +1 @@
|
|||
enable-pre-post-scripts=true
|
|
@ -0,0 +1,6 @@
|
|||
.snapshots/
|
||||
build/
|
||||
dist/
|
||||
node_modules/
|
||||
.next/
|
||||
.vercel/
|
|
@ -0,0 +1,16 @@
|
|||
module.exports = {
|
||||
plugins: [require('@trivago/prettier-plugin-sort-imports')],
|
||||
singleQuote: true,
|
||||
jsxSingleQuote: true,
|
||||
semi: false,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true,
|
||||
bracketSameLine: false,
|
||||
arrowParens: 'always',
|
||||
trailingComma: 'none',
|
||||
importOrder: ['^node:.*', '<THIRD_PARTY_MODULES>', '^[./]'],
|
||||
importOrderSeparation: true,
|
||||
importOrderSortSpecifiers: true,
|
||||
importOrderGroupNamespaceSpecifiers: true
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Travis Fischer
|
||||
|
||||
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.
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"name": "chatgpt",
|
||||
"version": "0.0.1",
|
||||
"description": "Node.js wrapper around ChatGPT. Uses headless Chrome as a temporary solution until the official API is released.",
|
||||
"author": "Travis Fischer <travis@transitivebullsh.it>",
|
||||
"repository": "transitive-bullshit/chatgpt-api",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
"exports": {
|
||||
"import": "./build/index.js",
|
||||
"default": "./build/index.js",
|
||||
"types": "./build/index.d.ts"
|
||||
},
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"clean": "del build",
|
||||
"prebuild": "run-s clean",
|
||||
"predev": "run-s clean",
|
||||
"pretest": "run-s build",
|
||||
"docs": "typedoc",
|
||||
"prepare": "husky install",
|
||||
"pre-commit": "lint-staged",
|
||||
"test": "run-p test:*",
|
||||
"test:prettier": "prettier '**/*.{js,jsx,ts,tsx}' --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"html-to-md": "npm:@fisch0920/html-to-md@^0.8.1",
|
||||
"joplin-turndown": "^4.0.30",
|
||||
"node-html-markdown": "^1.2.2",
|
||||
"playwright": "^1.28.1",
|
||||
"turndown": "^7.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
|
||||
"@types/node": "^18.11.9",
|
||||
"del-cli": "^5.0.0",
|
||||
"delay": "^5.0.0",
|
||||
"husky": "^8.0.2",
|
||||
"lint-staged": "^13.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"openapi-types": "^12.0.2",
|
||||
"ora": "^6.1.2",
|
||||
"prettier": "^2.8.0",
|
||||
"tsup": "^6.5.0",
|
||||
"tsx": "^3.12.1",
|
||||
"typedoc": "^0.23.21",
|
||||
"typedoc-plugin-markdown": "^3.13.6",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"openai",
|
||||
"chatgpt",
|
||||
"gpt",
|
||||
"gpt3",
|
||||
"gpt4",
|
||||
"chatbot",
|
||||
"chat",
|
||||
"machine learning",
|
||||
"conversation",
|
||||
"conversational ai",
|
||||
"ai",
|
||||
"ml",
|
||||
"bot"
|
||||
]
|
||||
}
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,34 @@
|
|||
# ChatGPT API <!-- omit in toc -->
|
||||
|
||||
> Node.js wrapper around ChatGPT. Uses headless Chrome as a temporary solution until the official API is released.
|
||||
|
||||
[](https://www.npmjs.com/package/chatgpt) [](https://github.com/transitive-bullshit/chatgpt-api/actions/workflows/test.yml) [](https://github.com/transitive-bullshit/chatgpt-api/blob/main/license) [](https://prettier.io)
|
||||
|
||||
- [Intro](#intro)
|
||||
- [Docs](#docs)
|
||||
- [Todo](#todo)
|
||||
- [Related](#related)
|
||||
- [License](#license)
|
||||
|
||||
## Intro
|
||||
|
||||
TODO
|
||||
|
||||
## Docs
|
||||
|
||||
See the [auto-generated docs](./docs/modules.md).
|
||||
|
||||
## Todo
|
||||
|
||||
- [ ] Add message and conversation IDs
|
||||
- [ ] Add support for streaming responses
|
||||
|
||||
## Related
|
||||
|
||||
- Inspired by the [Go module](https://github.com/danielgross/whatsapp-gpt) by [Daniel Gross](https://github.com/danielgross)
|
||||
|
||||
## License
|
||||
|
||||
MIT © [Travis Fischer](https://transitivebullsh.it)
|
||||
|
||||
Support my open source work by <a href="https://twitter.com/transitive_bs">following me on twitter <img src="https://storage.googleapis.com/saasify-assets/twitter-logo.svg" alt="twitter" height="24px" align="center"></a>
|
|
@ -0,0 +1,170 @@
|
|||
import delay from 'delay'
|
||||
import html2md from 'html-to-md'
|
||||
import { type ChromiumBrowserContext, type Page, chromium } from 'playwright'
|
||||
|
||||
export class ChatGPTAPI {
|
||||
protected _userDataDir: string
|
||||
protected _headless: boolean
|
||||
protected _markdown: boolean
|
||||
protected _chatUrl: string
|
||||
|
||||
protected _browser: ChromiumBrowserContext
|
||||
protected _page: Page
|
||||
|
||||
/**
|
||||
* @param opts.userDataDir — Path to a directory for storing persistent chromium session data
|
||||
* @param opts.chatUrl — OpenAI chat URL
|
||||
* @param opts.headless - Whether or not to use headless mode
|
||||
* @param opts.markdown — Whether or not to parse chat messages as markdown
|
||||
*/
|
||||
constructor(
|
||||
opts: {
|
||||
/** @defaultValue `'/tmp/chatgpt'` **/
|
||||
userDataDir?: string
|
||||
|
||||
/** @defaultValue `'https://chat.openai.com/'` **/
|
||||
chatUrl?: string
|
||||
|
||||
/** @defaultValue `false` **/
|
||||
headless?: boolean
|
||||
|
||||
/** @defaultValue `true` **/
|
||||
markdown?: boolean
|
||||
} = {}
|
||||
) {
|
||||
const {
|
||||
userDataDir = '/tmp/chatgpt',
|
||||
chatUrl = 'https://chat.openai.com/',
|
||||
headless = false,
|
||||
markdown = true
|
||||
} = opts
|
||||
|
||||
this._userDataDir = userDataDir
|
||||
this._headless = !!headless
|
||||
this._chatUrl = chatUrl
|
||||
this._markdown = !!markdown
|
||||
}
|
||||
|
||||
async init() {
|
||||
this._browser = await chromium.launchPersistentContext(this._userDataDir, {
|
||||
headless: this._headless
|
||||
})
|
||||
|
||||
this._page = await this._browser.newPage()
|
||||
await this._page.goto(this._chatUrl)
|
||||
|
||||
// dismiss welcome modal
|
||||
do {
|
||||
const modalSelector = '[data-headlessui-state="open"]'
|
||||
if (!(await this._page.isVisible(modalSelector, { timeout: 500 }))) {
|
||||
break
|
||||
}
|
||||
|
||||
const modal = await this._page.locator(modalSelector)
|
||||
if (modal) {
|
||||
await modal.locator('button').last().click()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} while (true)
|
||||
|
||||
return this._page
|
||||
}
|
||||
|
||||
async getIsSignedIn() {
|
||||
const inputBox = await this._getInputBox()
|
||||
return !!inputBox
|
||||
}
|
||||
|
||||
async getLastMessage(): Promise<string | null> {
|
||||
const messages = await this.getMessages()
|
||||
|
||||
if (messages) {
|
||||
return messages[messages.length - 1]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getPrompts(): Promise<string[]> {
|
||||
// Get all prompts
|
||||
const messages = await this._page.$$(
|
||||
'[class*="ConversationItem__Message"]:has([class*="ConversationItem__ActionButtons"]):has([class*="ConversationItem__Role"] [class*="Avatar__Wrapper"])'
|
||||
)
|
||||
|
||||
// prompts are always plaintext
|
||||
return Promise.all(messages.map((a) => a.innerText()))
|
||||
}
|
||||
|
||||
async getMessages(): Promise<string[]> {
|
||||
// Get all complete messages
|
||||
// (in-progress messages that are being streamed back don't contain action buttons)
|
||||
const messages = await this._page.$$(
|
||||
'[class*="ConversationItem__Message"]:has([class*="ConversationItem__ActionButtons"]):not(:has([class*="ConversationItem__Role"] [class*="Avatar__Wrapper"]))'
|
||||
)
|
||||
|
||||
if (this._markdown) {
|
||||
const htmlMessages = await Promise.all(messages.map((a) => a.innerHTML()))
|
||||
|
||||
const markdownMessages = htmlMessages.map((messageHtml) => {
|
||||
// parse markdown from message HTML
|
||||
messageHtml = messageHtml.replace('Copy code</button>', '</button>')
|
||||
return html2md(messageHtml, {
|
||||
ignoreTags: [
|
||||
'button',
|
||||
'svg',
|
||||
'style',
|
||||
'form',
|
||||
'noscript',
|
||||
'script',
|
||||
'meta',
|
||||
'head'
|
||||
],
|
||||
skipTags: ['button', 'svg']
|
||||
})
|
||||
})
|
||||
|
||||
return markdownMessages
|
||||
} else {
|
||||
// plaintext
|
||||
const plaintextMessages = await Promise.all(
|
||||
messages.map((a) => a.innerText())
|
||||
)
|
||||
return plaintextMessages
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(message: string): Promise<string> {
|
||||
const inputBox = await this._getInputBox()
|
||||
if (!inputBox) throw new Error('not signed in')
|
||||
|
||||
const lastMessage = await this.getLastMessage()
|
||||
|
||||
await inputBox.click()
|
||||
await inputBox.fill(message)
|
||||
await inputBox.press('Enter')
|
||||
|
||||
do {
|
||||
await delay(1000)
|
||||
|
||||
// TODO: this logic needs some work because we can have repeat messages...
|
||||
const newLastMessage = await this.getLastMessage()
|
||||
if (
|
||||
newLastMessage &&
|
||||
lastMessage?.toLowerCase() !== newLastMessage?.toLowerCase()
|
||||
) {
|
||||
return newLastMessage
|
||||
}
|
||||
} while (true)
|
||||
}
|
||||
|
||||
async close() {
|
||||
return await this._browser.close()
|
||||
}
|
||||
|
||||
protected async _getInputBox(): Promise<any> {
|
||||
return this._page.$(
|
||||
'div[class*="PromptTextarea__TextareaWrapper"] textarea'
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import delay from 'delay'
|
||||
import { oraPromise } from 'ora'
|
||||
|
||||
import { ChatGPTAPI } from './chatgpt-api'
|
||||
|
||||
/**
|
||||
* Example CLI for testing functionality.
|
||||
*/
|
||||
async function main() {
|
||||
const api = new ChatGPTAPI()
|
||||
await api.init()
|
||||
|
||||
const isSignedIn = await api.getIsSignedIn()
|
||||
|
||||
if (!isSignedIn) {
|
||||
// Wait until the user signs in via the chromium browser
|
||||
await oraPromise(
|
||||
new Promise<void>(async (resolve, reject) => {
|
||||
try {
|
||||
await delay(1000)
|
||||
const isSignedIn = await api.getIsSignedIn()
|
||||
if (isSignedIn) {
|
||||
return resolve()
|
||||
}
|
||||
} catch (err) {
|
||||
return reject(err)
|
||||
}
|
||||
}),
|
||||
'Please sign in to ChatGPT'
|
||||
)
|
||||
}
|
||||
|
||||
const response = await api.sendMessage(
|
||||
// 'Write a TypeScript function for conway sort.'
|
||||
'Write a python version of bubble sort. Do not include example usage.'
|
||||
)
|
||||
// const prompts = await api.getPrompts()
|
||||
// const messages = await api.getMessages()
|
||||
// console.log('prompts', prompts)
|
||||
// console.log('messages', messages)
|
||||
|
||||
// Wait forever; useful for debugging chromium session
|
||||
// await new Promise(() => {})
|
||||
|
||||
await api.close()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
main().then((res) => {
|
||||
console.log(res)
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
export * from './chatgpt-api'
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"lib": ["esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"noEmit": true
|
||||
},
|
||||
"exclude": ["node_modules", "build"],
|
||||
"include": ["**/*.ts"]
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
outDir: 'build',
|
||||
target: 'node14',
|
||||
platform: 'node',
|
||||
format: ['esm'],
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
minify: true,
|
||||
shims: false,
|
||||
dts: true
|
||||
})
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"entryPoints": ["./src/index.ts"],
|
||||
"exclude": ["**/*.test.ts"],
|
||||
"plugin": ["typedoc-plugin-markdown"],
|
||||
"out": "docs",
|
||||
"hideBreadcrumbs": false,
|
||||
"hideInPageTOC": false,
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"excludeExternals": true,
|
||||
"excludeInternal": true,
|
||||
"entryDocument": "readme.md"
|
||||
}
|
Ładowanie…
Reference in New Issue