mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
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:
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './attachments'
|
||||
export * from './fs'
|
||||
export * from './headers'
|
||||
export * from './subagent'
|
||||
export * from './subagent'
|
||||
|
||||
Reference in New Issue
Block a user