diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09f3c7e27..2b6ae4d3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: echo "tag=$tag" >> "$GITHUB_OUTPUT" - name: Setup pnpm - uses: pnpm/action-setup@v4.4.0 + uses: pnpm/action-setup@v5.0.0 with: version: 10.28.2 diff --git a/astrbot/dashboard/routes/open_api.py b/astrbot/dashboard/routes/open_api.py index c1b290ae1..8f2047326 100644 --- a/astrbot/dashboard/routes/open_api.py +++ b/astrbot/dashboard/routes/open_api.py @@ -40,7 +40,10 @@ class OpenApiRoute(Route): "/v1/chat": ("POST", self.chat_send), "/v1/chat/sessions": ("GET", self.get_chat_sessions), "/v1/configs": ("GET", self.get_chat_configs), - "/v1/file": ("POST", self.upload_file), + "/v1/file": [ + ("POST", self.openapi_upload_file), + ("GET", self.openapi_get_file), + ], "/v1/im/message": ("POST", self.send_message), "/v1/im/bots": ("GET", self.get_bots), } @@ -455,10 +458,7 @@ class OpenApiRoute(Route): if msg_type == "end": break if (streaming and msg_type == "complete") or not streaming: - if chain_type in ( - "tool_call", - "tool_call_result", - ): + if chain_type in ("tool_call", "tool_call_result"): continue try: refs = self.chat_route._extract_web_search_refs( @@ -537,9 +537,12 @@ class OpenApiRoute(Route): except Exception as e: logger.debug("Open API WS connection closed: %s", e) - async def upload_file(self): + async def openapi_upload_file(self): return await self.chat_route.post_file() + async def openapi_get_file(self): + return await self.chat_route.get_attachment() + async def get_chat_sessions(self): username, username_err = self._resolve_open_username( request.args.get("username") diff --git a/dashboard/scripts/subset-mdi-font.mjs b/dashboard/scripts/subset-mdi-font.mjs index 973009042..467c76bf7 100644 --- a/dashboard/scripts/subset-mdi-font.mjs +++ b/dashboard/scripts/subset-mdi-font.mjs @@ -33,6 +33,12 @@ const UTILITY_CLASSES = new Set([ "mdi-18px", "mdi-24px", "mdi-36px", "mdi-48px", "mdi-subset", ]); +// Icons used indirectly by Vuetify internals, so they won't appear in src/ static scans. +const REQUIRED_ICONS = new Set([ + "mdi-radiobox-blank", + "mdi-radiobox-marked", +]); + // Regex to match individual icon class definitions in MDI CSS export const ICON_CLASS_PATTERN = /\.(mdi-[a-z][a-z0-9-]*)::before\s*\{\s*content:\s*"\\([0-9A-Fa-f]+)"\s*;?\s*}/g; @@ -53,7 +59,7 @@ export function* collectFiles(dir, exts) { /** Scan source files and return a Set of used mdi-* icon names. */ export function scanUsedIcons(sourceFiles) { const iconPattern = /mdi-[a-z][a-z0-9-]*/g; - const usedIcons = new Set(); + const usedIcons = new Set(REQUIRED_ICONS); for (const file of sourceFiles) { const content = readFileSync(file, "utf-8"); for (const match of content.matchAll(iconPattern)) { diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css index 6117190cf..787e510a5 100644 --- a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -1,4 +1,4 @@ -/* Auto-generated MDI subset – 235 icons */ +/* Auto-generated MDI subset – 237 icons */ /* Do not edit manually. Run: pnpm run subset-icons */ @font-face { @@ -744,6 +744,14 @@ content: "\F0432"; } +.mdi-radiobox-blank::before { + content: "\F043D"; +} + +.mdi-radiobox-marked::before { + content: "\F043E"; +} + .mdi-refresh::before { content: "\F0450"; } diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff index 32e3eea05..0bec68760 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff differ diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 index 4b578d1d5..e062f2682 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 differ diff --git a/dashboard/src/components/shared/ObjectEditor.vue b/dashboard/src/components/shared/ObjectEditor.vue index 326db56f4..3089e3946 100644 --- a/dashboard/src/components/shared/ObjectEditor.vue +++ b/dashboard/src/components/shared/ObjectEditor.vue @@ -59,16 +59,8 @@ >
-
- +
+ + @focus="pair._originalKey = pair.key" + @blur="onKeyBlur(pair)" + > import { ref, computed, watch } from 'vue' import { useI18n, useModuleI18n } from '@/i18n/composables' +import { useToast } from '@/utils/toast' const { t } = useI18n() const { tm, getRaw } = useModuleI18n('features/config-metadata') +const { warning: toastWarning } = useToast() const props = defineProps({ modelValue: { @@ -357,6 +352,7 @@ const localKeyValuePairs = ref([]) const originalKeyValuePairs = ref([]) const newKey = ref('') const newValueType = ref('string') +const nextPairId = ref(0) // Template schema support const templateSchema = computed(() => { @@ -383,12 +379,26 @@ watch(() => props.modelValue, (newValue) => { // The dialog-based editing handles internal updates }, { immediate: true }) +function createPair({ key, value, type, slider, template, jsonError = '', _originalKey }) { + return { + _id: nextPairId.value++, + key, + value, + type, + slider, + template, + jsonError, + _originalKey + } +} + function initializeLocalKeyValuePairs() { localKeyValuePairs.value = [] + nextPairId.value = 0 for (const [key, value] of Object.entries(props.modelValue)) { let _type = (typeof value) === 'object' ? 'json':(typeof value) - let _value = _type === 'json'?JSON.stringify(value):value - + let _value = _type === 'json' ? JSON.stringify(value) : value + // Check if this key has a template schema const template = templateSchema.value[key] if (template) { @@ -399,20 +409,20 @@ function initializeLocalKeyValuePairs() { _value = template.default !== undefined ? template.default : _value } } - - localKeyValuePairs.value.push({ - key: key, + + localKeyValuePairs.value.push(createPair({ + key, value: _value, type: _type, slider: template?.slider, - template: template - }) + template + })) } } function openDialog() { initializeLocalKeyValuePairs() - originalKeyValuePairs.value = JSON.parse(JSON.stringify(localKeyValuePairs.value)) // Deep copy + originalKeyValuePairs.value = localKeyValuePairs.value.map(pair => ({ ...pair })) newKey.value = '' newValueType.value = 'string' dialog.value = true @@ -423,7 +433,7 @@ function addKeyValuePair() { if (key !== '') { const isKeyExists = localKeyValuePairs.value.some(pair => pair.key === key) if (isKeyExists) { - alert(t('core.common.objectEditor.keyExists')) + toastWarning(t('core.common.objectEditor.keyExists')) return } @@ -436,28 +446,28 @@ function addKeyValuePair() { defaultValue = false break case 'json': - defaultValue = "{}" + defaultValue = '{}' break default: // string - defaultValue = "" + defaultValue = '' break } - localKeyValuePairs.value.push({ - key: key, + localKeyValuePairs.value.push(createPair({ + key, value: defaultValue, type: newValueType.value - }) + })) newKey.value = '' } } -function updateJSON(index, newValue) { +function validateJSON(pair) { try { - JSON.parse(newValue) - localKeyValuePairs.value[index].jsonError = '' + JSON.parse(pair.value) + pair.jsonError = '' } catch (e) { - localKeyValuePairs.value[index].jsonError = t('core.common.objectEditor.invalidJson') + pair.jsonError = t('core.common.objectEditor.invalidJson') } } @@ -468,39 +478,30 @@ function removeKeyValuePairByKey(key) { } } -function updateKey(index, newKey) { - const originalKey = localKeyValuePairs.value[index].key - // 如果键名没有改变,则不执行任何操作 - if (originalKey === newKey) return +function onKeyBlur(pair) { + const originalKey = pair._originalKey + const newKey = pair.key + if (originalKey === undefined || originalKey === newKey) return - // 检查新键名是否已存在 - const isKeyExists = localKeyValuePairs.value.some((pair, i) => i !== index && pair.key === newKey) + const isKeyExists = localKeyValuePairs.value.some(p => p !== pair && p.key === newKey) if (isKeyExists) { - // 如果键名已存在,提示用户并恢复原值 - alert(t('core.common.objectEditor.keyExists')) - // 将键名恢复为修改前的原始值 - localKeyValuePairs.value[index].key = originalKey + toastWarning(t('core.common.objectEditor.keyExists')) + pair.key = originalKey return } - // 检查新键名是否有模板 const template = templateSchema.value[newKey] if (template) { - // 更新类型和默认值 - localKeyValuePairs.value[index].type = template.type || localKeyValuePairs.value[index].type - if (localKeyValuePairs.value[index].value === undefined || localKeyValuePairs.value[index].value === null || localKeyValuePairs.value[index].value === '') { - localKeyValuePairs.value[index].value = template.default !== undefined ? template.default : localKeyValuePairs.value[index].value + pair.type = template.type || pair.type + if (pair.value === undefined || pair.value === null || pair.value === '') { + pair.value = template.default !== undefined ? template.default : pair.value } - localKeyValuePairs.value[index].slider = template.slider - localKeyValuePairs.value[index].template = template + pair.slider = template.slider + pair.template = template } else { - // 清除模板信息 - localKeyValuePairs.value[index].slider = undefined - localKeyValuePairs.value[index].template = undefined + pair.slider = undefined + pair.template = undefined } - - // 更新本地副本 - localKeyValuePairs.value[index].key = newKey } function isTemplateKeyAdded(templateKey) { @@ -519,20 +520,20 @@ function getTemplateValue(templateKey) { function updateTemplateValue(templateKey, newValue) { const existingIndex = localKeyValuePairs.value.findIndex(pair => pair.key === templateKey) const template = templateSchema.value[templateKey] - + if (existingIndex >= 0) { // 更新现有值 localKeyValuePairs.value[existingIndex].value = newValue } else { // 添加新字段 - let valueType = template?.type || 'string' - localKeyValuePairs.value.push({ + const valueType = template?.type || 'string' + localKeyValuePairs.value.push(createPair({ key: templateKey, value: newValue, type: valueType, slider: template?.slider, - template: template - }) + template + })) } } @@ -553,10 +554,10 @@ function getDefaultValueForType(type) { case 'boolean': return false case 'json': - return "{}" + return '{}' case 'string': default: - return "" + return '' } } @@ -580,7 +581,7 @@ function confirmDialog() { case 'bool': case 'boolean': // 布尔值通常由 v-switch 正确处理,但为保险起见可以显式转换 - // 注意:在 JavaScript 中,只有严格的 false, 0, "", null, undefined, NaN 会被转换为 false + // 注意:在 JavaScript 中,只有严格的 false, 0, '', null, undefined, NaN 会被转换为 false // 这里直接赋值 pair.value 应该是安全的,因为 v-model 绑定的就是布尔值 // convertedValue = Boolean(pair.value) break @@ -601,7 +602,7 @@ function confirmDialog() { function cancelDialog() { // Reset to original state - localKeyValuePairs.value = JSON.parse(JSON.stringify(originalKeyValuePairs.value)) + localKeyValuePairs.value = originalKeyValuePairs.value.map(pair => ({ ...pair })) dialog.value = false } @@ -628,3 +629,4 @@ function getTemplateTitle(template, templateKey) { opacity: 0.8; } + diff --git a/dashboard/tests/subsetMdiFont.test.mjs b/dashboard/tests/subsetMdiFont.test.mjs index 85b0ad0ba..ac232df91 100644 --- a/dashboard/tests/subsetMdiFont.test.mjs +++ b/dashboard/tests/subsetMdiFont.test.mjs @@ -83,7 +83,9 @@ test('scanUsedIcons extracts mdi-* icon names from files', () => { assert.ok(icons instanceof Set); assert.ok(icons.has('mdi-home')); assert.ok(icons.has('mdi-close')); - assert.equal(icons.size, 2); // mdi-home deduplicated + assert.ok(icons.has('mdi-radiobox-blank')); + assert.ok(icons.has('mdi-radiobox-marked')); + assert.equal(icons.size, 4); // source icons + required radio icons rmSync(tmp, { recursive: true }); }); @@ -101,12 +103,26 @@ test('scanUsedIcons excludes utility classes', () => { rmSync(tmp, { recursive: true }); }); -test('scanUsedIcons returns empty set when no icons found', () => { +test('scanUsedIcons includes required radio icons even when no mdi-* icons are found in source', () => { const tmp = makeTmpDir(); writeFileSync(join(tmp, 'A.vue'), '
Hello
'); const icons = scanUsedIcons(collectFiles(tmp, ['.vue'])); - assert.equal(icons.size, 0); + assert.ok(icons.has('mdi-radiobox-blank')); + assert.ok(icons.has('mdi-radiobox-marked')); + assert.equal(icons.size, 2); + + rmSync(tmp, { recursive: true }); +}); + +test('scanUsedIcons deduplicates required radio icons when source already references them', () => { + const tmp = makeTmpDir(); + writeFileSync(join(tmp, 'A.vue'), 'mdi-radiobox-markedmdi-home'); + + const icons = [...scanUsedIcons(collectFiles(tmp, ['.vue']))]; + assert.equal(icons.filter(icon => icon === 'mdi-radiobox-marked').length, 1); + assert.ok(icons.includes('mdi-radiobox-blank')); + assert.ok(icons.includes('mdi-home')); rmSync(tmp, { recursive: true }); }); diff --git a/docs/public/openapi.json b/docs/public/openapi.json index 2fadecbc0..c767f5b31 100644 --- a/docs/public/openapi.json +++ b/docs/public/openapi.json @@ -97,6 +97,48 @@ "403": { "$ref": "#/components/responses/Forbidden" } + } + }, + "get": { + "tags": [ + "Open API" + ], + "summary": "Get attachment file", + "description": "Get an uploaded attachment file by attachment_id.", + "security": [ + { + "ApiKeyHeader": [] + } + ], + "parameters": [ + { + "name": "attachment_id", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "description": "Attachment ID returned by POST /api/v1/file." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } } } }, diff --git a/openapi.json b/openapi.json deleted file mode 100644 index 2fadecbc0..000000000 --- a/openapi.json +++ /dev/null @@ -1,685 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "AstrBot Open API", - "version": "1.0.0", - "description": "Developer HTTP APIs for AstrBot. Use API Key authentication for /api/v1/* endpoints." - }, - "servers": [ - { - "url": "http://localhost:6185" - } - ], - "tags": [ - { - "name": "Open API", - "description": "Developer APIs authenticated by API Key" - } - ], - "paths": { - "/api/v1/im/bots": { - "get": { - "tags": [ - "Open API" - ], - "summary": "List bot IDs", - "description": "Returns configured bot/platform IDs.", - "security": [ - { - "ApiKeyHeader": [] - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseBotList" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/v1/file": { - "post": { - "tags": [ - "Open API" - ], - "summary": "Upload attachment file", - "description": "Upload a file and get attachment_id for later use in chat/message APIs.", - "security": [ - { - "ApiKeyHeader": [] - } - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "file" - ], - "properties": { - "file": { - "type": "string", - "format": "binary" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseUpload" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/v1/chat": { - "post": { - "tags": [ - "Open API" - ], - "summary": "Send chat message (SSE)", - "description": "Send message to AstrBot chat pipeline and receive streaming SSE response. Reuses /api/chat/send behavior. If session_id/conversation_id is omitted, server will create a new UUID session_id.", - "security": [ - { - "ApiKeyHeader": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatSendRequest" - }, - "examples": { - "plain": { - "value": { - "message": "Hello", - "username": "alice", - "session_id": "my_session_001", - "enable_streaming": true - } - }, - "multipartMessage": { - "value": { - "message": [ - { - "type": "plain", - "text": "Please analyze this file" - }, - { - "type": "file", - "attachment_id": "9a2f8c72-e7af-4c0e-b352-111111111111" - } - ], - "username": "alice", - "session_id": "my_session_001", - "selected_provider": "openai_chat_completion", - "selected_model": "gpt-4.1-mini", - "enable_streaming": true - } - }, - "withConfig": { - "value": { - "message": "Use a specific config for this session", - "username": "alice", - "session_id": "my_session_001", - "config_id": "default", - "enable_streaming": true - } - }, - "autoSessionWithUsername": { - "value": { - "message": "hello", - "username": "alice", - "enable_streaming": true - } - } - } - } - } - }, - "responses": { - "200": { - "description": "SSE stream", - "content": { - "text/event-stream": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/v1/chat/sessions": { - "get": { - "tags": [ - "Open API" - ], - "summary": "List chat sessions with pagination", - "description": "List chat sessions for the specified username.", - "security": [ - { - "ApiKeyHeader": [] - } - ], - "parameters": [ - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "default": 1, - "minimum": 1 - } - }, - { - "name": "page_size", - "in": "query", - "schema": { - "type": "integer", - "default": 20, - "minimum": 1, - "maximum": 100 - } - }, - { - "name": "platform_id", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Optional platform filter" - }, - { - "name": "username", - "in": "query", - "required": true, - "schema": { - "type": "string" - }, - "description": "Target username." - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseChatSessions" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/v1/im/message": { - "post": { - "tags": [ - "Open API" - ], - "summary": "Send proactive message to a platform bot", - "description": "Send message directly to platform bot by umo + message chain payload.", - "security": [ - { - "ApiKeyHeader": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SendMessageRequest" - }, - "examples": { - "plain": { - "value": { - "umo": "webchat:FriendMessage:openapi_probe", - "message": "ping from api key" - } - }, - "chain": { - "value": { - "umo": "webchat:FriendMessage:openapi_probe", - "message": [ - { - "type": "plain", - "text": "hello" - }, - { - "type": "image", - "attachment_id": "9a2f8c72-e7af-4c0e-b352-111111111111" - } - ] - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseEmpty" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/v1/configs": { - "get": { - "tags": [ - "Open API" - ], - "summary": "List available chat config files", - "description": "Returns all available AstrBot config files that can be selected by Chat API using config_id/config_name.", - "security": [ - { - "ApiKeyHeader": [] - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseChatConfigList" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - } - }, - "components": { - "securitySchemes": { - "ApiKeyHeader": { - "type": "apiKey", - "in": "header", - "name": "X-API-Key", - "description": "Open API key. Authorization: Bearer is also accepted." - } - }, - "responses": { - "Unauthorized": { - "description": "Unauthorized" - }, - "Forbidden": { - "description": "Forbidden" - } - }, - "schemas": { - "ApiResponseEmpty": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "ok" - }, - "message": { - "type": [ - "string", - "null" - ] - }, - "data": { - "type": "object", - "additionalProperties": true - } - } - }, - "ApiResponseBotList": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "ok" - }, - "message": { - "type": [ - "string", - "null" - ] - }, - "data": { - "type": "object", - "properties": { - "bot_ids": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - }, - "ApiResponseUpload": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "ok" - }, - "message": { - "type": [ - "string", - "null" - ] - }, - "data": { - "type": "object", - "properties": { - "attachment_id": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "type": { - "type": "string" - } - } - } - } - }, - "ApiResponseChatSessions": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "ok" - }, - "message": { - "type": [ - "string", - "null" - ] - }, - "data": { - "type": "object", - "properties": { - "sessions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ChatSessionItem" - } - }, - "page": { - "type": "integer" - }, - "page_size": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } - }, - "ChatSessionItem": { - "type": "object", - "properties": { - "session_id": { - "type": "string" - }, - "platform_id": { - "type": "string" - }, - "creator": { - "type": "string" - }, - "display_name": { - "type": [ - "string", - "null" - ] - }, - "is_group": { - "type": "integer" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - } - } - }, - "MessagePart": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "plain", - "reply", - "image", - "record", - "file", - "video" - ] - }, - "text": { - "type": "string" - }, - "message_id": { - "type": [ - "string", - "integer" - ] - }, - "selected_text": { - "type": "string" - }, - "attachment_id": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "path": { - "type": "string" - } - }, - "required": [ - "type" - ] - }, - "ChatSendRequest": { - "type": "object", - "required": [ - "message", - "username" - ], - "properties": { - "message": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/MessagePart" - } - } - ] - }, - "session_id": { - "type": "string", - "description": "Optional chat session ID. If omitted (and conversation_id is also omitted), server creates a UUID automatically." - }, - "conversation_id": { - "type": "string", - "description": "Alias of session_id." - }, - "username": { - "type": "string", - "description": "Target username." - }, - "selected_provider": { - "type": "string" - }, - "selected_model": { - "type": "string" - }, - "enable_streaming": { - "type": "boolean", - "default": true - }, - "config_id": { - "type": "string", - "description": "Optional AstrBot config file ID. If provided, the chat session will use this config file. Use \"default\" to reset to default config." - }, - "config_name": { - "type": "string", - "description": "Optional AstrBot config file name. Used only when config_id is not provided." - } - } - }, - "SendMessageRequest": { - "type": "object", - "required": [ - "umo", - "message" - ], - "properties": { - "umo": { - "type": "string", - "description": "Unified message origin. Format: platform:message_type:session_id" - }, - "message": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/MessagePart" - } - } - ] - } - } - }, - "ChatConfigFile": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "is_default": { - "type": "boolean" - } - }, - "required": [ - "id", - "name", - "path", - "is_default" - ] - }, - "ApiResponseChatConfigList": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "ok" - }, - "message": { - "type": [ - "string", - "null" - ] - }, - "data": { - "type": "object", - "properties": { - "configs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ChatConfigFile" - } - } - } - } - } - } - } - } -}