/**
* 工具執行路由
*/
import { Router, type Request, type Response, type Router as RouterType } from 'express'
import { ErrorCode, type ToolRequest, type ToolResponse } from '@mcp-internal/shared'
import { executeOdooTool, odooToolDefinitions } from '../services/odoo/tools.js'
import { executeShopifyTool, shopifyToolDefinitions } from '../services/shopify/tools.js'
import { executeKnowledgeTool, knowledgeToolDefinitions } from '../services/knowledge/tools.js'
import { tokenAuth } from '../middleware/token-auth.js'
import { standardRateLimit } from '../middleware/rate-limit.js'
import { logger } from '../middleware/logging.js'
import { logApiCall, queryLogs, getLogStats } from '../services/audit/logger.js'
const router: RouterType = Router()
// 所有工具路由都需要認證
router.use(tokenAuth)
router.use(standardRateLimit)
/** 所有可用工具 */
const allTools = [
...odooToolDefinitions,
...shopifyToolDefinitions,
...knowledgeToolDefinitions,
]
/** 工具權限對照(簡化版本) */
const WRITE_TOOLS = [
'odoo_create', 'odoo_update', 'odoo_delete',
'shopify_update_product', 'shopify_update_price',
]
/**
* GET /api/v1/tools
* 列出所有可用工具
*/
router.get('/', (req: Request, res: Response) => {
const user = req.user!
const permission = user.permission
// 根據權限過濾工具
let availableTools = allTools
if (permission !== 'admin' && permission !== 'assistant') {
// 非管理員/助理只能使用讀取工具
availableTools = allTools.filter(t => !WRITE_TOOLS.includes(t.name))
}
res.json({
success: true,
result: { tools: availableTools },
})
})
/**
* POST /api/v1/tools/:name
* 執行指定工具
*/
router.post('/:name', async (req: Request, res: Response) => {
const toolName = req.params.name
const user = req.user!
const body = req.body as ToolRequest
logger.info({
tool: toolName,
user: user.fsuid,
permission: user.permission,
}, '工具執行請求')
// 檢查工具是否存在
const tool = allTools.find(t => t.name === toolName)
if (!tool) {
const response: ToolResponse = {
success: false,
error: {
code: ErrorCode.TOOL_NOT_FOUND,
message: `找不到工具: ${toolName}`,
},
}
res.status(404).json(response)
return
}
// 權限檢查
const permission = user.permission
if (WRITE_TOOLS.includes(toolName) && permission !== 'admin' && permission !== 'assistant') {
const response: ToolResponse = {
success: false,
error: {
code: ErrorCode.PERMISSION_DENIED,
message: `權限不足:${toolName} 需要 admin 或 assistant 權限`,
},
}
res.status(403).json(response)
return
}
// 執行工具
let result: ToolResponse
try {
if (toolName.startsWith('odoo_')) {
// 根據權限決定記錄層級存取
const recordAccess = (permission === 'admin' || permission === 'assistant')
? 'all' as const
: 'owner_only' as const
result = await executeOdooTool(toolName, body.arguments, recordAccess)
} else if (toolName.startsWith('shopify_')) {
result = await executeShopifyTool(toolName, body.arguments)
} else if (toolName.startsWith('knowledge_')) {
result = await executeKnowledgeTool(toolName, body.arguments)
} else {
result = {
success: false,
error: {
code: ErrorCode.TOOL_NOT_FOUND,
message: `未知的工具類型: ${toolName}`,
},
}
}
} catch (error) {
logger.error({ error, tool: toolName }, '工具執行錯誤')
result = {
success: false,
error: {
code: ErrorCode.INTERNAL_ERROR,
message: error instanceof Error ? error.message : String(error),
},
}
}
logger.info({
tool: toolName,
user: user.fsuid,
success: result.success,
executionTimeMs: result.metadata?.executionTimeMs,
}, '工具執行完成')
// 記錄審計日誌(非同步,不阻塞回應)
logApiCall({
timestamp: new Date(),
fsuid: user.fsuid,
userName: user.name,
permission: user.permission,
tool: toolName,
arguments: body.arguments || {},
success: result.success,
errorCode: result.error?.code,
errorMessage: result.error?.message,
executionTimeMs: result.metadata?.executionTimeMs,
ipAddress: req.ip || req.socket.remoteAddress,
}).catch(err => {
logger.error({ error: err }, '審計日誌記錄失敗')
})
const status = result.success ? 200 : (result.error?.code === ErrorCode.PERMISSION_DENIED ? 403 : 400)
res.status(status).json(result)
})
/**
* GET /api/v1/tools/logs
* 查詢審計日誌(僅 admin)
*/
router.get('/logs', async (req: Request, res: Response) => {
const user = req.user!
// 只有 admin 可以查詢日誌
if (user.permission !== 'admin') {
res.status(403).json({
success: false,
error: {
code: ErrorCode.PERMISSION_DENIED,
message: '只有管理員可以查詢審計日誌',
},
})
return
}
try {
const logs = await queryLogs({
fsuid: req.query.fsuid as string | undefined,
tool: req.query.tool as string | undefined,
startDate: req.query.startDate ? new Date(req.query.startDate as string) : undefined,
endDate: req.query.endDate ? new Date(req.query.endDate as string) : undefined,
limit: req.query.limit ? parseInt(req.query.limit as string) : 50,
offset: req.query.offset ? parseInt(req.query.offset as string) : 0,
})
res.json({
success: true,
result: { logs },
})
} catch (error) {
logger.error({ error }, '查詢審計日誌失敗')
res.status(500).json({
success: false,
error: {
code: ErrorCode.INTERNAL_ERROR,
message: error instanceof Error ? error.message : String(error),
},
})
}
})
/**
* GET /api/v1/tools/logs/stats
* 取得日誌統計(僅 admin)
*/
router.get('/logs/stats', async (req: Request, res: Response) => {
const user = req.user!
// 只有 admin 可以查詢統計
if (user.permission !== 'admin') {
res.status(403).json({
success: false,
error: {
code: ErrorCode.PERMISSION_DENIED,
message: '只有管理員可以查詢審計統計',
},
})
return
}
try {
const stats = await getLogStats(req.query.fsuid as string | undefined)
res.json({
success: true,
result: stats,
})
} catch (error) {
logger.error({ error }, '查詢審計統計失敗')
res.status(500).json({
success: false,
error: {
code: ErrorCode.INTERNAL_ERROR,
message: error instanceof Error ? error.message : String(error),
},
})
}
})
export default router