mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: add media asset system, channel lifecycle refactor, and chat attachments (#54)
This commit is contained in:
@@ -4,38 +4,77 @@ import inquirer from 'inquirer'
|
||||
import ora from 'ora'
|
||||
import { table } from 'table'
|
||||
|
||||
import {
|
||||
getChannels,
|
||||
getChannelsByPlatform,
|
||||
getBotsByIdChannelByPlatform,
|
||||
putBotsByIdChannelByPlatform,
|
||||
getUsersMeChannelsByPlatform,
|
||||
putUsersMeChannelsByPlatform,
|
||||
type HandlersChannelMeta,
|
||||
} from '@memoh/sdk'
|
||||
import { apiRequest } from '../core/api'
|
||||
import { ensureAuth, getErrorMessage, resolveBotId } from './shared'
|
||||
|
||||
const renderChannelsTable = (items: HandlersChannelMeta[]) => {
|
||||
type ChannelFieldSchema = {
|
||||
type: 'string' | 'secret' | 'bool' | 'number' | 'enum'
|
||||
required: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
enum?: string[]
|
||||
example?: unknown
|
||||
}
|
||||
|
||||
type ChannelConfigSchema = {
|
||||
version: number
|
||||
fields: Record<string, ChannelFieldSchema>
|
||||
}
|
||||
|
||||
type ChannelMeta = {
|
||||
type: string
|
||||
display_name: string
|
||||
configless: boolean
|
||||
capabilities: Record<string, boolean>
|
||||
config_schema: ChannelConfigSchema
|
||||
user_config_schema: ChannelConfigSchema
|
||||
}
|
||||
|
||||
type ChannelUserBinding = {
|
||||
id: string
|
||||
channel_type: string
|
||||
user_id: string
|
||||
config: Record<string, unknown>
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
type ChannelConfig = {
|
||||
id: string
|
||||
bot_id: string
|
||||
channel_type: string
|
||||
credentials: Record<string, unknown>
|
||||
external_identity: string
|
||||
self_identity: Record<string, unknown>
|
||||
routing: Record<string, unknown>
|
||||
capabilities: Record<string, unknown>
|
||||
disabled: boolean
|
||||
verified_at: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
const renderChannelsTable = (items: ChannelMeta[]) => {
|
||||
const rows: string[][] = [['Type', 'Name', 'Configless']]
|
||||
for (const item of items) {
|
||||
rows.push([item.type ?? '', item.display_name ?? '', item.configless ? 'yes' : 'no'])
|
||||
rows.push([item.type, item.display_name, item.configless ? 'yes' : 'no'])
|
||||
}
|
||||
return table(rows)
|
||||
}
|
||||
|
||||
const fetchChannelList = async () => {
|
||||
const { data } = await getChannels({ throwOnError: true })
|
||||
return data as HandlersChannelMeta[]
|
||||
const fetchChannels = async (token: ReturnType<typeof ensureAuth>) => {
|
||||
return apiRequest<ChannelMeta[]>('/channels', {}, token)
|
||||
}
|
||||
|
||||
const resolveChannelType = async (
|
||||
token: ReturnType<typeof ensureAuth>,
|
||||
preset?: string,
|
||||
options?: { allowConfigless?: boolean }
|
||||
) => {
|
||||
if (preset && preset.trim()) {
|
||||
return preset.trim()
|
||||
}
|
||||
const channels = await fetchChannelList()
|
||||
const channels = await fetchChannels(token)
|
||||
const allowConfigless = options?.allowConfigless ?? false
|
||||
const candidates = channels.filter(item => allowConfigless || !item.configless)
|
||||
if (candidates.length === 0) {
|
||||
@@ -128,8 +167,8 @@ export const registerChannelCommands = (program: Command) => {
|
||||
.command('list')
|
||||
.description('List available channels')
|
||||
.action(async () => {
|
||||
ensureAuth()
|
||||
const channels = await fetchChannelList()
|
||||
const token = ensureAuth()
|
||||
const channels = await fetchChannels(token)
|
||||
if (!channels.length) {
|
||||
console.log(chalk.yellow('No channels available.'))
|
||||
return
|
||||
@@ -142,13 +181,10 @@ export const registerChannelCommands = (program: Command) => {
|
||||
.description('Show channel meta and schema')
|
||||
.argument('[type]')
|
||||
.action(async (type) => {
|
||||
ensureAuth()
|
||||
const channelType = await resolveChannelType(type, { allowConfigless: true })
|
||||
const { data } = await getChannelsByPlatform({
|
||||
path: { platform: channelType },
|
||||
throwOnError: true,
|
||||
})
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
const token = ensureAuth()
|
||||
const channelType = await resolveChannelType(token, type, { allowConfigless: true })
|
||||
const meta = await apiRequest<ChannelMeta>(`/channels/${encodeURIComponent(channelType)}`, {}, token)
|
||||
console.log(JSON.stringify(meta, null, 2))
|
||||
})
|
||||
|
||||
const config = channel.command('config').description('Bot channel configuration')
|
||||
@@ -159,14 +195,11 @@ export const registerChannelCommands = (program: Command) => {
|
||||
.argument('[bot_id]')
|
||||
.option('--type <type>', 'Channel type')
|
||||
.action(async (botId, opts) => {
|
||||
ensureAuth()
|
||||
const resolvedBotId = await resolveBotId(botId)
|
||||
const channelType = await resolveChannelType(opts.type)
|
||||
const { data } = await getBotsByIdChannelByPlatform({
|
||||
path: { id: resolvedBotId, platform: channelType },
|
||||
throwOnError: true,
|
||||
})
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
const token = ensureAuth()
|
||||
const resolvedBotId = await resolveBotId(token, botId)
|
||||
const channelType = await resolveChannelType(token, opts.type)
|
||||
const resp = await apiRequest<ChannelConfig>(`/bots/${encodeURIComponent(resolvedBotId)}/channel/${encodeURIComponent(channelType)}`, {}, token)
|
||||
console.log(JSON.stringify(resp, null, 2))
|
||||
})
|
||||
|
||||
config
|
||||
@@ -179,9 +212,9 @@ export const registerChannelCommands = (program: Command) => {
|
||||
.option('--encrypt_key <encrypt_key>')
|
||||
.option('--verification_token <verification_token>')
|
||||
.action(async (botId, opts) => {
|
||||
ensureAuth()
|
||||
const resolvedBotId = await resolveBotId(botId)
|
||||
const channelType = await resolveChannelType(opts.type)
|
||||
const token = ensureAuth()
|
||||
const resolvedBotId = await resolveBotId(token, botId)
|
||||
const channelType = await resolveChannelType(token, opts.type)
|
||||
if (channelType !== 'feishu') {
|
||||
console.log(chalk.red(`Channel type ${channelType} is not supported by this command.`))
|
||||
process.exit(1)
|
||||
@@ -189,11 +222,10 @@ export const registerChannelCommands = (program: Command) => {
|
||||
const credentials = await collectFeishuCredentials(opts)
|
||||
const spinner = ora('Updating channel config...').start()
|
||||
try {
|
||||
await putBotsByIdChannelByPlatform({
|
||||
path: { id: resolvedBotId, platform: channelType },
|
||||
body: { credentials },
|
||||
throwOnError: true,
|
||||
})
|
||||
await apiRequest(`/bots/${encodeURIComponent(resolvedBotId)}/channel/${encodeURIComponent(channelType)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ credentials }),
|
||||
}, token)
|
||||
spinner.succeed('Channel config updated')
|
||||
} catch (err: unknown) {
|
||||
spinner.fail(getErrorMessage(err) || 'Failed to update channel config')
|
||||
@@ -208,13 +240,10 @@ export const registerChannelCommands = (program: Command) => {
|
||||
.description('Get current user channel binding')
|
||||
.option('--type <type>', 'Channel type')
|
||||
.action(async (opts) => {
|
||||
ensureAuth()
|
||||
const channelType = await resolveChannelType(opts.type)
|
||||
const { data } = await getUsersMeChannelsByPlatform({
|
||||
path: { platform: channelType },
|
||||
throwOnError: true,
|
||||
})
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
const token = ensureAuth()
|
||||
const channelType = await resolveChannelType(token, opts.type)
|
||||
const resp = await apiRequest<ChannelUserBinding>(`/users/me/channels/${encodeURIComponent(channelType)}`, {}, token)
|
||||
console.log(JSON.stringify(resp, null, 2))
|
||||
})
|
||||
|
||||
binding
|
||||
@@ -224,8 +253,8 @@ export const registerChannelCommands = (program: Command) => {
|
||||
.option('--open_id <open_id>')
|
||||
.option('--user_id <user_id>')
|
||||
.action(async (opts) => {
|
||||
ensureAuth()
|
||||
const channelType = await resolveChannelType(opts.type)
|
||||
const token = ensureAuth()
|
||||
const channelType = await resolveChannelType(token, opts.type)
|
||||
if (channelType !== 'feishu') {
|
||||
console.log(chalk.red(`Channel type ${channelType} is not supported by this command.`))
|
||||
process.exit(1)
|
||||
@@ -233,11 +262,10 @@ export const registerChannelCommands = (program: Command) => {
|
||||
const configPayload = await collectFeishuUserConfig(opts)
|
||||
const spinner = ora('Updating user binding...').start()
|
||||
try {
|
||||
await putUsersMeChannelsByPlatform({
|
||||
path: { platform: channelType },
|
||||
body: { config: configPayload },
|
||||
throwOnError: true,
|
||||
})
|
||||
await apiRequest(`/users/me/channels/${encodeURIComponent(channelType)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ config: configPayload }),
|
||||
}, token)
|
||||
spinner.succeed('User binding updated')
|
||||
} catch (err: unknown) {
|
||||
spinner.fail(getErrorMessage(err) || 'Failed to update user binding')
|
||||
@@ -245,3 +273,4 @@ export const registerChannelCommands = (program: Command) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ registerChannelCommands(program)
|
||||
const getModelId = (item: ModelsGetResponse) => item.model_id ?? ''
|
||||
const getProviderId = (item: ModelsGetResponse) => item.llm_provider_id ?? ''
|
||||
const getModelType = (item: ModelsGetResponse) => item.type ?? 'chat'
|
||||
const getModelMultimodal = (item: ModelsGetResponse) => item.is_multimodal ?? false
|
||||
const getModelInputModalities = (item: ModelsGetResponse) => item.input_modalities ?? ['text']
|
||||
|
||||
const ensureModelsReady = async () => {
|
||||
ensureAuth()
|
||||
@@ -98,13 +98,13 @@ const renderProvidersTable = (providers: ProvidersGetResponse[], models: ModelsG
|
||||
|
||||
const renderModelsTable = (models: ModelsGetResponse[], providers: ProvidersGetResponse[]) => {
|
||||
const providerMap = new Map(providers.map(p => [p.id, p.name]))
|
||||
const rows: string[][] = [['Model ID', 'Type', 'Provider', 'Multimodal']]
|
||||
const rows: string[][] = [['Model ID', 'Type', 'Provider', 'Input Modalities']]
|
||||
for (const item of models) {
|
||||
rows.push([
|
||||
getModelId(item),
|
||||
getModelType(item),
|
||||
providerMap.get(getProviderId(item)) ?? getProviderId(item),
|
||||
getModelMultimodal(item) ? 'yes' : 'no',
|
||||
getModelInputModalities(item).join(', '),
|
||||
])
|
||||
}
|
||||
return table(rows)
|
||||
@@ -389,7 +389,7 @@ model
|
||||
console.log(chalk.red('Embedding models require a valid dimensions value.'))
|
||||
process.exit(1)
|
||||
}
|
||||
const isMultimodal = Boolean(opts.multimodal)
|
||||
const inputModalities = opts.multimodal ? ['text', 'image'] : ['text']
|
||||
const spinner = ora('Creating model...').start()
|
||||
try {
|
||||
await postModels({
|
||||
@@ -397,7 +397,7 @@ model
|
||||
model_id: modelId,
|
||||
name: opts.name ?? modelId,
|
||||
llm_provider_id: provider.id,
|
||||
is_multimodal: isMultimodal,
|
||||
input_modalities: inputModalities,
|
||||
type: modelType,
|
||||
dimensions,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user