feat: add media asset system, channel lifecycle refactor, and chat attachments (#54)

This commit is contained in:
BBQ
2026-02-17 19:06:46 +08:00
committed by GitHub
parent 0bdc31311c
commit df7876a30c
106 changed files with 7942 additions and 1274 deletions
+83 -54
View File
@@ -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) => {
}
})
}
+5 -5
View File
@@ -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,
},