mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat: file operation restful api
This commit is contained in:
+2
-9
@@ -51,19 +51,12 @@ export const createAgent = ({
|
||||
const fs: HTTPMCPConnection = {
|
||||
type: 'http',
|
||||
name: 'fs',
|
||||
url: `${auth.baseUrl}/bots/${identity.botId}/container/fs`,
|
||||
url: `${auth.baseUrl}/bots/${identity.botId}/container/fs-mcp`,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${auth.bearer}`,
|
||||
},
|
||||
}
|
||||
const mcpFetch: StdioMCPConnection = {
|
||||
type: 'stdio',
|
||||
name: 'mcp-fetch',
|
||||
command: 'npx',
|
||||
args: ['fetch-mcp'],
|
||||
env: {},
|
||||
}
|
||||
return [fs, mcpFetch]
|
||||
return [fs]
|
||||
}
|
||||
|
||||
const generateSystemPrompt = () => {
|
||||
|
||||
+558
-3
@@ -354,6 +354,116 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs": {
|
||||
"get": {
|
||||
"description": "List entries under a relative path",
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "List files for a bot",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative directory path",
|
||||
"name": "path",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Recursive listing",
|
||||
"name": "recursive",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSListResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Delete a file or directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative path",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Recursive delete for directories",
|
||||
"name": "recursive",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSDeleteResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs-mcp": {
|
||||
"post": {
|
||||
"description": "Forwards MCP JSON-RPC requests to the MCP server inside the container.\nRequired:\n- container task is running\n- container has data mount (default /data) bound to \u003cdata_root\u003e/users/\u003cuser_id\u003e\n- container image contains the \"mcp\" binary\nAuth: Bearer JWT is used to determine user_id (sub or user_id).\nPaths must be relative (no leading slash) and must not contain \"..\".\n\nExample: tools/list\n{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}\n\nExample: tools/call (fs.read)\n{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"fs.read\",\"arguments\":{\"path\":\"notes.txt\"}}}",
|
||||
"tags": [
|
||||
@@ -413,12 +523,325 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs/dir": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Create a directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Directory payload",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSMkdirRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSWriteResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs/file": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Read file content",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative file path",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSReadResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Create or overwrite a file",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "File write payload",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSWriteRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSWriteResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs/stat": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Get file or directory metadata",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative path",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSStatResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs/upload": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Upload a file",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative file path or directory",
|
||||
"name": "path",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"description": "File to upload",
|
||||
"name": "file",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSWriteResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs/usage": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Get usage under a path",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative directory path",
|
||||
"name": "path",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSUsageResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/skills": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"containerd"
|
||||
],
|
||||
"summary": "List skills from container",
|
||||
"summary": "List skills from data directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -459,7 +882,7 @@ const docTemplate = `{
|
||||
"tags": [
|
||||
"containerd"
|
||||
],
|
||||
"summary": "Upload skills into container",
|
||||
"summary": "Upload skills into data directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -509,7 +932,7 @@ const docTemplate = `{
|
||||
"tags": [
|
||||
"containerd"
|
||||
],
|
||||
"summary": "Delete skills from container",
|
||||
"summary": "Delete skills from data directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -5236,6 +5659,138 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSDeleteResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/handlers.FSRestEntry"
|
||||
}
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSMkdirRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parents": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSReadResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"mod_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSRestEntry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"is_dir": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mod_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSStatResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"is_dir": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mod_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSUsageResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dir_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_bytes": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSWriteRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"overwrite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSWriteResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.GetContainerResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+558
-3
@@ -345,6 +345,116 @@
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs": {
|
||||
"get": {
|
||||
"description": "List entries under a relative path",
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "List files for a bot",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative directory path",
|
||||
"name": "path",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Recursive listing",
|
||||
"name": "recursive",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSListResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Delete a file or directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative path",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Recursive delete for directories",
|
||||
"name": "recursive",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSDeleteResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs-mcp": {
|
||||
"post": {
|
||||
"description": "Forwards MCP JSON-RPC requests to the MCP server inside the container.\nRequired:\n- container task is running\n- container has data mount (default /data) bound to \u003cdata_root\u003e/users/\u003cuser_id\u003e\n- container image contains the \"mcp\" binary\nAuth: Bearer JWT is used to determine user_id (sub or user_id).\nPaths must be relative (no leading slash) and must not contain \"..\".\n\nExample: tools/list\n{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}\n\nExample: tools/call (fs.read)\n{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"fs.read\",\"arguments\":{\"path\":\"notes.txt\"}}}",
|
||||
"tags": [
|
||||
@@ -404,12 +514,325 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs/dir": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Create a directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Directory payload",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSMkdirRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSWriteResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs/file": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Read file content",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative file path",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSReadResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Create or overwrite a file",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "File write payload",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSWriteRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSWriteResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs/stat": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Get file or directory metadata",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative path",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSStatResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs/upload": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Upload a file",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative file path or directory",
|
||||
"name": "path",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"description": "File to upload",
|
||||
"name": "file",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSWriteResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/fs/usage": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"fs"
|
||||
],
|
||||
"summary": "Get usage under a path",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Relative directory path",
|
||||
"name": "path",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.FSUsageResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/container/skills": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"containerd"
|
||||
],
|
||||
"summary": "List skills from container",
|
||||
"summary": "List skills from data directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -450,7 +873,7 @@
|
||||
"tags": [
|
||||
"containerd"
|
||||
],
|
||||
"summary": "Upload skills into container",
|
||||
"summary": "Upload skills into data directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -500,7 +923,7 @@
|
||||
"tags": [
|
||||
"containerd"
|
||||
],
|
||||
"summary": "Delete skills from container",
|
||||
"summary": "Delete skills from data directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -5227,6 +5650,138 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSDeleteResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/handlers.FSRestEntry"
|
||||
}
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSMkdirRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parents": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSReadResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"mod_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSRestEntry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"is_dir": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mod_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSStatResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"is_dir": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mod_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSUsageResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dir_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_bytes": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSWriteRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"overwrite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.FSWriteResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.GetContainerResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+367
-3
@@ -614,6 +614,91 @@ definitions:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
handlers.FSDeleteResponse:
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
type: object
|
||||
handlers.FSListResponse:
|
||||
properties:
|
||||
entries:
|
||||
items:
|
||||
$ref: '#/definitions/handlers.FSRestEntry'
|
||||
type: array
|
||||
path:
|
||||
type: string
|
||||
type: object
|
||||
handlers.FSMkdirRequest:
|
||||
properties:
|
||||
parents:
|
||||
type: boolean
|
||||
path:
|
||||
type: string
|
||||
type: object
|
||||
handlers.FSReadResponse:
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
mod_time:
|
||||
type: string
|
||||
mode:
|
||||
type: integer
|
||||
path:
|
||||
type: string
|
||||
size:
|
||||
type: integer
|
||||
type: object
|
||||
handlers.FSRestEntry:
|
||||
properties:
|
||||
is_dir:
|
||||
type: boolean
|
||||
mod_time:
|
||||
type: string
|
||||
mode:
|
||||
type: integer
|
||||
path:
|
||||
type: string
|
||||
size:
|
||||
type: integer
|
||||
type: object
|
||||
handlers.FSStatResponse:
|
||||
properties:
|
||||
is_dir:
|
||||
type: boolean
|
||||
mod_time:
|
||||
type: string
|
||||
mode:
|
||||
type: integer
|
||||
path:
|
||||
type: string
|
||||
size:
|
||||
type: integer
|
||||
type: object
|
||||
handlers.FSUsageResponse:
|
||||
properties:
|
||||
dir_count:
|
||||
type: integer
|
||||
file_count:
|
||||
type: integer
|
||||
path:
|
||||
type: string
|
||||
total_bytes:
|
||||
type: integer
|
||||
type: object
|
||||
handlers.FSWriteRequest:
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
overwrite:
|
||||
type: boolean
|
||||
path:
|
||||
type: string
|
||||
type: object
|
||||
handlers.FSWriteResponse:
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
type: object
|
||||
handlers.GetContainerResponse:
|
||||
properties:
|
||||
container_id:
|
||||
@@ -1617,6 +1702,79 @@ paths:
|
||||
tags:
|
||||
- containerd
|
||||
/bots/{bot_id}/container/fs:
|
||||
delete:
|
||||
parameters:
|
||||
- description: Bot ID
|
||||
in: path
|
||||
name: bot_id
|
||||
required: true
|
||||
type: string
|
||||
- description: Relative path
|
||||
in: query
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
- description: Recursive delete for directories
|
||||
in: query
|
||||
name: recursive
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.FSDeleteResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: Delete a file or directory
|
||||
tags:
|
||||
- fs
|
||||
get:
|
||||
description: List entries under a relative path
|
||||
parameters:
|
||||
- description: Bot ID
|
||||
in: path
|
||||
name: bot_id
|
||||
required: true
|
||||
type: string
|
||||
- description: Relative directory path
|
||||
in: query
|
||||
name: path
|
||||
type: string
|
||||
- description: Recursive listing
|
||||
in: query
|
||||
name: recursive
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.FSListResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: List files for a bot
|
||||
tags:
|
||||
- fs
|
||||
/bots/{bot_id}/container/fs-mcp:
|
||||
post:
|
||||
description: |-
|
||||
Forwards MCP JSON-RPC requests to the MCP server inside the container.
|
||||
@@ -1669,6 +1827,212 @@ paths:
|
||||
summary: MCP filesystem tools (JSON-RPC)
|
||||
tags:
|
||||
- containerd
|
||||
/bots/{bot_id}/container/fs/dir:
|
||||
post:
|
||||
parameters:
|
||||
- description: Bot ID
|
||||
in: path
|
||||
name: bot_id
|
||||
required: true
|
||||
type: string
|
||||
- description: Directory payload
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.FSMkdirRequest'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.FSWriteResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: Create a directory
|
||||
tags:
|
||||
- fs
|
||||
/bots/{bot_id}/container/fs/file:
|
||||
get:
|
||||
parameters:
|
||||
- description: Bot ID
|
||||
in: path
|
||||
name: bot_id
|
||||
required: true
|
||||
type: string
|
||||
- description: Relative file path
|
||||
in: query
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.FSReadResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: Read file content
|
||||
tags:
|
||||
- fs
|
||||
post:
|
||||
parameters:
|
||||
- description: Bot ID
|
||||
in: path
|
||||
name: bot_id
|
||||
required: true
|
||||
type: string
|
||||
- description: File write payload
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.FSWriteRequest'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.FSWriteResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"409":
|
||||
description: Conflict
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: Create or overwrite a file
|
||||
tags:
|
||||
- fs
|
||||
/bots/{bot_id}/container/fs/stat:
|
||||
get:
|
||||
parameters:
|
||||
- description: Bot ID
|
||||
in: path
|
||||
name: bot_id
|
||||
required: true
|
||||
type: string
|
||||
- description: Relative path
|
||||
in: query
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.FSStatResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: Get file or directory metadata
|
||||
tags:
|
||||
- fs
|
||||
/bots/{bot_id}/container/fs/upload:
|
||||
post:
|
||||
parameters:
|
||||
- description: Bot ID
|
||||
in: path
|
||||
name: bot_id
|
||||
required: true
|
||||
type: string
|
||||
- description: Relative file path or directory
|
||||
in: query
|
||||
name: path
|
||||
type: string
|
||||
- description: File to upload
|
||||
in: formData
|
||||
name: file
|
||||
required: true
|
||||
type: file
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.FSWriteResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: Upload a file
|
||||
tags:
|
||||
- fs
|
||||
/bots/{bot_id}/container/fs/usage:
|
||||
get:
|
||||
parameters:
|
||||
- description: Bot ID
|
||||
in: path
|
||||
name: bot_id
|
||||
required: true
|
||||
type: string
|
||||
- description: Relative directory path
|
||||
in: query
|
||||
name: path
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.FSUsageResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: Get usage under a path
|
||||
tags:
|
||||
- fs
|
||||
/bots/{bot_id}/container/skills:
|
||||
delete:
|
||||
parameters:
|
||||
@@ -1700,7 +2064,7 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: Delete skills from container
|
||||
summary: Delete skills from data directory
|
||||
tags:
|
||||
- containerd
|
||||
get:
|
||||
@@ -1727,7 +2091,7 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: List skills from container
|
||||
summary: List skills from data directory
|
||||
tags:
|
||||
- containerd
|
||||
post:
|
||||
@@ -1760,7 +2124,7 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: Upload skills into container
|
||||
summary: Upload skills into data directory
|
||||
tags:
|
||||
- containerd
|
||||
/bots/{bot_id}/container/snapshots:
|
||||
|
||||
@@ -120,9 +120,18 @@ func (h *ContainerdHandler) Register(e *echo.Echo) {
|
||||
group.GET("/skills", h.ListSkills)
|
||||
group.POST("/skills", h.UpsertSkills)
|
||||
group.DELETE("/skills", h.DeleteSkills)
|
||||
group.POST("/fs", h.HandleMCPFS)
|
||||
group.POST("/fs-mcp", h.HandleMCPFS)
|
||||
|
||||
root := e.Group("/bots/:bot_id")
|
||||
fs := e.Group("/bots/:bot_id/container/fs")
|
||||
fs.GET("", h.ListFS)
|
||||
fs.GET("/file", h.ReadFSFile)
|
||||
fs.GET("/stat", h.StatFS)
|
||||
fs.GET("/usage", h.UsageFS)
|
||||
fs.POST("/file", h.WriteFSFile)
|
||||
fs.POST("/dir", h.MkdirFS)
|
||||
fs.POST("/upload", h.UploadFS)
|
||||
fs.DELETE("", h.DeleteFS)
|
||||
root.POST("/mcp-stdio", h.CreateMCPStdio)
|
||||
root.POST("/mcp-stdio/:session_id", h.HandleMCPStdio)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import (
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /bots/{bot_id}/container/fs [post]
|
||||
// @Router /bots/{bot_id}/container/fs-mcp [post]
|
||||
func (h *ContainerdHandler) HandleMCPFS(c echo.Context) error {
|
||||
botID, err := h.requireBotAccess(c)
|
||||
if err != nil {
|
||||
@@ -429,4 +429,3 @@ func (s *mcpSession) writePayloads(payloads []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/memohai/memoh/internal/config"
|
||||
)
|
||||
|
||||
type FSListResponse struct {
|
||||
Path string `json:"path"`
|
||||
Entries []FSRestEntry `json:"entries"`
|
||||
}
|
||||
|
||||
type FSRestEntry struct {
|
||||
Path string `json:"path"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Size int64 `json:"size"`
|
||||
Mode uint32 `json:"mode"`
|
||||
ModTime time.Time `json:"mod_time"`
|
||||
}
|
||||
|
||||
type FSReadResponse struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Size int64 `json:"size"`
|
||||
Mode uint32 `json:"mode"`
|
||||
ModTime time.Time `json:"mod_time"`
|
||||
}
|
||||
|
||||
type FSStatResponse struct {
|
||||
Path string `json:"path"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Size int64 `json:"size"`
|
||||
Mode uint32 `json:"mode"`
|
||||
ModTime time.Time `json:"mod_time"`
|
||||
}
|
||||
|
||||
type FSUsageResponse struct {
|
||||
Path string `json:"path"`
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
FileCount int64 `json:"file_count"`
|
||||
DirCount int64 `json:"dir_count"`
|
||||
}
|
||||
|
||||
type FSWriteRequest struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Overwrite *bool `json:"overwrite"`
|
||||
}
|
||||
|
||||
type FSWriteResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
|
||||
type FSMkdirRequest struct {
|
||||
Path string `json:"path"`
|
||||
Parents *bool `json:"parents"`
|
||||
}
|
||||
|
||||
type FSDeleteResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
|
||||
// ListFS godoc
|
||||
// @Summary List files for a bot
|
||||
// @Description List entries under a relative path
|
||||
// @Tags fs
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Param path query string false "Relative directory path"
|
||||
// @Param recursive query bool false "Recursive listing"
|
||||
// @Success 200 {object} FSListResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /bots/{bot_id}/container/fs [get]
|
||||
func (h *ContainerdHandler) ListFS(c echo.Context) error {
|
||||
botID, err := h.requireBotAccess(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := h.ensureBotDataRoot(botID)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
recursive, err := parseBoolQuery(c, "recursive")
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
target, rel, err := resolveBotPath(root, c.QueryParam("path"), true)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "path is not a directory")
|
||||
}
|
||||
|
||||
entries := []FSRestEntry{}
|
||||
if recursive {
|
||||
err = filepath.WalkDir(target, func(p string, d os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if p == target {
|
||||
return nil
|
||||
}
|
||||
entryInfo, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry, err := entryForBotPath(root, p, entryInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
dirEntries, err := os.ReadDir(target)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
for _, entry := range dirEntries {
|
||||
entryInfo, err := entry.Info()
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
fullPath := filepath.Join(target, entry.Name())
|
||||
fileEntry, err := entryForBotPath(root, fullPath, entryInfo)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
entries = append(entries, fileEntry)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
|
||||
listedPath := strings.TrimSpace(rel)
|
||||
if listedPath == "" || listedPath == "." {
|
||||
listedPath = "."
|
||||
}
|
||||
return c.JSON(http.StatusOK, FSListResponse{Path: listedPath, Entries: entries})
|
||||
}
|
||||
|
||||
// ReadFSFile godoc
|
||||
// @Summary Read file content
|
||||
// @Tags fs
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Param path query string true "Relative file path"
|
||||
// @Success 200 {object} FSReadResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /bots/{bot_id}/container/fs/file [get]
|
||||
func (h *ContainerdHandler) ReadFSFile(c echo.Context) error {
|
||||
botID, err := h.requireBotAccess(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := h.ensureBotDataRoot(botID)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
target, rel, err := resolveBotPath(root, c.QueryParam("path"), false)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "path is a directory")
|
||||
}
|
||||
data, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, FSReadResponse{
|
||||
Path: rel,
|
||||
Content: string(data),
|
||||
Size: info.Size(),
|
||||
Mode: uint32(info.Mode().Perm()),
|
||||
ModTime: info.ModTime(),
|
||||
})
|
||||
}
|
||||
|
||||
// StatFS godoc
|
||||
// @Summary Get file or directory metadata
|
||||
// @Tags fs
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Param path query string true "Relative path"
|
||||
// @Success 200 {object} FSStatResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /bots/{bot_id}/container/fs/stat [get]
|
||||
func (h *ContainerdHandler) StatFS(c echo.Context) error {
|
||||
botID, err := h.requireBotAccess(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := h.ensureBotDataRoot(botID)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
target, rel, err := resolveBotPath(root, c.QueryParam("path"), false)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, FSStatResponse{
|
||||
Path: rel,
|
||||
IsDir: info.IsDir(),
|
||||
Size: info.Size(),
|
||||
Mode: uint32(info.Mode().Perm()),
|
||||
ModTime: info.ModTime(),
|
||||
})
|
||||
}
|
||||
|
||||
// UsageFS godoc
|
||||
// @Summary Get usage under a path
|
||||
// @Tags fs
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Param path query string false "Relative directory path"
|
||||
// @Success 200 {object} FSUsageResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /bots/{bot_id}/container/fs/usage [get]
|
||||
func (h *ContainerdHandler) UsageFS(c echo.Context) error {
|
||||
botID, err := h.requireBotAccess(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := h.ensureBotDataRoot(botID)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
target, rel, err := resolveBotPath(root, c.QueryParam("path"), true)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
|
||||
var totalBytes int64
|
||||
var fileCount int64
|
||||
var dirCount int64
|
||||
if info.IsDir() {
|
||||
err = filepath.WalkDir(target, func(p string, d os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if p == target {
|
||||
return nil
|
||||
}
|
||||
entryInfo, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entryInfo.IsDir() {
|
||||
dirCount++
|
||||
return nil
|
||||
}
|
||||
fileCount++
|
||||
totalBytes += entryInfo.Size()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
} else {
|
||||
fileCount = 1
|
||||
totalBytes = info.Size()
|
||||
}
|
||||
|
||||
usagePath := strings.TrimSpace(rel)
|
||||
if usagePath == "" || usagePath == "." {
|
||||
usagePath = "."
|
||||
}
|
||||
return c.JSON(http.StatusOK, FSUsageResponse{
|
||||
Path: usagePath,
|
||||
TotalBytes: totalBytes,
|
||||
FileCount: fileCount,
|
||||
DirCount: dirCount,
|
||||
})
|
||||
}
|
||||
|
||||
// WriteFSFile godoc
|
||||
// @Summary Create or overwrite a file
|
||||
// @Tags fs
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Param payload body FSWriteRequest true "File write payload"
|
||||
// @Success 200 {object} FSWriteResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 409 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /bots/{bot_id}/container/fs/file [post]
|
||||
func (h *ContainerdHandler) WriteFSFile(c echo.Context) error {
|
||||
botID, err := h.requireBotAccess(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var req FSWriteRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if strings.TrimSpace(req.Path) == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "path is required")
|
||||
}
|
||||
root, err := h.ensureBotDataRoot(botID)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
target, _, err := resolveBotPath(root, req.Path, false)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
overwrite := true
|
||||
if req.Overwrite != nil {
|
||||
overwrite = *req.Overwrite
|
||||
}
|
||||
if _, err := os.Stat(target); err == nil && !overwrite {
|
||||
return echo.NewHTTPError(http.StatusConflict, "file already exists")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err := os.WriteFile(target, []byte(req.Content), 0o644); err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, FSWriteResponse{OK: true})
|
||||
}
|
||||
|
||||
// MkdirFS godoc
|
||||
// @Summary Create a directory
|
||||
// @Tags fs
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Param payload body FSMkdirRequest true "Directory payload"
|
||||
// @Success 200 {object} FSWriteResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /bots/{bot_id}/container/fs/dir [post]
|
||||
func (h *ContainerdHandler) MkdirFS(c echo.Context) error {
|
||||
botID, err := h.requireBotAccess(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var req FSMkdirRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if strings.TrimSpace(req.Path) == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "path is required")
|
||||
}
|
||||
root, err := h.ensureBotDataRoot(botID)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
target, _, err := resolveBotPath(root, req.Path, false)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
parents := true
|
||||
if req.Parents != nil {
|
||||
parents = *req.Parents
|
||||
}
|
||||
if parents {
|
||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
} else if err := os.Mkdir(target, 0o755); err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, FSWriteResponse{OK: true})
|
||||
}
|
||||
|
||||
// UploadFS godoc
|
||||
// @Summary Upload a file
|
||||
// @Tags fs
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Param path query string false "Relative file path or directory"
|
||||
// @Param file formData file true "File to upload"
|
||||
// @Success 200 {object} FSWriteResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /bots/{bot_id}/container/fs/upload [post]
|
||||
func (h *ContainerdHandler) UploadFS(c echo.Context) error {
|
||||
botID, err := h.requireBotAccess(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "file is required")
|
||||
}
|
||||
rawPath := strings.TrimSpace(c.FormValue("path"))
|
||||
if rawPath == "" {
|
||||
rawPath = strings.TrimSpace(c.QueryParam("path"))
|
||||
}
|
||||
if rawPath == "" {
|
||||
rawPath = file.Filename
|
||||
}
|
||||
root, err := h.ensureBotDataRoot(botID)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
targetPath := rawPath
|
||||
if strings.HasSuffix(rawPath, "/") || strings.HasSuffix(rawPath, string(os.PathSeparator)) {
|
||||
targetPath = filepath.ToSlash(filepath.Join(rawPath, file.Filename))
|
||||
}
|
||||
target, _, err := resolveBotPath(root, targetPath, false)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
defer src.Close()
|
||||
dst, err := os.Create(target)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
defer dst.Close()
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return c.JSON(http.StatusOK, FSWriteResponse{OK: true})
|
||||
}
|
||||
|
||||
// DeleteFS godoc
|
||||
// @Summary Delete a file or directory
|
||||
// @Tags fs
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Param path query string true "Relative path"
|
||||
// @Param recursive query bool false "Recursive delete for directories"
|
||||
// @Success 200 {object} FSDeleteResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /bots/{bot_id}/container/fs [delete]
|
||||
func (h *ContainerdHandler) DeleteFS(c echo.Context) error {
|
||||
botID, err := h.requireBotAccess(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recursive, err := parseBoolQuery(c, "recursive")
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
root, err := h.ensureBotDataRoot(botID)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
target, rel, err := resolveBotPath(root, c.QueryParam("path"), false)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
if rel == "." || rel == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "refuse to delete root")
|
||||
}
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
if info.IsDir() && recursive {
|
||||
if err := os.RemoveAll(target); err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
} else if err := os.Remove(target); err != nil {
|
||||
return fsHTTPError(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, FSDeleteResponse{OK: true})
|
||||
}
|
||||
|
||||
func (h *ContainerdHandler) ensureBotDataRoot(botID string) (string, error) {
|
||||
dataRoot := strings.TrimSpace(h.cfg.DataRoot)
|
||||
if dataRoot == "" {
|
||||
dataRoot = config.DefaultDataRoot
|
||||
}
|
||||
dataRoot, err := filepath.Abs(dataRoot)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
root := filepath.Join(dataRoot, "bots", botID)
|
||||
if err := os.MkdirAll(root, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func resolveBotPath(root, requestPath string, allowRoot bool) (string, string, error) {
|
||||
raw := strings.TrimSpace(requestPath)
|
||||
if raw == "" {
|
||||
if allowRoot {
|
||||
return root, ".", nil
|
||||
}
|
||||
return "", "", os.ErrInvalid
|
||||
}
|
||||
clean := filepath.Clean(filepath.FromSlash(raw))
|
||||
if clean == "." || clean == "" {
|
||||
if allowRoot {
|
||||
return root, ".", nil
|
||||
}
|
||||
return "", "", os.ErrInvalid
|
||||
}
|
||||
if filepath.IsAbs(clean) || strings.HasPrefix(clean, "..") {
|
||||
return "", "", os.ErrInvalid
|
||||
}
|
||||
target := filepath.Join(root, clean)
|
||||
rel, err := filepath.Rel(root, target)
|
||||
if err != nil || strings.HasPrefix(rel, "..") {
|
||||
return "", "", os.ErrInvalid
|
||||
}
|
||||
return target, filepath.ToSlash(rel), nil
|
||||
}
|
||||
|
||||
func entryForBotPath(root, target string, info os.FileInfo) (FSRestEntry, error) {
|
||||
rel, err := filepath.Rel(root, target)
|
||||
if err != nil {
|
||||
return FSRestEntry{}, err
|
||||
}
|
||||
if strings.HasPrefix(rel, "..") {
|
||||
return FSRestEntry{}, os.ErrInvalid
|
||||
}
|
||||
if rel == "." {
|
||||
rel = ""
|
||||
}
|
||||
return FSRestEntry{
|
||||
Path: filepath.ToSlash(rel),
|
||||
IsDir: info.IsDir(),
|
||||
Size: info.Size(),
|
||||
Mode: uint32(info.Mode().Perm()),
|
||||
ModTime: info.ModTime(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseBoolQuery(c echo.Context, key string) (bool, error) {
|
||||
raw := strings.TrimSpace(c.QueryParam(key))
|
||||
if raw == "" {
|
||||
return false, nil
|
||||
}
|
||||
return strconv.ParseBool(raw)
|
||||
}
|
||||
|
||||
func fsHTTPError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, os.ErrInvalid) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid path")
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "path not found")
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
+62
-130
@@ -2,19 +2,14 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/memohai/memoh/internal/config"
|
||||
mcptools "github.com/memohai/memoh/internal/mcp"
|
||||
)
|
||||
|
||||
type SkillItem struct {
|
||||
@@ -41,7 +36,7 @@ type skillsOpResponse struct {
|
||||
}
|
||||
|
||||
// ListSkills godoc
|
||||
// @Summary List skills from container
|
||||
// @Summary List skills from data directory
|
||||
// @Tags containerd
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Success 200 {object} SkillsResponse
|
||||
@@ -54,26 +49,11 @@ func (h *ContainerdHandler) ListSkills(c echo.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
containerID, err := h.botContainerID(ctx, botID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.ensureTaskRunning(ctx, containerID); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err := h.ensureSkillsDirHost(botID); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
listPayload, err := h.callMCPTool(ctx, containerID, "list", map[string]any{
|
||||
"path": ".skills",
|
||||
"recursive": false,
|
||||
})
|
||||
skillsDir, err := h.ensureSkillsDirHost(botID)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
entries, err := extractListEntries(listPayload)
|
||||
entries, err := listSkillEntries(skillsDir)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -84,7 +64,7 @@ func (h *ContainerdHandler) ListSkills(c echo.Context) error {
|
||||
if skillPath == "" {
|
||||
continue
|
||||
}
|
||||
raw, err := h.readSkillFile(ctx, containerID, skillPath)
|
||||
raw, err := h.readSkillFile(skillsDir, skillPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -101,7 +81,7 @@ func (h *ContainerdHandler) ListSkills(c echo.Context) error {
|
||||
}
|
||||
|
||||
// UpsertSkills godoc
|
||||
// @Summary Upload skills into container
|
||||
// @Summary Upload skills into data directory
|
||||
// @Tags containerd
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Param payload body SkillsUpsertRequest true "Skills payload"
|
||||
@@ -123,12 +103,8 @@ func (h *ContainerdHandler) UpsertSkills(c echo.Context) error {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "skills is required")
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
containerID, err := h.botContainerID(ctx, botID)
|
||||
skillsDir, err := h.ensureSkillsDirHost(botID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.ensureTaskRunning(ctx, containerID); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
for _, skill := range req.Skills {
|
||||
@@ -140,11 +116,12 @@ func (h *ContainerdHandler) UpsertSkills(c echo.Context) error {
|
||||
if content == "" {
|
||||
content = buildSkillContent(name, strings.TrimSpace(skill.Description))
|
||||
}
|
||||
filePath := path.Join(".skills", name, "SKILL.md")
|
||||
if _, err := h.callMCPTool(ctx, containerID, "write", map[string]any{
|
||||
"path": filePath,
|
||||
"content": content,
|
||||
}); err != nil {
|
||||
dirPath := filepath.Join(skillsDir, name)
|
||||
if err := os.MkdirAll(dirPath, 0o755); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
filePath := filepath.Join(dirPath, "SKILL.md")
|
||||
if err := os.WriteFile(filePath, []byte(content), 0o644); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
@@ -153,7 +130,7 @@ func (h *ContainerdHandler) UpsertSkills(c echo.Context) error {
|
||||
}
|
||||
|
||||
// DeleteSkills godoc
|
||||
// @Summary Delete skills from container
|
||||
// @Summary Delete skills from data directory
|
||||
// @Tags containerd
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Param payload body SkillsDeleteRequest true "Delete skills payload"
|
||||
@@ -175,12 +152,8 @@ func (h *ContainerdHandler) DeleteSkills(c echo.Context) error {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "names is required")
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
containerID, err := h.botContainerID(ctx, botID)
|
||||
skillsDir, err := h.ensureSkillsDirHost(botID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.ensureTaskRunning(ctx, containerID); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
@@ -189,10 +162,8 @@ func (h *ContainerdHandler) DeleteSkills(c echo.Context) error {
|
||||
if !isValidSkillName(skillName) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid skill name")
|
||||
}
|
||||
deletePath := path.Join(".skills", skillName)
|
||||
if _, err := h.callMCPTool(ctx, containerID, "delete", map[string]any{
|
||||
"path": deletePath,
|
||||
}); err != nil {
|
||||
deletePath := filepath.Join(skillsDir, skillName)
|
||||
if err := os.RemoveAll(deletePath); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
@@ -203,25 +174,12 @@ func (h *ContainerdHandler) DeleteSkills(c echo.Context) error {
|
||||
// LoadSkills loads all skills from the container for the given bot.
|
||||
// This implements chat.SkillLoader.
|
||||
func (h *ContainerdHandler) LoadSkills(ctx context.Context, botID string) ([]SkillItem, error) {
|
||||
containerID, err := h.botContainerID(ctx, botID)
|
||||
skillsDir, err := h.ensureSkillsDirHost(botID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := h.ensureTaskRunning(ctx, containerID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := h.ensureSkillsDirHost(botID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listPayload, err := h.callMCPTool(ctx, containerID, "list", map[string]any{
|
||||
"path": ".skills",
|
||||
"recursive": false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := extractListEntries(listPayload)
|
||||
entries, err := listSkillEntries(skillsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -232,7 +190,7 @@ func (h *ContainerdHandler) LoadSkills(ctx context.Context, botID string) ([]Ski
|
||||
if skillPath == "" {
|
||||
continue
|
||||
}
|
||||
raw, err := h.readSkillFile(ctx, containerID, skillPath)
|
||||
raw, err := h.readSkillFile(skillsDir, skillPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -247,69 +205,55 @@ func (h *ContainerdHandler) LoadSkills(ctx context.Context, botID string) ([]Ski
|
||||
return skills, nil
|
||||
}
|
||||
|
||||
func (h *ContainerdHandler) ensureSkillsDirHost(botID string) error {
|
||||
dataRoot := strings.TrimSpace(h.cfg.DataRoot)
|
||||
if dataRoot == "" {
|
||||
dataRoot = config.DefaultDataRoot
|
||||
func (h *ContainerdHandler) ensureSkillsDirHost(botID string) (string, error) {
|
||||
root, err := h.ensureBotDataRoot(botID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
skillsDir := path.Join(dataRoot, "bots", botID, ".skills")
|
||||
return os.MkdirAll(skillsDir, 0o755)
|
||||
skillsDir := filepath.Join(root, ".skills")
|
||||
if err := os.MkdirAll(skillsDir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return skillsDir, nil
|
||||
}
|
||||
|
||||
func (h *ContainerdHandler) readSkillFile(ctx context.Context, containerID, filePath string) (string, error) {
|
||||
payload, err := h.callMCPTool(ctx, containerID, "read", map[string]any{
|
||||
"path": filePath,
|
||||
func (h *ContainerdHandler) readSkillFile(skillsDir, filePath string) (string, error) {
|
||||
safeRel := strings.TrimPrefix(strings.TrimPrefix(filePath, ".skills/"), "./.skills/")
|
||||
if safeRel == "" {
|
||||
return "", os.ErrInvalid
|
||||
}
|
||||
target := filepath.Join(skillsDir, filepath.FromSlash(safeRel))
|
||||
data, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func listSkillEntries(skillsDir string) ([]skillEntry, error) {
|
||||
dirEntries, err := os.ReadDir(skillsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries := make([]skillEntry, 0, len(dirEntries))
|
||||
for _, entry := range dirEntries {
|
||||
name := entry.Name()
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if entry.IsDir() {
|
||||
entries = append(entries, skillEntry{
|
||||
Path: path.Join(".skills", name),
|
||||
IsDir: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, err := extractContentString(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (h *ContainerdHandler) callMCPTool(ctx context.Context, containerID, toolName string, args map[string]any) (map[string]any, error) {
|
||||
id := "skills-" + strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
req, err := mcptools.NewToolCallRequest(id, toolName, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := h.callMCPServer(ctx, containerID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mcptools.PayloadError(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mcptools.ResultError(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func extractListEntries(payload map[string]any) ([]skillEntry, error) {
|
||||
result, err := mcptools.StructuredContent(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawEntries, ok := result["entries"].([]any)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid list response")
|
||||
}
|
||||
entries := make([]skillEntry, 0, len(rawEntries))
|
||||
for _, raw := range rawEntries {
|
||||
entryMap, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
entryPath, _ := entryMap["path"].(string)
|
||||
if entryPath == "" {
|
||||
continue
|
||||
if name == "SKILL.md" {
|
||||
entries = append(entries, skillEntry{
|
||||
Path: path.Join(".skills", name),
|
||||
IsDir: false,
|
||||
})
|
||||
}
|
||||
isDir, _ := entryMap["is_dir"].(bool)
|
||||
entries = append(entries, skillEntry{Path: entryPath, IsDir: isDir})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
@@ -319,18 +263,6 @@ type skillEntry struct {
|
||||
IsDir bool
|
||||
}
|
||||
|
||||
func extractContentString(payload map[string]any) (string, error) {
|
||||
result, err := mcptools.StructuredContent(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, _ := result["content"].(string)
|
||||
if content == "" {
|
||||
return "", errors.New("empty content")
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func skillNameFromPath(rel string) string {
|
||||
if rel == "" || rel == "SKILL.md" {
|
||||
return "default"
|
||||
|
||||
Reference in New Issue
Block a user