├── .gitignore
├── .github
└── workflows
│ ├── update-changelog.yml
│ ├── unit-tests.yml
│ └── cs-fixer.yml
├── tests
├── V2Test.php
├── Pest.php
└── V1Test.php
├── phpunit.xml
├── LICENSE
├── composer.json
├── .php-cs-fixer.dist.php
├── CHANGELOG.md
├── README.md
└── src
├── V2.php
└── V1.php
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.phar
2 | composer.lock
3 | /vendor/
4 | .idea
5 |
6 | .phpunit.result.cache
7 | .php-cs-fixer.cache
8 |
--------------------------------------------------------------------------------
/.github/workflows/update-changelog.yml:
--------------------------------------------------------------------------------
1 | name: "Update Changelog"
2 |
3 | on:
4 | release:
5 | types: [released]
6 |
7 | jobs:
8 | update:
9 | runs-on: ubuntu-latest
10 |
11 | permissions:
12 | contents: write
13 |
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v3
17 | with:
18 | ref: main
19 |
20 | - name: Update Changelog
21 | uses: stefanzweifel/changelog-updater-action@v1
22 | with:
23 | latest-version: ${{ github.event.release.tag_name }}
24 | release-notes: ${{ github.event.release.body }}
25 |
26 | - name: Commit updated CHANGELOG
27 | uses: stefanzweifel/git-auto-commit-action@v4
28 | with:
29 | branch: main
30 | commit_message: Update CHANGELOG
31 | file_pattern: CHANGELOG.md
--------------------------------------------------------------------------------
/tests/V2Test.php:
--------------------------------------------------------------------------------
1 | addMessage(
8 | 'You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.',
9 | 'system'
10 | );
11 |
12 | it('should get a new conversation', function () use ($chatGPT) {
13 | $return = $chatGPT->ask('Hello, how are you?');
14 | foreach ($return as $answer) {
15 | $this->assertArrayHasKey('answer', $answer);
16 | }
17 | })->group('working');
18 |
19 | it('should get a answer contact the context', function () use ($chatGPT) {
20 | $chatGPT->ask('Hello, how are you?');
21 | $return = $chatGPT->ask('What did I ask you just now?');
22 | foreach ($return as $answer) {
23 | $this->assertArrayHasKey('answer', $answer);
24 | $this->assertStringContainsString('Hello, how are you?', $answer['answer']);
25 | }
26 | })->group('working');
27 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests
15 |
16 |
17 |
18 |
19 | ./src
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.github/workflows/unit-tests.yml:
--------------------------------------------------------------------------------
1 | name: Unit Tests
2 |
3 | on:
4 | push:
5 | branches: [ "main", "develop" ]
6 | pull_request:
7 | branches: [ "main", "develop" ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | unit-tests:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | env:
18 | OPENAI_SECRET_KEY: ${{ secrets.OPENAI_SECRET_KEY }}
19 | OPENAI_ACCESS_TOKEN: ${{ secrets.OPENAI_ACCESS_TOKEN }}
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 |
24 | - name: Validate composer.json and composer.lock
25 | run: composer validate --strict
26 |
27 | - name: Install dependencies
28 | run: composer install --prefer-dist --no-progress
29 |
30 | - name: Cache Composer packages
31 | id: composer-cache
32 | uses: actions/cache@v3
33 | with:
34 | path: vendor
35 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
36 | restore-keys: |
37 | ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
38 |
39 | - name: Run test suite
40 | run: composer run-script test
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 HaoZi-Team
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "haozi-team/chatgpt-php",
3 | "description": "Real ChatGPT PHP SDK (Not GPT-3)",
4 | "keywords": [
5 | "chatgpt",
6 | "openai"
7 | ],
8 | "homepage": "https://github.com/HaoZi-Team/ChatGPT-PHP",
9 | "type": "library",
10 | "require": {
11 | "php": ">=7.4",
12 | "guzzlehttp/guzzle": "^7.2",
13 | "ramsey/uuid": "^4.2",
14 | "ext-json": "*",
15 | "ext-curl": "*"
16 | },
17 | "require-dev": {
18 | "pestphp/pest": "^1.22"
19 | },
20 | "license": "MIT",
21 | "autoload": {
22 | "psr-4": {
23 | "HaoZiTeam\\ChatGPT\\": "src/"
24 | }
25 | },
26 | "autoload-dev": {
27 | "psr-4": {
28 | "HaoZiTeam\\ChatGPT\\Tests\\": "tests/"
29 | }
30 | },
31 | "authors": [
32 | {
33 | "name": "耗子",
34 | "email": "i@haozi.net"
35 | }
36 | ],
37 | "scripts": {
38 | "test": "vendor/bin/pest",
39 | "test-coverage": "vendor/bin/pest --coverage",
40 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes"
41 | },
42 | "config": {
43 | "allow-plugins": {
44 | "pestphp/pest-plugin": true
45 | },
46 | "platform": {
47 | "php": "7.4"
48 | }
49 | },
50 | "prefer-stable": true
51 | }
52 |
--------------------------------------------------------------------------------
/.github/workflows/cs-fixer.yml:
--------------------------------------------------------------------------------
1 | name: CS Fixer
2 |
3 | on:
4 | push:
5 | branches: [ "main", "develop" ]
6 | pull_request:
7 | branches: [ "main", "develop" ]
8 |
9 | permissions:
10 | contents: write
11 |
12 | jobs:
13 | cs-fixer:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 |
20 | - name: Validate composer.json and composer.lock
21 | run: composer validate --strict
22 |
23 | - name: Install dependencies
24 | run: composer install --prefer-dist --no-progress
25 |
26 | - name: Cache Composer packages
27 | id: composer-cache
28 | uses: actions/cache@v3
29 | with:
30 | path: vendor
31 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
32 | restore-keys: |
33 | ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
34 |
35 | - name: Run PHP CS Fixer
36 | uses: docker://oskarstark/php-cs-fixer-ga
37 | with:
38 | args: --config=.php-cs-fixer.dist.php --allow-risky=yes
39 |
40 | - name: Commit changes
41 | uses: stefanzweifel/git-auto-commit-action@v4
42 | with:
43 | commit_message: Fix styling
44 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | in([
5 | __DIR__ . '/src',
6 | __DIR__ . '/tests',
7 | ])
8 | ->name('*.php')
9 | ->ignoreDotFiles(true)
10 | ->ignoreVCS(true);
11 |
12 | return (new PhpCsFixer\Config())
13 | ->setRules([
14 | '@PSR12' => true,
15 | 'array_syntax' => ['syntax' => 'short'],
16 | 'ordered_imports' => ['sort_algorithm' => 'alpha'],
17 | 'no_unused_imports' => true,
18 | 'not_operator_with_successor_space' => true,
19 | 'trailing_comma_in_multiline' => true,
20 | 'ternary_to_null_coalescing' => true,
21 | 'phpdoc_scalar' => true,
22 | 'unary_operator_spaces' => true,
23 | 'binary_operator_spaces' => true,
24 | 'blank_line_before_statement' => [
25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
26 | ],
27 | 'phpdoc_single_line_var_spacing' => true,
28 | 'phpdoc_var_without_name' => true,
29 | 'class_attributes_separation' => [
30 | 'elements' => ['const' => 'one', 'method' => 'one', 'property' => 'one', 'trait_import' => 'none', 'case' => 'none'],
31 | ],
32 | ])
33 | ->setFinder($finder);
34 |
--------------------------------------------------------------------------------
/tests/Pest.php:
--------------------------------------------------------------------------------
1 | in('Feature');
15 |
16 | /*
17 | |--------------------------------------------------------------------------
18 | | Expectations
19 | |--------------------------------------------------------------------------
20 | |
21 | | When you're writing tests, you often need to check that values meet certain conditions. The
22 | | "expect()" function gives you access to a set of "expectations" methods that you can use
23 | | to assert different things. Of course, you may extend the Expectation API at any time.
24 | |
25 | */
26 |
27 | expect()->extend('toBeOne', function () {
28 | return $this->toBe(1);
29 | });
30 |
31 | /*
32 | |--------------------------------------------------------------------------
33 | | Functions
34 | |--------------------------------------------------------------------------
35 | |
36 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your
37 | | project that you don't want to repeat in every file. Here you can also expose helpers as
38 | | global functions to help you to reduce the number of lines of code in your test files.
39 | |
40 | */
41 |
42 | function something()
43 | {
44 | // ..
45 | }
46 |
--------------------------------------------------------------------------------
/tests/V1Test.php:
--------------------------------------------------------------------------------
1 | addAccount($accessToken, 0, 'gpt-4');
8 | $test = $chatGPT->ask('Hello');
9 | foreach ($test as $answer) {
10 | $conversationId = $answer['conversation_id'];
11 | $parentId = $answer['id'];
12 | }
13 |
14 | it('should get a new conversation', function () use ($chatGPT) {
15 | $return = $chatGPT->ask('Hello');
16 | foreach ($return as $answer) {
17 | $this->assertArrayHasKey('answer', $answer);
18 | }
19 | })->group('working');
20 |
21 | it('should get a conversations array', function () use ($chatGPT) {
22 | $return = $chatGPT->getConversations();
23 | $this->assertIsArray($return);
24 | })->group('working');
25 |
26 | it('should get an array of a conversation', function () use ($chatGPT, $conversationId, $parentId) {
27 | $return = $chatGPT->getConversationMessages($conversationId);
28 | $this->assertIsArray($return);
29 | })->group('working');
30 |
31 | it('should auto generate conversation title', function () use ($chatGPT, $conversationId, $parentId) {
32 | $return = $chatGPT->generateConversationTitle($conversationId, $parentId);
33 | $this->assertTrue($return);
34 | })->group('working');
35 |
36 | it('should setting conversation title', function () use ($chatGPT, $conversationId, $parentId) {
37 | $return = $chatGPT->updateConversationTitle($conversationId, 'test');
38 | $this->assertTrue($return);
39 | })->group('working');
40 |
41 | it('should delete conversation', function () use ($chatGPT, $conversationId, $parentId) {
42 | $return = $chatGPT->deleteConversation($conversationId);
43 | $this->assertTrue($return);
44 | })->group('working');
45 |
46 | it('should delete conversations', function () use ($chatGPT) {
47 | $return = $chatGPT->clearConversations();
48 | $this->assertTrue($return);
49 | })->group('working');
50 |
51 | it('should return plugins list', function () use ($chatGPT) {
52 | $return = $chatGPT->getPlugins();
53 | $this->assertIsArray($return);
54 | })->group('working');
55 |
56 | it('should change history and training status', function () use ($chatGPT) {
57 | $return = $chatGPT->setChatHistoryAndTraining(true);
58 | $this->assertTrue($return);
59 | })->group('working');
60 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to `chatgpt-php` will be documented in this file.
4 |
5 | ## 2.1.2 - 2023-07-17
6 |
7 | 1. fix undefined paid.
8 |
9 | ## 2.1.1 - 2023-07-17
10 |
11 | 1. fix name undefined.
12 |
13 | ## 2.1.0 - 2023-07-06
14 |
15 | 1. **Deprecated: `paid` parameter in `addAccount` method.**
16 | 2. Add arkose_token and fix GPT-4.
17 | 3. `addAccount` method add arkose_token parameter.
18 | 4. `ask` method adds more return values to support continue writing.
19 | 5. Add some new method.
20 |
21 | ## 2.0.4 - 2023-04-25
22 |
23 | 1. Update V1 API
24 |
25 | ## 2.0.3 - 2023-03-31
26 |
27 | 1. Sync V1 V2 return.
28 | 2. Update tests.
29 |
30 | ## 2.0.2 - 2023-03-31
31 |
32 | 1. Bug Fix
33 | 2. Doc update
34 |
35 | ## 2.0.1 - 2023-03-31
36 |
37 | 1. Doc update.
38 |
39 | ## 2.0.0 - 2023-03-31
40 |
41 | 1. Update stream mode return.
42 |
43 | ## 1.8.0 - 2023-03-26
44 |
45 | Update V1 API and change error thrown.
46 |
47 | ## 1.7.2 - 2023-03-17
48 |
49 | Fix V1 PHP7.4 support.
50 |
51 | ## 1.7.1 - 2023-03-15
52 |
53 | V1 support GPT-4 now.
54 |
55 | ## 1.7.0 - 2023-03-10
56 |
57 | Update new V1 api endpoint.
58 |
59 | ## 1.6.2 - 2023-03-09
60 |
61 | Fix errors
62 |
63 | ## 1.6.1 - 2023-03-09
64 |
65 | Add headers for bypass cloudflare 403.
66 |
67 | ## 1.6.0 - 2023-03-09
68 |
69 | Add PHP 7.4 Support.
70 |
71 | ## 1.5.3 - 2023-03-09
72 |
73 | Fix V1 endpoint. (By Pawan's API)
74 |
75 | ## 1.5.2 - 2023-03-06
76 |
77 | Use apps.openai.com as V1 default api endpoint.
78 |
79 | ## 1.5.1 - 2023-03-06
80 |
81 | Remove str_replace.
82 | It may cause return code string error.
83 |
84 | ## 1.5.0 - 2023-03-03
85 |
86 | Fix spelling error.
87 |
88 | ## 1.4.5 - 2023-03-03
89 |
90 | Add checkStreamFields method.
91 |
92 | ## 1.4.4 - 2023-03-03
93 |
94 | Update V2 tests.
95 |
96 | ## 1.4.3 - 2023-03-03
97 |
98 | Update CHANGELOG.
99 |
100 | ## 1.4.2 - 2023-03-03
101 |
102 | Add unit tests and some actions.
103 | Fix some errors.
104 |
105 | ## 1.4.1 - 2023-03-03
106 |
107 | Fix construct error.
108 |
109 | ## 1.4.0 - 2023-03-02
110 |
111 | Update LICENSE.
112 |
113 | ## 1.3.1 - 2023-03-02
114 |
115 | Update README.
116 |
117 | ## 1.3.0 - 2023-03-02
118 |
119 | Rename message return field to answer.
120 |
121 | ## 1.2.0 - 2023-03-02
122 |
123 | Add BaseUrl setting for V2.
124 |
125 | ## 1.1.2 - 2023-03-02
126 |
127 | Update V1 check fields.
128 |
129 | ## 1.1.1 - 2023-03-02
130 |
131 | Add catch for Guzzle.
132 |
133 | ## 1.1.0 - 2023-03-02
134 |
135 | Fix V2 Official API Authorization Error.
136 |
137 | ## 1.0.0 - 2023-03-02
138 |
139 | Ready for production.
140 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ChatGPT PHP SDK | [Package](https://packagist.org/packages/haozi-team/chatgpt-php)
2 |
3 | [](https://packagist.org/packages/haozi-team/chatgpt-php)
4 | [](https://packagist.org/packages/haozi-team/chatgpt-php)
5 | [](https://packagist.org/packages/haozi-team/chatgpt-php)
6 |
7 | Official and Reverse Engineered ChatGPT API for PHP.
8 |
9 | Reconstruct from @acheong08's [ChatGPT](https://github.com/acheong08/ChatGPT)
10 |
11 | # Installation
12 |
13 | `composer require haozi-team/chatgpt-php`
14 |
15 | # V1 Web ChatGPT
16 |
17 | > Uses `chat.openai.com`
18 | > - Free
19 | > - Rate limited
20 | > - Needs Bypassing Cloudflare
21 |
22 | > Default api endpoint is `https://ai.fakeopen.com/api/` by @pengzhile
23 | >
24 | > OpenAI rate limit: 50 requests per hour on free accounts. You can get around it with multi-account cycling
25 | >
26 | > Plus accounts has around 150 requests per hour rate limit
27 | >
28 | > Arkose Token: Recently, OpenAI began to require Arkose Token while bypassing Cloudflare request conversation API, usually the SDK can get it automatically through @pengzhile's API
29 |
30 | ## Configuration
31 |
32 | 1. Create account on [OpenAI's ChatGPT](https://chat.openai.com/)
33 | 2. Save your email and password
34 |
35 | ### Authentication
36 |
37 | #### - Access token
38 |
39 | Login OpenAI account and go to [https://chat.openai.com/api/auth/session](https://chat.openai.com/api/auth/session)
40 | to get your access_token.
41 |
42 | ```json
43 | {
44 | "access_token": ""
45 | }
46 | ```
47 |
48 | The access_token is valid for 30 days.
49 |
50 | ## Developer API
51 |
52 | ### Basic example
53 |
54 | ```php
55 | addAccount('');
60 |
61 | $answers = $chatGPT->ask('Hello, how are you?');
62 | foreach ($answers as $item) {
63 | print_r($item);
64 | }
65 | //Array(
66 | // 'answer' => 'I am fine, thank you.',
67 | // 'conversation_id' => '',
68 | // 'parent_id' => '',
69 | // 'model' => 'text-davinci-002-render-sha',
70 | // 'account' => '0',
71 | //)
72 | ```
73 |
74 | ### Advanced example
75 |
76 | You can pass "baseUrl" to the first parameter to set a custom API endpoint.
77 |
78 | ```php
79 | Recently released by OpenAI
91 | > - Costs money
92 |
93 | Get API key from https://platform.openai.com/account/api-keys
94 |
95 | ## Developer API
96 |
97 | ### Basic example
98 |
99 | ```php
100 | ');
104 | $chatGPT->addMessage('You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.', 'system');
105 |
106 | $answers = $chatGPT->ask('Hello, how are you?');
107 | foreach ($answers as $item) {
108 | print_r($item);
109 | }
110 | ```
111 |
112 | ### Advanced example
113 |
114 | You can pass "baseUrl" to the second parameter to set a custom API endpoint.
115 |
116 | ```php
117 | ', 'https://api.openai.com/');
121 | ```
122 |
123 | You can use `addMessage` to add messages to the conversation.
124 |
125 | ```php
126 | ');
130 | $chatGPT->addMessage('You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.', 'system');
131 | $chatGPT->addMessage('Hello, how are you?', 'user');
132 | $chatGPT->addMessage('I am fine, thank you.', 'assistant');
133 |
134 | $answers = $chatGPT->ask('What did I ask you before?');
135 | foreach ($answers as $item) {
136 | print_r($item);
137 | }
138 | //Array(
139 | // 'answer' => 'Hello, how are you?',
140 | // 'id' => 'cmpl-xxxxx',
141 | // 'model' => 'gpt-3.5-turbo',
142 | // 'usage' => [
143 | // "prompt_tokens": 9,
144 | // "completion_tokens": 12,
145 | // "total_tokens": 21,
146 | // ],
147 | //)
148 | ```
149 |
150 | You can set the `stream` parameter to `true` to get a stream for output answers as they are generated.
151 |
152 | ```php
153 | ');
157 | $chatGPT->addMessage('You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.', 'system');
158 |
159 | $answers = $chatGPT->ask('Hello, how are you?', null, true);// A Generator
160 | foreach ($answers as $item) {
161 | print_r($item);
162 | }
163 | ```
164 |
165 | # Disclaimers
166 |
167 | This is not an official OpenAI product. This is a personal project and is not affiliated with OpenAI in any way. Don't
168 | sue me.
169 |
170 | # Credits
171 |
172 | - [acheong08](https://github.com/acheong08) - Python ChatGPT API
173 |
--------------------------------------------------------------------------------
/src/V2.php:
--------------------------------------------------------------------------------
1 | key = 'Bearer '.$key;
37 | if ($baseUrl) {
38 | $this->baseUrl = $baseUrl;
39 | }
40 | if ($model) {
41 | $this->model = $model;
42 | }
43 | if ($temperature) {
44 | $this->temperature = $temperature;
45 | }
46 | if ($topP) {
47 | $this->topP = $topP;
48 | }
49 |
50 | $this->http = new Client([
51 | 'base_uri' => $this->baseUrl,
52 | 'timeout' => $timeout,
53 | 'stream' => true,
54 | ]);
55 | }
56 |
57 | /**
58 | * 添加消息
59 | *
60 | * @param string $message
61 | * @param string $role
62 | *
63 | * @return void
64 | */
65 | public function addMessage(string $message, string $role = 'user'): void
66 | {
67 | $this->messages[] = [
68 | 'role' => $role,
69 | 'content' => $message,
70 | ];
71 | }
72 |
73 | /**
74 | * 发送消息
75 | *
76 | * @param string $prompt
77 | * @param string|null $user
78 | * @param bool $stream
79 | *
80 | * @return Generator
81 | * @throws Exception|GuzzleException
82 | */
83 | public function ask(string $prompt, string $user = null, bool $stream = false): Generator
84 | {
85 | // 将消息添加到消息列表中
86 | $this->addMessage($prompt);
87 |
88 | $data = [
89 | 'model' => $this->model,
90 | 'messages' => $this->messages,
91 | 'stream' => $stream,
92 | 'temperature' => $this->temperature,
93 | 'top_p' => $this->topP,
94 | 'n' => 1,
95 | 'user' => $user ?? 'chatgpt-php',
96 | ];
97 |
98 | try {
99 | $response = $this->http->post(
100 | 'v1/chat/completions',
101 | [
102 | 'json' => $data,
103 | 'headers' => [
104 | 'Authorization' => $this->key,
105 | ],
106 | 'stream' => $stream,
107 | ]
108 | );
109 | } catch (RequestException $e) {
110 | if ($e->hasResponse()) {
111 | throw new Exception(Psr7\Message::toString($e->getResponse()));
112 | } else {
113 | throw new Exception($e->getMessage());
114 | }
115 | }
116 |
117 | $answer = '';
118 |
119 | // 流模式下,返回一个生成器
120 | if ($stream) {
121 | $data = $response->getBody();
122 | while (! $data->eof()) {
123 | $raw = Psr7\Utils::readLine($data);
124 | $line = self::formatStreamMessage($raw);
125 | if (self::checkStreamFields($line)) {
126 | $answer = $line['choices'][0]['delta']['content'];
127 | $messageId = $line['id'];
128 |
129 | yield [
130 | "answer" => $answer,
131 | "id" => $messageId,
132 | "model" => $this->model,
133 | ];
134 | }
135 | unset($raw, $line);
136 | }
137 | $this->addMessage($answer, 'assistant');
138 | } else {
139 | $data = json_decode($response->getBody()->getContents(), true);
140 | if (json_last_error() !== JSON_ERROR_NONE) {
141 | throw new Exception('Response is not json');
142 | }
143 |
144 | if (! $this->checkFields($data)) {
145 | throw new Exception('Field missing');
146 | }
147 |
148 | $answer = $data['choices'][0]['message']['content'];
149 | $this->addMessage($answer, 'assistant');
150 |
151 | yield [
152 | 'answer' => $answer,
153 | 'id' => $data['id'],
154 | 'model' => $this->model,
155 | 'usage' => $data['usage'],
156 | ];
157 | }
158 | }
159 |
160 | /**
161 | * 检查响应行是否包含必要的字段
162 | *
163 | * @param mixed $line
164 | *
165 | * @return bool
166 | */
167 | public function checkFields($line): bool
168 | {
169 | return isset($line['choices'][0]['message']['content']) && isset($line['id']) && isset($line['usage']);
170 | }
171 |
172 | /**
173 | * 检查流响应行是否包含必要的字段
174 | *
175 | * @param mixed $line
176 | *
177 | * @return bool
178 | */
179 | public function checkStreamFields($line): bool
180 | {
181 | return isset($line['choices'][0]['delta']['content']) && isset($line['id']);
182 | }
183 |
184 | /**
185 | * 格式化流消息为数组
186 | *
187 | * @param string $line
188 | *
189 | * @return mixed
190 | */
191 | public function formatStreamMessage(string $line)
192 | {
193 | preg_match('/data: (.*)/', $line, $matches);
194 | if (empty($matches[1])) {
195 | return false;
196 | }
197 |
198 | $line = $matches[1];
199 | $data = json_decode($line, true);
200 |
201 | if (json_last_error() !== JSON_ERROR_NONE) {
202 | return false;
203 | }
204 |
205 | return $data;
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/src/V1.php:
--------------------------------------------------------------------------------
1 | baseUrl = $baseUrl;
25 | }
26 |
27 | $this->http = new Client([
28 | 'base_uri' => $this->baseUrl,
29 | 'timeout' => $timeout,
30 | 'stream' => true,
31 | ]);
32 | }
33 |
34 | /**
35 | * 设置账号
36 | *
37 | * @param string $accessToken
38 | * @param mixed $name
39 | * @param string $model
40 | * @param bool $historyAndTrainingDisabled
41 | * @param string|null $arkoseToken
42 | * @return void
43 | */
44 | public function addAccount(string $accessToken, $name = null, string $model = 'text-davinci-002-render-sha', bool $historyAndTrainingDisabled = false, string $arkoseToken = null): void
45 | {
46 | if ($name === null) {
47 | $this->accounts[] = [
48 | 'access_token' => $accessToken,
49 | 'model' => $model,
50 | 'history_and_training_disabled' => $historyAndTrainingDisabled,
51 | 'arkose_token' => $arkoseToken,
52 | ];
53 | if ($arkoseToken === null) {
54 | try {
55 | $this->accounts[count($this->accounts) - 1]['arkose_token'] = $this->getArkoseToken();
56 | } catch (Exception $e) {
57 | $this->accounts[count($this->accounts) - 1]['arkose_token'] = '';
58 | }
59 | }
60 | } else {
61 | $this->accounts[$name] = [
62 | 'access_token' => $accessToken,
63 | 'model' => $model,
64 | 'history_and_training_disabled' => $historyAndTrainingDisabled,
65 | 'arkose_token' => $arkoseToken,
66 | ];
67 | if ($arkoseToken === null) {
68 | try {
69 | $this->accounts[$name]['arkose_token'] = $this->getArkoseToken();
70 | } catch (Exception $e) {
71 | $this->accounts[$name]['arkose_token'] = '';
72 | }
73 | }
74 | }
75 | }
76 |
77 | /**
78 | * 获取账号
79 | *
80 | * @param string $name
81 | *
82 | * @return array
83 | */
84 | public function getAccount(string $name): array
85 | {
86 | return $this->accounts[$name];
87 | }
88 |
89 | /**
90 | * 获取所有账号
91 | * @return array
92 | */
93 | public function getAccounts(): array
94 | {
95 | return $this->accounts;
96 | }
97 |
98 | /**
99 | * 发送消息
100 | *
101 | * @param string $prompt
102 | * @param string|null $conversationId
103 | * @param string|null $parentId
104 | * @param mixed $account
105 | * @param bool $stream
106 | *
107 | * @return Generator
108 | * @throws Exception|GuzzleException
109 | */
110 | public function ask(
111 | string $prompt,
112 | string $conversationId = null,
113 | string $parentId = null,
114 | $account = null,
115 | bool $stream = false
116 | ): Generator {
117 | // 如果账号为空,则随机选择一个账号
118 | if ($account === null) {
119 | $account = array_rand($this->accounts);
120 |
121 | try {
122 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']);
123 | } catch (Exception $e) {
124 | throw new Exception("Account " . $account . " is invalid");
125 | }
126 | } else {
127 | $token = isset($this->accounts[$account]['access_token']) ? $this->accessTokenToJWT($this->accounts[$account]['access_token']) : null;
128 | }
129 |
130 | // 如果账号为空,则抛出异常
131 | if ($token === null) {
132 | throw new Exception("No account available");
133 | }
134 |
135 | // 设置了父消息ID,必须设置会话ID
136 | if ($parentId !== null && $conversationId === null) {
137 | throw new Exception("conversation_id must be set once parent_id is set");
138 | }
139 |
140 | // 如果会话ID与父消息ID都为空,则开启新的会话
141 | if ($conversationId === null && $parentId === null) {
142 | $parentId = (string)Uuid::uuid4();
143 | }
144 |
145 | // 如果会话ID不为空,但是父消息ID为空,则尝试从ChatGPT获取历史记录
146 | if ($conversationId !== null && $parentId === null) {
147 | try {
148 | $response = $this->http->get('conversation/' . $conversationId, [
149 | 'headers' => [
150 | 'Authorization' => $token,
151 | 'Content-Type' => 'application/json',
152 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
153 | 'Referer' => 'https://chat.openai.com/chat',
154 | ],
155 | ]);
156 | } catch (GuzzleException $e) {
157 | throw new Exception("Request failed: " . $e->getMessage());
158 | }
159 |
160 | $response = json_decode($response->getBody()->getContents(), true);
161 | if (isset($response['current_node'])) {
162 | // 如果获取到了父消息ID,则使用该父消息ID
163 | $conversationId = $response['current_node'];
164 | } else {
165 | // 如果没有获取到父消息ID,则开启新的会话
166 | $conversationId = null;
167 | $parentId = (string)Uuid::uuid4();
168 | }
169 | }
170 |
171 | $data = [
172 | 'action' => 'next',
173 | 'messages' => [
174 | [
175 | 'id' => (string)Uuid::uuid4(),
176 | 'role' => 'user',
177 | 'author' => ['role' => 'user'],
178 | 'content' => ['content_type' => 'text', 'parts' => [$prompt]],
179 | ],
180 | ],
181 | 'suggestions' => [],
182 | 'conversation_id' => $conversationId,
183 | 'parent_message_id' => $parentId,
184 | 'model' => $this->accounts[$account]['model'],
185 | 'arkose_token' => $this->accounts[$account]['arkose_token'],
186 | 'history_and_training_disabled' => $this->accounts[$account]['history_and_training_disabled'],
187 | 'timezone_offset_min' => -480,
188 | ];
189 |
190 | try {
191 | $response = $this->http->post(
192 | 'conversation',
193 | [
194 | 'json' => $data,
195 | 'headers' => [
196 | 'Authorization' => $token,
197 | 'Accept' => 'text/event-stream',
198 | 'Content-Type' => 'application/json',
199 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
200 | 'X-Openai-Assistant-App-Id' => '',
201 | 'Connection' => 'close',
202 | 'Accept-Language' => 'en-US,en;q=0.9',
203 | 'Referer' => 'https://chat.openai.com/chat',
204 | ],
205 | 'stream' => true,
206 | ]
207 | );
208 | } catch (RequestException $e) {
209 | if ($e->hasResponse()) {
210 | throw new Exception(Psr7\Message::toString($e->getResponse()));
211 | } else {
212 | throw new Exception($e->getMessage());
213 | }
214 | }
215 |
216 | $answer = '';
217 | $conversationId = '';
218 | $messageId = '';
219 | $model = '';
220 |
221 | // 流模式下,返回一个生成器
222 | if ($stream) {
223 | $data = $response->getBody();
224 | while (! $data->eof()) {
225 | $raw = Psr7\Utils::readLine($data);
226 | $line = self::formatStreamMessage($raw);
227 | if (self::checkFields($line)) {
228 | $answer = $line['message']['content']['parts'][0];
229 | $conversationId = $line['conversation_id'] ?? null;
230 | $messageId = $line['message']['id'] ?? null;
231 | $model = $line["message"]["metadata"]["model_slug"] ?? null;
232 | $finish_details = $line["message"]["metadata"]["finish_details"]['type'] ?? null;
233 | $end_turn = $line["message"]["end_turn"] ?? true;
234 | $recipient = $line["message"]["recipient"] ?? "all";
235 | $citations = $line["message"]["metadata"]["citations"] ?? [];
236 |
237 | yield [
238 | "answer" => $answer,
239 | "id" => $messageId,
240 | 'conversation_id' => $conversationId,
241 | "model" => $model,
242 | "account" => $account,
243 | 'finish_details' => $finish_details,
244 | 'end_turn' => $end_turn,
245 | 'recipient' => $recipient,
246 | 'citations' => $citations,
247 | ];
248 | }
249 | unset($raw, $line);
250 | }
251 | } else {
252 | foreach (explode("\n", $response->getBody()) as $line) {
253 | $line = trim($line);
254 | if ($line === 'Internal Server Error') {
255 | throw new Exception($line);
256 | }
257 | if ($line === '') {
258 | continue;
259 | }
260 |
261 | $line = $this->formatStreamMessage($line);
262 |
263 | if (! $this->checkFields($line)) {
264 | if (isset($line["detail"]) && $line["detail"] === "Too many requests in 1 hour. Try again later.") {
265 | throw new Exception("Rate limit exceeded");
266 | }
267 | if (isset($line["detail"]) && $line["detail"] === "Conversation not found") {
268 | throw new Exception("Conversation not found");
269 | }
270 | if (isset($line["detail"]) && $line["detail"] === "Something went wrong, please try reloading the conversation.") {
271 | throw new Exception("Something went wrong, please try reloading the conversation.");
272 | }
273 | if (isset($line["detail"]) && $line["detail"] === "invalid_api_key") {
274 | throw new Exception("Invalid access token");
275 | }
276 | if (isset($line["detail"]) && $line["detail"] === "invalid_token") {
277 | throw new Exception("Invalid access token");
278 | }
279 |
280 | continue;
281 | }
282 |
283 | if ($line['message']['content']['parts'][0] === $prompt) {
284 | continue;
285 | }
286 |
287 | $answer = $line['message']['content']['parts'][0];
288 | $conversationId = $line['conversation_id'] ?? null;
289 | $messageId = $line['message']['id'] ?? null;
290 | $model = $line["message"]["metadata"]["model_slug"] ?? null;
291 | $finish_details = $line["message"]["metadata"]["finish_details"]['type'] ?? null;
292 | $end_turn = $line["message"]["end_turn"] ?? true;
293 | $recipient = $line["message"]["recipient"] ?? "all";
294 | $citations = $line["message"]["metadata"]["citations"] ?? [];
295 | }
296 |
297 | yield [
298 | 'answer' => $answer,
299 | 'id' => $messageId,
300 | 'conversation_id' => $conversationId,
301 | 'model' => $model,
302 | 'account' => $account,
303 | 'finish_details' => $finish_details,
304 | 'end_turn' => $end_turn,
305 | 'recipient' => $recipient,
306 | 'citations' => $citations,
307 | ];
308 | }
309 | }
310 |
311 | /**
312 | * 续写
313 | *
314 | * @param string $prompt
315 | * @param string|null $conversationId
316 | * @param string|null $parentId
317 | * @param mixed $account
318 | * @param bool $stream
319 | *
320 | * @return Generator
321 | * @throws Exception
322 | */
323 | public function continueWrite(
324 | string $prompt,
325 | string $conversationId = null,
326 | string $parentId = null,
327 | $account = null,
328 | bool $stream = false
329 | ): Generator {
330 | if ($account === null) {
331 | throw new Exception("Continue write must set account");
332 | } else {
333 | $token = isset($this->accounts[$account]['access_token']) ? $this->accessTokenToJWT($this->accounts[$account]['access_token']) : null;
334 | }
335 |
336 | if ($token === null) {
337 | throw new Exception("No account available");
338 | }
339 |
340 | if ($parentId == null || $conversationId === null) {
341 | throw new Exception("Continue write must set conversationId and parentId");
342 | }
343 |
344 | $data = [
345 | 'action' => 'continue',
346 | 'conversation_id' => $conversationId,
347 | 'parent_message_id' => $parentId,
348 | 'model' => $this->accounts[$account]['model'],
349 | 'arkose_token' => $this->accounts[$account]['arkose_token'],
350 | 'history_and_training_disabled' => $this->accounts[$account]['history_and_training_disabled'],
351 | ];
352 |
353 | try {
354 | $response = $this->http->post(
355 | 'conversation',
356 | [
357 | 'json' => $data,
358 | 'headers' => [
359 | 'Authorization' => $token,
360 | 'Accept' => 'text/event-stream',
361 | 'Content-Type' => 'application/json',
362 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
363 | 'X-Openai-Assistant-App-Id' => '',
364 | 'Connection' => 'close',
365 | 'Accept-Language' => 'en-US,en;q=0.9',
366 | 'Referer' => 'https://chat.openai.com/chat',
367 | ],
368 | 'stream' => true,
369 | ]
370 | );
371 | } catch (RequestException $e) {
372 | if ($e->hasResponse()) {
373 | throw new Exception(Psr7\Message::toString($e->getResponse()));
374 | } else {
375 | throw new Exception($e->getMessage());
376 | }
377 | }
378 |
379 | $answer = '';
380 | $conversationId = '';
381 | $messageId = '';
382 | $model = '';
383 |
384 | // 流模式下,返回一个生成器
385 | if ($stream) {
386 | $data = $response->getBody();
387 | while (! $data->eof()) {
388 | $raw = Psr7\Utils::readLine($data);
389 | $line = self::formatStreamMessage($raw);
390 | if (self::checkFields($line)) {
391 | $answer = $line['message']['content']['parts'][0];
392 | $conversationId = $line['conversation_id'] ?? null;
393 | $messageId = $line['message']['id'] ?? null;
394 | $model = $line["message"]["metadata"]["model_slug"] ?? null;
395 | $finish_details = $line["message"]["metadata"]["finish_details"]['type'] ?? null;
396 | $end_turn = $line["message"]["end_turn"] ?? true;
397 | $recipient = $line["message"]["recipient"] ?? "all";
398 | $citations = $line["message"]["metadata"]["citations"] ?? [];
399 |
400 | yield [
401 | "answer" => $answer,
402 | "id" => $messageId,
403 | 'conversation_id' => $conversationId,
404 | "model" => $model,
405 | "account" => $account,
406 | 'finish_details' => $finish_details,
407 | 'end_turn' => $end_turn,
408 | 'recipient' => $recipient,
409 | 'citations' => $citations,
410 | ];
411 | }
412 | unset($raw, $line);
413 | }
414 | } else {
415 | foreach (explode("\n", $response->getBody()) as $line) {
416 | $line = trim($line);
417 | if ($line === 'Internal Server Error') {
418 | throw new Exception($line);
419 | }
420 | if ($line === '') {
421 | continue;
422 | }
423 |
424 | $line = $this->formatStreamMessage($line);
425 |
426 | if (! $this->checkFields($line)) {
427 | if (isset($line["detail"]) && $line["detail"] === "Too many requests in 1 hour. Try again later.") {
428 | throw new Exception("Rate limit exceeded");
429 | }
430 | if (isset($line["detail"]) && $line["detail"] === "Conversation not found") {
431 | throw new Exception("Conversation not found");
432 | }
433 | if (isset($line["detail"]) && $line["detail"] === "Something went wrong, please try reloading the conversation.") {
434 | throw new Exception("Something went wrong, please try reloading the conversation.");
435 | }
436 | if (isset($line["detail"]) && $line["detail"] === "invalid_api_key") {
437 | throw new Exception("Invalid access token");
438 | }
439 | if (isset($line["detail"]) && $line["detail"] === "invalid_token") {
440 | throw new Exception("Invalid access token");
441 | }
442 |
443 | continue;
444 | }
445 |
446 | if ($line['message']['content']['parts'][0] === $prompt) {
447 | continue;
448 | }
449 |
450 | $answer = $line['message']['content']['parts'][0];
451 | $conversationId = $line['conversation_id'] ?? null;
452 | $messageId = $line['message']['id'] ?? null;
453 | $model = $line["message"]["metadata"]["model_slug"] ?? null;
454 | $finish_details = $line["message"]["metadata"]["finish_details"]['type'] ?? null;
455 | $end_turn = $line["message"]["end_turn"] ?? true;
456 | $recipient = $line["message"]["recipient"] ?? "all";
457 | $citations = $line["message"]["metadata"]["citations"] ?? [];
458 | }
459 |
460 | yield [
461 | 'answer' => $answer,
462 | 'id' => $messageId,
463 | 'conversation_id' => $conversationId,
464 | 'model' => $model,
465 | 'account' => $account,
466 | 'finish_details' => $finish_details,
467 | 'end_turn' => $end_turn,
468 | 'recipient' => $recipient,
469 | 'citations' => $citations,
470 | ];
471 | }
472 | }
473 |
474 | /**
475 | * 获取会话列表
476 | *
477 | * @param int $offset
478 | * @param int $limit
479 | * @param mixed $account
480 | *
481 | * @return array
482 | * @throws Exception
483 | */
484 | public function getConversations(int $offset = 0, int $limit = 20, $account = 0): array
485 | {
486 | try {
487 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']);
488 | } catch (Exception $e) {
489 | throw new Exception("Invalid account");
490 | }
491 |
492 | try {
493 | $response = $this->http->get('conversations', [
494 | 'headers' => [
495 | 'Authorization' => $token,
496 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
497 | 'Referer' => 'https://chat.openai.com/chat',
498 | ],
499 | 'query' => [
500 | 'offset' => $offset,
501 | 'limit' => $limit,
502 | ],
503 | ])->getBody()->getContents();
504 | } catch (GuzzleException $e) {
505 | throw new Exception($e->getMessage());
506 | }
507 |
508 | $data = json_decode($response, true);
509 | if (json_last_error() !== JSON_ERROR_NONE) {
510 | throw new Exception('Response is not json');
511 | }
512 |
513 | if (! isset($data['items'])) {
514 | throw new Exception('Field missing');
515 | }
516 |
517 | return $data['items'];
518 | }
519 |
520 | /**
521 | * 获取会话消息列表
522 | *
523 | * @param string $conversationId
524 | * @param mixed $account
525 | *
526 | * @return array
527 | * @throws Exception
528 | */
529 | public function getConversationMessages(string $conversationId, $account = 0): array
530 | {
531 | try {
532 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']);
533 | } catch (Exception $e) {
534 | throw new Exception("Invalid account");
535 | }
536 |
537 | try {
538 | $response = $this->http->get('conversation/' . $conversationId, [
539 | 'headers' => [
540 | 'Authorization' => $token,
541 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
542 | 'Referer' => 'https://chat.openai.com/chat',
543 | ],
544 | ])->getBody()->getContents();
545 | } catch (GuzzleException $e) {
546 | throw new Exception($e->getMessage());
547 | }
548 |
549 | $data = json_decode($response, true);
550 | if (json_last_error() !== JSON_ERROR_NONE) {
551 | throw new Exception('Response is not json');
552 | }
553 |
554 | return $data;
555 | }
556 |
557 | /**
558 | * 生成会话标题
559 | *
560 | * @param string $conversationId
561 | * @param string $messageId
562 | * @param mixed $account
563 | *
564 | * @return bool
565 | * @throws Exception
566 | */
567 | public function generateConversationTitle(string $conversationId, string $messageId, $account = 0): bool
568 | {
569 | try {
570 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']);
571 | } catch (Exception $e) {
572 | throw new Exception("Invalid account");
573 | }
574 |
575 | try {
576 | $response = $this->http->post('conversation/gen_title/' . $conversationId, [
577 | 'headers' => [
578 | 'Authorization' => $token,
579 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
580 | 'Referer' => 'https://chat.openai.com/chat',
581 | ],
582 | 'json' => [
583 | 'message_id' => $messageId,
584 | 'model' => 'text-davinci-002-render',
585 | ],
586 | ])->getBody()->getContents();
587 | } catch (GuzzleException $e) {
588 | throw new Exception($e->getMessage());
589 | }
590 |
591 | $data = json_decode($response, true);
592 | if (json_last_error() !== JSON_ERROR_NONE) {
593 | throw new Exception('Response is not json');
594 | }
595 |
596 | if (isset($data['title'])) {
597 | return true;
598 | }
599 |
600 | return false;
601 | }
602 |
603 | /**
604 | * 修改会话标题
605 | *
606 | * @param string $conversationId
607 | * @param string $title
608 | * @param mixed $account
609 | *
610 | * @return bool
611 | * @throws Exception
612 | */
613 | public function updateConversationTitle(string $conversationId, string $title, $account = 0): bool
614 | {
615 | try {
616 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']);
617 | } catch (Exception $e) {
618 | throw new Exception("Invalid account");
619 | }
620 |
621 | try {
622 | $response = $this->http->patch('conversation/' . $conversationId, [
623 | 'headers' => [
624 | 'Authorization' => $token,
625 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
626 | 'Referer' => 'https://chat.openai.com/chat',
627 | ],
628 | 'json' => [
629 | 'title' => $title,
630 | ],
631 | ])->getBody()->getContents();
632 | } catch (GuzzleException $e) {
633 | throw new Exception($e->getMessage());
634 | }
635 |
636 | $data = json_decode($response, true);
637 | if (json_last_error() !== JSON_ERROR_NONE) {
638 | throw new Exception('Response is not json');
639 | }
640 |
641 | if (isset($data['success']) && $data['success'] === true) {
642 | return true;
643 | }
644 |
645 | return false;
646 | }
647 |
648 | /**
649 | * 删除会话
650 | *
651 | * @param string $conversationId
652 | * @param mixed $account
653 | *
654 | * @return bool
655 | * @throws Exception
656 | */
657 | public function deleteConversation(string $conversationId, $account = 0): bool
658 | {
659 | try {
660 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']);
661 | } catch (Exception $e) {
662 | throw new Exception("Invalid account");
663 | }
664 |
665 | try {
666 | $response = $this->http->patch('conversation/' . $conversationId, [
667 | 'headers' => [
668 | 'Authorization' => $token,
669 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
670 | 'Referer' => 'https://chat.openai.com/chat',
671 | ],
672 | 'json' => [
673 | 'is_visible' => false,
674 | ],
675 | ])->getBody()->getContents();
676 | } catch (GuzzleException $e) {
677 | throw new Exception($e->getMessage());
678 | }
679 |
680 | $data = json_decode($response, true);
681 | if (json_last_error() !== JSON_ERROR_NONE) {
682 | throw new Exception('Response is not json');
683 | }
684 |
685 | if (isset($data['success']) && $data['success'] === true) {
686 | return true;
687 | }
688 |
689 | return false;
690 | }
691 |
692 | /**
693 | * 清空会话
694 | *
695 | * @param mixed $account
696 | *
697 | * @return bool
698 | * @throws Exception
699 | */
700 | public function clearConversations($account = 0): bool
701 | {
702 | try {
703 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']);
704 | } catch (Exception $e) {
705 | throw new Exception("Invalid account");
706 | }
707 |
708 | try {
709 | $response = $this->http->patch('conversations', [
710 | 'headers' => [
711 | 'Authorization' => $token,
712 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
713 | 'Referer' => 'https://chat.openai.com/chat',
714 | ],
715 | 'json' => [
716 | 'is_visible' => false,
717 | ],
718 | ])->getBody()->getContents();
719 | } catch (GuzzleException $e) {
720 | throw new Exception($e->getMessage());
721 | }
722 |
723 | $data = json_decode($response, true);
724 | if (json_last_error() !== JSON_ERROR_NONE) {
725 | throw new Exception('Response is not json');
726 | }
727 |
728 | if (isset($data['success']) && $data['success'] === true) {
729 | return true;
730 | }
731 |
732 | return false;
733 | }
734 |
735 | /**
736 | * 获取插件列表
737 | *
738 | * @param int $offset
739 | * @param int $limit
740 | * @param string $status
741 | * @param mixed $account
742 | *
743 | * @return array
744 | */
745 | public function getPlugins(int $offset = 0, int $limit = 250, string $status = 'approved', $account = 0): array
746 | {
747 | try {
748 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']);
749 | } catch (Exception $e) {
750 | throw new Exception("Invalid account");
751 | }
752 |
753 | try {
754 | $response = $this->http->get('aip/p', [
755 | 'headers' => [
756 | 'Authorization' => $token,
757 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
758 | 'Referer' => 'https://chat.openai.com/chat',
759 | ],
760 | 'query' => [
761 | 'offset' => $offset,
762 | 'limit' => $limit,
763 | 'status' => $status,
764 | ],
765 | ])->getBody()->getContents();
766 | } catch (GuzzleException $e) {
767 | return [];
768 | }
769 |
770 | $data = json_decode($response, true);
771 | if (json_last_error() !== JSON_ERROR_NONE) {
772 | return [];
773 | }
774 |
775 | return $data;
776 | }
777 |
778 | /**
779 | * 安装插件
780 | *
781 | * @param string $pluginId
782 | * @param mixed $account
783 | *
784 | * @return bool
785 | */
786 | public function installPlugin(string $pluginId, $account = 0): bool
787 | {
788 | try {
789 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']);
790 | } catch (Exception $e) {
791 | throw new Exception("Invalid account");
792 | }
793 |
794 | try {
795 | $response = $this->http->patch('aip/p/' . $pluginId . '/user-settings', [
796 | 'headers' => [
797 | 'Authorization' => $token,
798 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
799 | 'Referer' => 'https://chat.openai.com/chat',
800 | ],
801 | ])->getBody()->getContents();
802 | } catch (GuzzleException $e) {
803 | return false;
804 | }
805 |
806 | $data = json_decode($response, true);
807 | if (json_last_error() !== JSON_ERROR_NONE) {
808 | return false;
809 | }
810 |
811 | return true;
812 | }
813 |
814 | /**
815 | * 获取未验证插件
816 | *
817 | * @param string $domain
818 | * @param mixed $account
819 | *
820 | * @return array
821 | */
822 | public function getUnverifiedPlugins(string $domain = '', $account = 0): array
823 | {
824 | try {
825 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']);
826 | } catch (Exception $e) {
827 | throw new Exception("Invalid account");
828 | }
829 |
830 | try {
831 | $response = $this->http->get('aip/p/domain', [
832 | 'headers' => [
833 | 'Authorization' => $token,
834 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
835 | 'Referer' => 'https://chat.openai.com/chat',
836 | ],
837 | 'query' => [
838 | 'domain' => $domain,
839 | ],
840 | ])->getBody()->getContents();
841 | } catch (GuzzleException $e) {
842 | return [];
843 | }
844 |
845 | $data = json_decode($response, true);
846 | if (json_last_error() !== JSON_ERROR_NONE) {
847 | return [];
848 | }
849 |
850 | return $data;
851 | }
852 |
853 | /**
854 | * 设置保存聊天记录与训练
855 | *
856 | * @param bool $save
857 | * @param mixed $account
858 | *
859 | * @return bool
860 | */
861 | public function setChatHistoryAndTraining(bool $save, $account = 0): bool
862 | {
863 | try {
864 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']);
865 | } catch (Exception $e) {
866 | throw new Exception("Invalid account");
867 | }
868 |
869 | try {
870 | $response = $this->http->get('models', [
871 | 'headers' => [
872 | 'Authorization' => $token,
873 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
874 | 'Referer' => 'https://chat.openai.com/chat',
875 | ],
876 | 'query' => [
877 | 'history_and_training_disabled' => ! $save,
878 | ],
879 | ])->getBody()->getContents();
880 | } catch (GuzzleException $e) {
881 | return false;
882 | }
883 |
884 | $data = json_decode($response, true);
885 | if (json_last_error() !== JSON_ERROR_NONE) {
886 | return false;
887 | }
888 |
889 | return true;
890 | }
891 |
892 | /**
893 | * 检查响应行是否包含必要的字段
894 | *
895 | * @param mixed $line
896 | *
897 | * @return bool
898 | */
899 | public function checkFields($line): bool
900 | {
901 | return isset($line['message']['content']['parts'][0])
902 | && isset($line['conversation_id'])
903 | && isset($line['message']['id']);
904 | }
905 |
906 | /**
907 | * 格式化流消息为数组
908 | *
909 | * @param string $line
910 | *
911 | * @return array|false
912 | */
913 | public function formatStreamMessage(string $line)
914 | {
915 | preg_match('/data: (.*)/', $line, $matches);
916 | if (empty($matches[1])) {
917 | return false;
918 | }
919 |
920 | $line = $matches[1];
921 | $data = json_decode($line, true);
922 |
923 | if (json_last_error() !== JSON_ERROR_NONE) {
924 | return false;
925 | }
926 |
927 | return $data;
928 | }
929 |
930 | /**
931 | * access_token 转换为 JWT
932 | *
933 | * @param string $accessToken
934 | *
935 | * @return string
936 | * @throws Exception
937 | */
938 | private function accessTokenToJWT(string $accessToken): string
939 | {
940 | return 'Bearer ' . $accessToken;
941 | }
942 |
943 | /**
944 | * 获取arkose_token
945 | *
946 | * @return string
947 | * @throws Exception
948 | */
949 | public function getArkoseToken(): string
950 | {
951 | $ch = curl_init();
952 | curl_setopt($ch, CURLOPT_URL, 'https://ai.fakeopen.com/api/arkose/token');
953 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
954 | $response = curl_exec($ch);
955 | curl_close($ch);
956 |
957 | if ($response === false) {
958 | throw new Exception('Request arkose token failed');
959 | }
960 |
961 | $data = json_decode($response, true);
962 | if (json_last_error() !== JSON_ERROR_NONE) {
963 | throw new Exception('Request arkose response is not json');
964 | }
965 |
966 | if (! isset($data['token'])) {
967 | throw new Exception('Request arkose token failed');
968 | }
969 |
970 | return $data['token'];
971 | }
972 | }
973 |
--------------------------------------------------------------------------------