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"
- }
- }
- }
- }
- }
- }
- }
- }
-}