mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: file operation restful api
This commit is contained in:
+2
-9
@@ -51,19 +51,12 @@ export const createAgent = ({
|
|||||||
const fs: HTTPMCPConnection = {
|
const fs: HTTPMCPConnection = {
|
||||||
type: 'http',
|
type: 'http',
|
||||||
name: 'fs',
|
name: 'fs',
|
||||||
url: `${auth.baseUrl}/bots/${identity.botId}/container/fs`,
|
url: `${auth.baseUrl}/bots/${identity.botId}/container/fs-mcp`,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${auth.bearer}`,
|
'Authorization': `Bearer ${auth.bearer}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const mcpFetch: StdioMCPConnection = {
|
return [fs]
|
||||||
type: 'stdio',
|
|
||||||
name: 'mcp-fetch',
|
|
||||||
command: 'npx',
|
|
||||||
args: ['fetch-mcp'],
|
|
||||||
env: {},
|
|
||||||
}
|
|
||||||
return [fs, mcpFetch]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateSystemPrompt = () => {
|
const generateSystemPrompt = () => {
|
||||||
|
|||||||
+558
-3
@@ -354,6 +354,116 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/bots/{bot_id}/container/fs": {
|
"/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": {
|
"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\"}}}",
|
"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": [
|
"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": {
|
"/bots/{bot_id}/container/skills": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"containerd"
|
"containerd"
|
||||||
],
|
],
|
||||||
"summary": "List skills from container",
|
"summary": "List skills from data directory",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -459,7 +882,7 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"containerd"
|
"containerd"
|
||||||
],
|
],
|
||||||
"summary": "Upload skills into container",
|
"summary": "Upload skills into data directory",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -509,7 +932,7 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"containerd"
|
"containerd"
|
||||||
],
|
],
|
||||||
"summary": "Delete skills from container",
|
"summary": "Delete skills from data directory",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"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": {
|
"handlers.GetContainerResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
+558
-3
@@ -345,6 +345,116 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/bots/{bot_id}/container/fs": {
|
"/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": {
|
"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\"}}}",
|
"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": [
|
"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": {
|
"/bots/{bot_id}/container/skills": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"containerd"
|
"containerd"
|
||||||
],
|
],
|
||||||
"summary": "List skills from container",
|
"summary": "List skills from data directory",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -450,7 +873,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"containerd"
|
"containerd"
|
||||||
],
|
],
|
||||||
"summary": "Upload skills into container",
|
"summary": "Upload skills into data directory",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -500,7 +923,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"containerd"
|
"containerd"
|
||||||
],
|
],
|
||||||
"summary": "Delete skills from container",
|
"summary": "Delete skills from data directory",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"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": {
|
"handlers.GetContainerResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
+367
-3
@@ -614,6 +614,91 @@ definitions:
|
|||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
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:
|
handlers.GetContainerResponse:
|
||||||
properties:
|
properties:
|
||||||
container_id:
|
container_id:
|
||||||
@@ -1617,6 +1702,79 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- containerd
|
- containerd
|
||||||
/bots/{bot_id}/container/fs:
|
/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:
|
post:
|
||||||
description: |-
|
description: |-
|
||||||
Forwards MCP JSON-RPC requests to the MCP server inside the container.
|
Forwards MCP JSON-RPC requests to the MCP server inside the container.
|
||||||
@@ -1669,6 +1827,212 @@ paths:
|
|||||||
summary: MCP filesystem tools (JSON-RPC)
|
summary: MCP filesystem tools (JSON-RPC)
|
||||||
tags:
|
tags:
|
||||||
- containerd
|
- 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:
|
/bots/{bot_id}/container/skills:
|
||||||
delete:
|
delete:
|
||||||
parameters:
|
parameters:
|
||||||
@@ -1700,7 +2064,7 @@ paths:
|
|||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/handlers.ErrorResponse'
|
$ref: '#/definitions/handlers.ErrorResponse'
|
||||||
summary: Delete skills from container
|
summary: Delete skills from data directory
|
||||||
tags:
|
tags:
|
||||||
- containerd
|
- containerd
|
||||||
get:
|
get:
|
||||||
@@ -1727,7 +2091,7 @@ paths:
|
|||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/handlers.ErrorResponse'
|
$ref: '#/definitions/handlers.ErrorResponse'
|
||||||
summary: List skills from container
|
summary: List skills from data directory
|
||||||
tags:
|
tags:
|
||||||
- containerd
|
- containerd
|
||||||
post:
|
post:
|
||||||
@@ -1760,7 +2124,7 @@ paths:
|
|||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/handlers.ErrorResponse'
|
$ref: '#/definitions/handlers.ErrorResponse'
|
||||||
summary: Upload skills into container
|
summary: Upload skills into data directory
|
||||||
tags:
|
tags:
|
||||||
- containerd
|
- containerd
|
||||||
/bots/{bot_id}/container/snapshots:
|
/bots/{bot_id}/container/snapshots:
|
||||||
|
|||||||
@@ -120,9 +120,18 @@ func (h *ContainerdHandler) Register(e *echo.Echo) {
|
|||||||
group.GET("/skills", h.ListSkills)
|
group.GET("/skills", h.ListSkills)
|
||||||
group.POST("/skills", h.UpsertSkills)
|
group.POST("/skills", h.UpsertSkills)
|
||||||
group.DELETE("/skills", h.DeleteSkills)
|
group.DELETE("/skills", h.DeleteSkills)
|
||||||
group.POST("/fs", h.HandleMCPFS)
|
group.POST("/fs-mcp", h.HandleMCPFS)
|
||||||
|
|
||||||
root := e.Group("/bots/:bot_id")
|
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", h.CreateMCPStdio)
|
||||||
root.POST("/mcp-stdio/:session_id", h.HandleMCPStdio)
|
root.POST("/mcp-stdio/:session_id", h.HandleMCPStdio)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import (
|
|||||||
// @Failure 400 {object} ErrorResponse
|
// @Failure 400 {object} ErrorResponse
|
||||||
// @Failure 404 {object} ErrorResponse
|
// @Failure 404 {object} ErrorResponse
|
||||||
// @Failure 500 {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 {
|
func (h *ContainerdHandler) HandleMCPFS(c echo.Context) error {
|
||||||
botID, err := h.requireBotAccess(c)
|
botID, err := h.requireBotAccess(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -429,4 +429,3 @@ func (s *mcpSession) writePayloads(payloads []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/memohai/memoh/internal/config"
|
|
||||||
mcptools "github.com/memohai/memoh/internal/mcp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SkillItem struct {
|
type SkillItem struct {
|
||||||
@@ -41,7 +36,7 @@ type skillsOpResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListSkills godoc
|
// ListSkills godoc
|
||||||
// @Summary List skills from container
|
// @Summary List skills from data directory
|
||||||
// @Tags containerd
|
// @Tags containerd
|
||||||
// @Param bot_id path string true "Bot ID"
|
// @Param bot_id path string true "Bot ID"
|
||||||
// @Success 200 {object} SkillsResponse
|
// @Success 200 {object} SkillsResponse
|
||||||
@@ -54,26 +49,11 @@ func (h *ContainerdHandler) ListSkills(c echo.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ctx := c.Request().Context()
|
skillsDir, err := h.ensureSkillsDirHost(botID)
|
||||||
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,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
entries, err := extractListEntries(listPayload)
|
entries, err := listSkillEntries(skillsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
@@ -84,7 +64,7 @@ func (h *ContainerdHandler) ListSkills(c echo.Context) error {
|
|||||||
if skillPath == "" {
|
if skillPath == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
raw, err := h.readSkillFile(ctx, containerID, skillPath)
|
raw, err := h.readSkillFile(skillsDir, skillPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -101,7 +81,7 @@ func (h *ContainerdHandler) ListSkills(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpsertSkills godoc
|
// UpsertSkills godoc
|
||||||
// @Summary Upload skills into container
|
// @Summary Upload skills into data directory
|
||||||
// @Tags containerd
|
// @Tags containerd
|
||||||
// @Param bot_id path string true "Bot ID"
|
// @Param bot_id path string true "Bot ID"
|
||||||
// @Param payload body SkillsUpsertRequest true "Skills payload"
|
// @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")
|
return echo.NewHTTPError(http.StatusBadRequest, "skills is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Request().Context()
|
skillsDir, err := h.ensureSkillsDirHost(botID)
|
||||||
containerID, err := h.botContainerID(ctx, botID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := h.ensureTaskRunning(ctx, containerID); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
for _, skill := range req.Skills {
|
for _, skill := range req.Skills {
|
||||||
@@ -140,11 +116,12 @@ func (h *ContainerdHandler) UpsertSkills(c echo.Context) error {
|
|||||||
if content == "" {
|
if content == "" {
|
||||||
content = buildSkillContent(name, strings.TrimSpace(skill.Description))
|
content = buildSkillContent(name, strings.TrimSpace(skill.Description))
|
||||||
}
|
}
|
||||||
filePath := path.Join(".skills", name, "SKILL.md")
|
dirPath := filepath.Join(skillsDir, name)
|
||||||
if _, err := h.callMCPTool(ctx, containerID, "write", map[string]any{
|
if err := os.MkdirAll(dirPath, 0o755); err != nil {
|
||||||
"path": filePath,
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
"content": content,
|
}
|
||||||
}); err != nil {
|
filePath := filepath.Join(dirPath, "SKILL.md")
|
||||||
|
if err := os.WriteFile(filePath, []byte(content), 0o644); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +130,7 @@ func (h *ContainerdHandler) UpsertSkills(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSkills godoc
|
// DeleteSkills godoc
|
||||||
// @Summary Delete skills from container
|
// @Summary Delete skills from data directory
|
||||||
// @Tags containerd
|
// @Tags containerd
|
||||||
// @Param bot_id path string true "Bot ID"
|
// @Param bot_id path string true "Bot ID"
|
||||||
// @Param payload body SkillsDeleteRequest true "Delete skills payload"
|
// @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")
|
return echo.NewHTTPError(http.StatusBadRequest, "names is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Request().Context()
|
skillsDir, err := h.ensureSkillsDirHost(botID)
|
||||||
containerID, err := h.botContainerID(ctx, botID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := h.ensureTaskRunning(ctx, containerID); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,10 +162,8 @@ func (h *ContainerdHandler) DeleteSkills(c echo.Context) error {
|
|||||||
if !isValidSkillName(skillName) {
|
if !isValidSkillName(skillName) {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid skill name")
|
return echo.NewHTTPError(http.StatusBadRequest, "invalid skill name")
|
||||||
}
|
}
|
||||||
deletePath := path.Join(".skills", skillName)
|
deletePath := filepath.Join(skillsDir, skillName)
|
||||||
if _, err := h.callMCPTool(ctx, containerID, "delete", map[string]any{
|
if err := os.RemoveAll(deletePath); err != nil {
|
||||||
"path": deletePath,
|
|
||||||
}); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
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.
|
// LoadSkills loads all skills from the container for the given bot.
|
||||||
// This implements chat.SkillLoader.
|
// This implements chat.SkillLoader.
|
||||||
func (h *ContainerdHandler) LoadSkills(ctx context.Context, botID string) ([]SkillItem, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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{
|
entries, err := listSkillEntries(skillsDir)
|
||||||
"path": ".skills",
|
|
||||||
"recursive": false,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
entries, err := extractListEntries(listPayload)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -232,7 +190,7 @@ func (h *ContainerdHandler) LoadSkills(ctx context.Context, botID string) ([]Ski
|
|||||||
if skillPath == "" {
|
if skillPath == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
raw, err := h.readSkillFile(ctx, containerID, skillPath)
|
raw, err := h.readSkillFile(skillsDir, skillPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -247,69 +205,55 @@ func (h *ContainerdHandler) LoadSkills(ctx context.Context, botID string) ([]Ski
|
|||||||
return skills, nil
|
return skills, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ContainerdHandler) ensureSkillsDirHost(botID string) error {
|
func (h *ContainerdHandler) ensureSkillsDirHost(botID string) (string, error) {
|
||||||
dataRoot := strings.TrimSpace(h.cfg.DataRoot)
|
root, err := h.ensureBotDataRoot(botID)
|
||||||
if dataRoot == "" {
|
if err != nil {
|
||||||
dataRoot = config.DefaultDataRoot
|
return "", err
|
||||||
}
|
}
|
||||||
skillsDir := path.Join(dataRoot, "bots", botID, ".skills")
|
skillsDir := filepath.Join(root, ".skills")
|
||||||
return os.MkdirAll(skillsDir, 0o755)
|
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) {
|
func (h *ContainerdHandler) readSkillFile(skillsDir, filePath string) (string, error) {
|
||||||
payload, err := h.callMCPTool(ctx, containerID, "read", map[string]any{
|
safeRel := strings.TrimPrefix(strings.TrimPrefix(filePath, ".skills/"), "./.skills/")
|
||||||
"path": filePath,
|
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
|
continue
|
||||||
}
|
}
|
||||||
entryPath, _ := entryMap["path"].(string)
|
if name == "SKILL.md" {
|
||||||
if entryPath == "" {
|
entries = append(entries, skillEntry{
|
||||||
continue
|
Path: path.Join(".skills", name),
|
||||||
|
IsDir: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
isDir, _ := entryMap["is_dir"].(bool)
|
|
||||||
entries = append(entries, skillEntry{Path: entryPath, IsDir: isDir})
|
|
||||||
}
|
}
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
@@ -319,18 +263,6 @@ type skillEntry struct {
|
|||||||
IsDir bool
|
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 {
|
func skillNameFromPath(rel string) string {
|
||||||
if rel == "" || rel == "SKILL.md" {
|
if rel == "" || rel == "SKILL.md" {
|
||||||
return "default"
|
return "default"
|
||||||
|
|||||||
Reference in New Issue
Block a user