mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-17 01:49:15 +08:00
merge: resolve ObjectEditor.vue conflict from master
This commit is contained in:
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -59,16 +59,8 @@
|
||||
>
|
||||
<!-- Regular key-value pairs (non-template) -->
|
||||
<div v-if="nonTemplatePairs.length > 0">
|
||||
<div
|
||||
v-for="(pair, index) in nonTemplatePairs"
|
||||
:key="index"
|
||||
class="key-value-pair"
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
align="center"
|
||||
class="mb-2"
|
||||
>
|
||||
<div v-for="pair in nonTemplatePairs" :key="pair._id" class="key-value-pair">
|
||||
<v-row no-gutters align="center" class="mb-2">
|
||||
<v-col cols="4">
|
||||
<v-text-field
|
||||
v-model="pair.key"
|
||||
@@ -76,8 +68,9 @@
|
||||
variant="outlined"
|
||||
hide-details
|
||||
:placeholder="t('core.common.objectEditor.placeholders.keyName')"
|
||||
@blur="updateKey(index, pair.key)"
|
||||
/>
|
||||
@focus="pair._originalKey = pair.key"
|
||||
@blur="onKeyBlur(pair)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="7"
|
||||
@@ -131,8 +124,8 @@
|
||||
variant="outlined"
|
||||
hide-details="auto"
|
||||
:placeholder="t('core.common.objectEditor.placeholders.jsonValue')"
|
||||
@blur="validateJSON(pair)"
|
||||
:error-messages="pair.jsonError"
|
||||
@blur="updateJSON(index, pair.value)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
@@ -320,9 +313,11 @@
|
||||
<script setup>
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -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'), '<div>Hello</div>');
|
||||
|
||||
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'), '<v-icon>mdi-radiobox-marked</v-icon><v-icon>mdi-home</v-icon>');
|
||||
|
||||
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 });
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
685
openapi.json
685
openapi.json
@@ -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 <api_key> 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user