Selaa lähdekoodia

test(api-test): 新增 agent-grants 用例;客户端按新规格重生

- cases/08_agent_grants.php:无 token 401、模型不存在 422、target_type 非法 422,
  以及 AI 代写一轮(签授权 → 不带授权 403 → 带授权建批注,editor=人、co_editor=模型 → 人删掉)
- 代写一轮需要归测试账号所有的模型,在 config.local.php 的 fixtures.agent_model 配,没配则 SKIP
- fixtures 增加 discussion_channel、agent_model

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
visuddhinanda 4 päivää sitten
vanhempi
sitoutus
167f9e0855

+ 82 - 0
api-test/cases/08_agent_grants.php

@@ -0,0 +1,82 @@
+<?php
+
+use \WikipaliApi\Api\AgentGrantsApi;
+use \WikipaliApi\Model\PostApiV3AgentGrantsRequest;
+
+/*
+ * agent 协作:人类签发写入授权(POST /v3/agent-grants),模型凭模型 token + X-Agent-Grant 代写。
+ *
+ * 完整代写链路需要一个归测试账号所有的 AI 模型:在 config.local.php 的
+ * fixtures.agent_model 填它的 uid,没填则 SKIP。模型 token 目前只有 v2 端点能取
+ * (GET /v2/ai-model-token/{uid}),这里只把它当准备步骤用。
+ * 权限矩阵(别的 channel / book、撤销、伪造、v2 token 冒充)由 api-v13 的 AgentGrantV3Test 覆盖。
+ */
+
+function agent_grant_body(string $model, string $channel): PostApiV3AgentGrantsRequest
+{
+    $body = new PostApiV3AgentGrantsRequest();
+    $body->setAiModel($model);
+    $body->setTargetType('channel');
+    $body->setTargetId($channel);
+
+    return $body;
+}
+
+test('POST /v3/agent-grants(无 token)→ 401', function () {
+    $res = request(fn () => api(AgentGrantsApi::class)->postApiV3AgentGrantsWithHttpInfo(
+        agent_grant_body('00000000-0000-4000-8000-000000000000', fixtures()['channel'])
+    ));
+    assert_status(401, $res, 'agent-grants 无 token');
+    assert_problem($res, 'agent-grants 无 token');
+});
+
+test('POST /v3/agent-grants(模型不存在)→ 422 errors.ai_model', function () {
+    $res = request(fn () => api(AgentGrantsApi::class, required_token())->postApiV3AgentGrantsWithHttpInfo(
+        agent_grant_body('00000000-0000-4000-8000-000000000000', fixtures()['channel'])
+    ));
+    assert_field_error('ai_model', $res, 'agent-grants 模型不存在');
+});
+
+test('POST /v3/agent-grants(target_type 不是 channel)→ 422', function () {
+    $res = raw('POST', '/v3/agent-grants', null, [
+        'ai_model' => '00000000-0000-4000-8000-000000000000', 'target_type' => 'article', 'target_id' => fixtures()['channel'],
+    ], required_token());
+    assert_field_error('target_type', $res, 'agent-grants target_type');
+});
+
+test('agent 代写一轮:签授权 → 模型带授权建批注(editor=人、co_editor=模型)→ 不带授权 403 → 人删掉', function () {
+    $model = fixtures()['agent_model'] ?? null;
+    if (! $model) {
+        skip('config.local.php 未配置 fixtures.agent_model(归测试账号所有的 AI 模型 uid)');
+    }
+    $token = required_token();
+    $sentence = fixtures()['discussion_sentence'];
+    $channel = fixtures()['discussion_channel'];
+
+    $grant = request(fn () => api(AgentGrantsApi::class, $token)->postApiV3AgentGrantsWithHttpInfo(agent_grant_body($model, $channel)));
+    assert_status(201, $grant, '签授权');
+    $grantToken = $grant['body']['data']['token'] ?? null;
+
+    // 准备:取模型 token(只有 v2 端点)
+    $modelToken = raw('GET', "/v2/ai-model-token/{$model}", null, null, $token)['body']['data']['token'] ?? null;
+    if (! $modelToken) {
+        fail('取不到模型 token:fixtures.agent_model 是否归测试账号所有?');
+    }
+
+    $body = ['target_type' => 'sentence', 'target_id' => $sentence, 'type' => 'note', 'content' => 'api-test 代写的批注'];
+    $denied = raw('POST', '/v3/discussions', null, $body, $modelToken);
+    assert_status(403, $denied, '模型不带授权');
+
+    $created = raw('POST', '/v3/discussions', null, $body, $modelToken, ['X-Agent-Grant' => $grantToken]);
+    assert_status(201, $created, '模型带授权');
+    $id = $created['body']['data']['id'] ?? null;
+    try {
+        assert_eq($model, $created['body']['data']['co_editor']['id'] ?? null, 'co_editor 是模型');
+        if (($created['body']['data']['editor']['id'] ?? null) === $model) {
+            fail('editor 应是授权人,不是模型');
+        }
+    } finally {
+        $del = raw('DELETE', "/v3/discussions/{$id}", null, null, $token);
+    }
+    assert_status(200, $del, '人删掉模型写的');
+});

+ 12 - 0
api-test/client/.openapi-generator/FILES

@@ -4,6 +4,7 @@
 .travis.yml
 .travis.yml
 README.md
 README.md
 composer.json
 composer.json
+docs/Api/AgentGrantsApi.md
 docs/Api/DiscussionsApi.md
 docs/Api/DiscussionsApi.md
 docs/Api/EmailCertificationsApi.md
 docs/Api/EmailCertificationsApi.md
 docs/Api/HeartbeatApi.md
 docs/Api/HeartbeatApi.md
@@ -54,6 +55,9 @@ docs/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200ResponseDataInne
 docs/Model/PaginationMeta.md
 docs/Model/PaginationMeta.md
 docs/Model/PatchApiV3DiscussionsDiscussionRequest.md
 docs/Model/PatchApiV3DiscussionsDiscussionRequest.md
 docs/Model/PatchApiV3PasswordResetsTokenRequest.md
 docs/Model/PatchApiV3PasswordResetsTokenRequest.md
+docs/Model/PostApiV3AgentGrants200Response.md
+docs/Model/PostApiV3AgentGrants200ResponseData.md
+docs/Model/PostApiV3AgentGrantsRequest.md
 docs/Model/PostApiV3Discussions200Response.md
 docs/Model/PostApiV3Discussions200Response.md
 docs/Model/PostApiV3DiscussionsDiscussionRepliesRequest.md
 docs/Model/PostApiV3DiscussionsDiscussionRepliesRequest.md
 docs/Model/PostApiV3DiscussionsRequest.md
 docs/Model/PostApiV3DiscussionsRequest.md
@@ -73,6 +77,7 @@ docs/Model/PostApiV3Users200ResponseData.md
 docs/Model/PostApiV3UsersRequest.md
 docs/Model/PostApiV3UsersRequest.md
 docs/Model/ProblemDetails.md
 docs/Model/ProblemDetails.md
 git_push.sh
 git_push.sh
+lib/Api/AgentGrantsApi.php
 lib/Api/DiscussionsApi.php
 lib/Api/DiscussionsApi.php
 lib/Api/EmailCertificationsApi.php
 lib/Api/EmailCertificationsApi.php
 lib/Api/HeartbeatApi.php
 lib/Api/HeartbeatApi.php
@@ -128,6 +133,9 @@ lib/Model/ModelInterface.php
 lib/Model/PaginationMeta.php
 lib/Model/PaginationMeta.php
 lib/Model/PatchApiV3DiscussionsDiscussionRequest.php
 lib/Model/PatchApiV3DiscussionsDiscussionRequest.php
 lib/Model/PatchApiV3PasswordResetsTokenRequest.php
 lib/Model/PatchApiV3PasswordResetsTokenRequest.php
+lib/Model/PostApiV3AgentGrants200Response.php
+lib/Model/PostApiV3AgentGrants200ResponseData.php
+lib/Model/PostApiV3AgentGrantsRequest.php
 lib/Model/PostApiV3Discussions200Response.php
 lib/Model/PostApiV3Discussions200Response.php
 lib/Model/PostApiV3DiscussionsDiscussionRepliesRequest.php
 lib/Model/PostApiV3DiscussionsDiscussionRepliesRequest.php
 lib/Model/PostApiV3DiscussionsRequest.php
 lib/Model/PostApiV3DiscussionsRequest.php
@@ -148,6 +156,7 @@ lib/Model/PostApiV3UsersRequest.php
 lib/Model/ProblemDetails.php
 lib/Model/ProblemDetails.php
 lib/ObjectSerializer.php
 lib/ObjectSerializer.php
 phpunit.xml.dist
 phpunit.xml.dist
+test/Api/AgentGrantsApiTest.php
 test/Api/DiscussionsApiTest.php
 test/Api/DiscussionsApiTest.php
 test/Api/EmailCertificationsApiTest.php
 test/Api/EmailCertificationsApiTest.php
 test/Api/HeartbeatApiTest.php
 test/Api/HeartbeatApiTest.php
@@ -198,6 +207,9 @@ test/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200ResponseTest.php
 test/Model/PaginationMetaTest.php
 test/Model/PaginationMetaTest.php
 test/Model/PatchApiV3DiscussionsDiscussionRequestTest.php
 test/Model/PatchApiV3DiscussionsDiscussionRequestTest.php
 test/Model/PatchApiV3PasswordResetsTokenRequestTest.php
 test/Model/PatchApiV3PasswordResetsTokenRequestTest.php
+test/Model/PostApiV3AgentGrants200ResponseDataTest.php
+test/Model/PostApiV3AgentGrants200ResponseTest.php
+test/Model/PostApiV3AgentGrantsRequestTest.php
 test/Model/PostApiV3Discussions200ResponseTest.php
 test/Model/PostApiV3Discussions200ResponseTest.php
 test/Model/PostApiV3DiscussionsDiscussionRepliesRequestTest.php
 test/Model/PostApiV3DiscussionsDiscussionRepliesRequestTest.php
 test/Model/PostApiV3DiscussionsRequestTest.php
 test/Model/PostApiV3DiscussionsRequestTest.php

+ 532 - 0
api-test/client/lib/Api/AgentGrantsApi.php

@@ -0,0 +1,532 @@
+<?php
+/**
+ * AgentGrantsApi
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Api;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Exception\ConnectException;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Psr7\MultipartStream;
+use GuzzleHttp\Psr7\Request;
+use GuzzleHttp\RequestOptions;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use WikipaliApi\ApiException;
+use WikipaliApi\Configuration;
+use WikipaliApi\FormDataProcessor;
+use WikipaliApi\HeaderSelector;
+use WikipaliApi\ObjectSerializer;
+
+/**
+ * AgentGrantsApi Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+class AgentGrantsApi
+{
+    /**
+     * @var ClientInterface
+     */
+    protected $client;
+
+    /**
+     * @var Configuration
+     */
+    protected $config;
+
+    /**
+     * @var HeaderSelector
+     */
+    protected $headerSelector;
+
+    /**
+     * @var int Host index
+     */
+    protected $hostIndex;
+
+    /** @var string[] $contentTypes **/
+    public const contentTypes = [
+        'postApiV3AgentGrants' => [
+            'application/json',
+        ],
+    ];
+
+    /**
+     * @param ClientInterface $client
+     * @param Configuration   $config
+     * @param HeaderSelector  $selector
+     * @param int             $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec
+     */
+    public function __construct(
+        ?ClientInterface $client = null,
+        ?Configuration $config = null,
+        ?HeaderSelector $selector = null,
+        int $hostIndex = 0
+    ) {
+        $this->client = $client ?: new Client();
+        $this->config = $config ?: Configuration::getDefaultConfiguration();
+        $this->headerSelector = $selector ?: new HeaderSelector();
+        $this->hostIndex = $hostIndex;
+    }
+
+    /**
+     * Set the host index
+     *
+     * @param int $hostIndex Host index (required)
+     */
+    public function setHostIndex($hostIndex): void
+    {
+        $this->hostIndex = $hostIndex;
+    }
+
+    /**
+     * Get the host index
+     *
+     * @return int Host index
+     */
+    public function getHostIndex()
+    {
+        return $this->hostIndex;
+    }
+
+    /**
+     * @return Configuration
+     */
+    public function getConfig()
+    {
+        return $this->config;
+    }
+
+    /**
+     * Operation postApiV3AgentGrants
+     *
+     * 签发 agent 写入授权
+     *
+     * @param  \WikipaliApi\Model\PostApiV3AgentGrantsRequest $post_api_v3_agent_grants_request post_api_v3_agent_grants_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3AgentGrants'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\PostApiV3AgentGrants200Response|\WikipaliApi\Model\PostApiV3AgentGrants200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails
+     */
+    public function postApiV3AgentGrants($post_api_v3_agent_grants_request, string $contentType = self::contentTypes['postApiV3AgentGrants'][0])
+    {
+        list($response) = $this->postApiV3AgentGrantsWithHttpInfo($post_api_v3_agent_grants_request, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation postApiV3AgentGrantsWithHttpInfo
+     *
+     * 签发 agent 写入授权
+     *
+     * @param  \WikipaliApi\Model\PostApiV3AgentGrantsRequest $post_api_v3_agent_grants_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3AgentGrants'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\PostApiV3AgentGrants200Response|\WikipaliApi\Model\PostApiV3AgentGrants200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function postApiV3AgentGrantsWithHttpInfo($post_api_v3_agent_grants_request, string $contentType = self::contentTypes['postApiV3AgentGrants'][0])
+    {
+        $request = $this->postApiV3AgentGrantsRequest($post_api_v3_agent_grants_request, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3AgentGrants200Response',
+                        $request,
+                        $response,
+                    );
+                case 201:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3AgentGrants200Response',
+                        $request,
+                        $response,
+                    );
+                case 401:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 403:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\PostApiV3AgentGrants200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3AgentGrants200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 201:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3AgentGrants200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 401:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 403:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation postApiV3AgentGrantsAsync
+     *
+     * 签发 agent 写入授权
+     *
+     * @param  \WikipaliApi\Model\PostApiV3AgentGrantsRequest $post_api_v3_agent_grants_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3AgentGrants'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function postApiV3AgentGrantsAsync($post_api_v3_agent_grants_request, string $contentType = self::contentTypes['postApiV3AgentGrants'][0])
+    {
+        return $this->postApiV3AgentGrantsAsyncWithHttpInfo($post_api_v3_agent_grants_request, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation postApiV3AgentGrantsAsyncWithHttpInfo
+     *
+     * 签发 agent 写入授权
+     *
+     * @param  \WikipaliApi\Model\PostApiV3AgentGrantsRequest $post_api_v3_agent_grants_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3AgentGrants'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function postApiV3AgentGrantsAsyncWithHttpInfo($post_api_v3_agent_grants_request, string $contentType = self::contentTypes['postApiV3AgentGrants'][0])
+    {
+        $returnType = '\WikipaliApi\Model\PostApiV3AgentGrants200Response';
+        $request = $this->postApiV3AgentGrantsRequest($post_api_v3_agent_grants_request, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'postApiV3AgentGrants'
+     *
+     * @param  \WikipaliApi\Model\PostApiV3AgentGrantsRequest $post_api_v3_agent_grants_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3AgentGrants'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function postApiV3AgentGrantsRequest($post_api_v3_agent_grants_request, string $contentType = self::contentTypes['postApiV3AgentGrants'][0])
+    {
+
+        // verify the required parameter 'post_api_v3_agent_grants_request' is set
+        if ($post_api_v3_agent_grants_request === null || (is_array($post_api_v3_agent_grants_request) && count($post_api_v3_agent_grants_request) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $post_api_v3_agent_grants_request when calling postApiV3AgentGrants'
+            );
+        }
+
+
+        $resourcePath = '/v3/agent-grants';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+
+
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (isset($post_api_v3_agent_grants_request)) {
+            if (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the body
+                try {
+                    $httpBody = json_encode(ObjectSerializer::sanitizeForSerialization($post_api_v3_agent_grants_request), JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                $httpBody = $post_api_v3_agent_grants_request;
+            }
+        } elseif (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'POST',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Create http client option
+     *
+     * @throws \RuntimeException on file opening failure
+     * @return array of http client options
+     */
+    protected function createHttpClientOption()
+    {
+        $options = [];
+        if ($this->config->getDebug()) {
+            $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a');
+            if (!$options[RequestOptions::DEBUG]) {
+                throw new \RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile());
+            }
+        }
+
+        if ($this->config->getCertFile()) {
+            $options[RequestOptions::CERT] = $this->config->getCertFile();
+        }
+
+        if ($this->config->getKeyFile()) {
+            $options[RequestOptions::SSL_KEY] = $this->config->getKeyFile();
+        }
+
+        return $options;
+    }
+
+    private function handleResponseWithDataType(
+        string $dataType,
+        RequestInterface $request,
+        ResponseInterface $response
+    ): array {
+        if ($dataType === '\SplFileObject') {
+            $content = $response->getBody(); //stream goes to serializer
+        } else {
+            $content = (string) $response->getBody();
+            if ($dataType !== 'string') {
+                try {
+                    $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $exception) {
+                    throw new ApiException(
+                        sprintf(
+                            'Error JSON decoding server response (%s)',
+                            $request->getUri()
+                        ),
+                        $response->getStatusCode(),
+                        $response->getHeaders(),
+                        $content
+                    );
+                }
+            }
+        }
+
+        return [
+            ObjectSerializer::deserialize($content, $dataType, []),
+            $response->getStatusCode(),
+            $response->getHeaders()
+        ];
+    }
+
+    private function responseWithinRangeCode(
+        string $rangeCode,
+        int $statusCode
+    ): bool {
+        $left = (int) ($rangeCode[0].'00');
+        $right = (int) ($rangeCode[0].'99');
+
+        return $statusCode >= $left && $statusCode <= $right;
+    }
+}

+ 14 - 14
api-test/client/lib/Api/DiscussionsApi.php

@@ -472,7 +472,7 @@ class DiscussionsApi
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
      * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
-     * @param  string|null $type 话题类型 (optional)
+     * @param  string|null $type 类型,不传则只列评论线程 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
      * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
      * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
      * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
      * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
@@ -497,7 +497,7 @@ class DiscussionsApi
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
      * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
-     * @param  string|null $type 话题类型 (optional)
+     * @param  string|null $type 类型,不传则只列评论线程 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
      * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
      * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
      * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
      * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
@@ -603,7 +603,7 @@ class DiscussionsApi
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
      * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
-     * @param  string|null $type 话题类型 (optional)
+     * @param  string|null $type 类型,不传则只列评论线程 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
      * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
      * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
      * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
      * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
@@ -631,7 +631,7 @@ class DiscussionsApi
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
      * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
-     * @param  string|null $type 话题类型 (optional)
+     * @param  string|null $type 类型,不传则只列评论线程 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
      * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
      * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
      * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
      * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
@@ -688,7 +688,7 @@ class DiscussionsApi
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
      * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
-     * @param  string|null $type 话题类型 (optional)
+     * @param  string|null $type 类型,不传则只列评论线程 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
      * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
      * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
      * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
      * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
@@ -1483,7 +1483,7 @@ class DiscussionsApi
      *
      *
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
-     * @param  string|null $type 只数这种类型的话题 (optional)
+     * @param  string|null $type 只数这种类型,不传则只数评论线程 (optional)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
      *
      *
      * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
      * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
@@ -1503,7 +1503,7 @@ class DiscussionsApi
      *
      *
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
-     * @param  string|null $type 只数这种类型的话题 (optional)
+     * @param  string|null $type 只数这种类型,不传则只数评论线程 (optional)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
      *
      *
      * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
      * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
@@ -1604,7 +1604,7 @@ class DiscussionsApi
      *
      *
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
-     * @param  string|null $type 只数这种类型的话题 (optional)
+     * @param  string|null $type 只数这种类型,不传则只数评论线程 (optional)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
      *
      *
      * @throws \InvalidArgumentException
      * @throws \InvalidArgumentException
@@ -1627,7 +1627,7 @@ class DiscussionsApi
      *
      *
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
-     * @param  string|null $type 只数这种类型的话题 (optional)
+     * @param  string|null $type 只数这种类型,不传则只数评论线程 (optional)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
      *
      *
      * @throws \InvalidArgumentException
      * @throws \InvalidArgumentException
@@ -1679,7 +1679,7 @@ class DiscussionsApi
      *
      *
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_type 挂靠对象的类型 (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
      * @param  string $target_id 挂靠对象的 uuid (required)
-     * @param  string|null $type 只数这种类型的话题 (optional)
+     * @param  string|null $type 只数这种类型,不传则只数评论线程 (optional)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
      *
      *
      * @throws \InvalidArgumentException
      * @throws \InvalidArgumentException
@@ -2141,7 +2141,7 @@ class DiscussionsApi
     /**
     /**
      * Operation postApiV3Discussions
      * Operation postApiV3Discussions
      *
      *
-     * 发起话题
+     * 发起话题或新建批注
      *
      *
      * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request post_api_v3_discussions_request (required)
      * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request post_api_v3_discussions_request (required)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
@@ -2159,7 +2159,7 @@ class DiscussionsApi
     /**
     /**
      * Operation postApiV3DiscussionsWithHttpInfo
      * Operation postApiV3DiscussionsWithHttpInfo
      *
      *
-     * 发起话题
+     * 发起话题或新建批注
      *
      *
      * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request (required)
      * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request (required)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
@@ -2300,7 +2300,7 @@ class DiscussionsApi
     /**
     /**
      * Operation postApiV3DiscussionsAsync
      * Operation postApiV3DiscussionsAsync
      *
      *
-     * 发起话题
+     * 发起话题或新建批注
      *
      *
      * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request (required)
      * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request (required)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
@@ -2321,7 +2321,7 @@ class DiscussionsApi
     /**
     /**
      * Operation postApiV3DiscussionsAsyncWithHttpInfo
      * Operation postApiV3DiscussionsAsyncWithHttpInfo
      *
      *
-     * 发起话题
+     * 发起话题或新建批注
      *
      *
      * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request (required)
      * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request (required)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation

+ 246 - 0
api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInner.php

@@ -70,8 +70,14 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'html' => 'string',
         'html' => 'string',
         'summary' => 'string',
         'summary' => 'string',
         'tpl_id' => 'string',
         'tpl_id' => 'string',
+        'pos_start' => 'int',
+        'pos_end' => 'int',
+        'quote_exact' => 'string',
+        'quote_prefix' => 'string',
+        'quote_suffix' => 'string',
         'reply_count' => 'int',
         'reply_count' => 'int',
         'editor' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor',
         'editor' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor',
+        'co_editor' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor',
         'abilities' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerAbilities',
         'abilities' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerAbilities',
         'created_at' => 'string',
         'created_at' => 'string',
         'updated_at' => 'string',
         'updated_at' => 'string',
@@ -99,8 +105,14 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'html' => null,
         'html' => null,
         'summary' => null,
         'summary' => null,
         'tpl_id' => null,
         'tpl_id' => null,
+        'pos_start' => null,
+        'pos_end' => null,
+        'quote_exact' => null,
+        'quote_prefix' => null,
+        'quote_suffix' => null,
         'reply_count' => null,
         'reply_count' => null,
         'editor' => null,
         'editor' => null,
+        'co_editor' => null,
         'abilities' => null,
         'abilities' => null,
         'created_at' => null,
         'created_at' => null,
         'updated_at' => null,
         'updated_at' => null,
@@ -126,8 +138,14 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'html' => false,
         'html' => false,
         'summary' => false,
         'summary' => false,
         'tpl_id' => true,
         'tpl_id' => true,
+        'pos_start' => true,
+        'pos_end' => true,
+        'quote_exact' => true,
+        'quote_prefix' => true,
+        'quote_suffix' => true,
         'reply_count' => false,
         'reply_count' => false,
         'editor' => true,
         'editor' => true,
+        'co_editor' => true,
         'abilities' => false,
         'abilities' => false,
         'created_at' => false,
         'created_at' => false,
         'updated_at' => false,
         'updated_at' => false,
@@ -233,8 +251,14 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'html' => 'html',
         'html' => 'html',
         'summary' => 'summary',
         'summary' => 'summary',
         'tpl_id' => 'tpl_id',
         'tpl_id' => 'tpl_id',
+        'pos_start' => 'pos_start',
+        'pos_end' => 'pos_end',
+        'quote_exact' => 'quote_exact',
+        'quote_prefix' => 'quote_prefix',
+        'quote_suffix' => 'quote_suffix',
         'reply_count' => 'reply_count',
         'reply_count' => 'reply_count',
         'editor' => 'editor',
         'editor' => 'editor',
+        'co_editor' => 'co_editor',
         'abilities' => 'abilities',
         'abilities' => 'abilities',
         'created_at' => 'created_at',
         'created_at' => 'created_at',
         'updated_at' => 'updated_at',
         'updated_at' => 'updated_at',
@@ -260,8 +284,14 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'html' => 'setHtml',
         'html' => 'setHtml',
         'summary' => 'setSummary',
         'summary' => 'setSummary',
         'tpl_id' => 'setTplId',
         'tpl_id' => 'setTplId',
+        'pos_start' => 'setPosStart',
+        'pos_end' => 'setPosEnd',
+        'quote_exact' => 'setQuoteExact',
+        'quote_prefix' => 'setQuotePrefix',
+        'quote_suffix' => 'setQuoteSuffix',
         'reply_count' => 'setReplyCount',
         'reply_count' => 'setReplyCount',
         'editor' => 'setEditor',
         'editor' => 'setEditor',
+        'co_editor' => 'setCoEditor',
         'abilities' => 'setAbilities',
         'abilities' => 'setAbilities',
         'created_at' => 'setCreatedAt',
         'created_at' => 'setCreatedAt',
         'updated_at' => 'setUpdatedAt',
         'updated_at' => 'setUpdatedAt',
@@ -287,8 +317,14 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'html' => 'getHtml',
         'html' => 'getHtml',
         'summary' => 'getSummary',
         'summary' => 'getSummary',
         'tpl_id' => 'getTplId',
         'tpl_id' => 'getTplId',
+        'pos_start' => 'getPosStart',
+        'pos_end' => 'getPosEnd',
+        'quote_exact' => 'getQuoteExact',
+        'quote_prefix' => 'getQuotePrefix',
+        'quote_suffix' => 'getQuoteSuffix',
         'reply_count' => 'getReplyCount',
         'reply_count' => 'getReplyCount',
         'editor' => 'getEditor',
         'editor' => 'getEditor',
+        'co_editor' => 'getCoEditor',
         'abilities' => 'getAbilities',
         'abilities' => 'getAbilities',
         'created_at' => 'getCreatedAt',
         'created_at' => 'getCreatedAt',
         'updated_at' => 'getUpdatedAt',
         'updated_at' => 'getUpdatedAt',
@@ -365,8 +401,14 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         $this->setIfExists('html', $data ?? [], null);
         $this->setIfExists('html', $data ?? [], null);
         $this->setIfExists('summary', $data ?? [], null);
         $this->setIfExists('summary', $data ?? [], null);
         $this->setIfExists('tpl_id', $data ?? [], null);
         $this->setIfExists('tpl_id', $data ?? [], null);
+        $this->setIfExists('pos_start', $data ?? [], null);
+        $this->setIfExists('pos_end', $data ?? [], null);
+        $this->setIfExists('quote_exact', $data ?? [], null);
+        $this->setIfExists('quote_prefix', $data ?? [], null);
+        $this->setIfExists('quote_suffix', $data ?? [], null);
         $this->setIfExists('reply_count', $data ?? [], null);
         $this->setIfExists('reply_count', $data ?? [], null);
         $this->setIfExists('editor', $data ?? [], null);
         $this->setIfExists('editor', $data ?? [], null);
+        $this->setIfExists('co_editor', $data ?? [], null);
         $this->setIfExists('abilities', $data ?? [], null);
         $this->setIfExists('abilities', $data ?? [], null);
         $this->setIfExists('created_at', $data ?? [], null);
         $this->setIfExists('created_at', $data ?? [], null);
         $this->setIfExists('updated_at', $data ?? [], null);
         $this->setIfExists('updated_at', $data ?? [], null);
@@ -801,6 +843,176 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         return $this;
         return $this;
     }
     }
 
 
+    /**
+     * Gets pos_start
+     *
+     * @return int|null
+     */
+    public function getPosStart()
+    {
+        return $this->container['pos_start'];
+    }
+
+    /**
+     * Sets pos_start
+     *
+     * @param int|null $pos_start pos_start
+     *
+     * @return self
+     */
+    public function setPosStart($pos_start)
+    {
+        if (is_null($pos_start)) {
+            array_push($this->openAPINullablesSetToNull, 'pos_start');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('pos_start', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['pos_start'] = $pos_start;
+
+        return $this;
+    }
+
+    /**
+     * Gets pos_end
+     *
+     * @return int|null
+     */
+    public function getPosEnd()
+    {
+        return $this->container['pos_end'];
+    }
+
+    /**
+     * Sets pos_end
+     *
+     * @param int|null $pos_end pos_end
+     *
+     * @return self
+     */
+    public function setPosEnd($pos_end)
+    {
+        if (is_null($pos_end)) {
+            array_push($this->openAPINullablesSetToNull, 'pos_end');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('pos_end', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['pos_end'] = $pos_end;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_exact
+     *
+     * @return string|null
+     */
+    public function getQuoteExact()
+    {
+        return $this->container['quote_exact'];
+    }
+
+    /**
+     * Sets quote_exact
+     *
+     * @param string|null $quote_exact quote_exact
+     *
+     * @return self
+     */
+    public function setQuoteExact($quote_exact)
+    {
+        if (is_null($quote_exact)) {
+            array_push($this->openAPINullablesSetToNull, 'quote_exact');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('quote_exact', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['quote_exact'] = $quote_exact;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_prefix
+     *
+     * @return string|null
+     */
+    public function getQuotePrefix()
+    {
+        return $this->container['quote_prefix'];
+    }
+
+    /**
+     * Sets quote_prefix
+     *
+     * @param string|null $quote_prefix quote_prefix
+     *
+     * @return self
+     */
+    public function setQuotePrefix($quote_prefix)
+    {
+        if (is_null($quote_prefix)) {
+            array_push($this->openAPINullablesSetToNull, 'quote_prefix');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('quote_prefix', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['quote_prefix'] = $quote_prefix;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_suffix
+     *
+     * @return string|null
+     */
+    public function getQuoteSuffix()
+    {
+        return $this->container['quote_suffix'];
+    }
+
+    /**
+     * Sets quote_suffix
+     *
+     * @param string|null $quote_suffix quote_suffix
+     *
+     * @return self
+     */
+    public function setQuoteSuffix($quote_suffix)
+    {
+        if (is_null($quote_suffix)) {
+            array_push($this->openAPINullablesSetToNull, 'quote_suffix');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('quote_suffix', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['quote_suffix'] = $quote_suffix;
+
+        return $this;
+    }
+
     /**
     /**
      * Gets reply_count
      * Gets reply_count
      *
      *
@@ -862,6 +1074,40 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         return $this;
         return $this;
     }
     }
 
 
+    /**
+     * Gets co_editor
+     *
+     * @return \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor|null
+     */
+    public function getCoEditor()
+    {
+        return $this->container['co_editor'];
+    }
+
+    /**
+     * Sets co_editor
+     *
+     * @param \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor|null $co_editor co_editor
+     *
+     * @return self
+     */
+    public function setCoEditor($co_editor)
+    {
+        if (is_null($co_editor)) {
+            array_push($this->openAPINullablesSetToNull, 'co_editor');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('co_editor', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['co_editor'] = $co_editor;
+
+        return $this;
+    }
+
     /**
     /**
      * Gets abilities
      * Gets abilities
      *
      *

+ 181 - 7
api-test/client/lib/Model/PatchApiV3DiscussionsDiscussionRequest.php

@@ -61,7 +61,12 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'content' => 'string',
         'content' => 'string',
         'status' => 'string',
         'status' => 'string',
         'type' => 'string',
         'type' => 'string',
-        'severity' => 'string'
+        'severity' => 'string',
+        'pos_start' => 'int',
+        'pos_end' => 'int',
+        'quote_exact' => 'string',
+        'quote_prefix' => 'string',
+        'quote_suffix' => 'string'
     ];
     ];
 
 
     /**
     /**
@@ -76,7 +81,12 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'content' => null,
         'content' => null,
         'status' => null,
         'status' => null,
         'type' => null,
         'type' => null,
-        'severity' => null
+        'severity' => null,
+        'pos_start' => null,
+        'pos_end' => null,
+        'quote_exact' => null,
+        'quote_prefix' => null,
+        'quote_suffix' => null
     ];
     ];
 
 
     /**
     /**
@@ -89,7 +99,12 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'content' => false,
         'content' => false,
         'status' => false,
         'status' => false,
         'type' => false,
         'type' => false,
-        'severity' => false
+        'severity' => false,
+        'pos_start' => false,
+        'pos_end' => false,
+        'quote_exact' => false,
+        'quote_prefix' => false,
+        'quote_suffix' => false
     ];
     ];
 
 
     /**
     /**
@@ -182,7 +197,12 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'content' => 'content',
         'content' => 'content',
         'status' => 'status',
         'status' => 'status',
         'type' => 'type',
         'type' => 'type',
-        'severity' => 'severity'
+        'severity' => 'severity',
+        'pos_start' => 'pos_start',
+        'pos_end' => 'pos_end',
+        'quote_exact' => 'quote_exact',
+        'quote_prefix' => 'quote_prefix',
+        'quote_suffix' => 'quote_suffix'
     ];
     ];
 
 
     /**
     /**
@@ -195,7 +215,12 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'content' => 'setContent',
         'content' => 'setContent',
         'status' => 'setStatus',
         'status' => 'setStatus',
         'type' => 'setType',
         'type' => 'setType',
-        'severity' => 'setSeverity'
+        'severity' => 'setSeverity',
+        'pos_start' => 'setPosStart',
+        'pos_end' => 'setPosEnd',
+        'quote_exact' => 'setQuoteExact',
+        'quote_prefix' => 'setQuotePrefix',
+        'quote_suffix' => 'setQuoteSuffix'
     ];
     ];
 
 
     /**
     /**
@@ -208,7 +233,12 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'content' => 'getContent',
         'content' => 'getContent',
         'status' => 'getStatus',
         'status' => 'getStatus',
         'type' => 'getType',
         'type' => 'getType',
-        'severity' => 'getSeverity'
+        'severity' => 'getSeverity',
+        'pos_start' => 'getPosStart',
+        'pos_end' => 'getPosEnd',
+        'quote_exact' => 'getQuoteExact',
+        'quote_prefix' => 'getQuotePrefix',
+        'quote_suffix' => 'getQuoteSuffix'
     ];
     ];
 
 
     /**
     /**
@@ -258,6 +288,8 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
     public const TYPE_QA = 'qa';
     public const TYPE_QA = 'qa';
     public const TYPE_HELP = 'help';
     public const TYPE_HELP = 'help';
     public const TYPE_ISSUE = 'issue';
     public const TYPE_ISSUE = 'issue';
+    public const TYPE_NOTE = 'note';
+    public const TYPE_COMMENTARY = 'commentary';
     public const SEVERITY_ERROR = 'error';
     public const SEVERITY_ERROR = 'error';
     public const SEVERITY_WARNING = 'warning';
     public const SEVERITY_WARNING = 'warning';
     public const SEVERITY_INFO = 'info';
     public const SEVERITY_INFO = 'info';
@@ -289,6 +321,8 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
             self::TYPE_QA,
             self::TYPE_QA,
             self::TYPE_HELP,
             self::TYPE_HELP,
             self::TYPE_ISSUE,
             self::TYPE_ISSUE,
+            self::TYPE_NOTE,
+            self::TYPE_COMMENTARY,
         ];
         ];
     }
     }
 
 
@@ -328,6 +362,11 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         $this->setIfExists('status', $data ?? [], null);
         $this->setIfExists('status', $data ?? [], null);
         $this->setIfExists('type', $data ?? [], null);
         $this->setIfExists('type', $data ?? [], null);
         $this->setIfExists('severity', $data ?? [], null);
         $this->setIfExists('severity', $data ?? [], null);
+        $this->setIfExists('pos_start', $data ?? [], null);
+        $this->setIfExists('pos_end', $data ?? [], null);
+        $this->setIfExists('quote_exact', $data ?? [], null);
+        $this->setIfExists('quote_prefix', $data ?? [], null);
+        $this->setIfExists('quote_suffix', $data ?? [], null);
     }
     }
 
 
     /**
     /**
@@ -503,7 +542,7 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
     /**
     /**
      * Sets type
      * Sets type
      *
      *
-     * @param string|null $type 话题类型(只限话题)
+     * @param string|null $type 类型(只限话题;评论线程与批注不能互转)
      *
      *
      * @return self
      * @return self
      */
      */
@@ -563,6 +602,141 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
 
 
         return $this;
         return $this;
     }
     }
+
+    /**
+     * Gets pos_start
+     *
+     * @return int|null
+     */
+    public function getPosStart()
+    {
+        return $this->container['pos_start'];
+    }
+
+    /**
+     * Sets pos_start
+     *
+     * @param int|null $pos_start 锚点起始偏移(只限批注)
+     *
+     * @return self
+     */
+    public function setPosStart($pos_start)
+    {
+        if (is_null($pos_start)) {
+            throw new \InvalidArgumentException('non-nullable pos_start cannot be null');
+        }
+        $this->container['pos_start'] = $pos_start;
+
+        return $this;
+    }
+
+    /**
+     * Gets pos_end
+     *
+     * @return int|null
+     */
+    public function getPosEnd()
+    {
+        return $this->container['pos_end'];
+    }
+
+    /**
+     * Sets pos_end
+     *
+     * @param int|null $pos_end 锚点结束偏移(只限批注)
+     *
+     * @return self
+     */
+    public function setPosEnd($pos_end)
+    {
+        if (is_null($pos_end)) {
+            throw new \InvalidArgumentException('non-nullable pos_end cannot be null');
+        }
+        $this->container['pos_end'] = $pos_end;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_exact
+     *
+     * @return string|null
+     */
+    public function getQuoteExact()
+    {
+        return $this->container['quote_exact'];
+    }
+
+    /**
+     * Sets quote_exact
+     *
+     * @param string|null $quote_exact 锚定的原文片段(只限批注)
+     *
+     * @return self
+     */
+    public function setQuoteExact($quote_exact)
+    {
+        if (is_null($quote_exact)) {
+            throw new \InvalidArgumentException('non-nullable quote_exact cannot be null');
+        }
+        $this->container['quote_exact'] = $quote_exact;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_prefix
+     *
+     * @return string|null
+     */
+    public function getQuotePrefix()
+    {
+        return $this->container['quote_prefix'];
+    }
+
+    /**
+     * Sets quote_prefix
+     *
+     * @param string|null $quote_prefix 锚定片段前的上下文(只限批注)
+     *
+     * @return self
+     */
+    public function setQuotePrefix($quote_prefix)
+    {
+        if (is_null($quote_prefix)) {
+            throw new \InvalidArgumentException('non-nullable quote_prefix cannot be null');
+        }
+        $this->container['quote_prefix'] = $quote_prefix;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_suffix
+     *
+     * @return string|null
+     */
+    public function getQuoteSuffix()
+    {
+        return $this->container['quote_suffix'];
+    }
+
+    /**
+     * Sets quote_suffix
+     *
+     * @param string|null $quote_suffix 锚定片段后的上下文(只限批注)
+     *
+     * @return self
+     */
+    public function setQuoteSuffix($quote_suffix)
+    {
+        if (is_null($quote_suffix)) {
+            throw new \InvalidArgumentException('non-nullable quote_suffix cannot be null');
+        }
+        $this->container['quote_suffix'] = $quote_suffix;
+
+        return $this;
+    }
     /**
     /**
      * Returns true if offset exists. False otherwise.
      * Returns true if offset exists. False otherwise.
      *
      *

+ 409 - 0
api-test/client/lib/Model/PostApiV3AgentGrants200Response.php

@@ -0,0 +1,409 @@
+<?php
+/**
+ * PostApiV3AgentGrants200Response
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3AgentGrants200Response Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3AgentGrants200Response implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_agent_grants_200_response';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'data' => '\WikipaliApi\Model\PostApiV3AgentGrants200ResponseData'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'data' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'data' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'data' => 'data'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'data' => 'setData'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'data' => 'getData'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('data', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets data
+     *
+     * @return \WikipaliApi\Model\PostApiV3AgentGrants200ResponseData|null
+     */
+    public function getData()
+    {
+        return $this->container['data'];
+    }
+
+    /**
+     * Sets data
+     *
+     * @param \WikipaliApi\Model\PostApiV3AgentGrants200ResponseData|null $data data
+     *
+     * @return self
+     */
+    public function setData($data)
+    {
+        if (is_null($data)) {
+            throw new \InvalidArgumentException('non-nullable data cannot be null');
+        }
+        $this->container['data'] = $data;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 613 - 0
api-test/client/lib/Model/PostApiV3AgentGrants200ResponseData.php

@@ -0,0 +1,613 @@
+<?php
+/**
+ * PostApiV3AgentGrants200ResponseData
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3AgentGrants200ResponseData Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3AgentGrants200ResponseData implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_agent_grants_200_response_data';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'token' => 'string',
+        'grantor' => 'string',
+        'ai_model' => 'string',
+        'target_type' => 'string',
+        'target_id' => 'string',
+        'book' => 'int',
+        'expires_at' => 'string'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'token' => null,
+        'grantor' => null,
+        'ai_model' => null,
+        'target_type' => null,
+        'target_id' => null,
+        'book' => null,
+        'expires_at' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'token' => false,
+        'grantor' => false,
+        'ai_model' => false,
+        'target_type' => false,
+        'target_id' => false,
+        'book' => false,
+        'expires_at' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'token' => 'token',
+        'grantor' => 'grantor',
+        'ai_model' => 'ai_model',
+        'target_type' => 'target_type',
+        'target_id' => 'target_id',
+        'book' => 'book',
+        'expires_at' => 'expires_at'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'token' => 'setToken',
+        'grantor' => 'setGrantor',
+        'ai_model' => 'setAiModel',
+        'target_type' => 'setTargetType',
+        'target_id' => 'setTargetId',
+        'book' => 'setBook',
+        'expires_at' => 'setExpiresAt'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'token' => 'getToken',
+        'grantor' => 'getGrantor',
+        'ai_model' => 'getAiModel',
+        'target_type' => 'getTargetType',
+        'target_id' => 'getTargetId',
+        'book' => 'getBook',
+        'expires_at' => 'getExpiresAt'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('token', $data ?? [], null);
+        $this->setIfExists('grantor', $data ?? [], null);
+        $this->setIfExists('ai_model', $data ?? [], null);
+        $this->setIfExists('target_type', $data ?? [], null);
+        $this->setIfExists('target_id', $data ?? [], null);
+        $this->setIfExists('book', $data ?? [], null);
+        $this->setIfExists('expires_at', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets token
+     *
+     * @return string|null
+     */
+    public function getToken()
+    {
+        return $this->container['token'];
+    }
+
+    /**
+     * Sets token
+     *
+     * @param string|null $token token
+     *
+     * @return self
+     */
+    public function setToken($token)
+    {
+        if (is_null($token)) {
+            throw new \InvalidArgumentException('non-nullable token cannot be null');
+        }
+        $this->container['token'] = $token;
+
+        return $this;
+    }
+
+    /**
+     * Gets grantor
+     *
+     * @return string|null
+     */
+    public function getGrantor()
+    {
+        return $this->container['grantor'];
+    }
+
+    /**
+     * Sets grantor
+     *
+     * @param string|null $grantor grantor
+     *
+     * @return self
+     */
+    public function setGrantor($grantor)
+    {
+        if (is_null($grantor)) {
+            throw new \InvalidArgumentException('non-nullable grantor cannot be null');
+        }
+        $this->container['grantor'] = $grantor;
+
+        return $this;
+    }
+
+    /**
+     * Gets ai_model
+     *
+     * @return string|null
+     */
+    public function getAiModel()
+    {
+        return $this->container['ai_model'];
+    }
+
+    /**
+     * Sets ai_model
+     *
+     * @param string|null $ai_model ai_model
+     *
+     * @return self
+     */
+    public function setAiModel($ai_model)
+    {
+        if (is_null($ai_model)) {
+            throw new \InvalidArgumentException('non-nullable ai_model cannot be null');
+        }
+        $this->container['ai_model'] = $ai_model;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_type
+     *
+     * @return string|null
+     */
+    public function getTargetType()
+    {
+        return $this->container['target_type'];
+    }
+
+    /**
+     * Sets target_type
+     *
+     * @param string|null $target_type target_type
+     *
+     * @return self
+     */
+    public function setTargetType($target_type)
+    {
+        if (is_null($target_type)) {
+            throw new \InvalidArgumentException('non-nullable target_type cannot be null');
+        }
+        $this->container['target_type'] = $target_type;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_id
+     *
+     * @return string|null
+     */
+    public function getTargetId()
+    {
+        return $this->container['target_id'];
+    }
+
+    /**
+     * Sets target_id
+     *
+     * @param string|null $target_id target_id
+     *
+     * @return self
+     */
+    public function setTargetId($target_id)
+    {
+        if (is_null($target_id)) {
+            throw new \InvalidArgumentException('non-nullable target_id cannot be null');
+        }
+        $this->container['target_id'] = $target_id;
+
+        return $this;
+    }
+
+    /**
+     * Gets book
+     *
+     * @return int|null
+     */
+    public function getBook()
+    {
+        return $this->container['book'];
+    }
+
+    /**
+     * Sets book
+     *
+     * @param int|null $book book
+     *
+     * @return self
+     */
+    public function setBook($book)
+    {
+        if (is_null($book)) {
+            throw new \InvalidArgumentException('non-nullable book cannot be null');
+        }
+        $this->container['book'] = $book;
+
+        return $this;
+    }
+
+    /**
+     * Gets expires_at
+     *
+     * @return string|null
+     */
+    public function getExpiresAt()
+    {
+        return $this->container['expires_at'];
+    }
+
+    /**
+     * Sets expires_at
+     *
+     * @param string|null $expires_at expires_at
+     *
+     * @return self
+     */
+    public function setExpiresAt($expires_at)
+    {
+        if (is_null($expires_at)) {
+            throw new \InvalidArgumentException('non-nullable expires_at cannot be null');
+        }
+        $this->container['expires_at'] = $expires_at;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 552 - 0
api-test/client/lib/Model/PostApiV3AgentGrantsRequest.php

@@ -0,0 +1,552 @@
+<?php
+/**
+ * PostApiV3AgentGrantsRequest
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3AgentGrantsRequest Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3AgentGrantsRequest implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_agent_grants_request';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'ai_model' => 'string',
+        'target_type' => 'string',
+        'target_id' => 'string',
+        'book' => 'int'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'ai_model' => null,
+        'target_type' => null,
+        'target_id' => null,
+        'book' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'ai_model' => false,
+        'target_type' => false,
+        'target_id' => false,
+        'book' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'ai_model' => 'ai_model',
+        'target_type' => 'target_type',
+        'target_id' => 'target_id',
+        'book' => 'book'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'ai_model' => 'setAiModel',
+        'target_type' => 'setTargetType',
+        'target_id' => 'setTargetId',
+        'book' => 'setBook'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'ai_model' => 'getAiModel',
+        'target_type' => 'getTargetType',
+        'target_id' => 'getTargetId',
+        'book' => 'getBook'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+    public const TARGET_TYPE_CHANNEL = 'channel';
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getTargetTypeAllowableValues()
+    {
+        return [
+            self::TARGET_TYPE_CHANNEL,
+        ];
+    }
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('ai_model', $data ?? [], null);
+        $this->setIfExists('target_type', $data ?? [], null);
+        $this->setIfExists('target_id', $data ?? [], null);
+        $this->setIfExists('book', $data ?? [], 0);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        if ($this->container['ai_model'] === null) {
+            $invalidProperties[] = "'ai_model' can't be null";
+        }
+        if ($this->container['target_type'] === null) {
+            $invalidProperties[] = "'target_type' can't be null";
+        }
+        $allowedValues = $this->getTargetTypeAllowableValues();
+        if (!is_null($this->container['target_type']) && !in_array($this->container['target_type'], $allowedValues, true)) {
+            $invalidProperties[] = sprintf(
+                "invalid value '%s' for 'target_type', must be one of '%s'",
+                $this->container['target_type'],
+                implode("', '", $allowedValues)
+            );
+        }
+
+        if ($this->container['target_id'] === null) {
+            $invalidProperties[] = "'target_id' can't be null";
+        }
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets ai_model
+     *
+     * @return string
+     */
+    public function getAiModel()
+    {
+        return $this->container['ai_model'];
+    }
+
+    /**
+     * Sets ai_model
+     *
+     * @param string $ai_model 被授权的 AI 模型 uid
+     *
+     * @return self
+     */
+    public function setAiModel($ai_model)
+    {
+        if (is_null($ai_model)) {
+            throw new \InvalidArgumentException('non-nullable ai_model cannot be null');
+        }
+        $this->container['ai_model'] = $ai_model;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_type
+     *
+     * @return string
+     */
+    public function getTargetType()
+    {
+        return $this->container['target_type'];
+    }
+
+    /**
+     * Sets target_type
+     *
+     * @param string $target_type 授权范围的类型
+     *
+     * @return self
+     */
+    public function setTargetType($target_type)
+    {
+        if (is_null($target_type)) {
+            throw new \InvalidArgumentException('non-nullable target_type cannot be null');
+        }
+        $allowedValues = $this->getTargetTypeAllowableValues();
+        if (!in_array($target_type, $allowedValues, true)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    "Invalid value '%s' for 'target_type', must be one of '%s'",
+                    $target_type,
+                    implode("', '", $allowedValues)
+                )
+            );
+        }
+        $this->container['target_type'] = $target_type;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_id
+     *
+     * @return string
+     */
+    public function getTargetId()
+    {
+        return $this->container['target_id'];
+    }
+
+    /**
+     * Sets target_id
+     *
+     * @param string $target_id channel 的 uid
+     *
+     * @return self
+     */
+    public function setTargetId($target_id)
+    {
+        if (is_null($target_id)) {
+            throw new \InvalidArgumentException('non-nullable target_id cannot be null');
+        }
+        $this->container['target_id'] = $target_id;
+
+        return $this;
+    }
+
+    /**
+     * Gets book
+     *
+     * @return int|null
+     */
+    public function getBook()
+    {
+        return $this->container['book'];
+    }
+
+    /**
+     * Sets book
+     *
+     * @param int|null $book 限定 book,0 或不传表示不限
+     *
+     * @return self
+     */
+    public function setBook($book)
+    {
+        if (is_null($book)) {
+            throw new \InvalidArgumentException('non-nullable book cannot be null');
+        }
+        $this->container['book'] = $book;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 179 - 8
api-test/client/lib/Model/PostApiV3DiscussionsRequest.php

@@ -63,6 +63,11 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => 'string',
         'title' => 'string',
         'content' => 'string',
         'content' => 'string',
         'severity' => 'string',
         'severity' => 'string',
+        'pos_start' => 'int',
+        'pos_end' => 'int',
+        'quote_exact' => 'string',
+        'quote_prefix' => 'string',
+        'quote_suffix' => 'string',
         'tpl_id' => 'string',
         'tpl_id' => 'string',
         'notify' => 'bool'
         'notify' => 'bool'
     ];
     ];
@@ -81,6 +86,11 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => null,
         'title' => null,
         'content' => null,
         'content' => null,
         'severity' => null,
         'severity' => null,
+        'pos_start' => null,
+        'pos_end' => null,
+        'quote_exact' => null,
+        'quote_prefix' => null,
+        'quote_suffix' => null,
         'tpl_id' => null,
         'tpl_id' => null,
         'notify' => null
         'notify' => null
     ];
     ];
@@ -97,6 +107,11 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => false,
         'title' => false,
         'content' => false,
         'content' => false,
         'severity' => false,
         'severity' => false,
+        'pos_start' => false,
+        'pos_end' => false,
+        'quote_exact' => false,
+        'quote_prefix' => false,
+        'quote_suffix' => false,
         'tpl_id' => false,
         'tpl_id' => false,
         'notify' => false
         'notify' => false
     ];
     ];
@@ -193,6 +208,11 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => 'title',
         'title' => 'title',
         'content' => 'content',
         'content' => 'content',
         'severity' => 'severity',
         'severity' => 'severity',
+        'pos_start' => 'pos_start',
+        'pos_end' => 'pos_end',
+        'quote_exact' => 'quote_exact',
+        'quote_prefix' => 'quote_prefix',
+        'quote_suffix' => 'quote_suffix',
         'tpl_id' => 'tpl_id',
         'tpl_id' => 'tpl_id',
         'notify' => 'notify'
         'notify' => 'notify'
     ];
     ];
@@ -209,6 +229,11 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => 'setTitle',
         'title' => 'setTitle',
         'content' => 'setContent',
         'content' => 'setContent',
         'severity' => 'setSeverity',
         'severity' => 'setSeverity',
+        'pos_start' => 'setPosStart',
+        'pos_end' => 'setPosEnd',
+        'quote_exact' => 'setQuoteExact',
+        'quote_prefix' => 'setQuotePrefix',
+        'quote_suffix' => 'setQuoteSuffix',
         'tpl_id' => 'setTplId',
         'tpl_id' => 'setTplId',
         'notify' => 'setNotify'
         'notify' => 'setNotify'
     ];
     ];
@@ -225,6 +250,11 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => 'getTitle',
         'title' => 'getTitle',
         'content' => 'getContent',
         'content' => 'getContent',
         'severity' => 'getSeverity',
         'severity' => 'getSeverity',
+        'pos_start' => 'getPosStart',
+        'pos_end' => 'getPosEnd',
+        'quote_exact' => 'getQuoteExact',
+        'quote_prefix' => 'getQuotePrefix',
+        'quote_suffix' => 'getQuoteSuffix',
         'tpl_id' => 'getTplId',
         'tpl_id' => 'getTplId',
         'notify' => 'getNotify'
         'notify' => 'getNotify'
     ];
     ];
@@ -279,6 +309,8 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
     public const TYPE_QA = 'qa';
     public const TYPE_QA = 'qa';
     public const TYPE_HELP = 'help';
     public const TYPE_HELP = 'help';
     public const TYPE_ISSUE = 'issue';
     public const TYPE_ISSUE = 'issue';
+    public const TYPE_NOTE = 'note';
+    public const TYPE_COMMENTARY = 'commentary';
     public const SEVERITY_ERROR = 'error';
     public const SEVERITY_ERROR = 'error';
     public const SEVERITY_WARNING = 'warning';
     public const SEVERITY_WARNING = 'warning';
     public const SEVERITY_INFO = 'info';
     public const SEVERITY_INFO = 'info';
@@ -313,6 +345,8 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
             self::TYPE_QA,
             self::TYPE_QA,
             self::TYPE_HELP,
             self::TYPE_HELP,
             self::TYPE_ISSUE,
             self::TYPE_ISSUE,
+            self::TYPE_NOTE,
+            self::TYPE_COMMENTARY,
         ];
         ];
     }
     }
 
 
@@ -353,6 +387,11 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         $this->setIfExists('title', $data ?? [], null);
         $this->setIfExists('title', $data ?? [], null);
         $this->setIfExists('content', $data ?? [], null);
         $this->setIfExists('content', $data ?? [], null);
         $this->setIfExists('severity', $data ?? [], null);
         $this->setIfExists('severity', $data ?? [], null);
+        $this->setIfExists('pos_start', $data ?? [], null);
+        $this->setIfExists('pos_end', $data ?? [], null);
+        $this->setIfExists('quote_exact', $data ?? [], null);
+        $this->setIfExists('quote_prefix', $data ?? [], null);
+        $this->setIfExists('quote_suffix', $data ?? [], null);
         $this->setIfExists('tpl_id', $data ?? [], null);
         $this->setIfExists('tpl_id', $data ?? [], null);
         $this->setIfExists('notify', $data ?? [], true);
         $this->setIfExists('notify', $data ?? [], true);
     }
     }
@@ -408,9 +447,6 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
             );
             );
         }
         }
 
 
-        if ($this->container['title'] === null) {
-            $invalidProperties[] = "'title' can't be null";
-        }
         $allowedValues = $this->getSeverityAllowableValues();
         $allowedValues = $this->getSeverityAllowableValues();
         if (!is_null($this->container['severity']) && !in_array($this->container['severity'], $allowedValues, true)) {
         if (!is_null($this->container['severity']) && !in_array($this->container['severity'], $allowedValues, true)) {
             $invalidProperties[] = sprintf(
             $invalidProperties[] = sprintf(
@@ -512,7 +548,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
     /**
     /**
      * Sets type
      * Sets type
      *
      *
-     * @param string|null $type 话题类型
+     * @param string|null $type 类型
      *
      *
      * @return self
      * @return self
      */
      */
@@ -539,7 +575,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
     /**
     /**
      * Gets title
      * Gets title
      *
      *
-     * @return string
+     * @return string|null
      */
      */
     public function getTitle()
     public function getTitle()
     {
     {
@@ -549,7 +585,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
     /**
     /**
      * Sets title
      * Sets title
      *
      *
-     * @param string $title 标题,最长 256 字符
+     * @param string|null $title 标题,最长 256 字符;评论线程必填,批注可省
      *
      *
      * @return self
      * @return self
      */
      */
@@ -576,7 +612,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
     /**
     /**
      * Sets content
      * Sets content
      *
      *
-     * @param string|null $content 正文(markdown)
+     * @param string|null $content 正文(markdown);批注必填
      *
      *
      * @return self
      * @return self
      */
      */
@@ -627,6 +663,141 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         return $this;
         return $this;
     }
     }
 
 
+    /**
+     * Gets pos_start
+     *
+     * @return int|null
+     */
+    public function getPosStart()
+    {
+        return $this->container['pos_start'];
+    }
+
+    /**
+     * Sets pos_start
+     *
+     * @param int|null $pos_start 批注锚点在句子文本中的起始偏移(只限批注)
+     *
+     * @return self
+     */
+    public function setPosStart($pos_start)
+    {
+        if (is_null($pos_start)) {
+            throw new \InvalidArgumentException('non-nullable pos_start cannot be null');
+        }
+        $this->container['pos_start'] = $pos_start;
+
+        return $this;
+    }
+
+    /**
+     * Gets pos_end
+     *
+     * @return int|null
+     */
+    public function getPosEnd()
+    {
+        return $this->container['pos_end'];
+    }
+
+    /**
+     * Sets pos_end
+     *
+     * @param int|null $pos_end 批注锚点的结束偏移,批注插在这里(只限批注)
+     *
+     * @return self
+     */
+    public function setPosEnd($pos_end)
+    {
+        if (is_null($pos_end)) {
+            throw new \InvalidArgumentException('non-nullable pos_end cannot be null');
+        }
+        $this->container['pos_end'] = $pos_end;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_exact
+     *
+     * @return string|null
+     */
+    public function getQuoteExact()
+    {
+        return $this->container['quote_exact'];
+    }
+
+    /**
+     * Sets quote_exact
+     *
+     * @param string|null $quote_exact 锚定的原文片段(只限批注)
+     *
+     * @return self
+     */
+    public function setQuoteExact($quote_exact)
+    {
+        if (is_null($quote_exact)) {
+            throw new \InvalidArgumentException('non-nullable quote_exact cannot be null');
+        }
+        $this->container['quote_exact'] = $quote_exact;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_prefix
+     *
+     * @return string|null
+     */
+    public function getQuotePrefix()
+    {
+        return $this->container['quote_prefix'];
+    }
+
+    /**
+     * Sets quote_prefix
+     *
+     * @param string|null $quote_prefix 锚定片段前的上下文,用于重新定位(只限批注)
+     *
+     * @return self
+     */
+    public function setQuotePrefix($quote_prefix)
+    {
+        if (is_null($quote_prefix)) {
+            throw new \InvalidArgumentException('non-nullable quote_prefix cannot be null');
+        }
+        $this->container['quote_prefix'] = $quote_prefix;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_suffix
+     *
+     * @return string|null
+     */
+    public function getQuoteSuffix()
+    {
+        return $this->container['quote_suffix'];
+    }
+
+    /**
+     * Sets quote_suffix
+     *
+     * @param string|null $quote_suffix 锚定片段后的上下文,用于重新定位(只限批注)
+     *
+     * @return self
+     */
+    public function setQuoteSuffix($quote_suffix)
+    {
+        if (is_null($quote_suffix)) {
+            throw new \InvalidArgumentException('non-nullable quote_suffix cannot be null');
+        }
+        $this->container['quote_suffix'] = $quote_suffix;
+
+        return $this;
+    }
+
     /**
     /**
      * Gets tpl_id
      * Gets tpl_id
      *
      *
@@ -667,7 +838,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
     /**
     /**
      * Sets notify
      * Sets notify
      *
      *
-     * @param bool|null $notify 是否发通知
+     * @param bool|null $notify 是否发通知(批注不发)
      *
      *
      * @return self
      * @return self
      */
      */

+ 3 - 0
api-test/config.local.example.php

@@ -22,6 +22,9 @@ return [
     'fixtures' => [
     'fixtures' => [
         // 讨论写用例挂的句子:必须在测试账号自己的 channel 里、由测试账号编辑过
         // 讨论写用例挂的句子:必须在测试账号自己的 channel 里、由测试账号编辑过
         // 'discussion_sentence' => 'ee5259be-5d1e-4408-a2a6-566a5773eef1',
         // 'discussion_sentence' => 'ee5259be-5d1e-4408-a2a6-566a5773eef1',
+        // 'discussion_channel' => '1721b7fa-e471-46fd-b860-64b9e79d370e',
+        // agent 代写用例:归测试账号所有的 AI 模型 uid;不给则 SKIP
+        // 'agent_model' => null,
         // email-certifications「已注册邮箱」用例;不给则 SKIP
         // email-certifications「已注册邮箱」用例;不给则 SKIP
         // 'registered_email' => 'someone@example.com',
         // 'registered_email' => 'someone@example.com',
     ],
     ],

+ 5 - 0
api-test/fixtures.php

@@ -28,6 +28,11 @@ return [
     // discussions 写用例挂的句子:放在测试账号自己的 channel 里、由测试账号编辑,
     // discussions 写用例挂的句子:放在测试账号自己的 channel 里、由测试账号编辑,
     // 这样即便有通知也只发给自己(用例本身还会带 notify=false)。换环境时按需覆盖。
     // 这样即便有通知也只发给自己(用例本身还会带 notify=false)。换环境时按需覆盖。
     'discussion_sentence' => 'ee5259be-5d1e-4408-a2a6-566a5773eef1',
     'discussion_sentence' => 'ee5259be-5d1e-4408-a2a6-566a5773eef1',
+    // discussion_sentence 所在的 channel(agent 授权的范围)
+    'discussion_channel' => '1721b7fa-e471-46fd-b860-64b9e79d370e',
+
+    // agent 代写用例:归测试账号所有的 AI 模型 uid,按环境在 config.local.php 里配,没配则 SKIP
+    'agent_model' => null,
 
 
     // 一个已注册账号的邮箱(email-certifications 的「已注册」用例;只校验、不发信)。
     // 一个已注册账号的邮箱(email-certifications 的「已注册」用例;只校验、不发信)。
     // 留空则该用例 SKIP。
     // 留空则该用例 SKIP。