feat: add restful apis of container file system (#92)

* feat: add restful apis of container file system

* feat: add fs tools in agent
This commit is contained in:
Acbox Liu
2026-02-22 16:42:30 +08:00
committed by GitHub
parent 928b0c0ee5
commit ee0aa319e2
12 changed files with 3277 additions and 57 deletions
+6 -50
View File
@@ -31,6 +31,7 @@ import type { GatewayInputAttachment } from './types/attachment'
import { getMCPTools } from './tools/mcp'
import { getTools } from './tools'
import { buildIdentityHeaders } from './utils/headers'
import { createFS } from './utils'
const buildStepUsages = (
steps: { usage: LanguageModelUsage; response: { messages: unknown[] } }[],
@@ -77,6 +78,7 @@ export const createAgent = (
) => {
const model = createModel(modelConfig)
const enabledSkills: AgentSkill[] = []
const fs = createFS({ fetch, botId: identity.botId })
const enableSkill = (skill: string) => {
const agentSkill = skills.find((s) => s.name === skill)
@@ -90,58 +92,12 @@ export const createAgent = (
}
const loadSystemFiles = async () => {
if (!auth?.bearer || !identity.botId) {
return {
identityContent: '',
soulContent: '',
toolsContent: '',
}
}
const readViaMCP = async (path: string): Promise<string> => {
const url = `${auth.baseUrl.replace(/\/$/, '')}/bots/${identity.botId}/tools`
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
Authorization: `Bearer ${auth.bearer}`,
}
if (identity.channelIdentityId) {
headers['X-Memoh-Channel-Identity-Id'] = identity.channelIdentityId
}
const body = JSON.stringify({
jsonrpc: '2.0',
id: `read-${path}`,
method: 'tools/call',
params: { name: 'read', arguments: { path } },
})
const response = await fetch(url, { method: 'POST', headers, body })
if (!response.ok) return ''
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = await response.json().catch(() => ({})) as any
const structured =
data?.result?.structuredContent ?? data?.result?.content?.[0]?.text
if (typeof structured === 'string') {
try {
const parsed = JSON.parse(structured)
return typeof parsed?.content === 'string' ? parsed.content : ''
} catch {
return structured
}
}
if (typeof structured === 'object' && structured?.content) {
return typeof structured.content === 'string' ? structured.content : ''
}
return ''
}
const [identityContent, soulContent, toolsContent] = await Promise.all([
readViaMCP('IDENTITY.md'),
readViaMCP('SOUL.md'),
readViaMCP('TOOLS.md'),
fs.readText('/data/IDENTITY.md'),
fs.readText('/data/SOUL.md'),
fs.readText('/data/TOOLS.md'),
])
return {
identityContent,
soulContent,
toolsContent,
}
return { identityContent, soulContent, toolsContent }
}
const generateSystemPrompt = async () => {
+207
View File
@@ -0,0 +1,207 @@
import type { AuthFetcher } from '../types'
// ---------- types ----------
export interface FSFileInfo {
name: string
path: string
size: number
mode: string
modTime: string
isDir: boolean
}
export interface FSListResponse {
path: string
entries: FSFileInfo[]
}
export interface FSReadResponse {
path: string
content: string
size: number
}
export interface FSWriteParams {
path: string
content: string
}
export interface FSUploadResponse {
path: string
size: number
}
export interface FSMkdirParams {
path: string
}
export interface FSDeleteParams {
path: string
recursive?: boolean
}
export interface FSRenameParams {
oldPath: string
newPath: string
}
export interface FSOkResponse {
ok: boolean
}
export interface FSClientOptions {
fetch: AuthFetcher
botId: string
}
// ---------- helpers ----------
const encodeQuery = (path: string) => encodeURIComponent(path)
const ensureOk = async (response: Response, action: string): Promise<void> => {
if (!response.ok) {
const text = await response.text().catch(() => '')
throw new Error(`fs ${action} failed (${response.status}): ${text}`)
}
}
// ---------- public API ----------
/**
* Creates a set of filesystem utility functions that operate on a bot's
* container via the REST file-manager API.
*
* All functions use `AuthFetcher` so auth headers are injected automatically.
*/
export const createFS = ({ fetch, botId }: FSClientOptions) => {
const base = `/bots/${botId}/container/fs`
/** Get file or directory metadata. */
const stat = async (path: string): Promise<FSFileInfo> => {
const response = await fetch(`${base}?path=${encodeQuery(path)}`)
await ensureOk(response, 'stat')
return response.json() as Promise<FSFileInfo>
}
/** List directory contents. */
const list = async (path: string): Promise<FSListResponse> => {
const response = await fetch(`${base}/list?path=${encodeQuery(path)}`)
await ensureOk(response, 'list')
return response.json() as Promise<FSListResponse>
}
/** Read a file as text. */
const read = async (path: string): Promise<FSReadResponse> => {
const response = await fetch(`${base}/read?path=${encodeQuery(path)}`)
await ensureOk(response, 'read')
return response.json() as Promise<FSReadResponse>
}
/** Download a file as a binary `Response` (stream-ready). */
const download = async (path: string): Promise<Response> => {
const response = await fetch(`${base}/download?path=${encodeQuery(path)}`)
await ensureOk(response, 'download')
return response
}
/** Write text content to a file (creates parent dirs automatically). */
const write = async (params: FSWriteParams): Promise<FSOkResponse> => {
const response = await fetch(`${base}/write`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
})
await ensureOk(response, 'write')
return response.json() as Promise<FSOkResponse>
}
/** Upload a binary file via multipart/form-data. */
const upload = async (
path: string,
file: Blob | File,
fileName?: string,
): Promise<FSUploadResponse> => {
const form = new FormData()
form.append('path', path)
form.append('file', file, fileName ?? (file instanceof File ? file.name : 'upload'))
const response = await fetch(`${base}/upload`, {
method: 'POST',
body: form,
})
await ensureOk(response, 'upload')
return response.json() as Promise<FSUploadResponse>
}
/** Create a directory (and parents). */
const mkdir = async (params: FSMkdirParams): Promise<FSOkResponse> => {
const response = await fetch(`${base}/mkdir`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
})
await ensureOk(response, 'mkdir')
return response.json() as Promise<FSOkResponse>
}
/** Delete a file or directory. */
const remove = async (params: FSDeleteParams): Promise<FSOkResponse> => {
const response = await fetch(`${base}/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
})
await ensureOk(response, 'delete')
return response.json() as Promise<FSOkResponse>
}
/** Rename or move a file / directory. */
const rename = async (params: FSRenameParams): Promise<FSOkResponse> => {
const response = await fetch(`${base}/rename`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
})
await ensureOk(response, 'rename')
return response.json() as Promise<FSOkResponse>
}
/** Check whether a path exists. */
const exists = async (path: string): Promise<boolean> => {
try {
await stat(path)
return true
} catch {
return false
}
}
/** Read a file and return only the text content string. */
const readText = async (path: string): Promise<string> => {
const result = await read(path)
return result.content
}
/** Shorthand: write text content to a path. */
const writeText = async (path: string, content: string): Promise<FSOkResponse> => {
return write({ path, content })
}
return {
stat,
list,
read,
readText,
download,
write,
writeText,
upload,
mkdir,
remove,
rename,
exists,
}
}
export type FSClient = ReturnType<typeof createFS>
+2 -1
View File
@@ -1,3 +1,4 @@
export * from './attachments'
export * from './fs'
export * from './headers'
export * from './subagent'
export * from './subagent'