feat: overhaul task board and cross-conversation workflows
This commit is contained in:
220
lib/task-board-mcp.js
Normal file
220
lib/task-board-mcp.js
Normal file
@@ -0,0 +1,220 @@
|
||||
'use strict';
|
||||
|
||||
const { TaskBoardError } = require('./task-board-service');
|
||||
|
||||
// 任务工具不能静态注册;缺少来源会话上下文时必须保持为空。
|
||||
const TASK_BOARD_MCP_TOOL_DEFINITIONS = Object.freeze([]);
|
||||
const TASK_BOARD_MCP_TOOLS = TASK_BOARD_MCP_TOOL_DEFINITIONS;
|
||||
const TOOLS = TASK_BOARD_MCP_TOOL_DEFINITIONS;
|
||||
|
||||
function isPlainObject(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === Object.prototype || prototype === null;
|
||||
}
|
||||
|
||||
function assertArgsObject(args, allowedKeys, toolName) {
|
||||
const value = args === undefined || args === null ? {} : args;
|
||||
if (!isPlainObject(value)) {
|
||||
throw new TaskBoardError('task_status_invalid', `${toolName} 参数必须是对象。`);
|
||||
}
|
||||
const unexpected = Object.keys(value).filter((key) => !allowedKeys.has(key));
|
||||
if (unexpected.length > 0) {
|
||||
throw new TaskBoardError('task_status_invalid', `${toolName} 包含不支持的字段。`, {
|
||||
fields: unexpected,
|
||||
});
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function contextSessionId(context, resolver) {
|
||||
if (!isPlainObject(context)) {
|
||||
throw new TaskBoardError('task_session_not_found', 'MCP 调用上下文缺少来源会话。');
|
||||
}
|
||||
let value;
|
||||
if (typeof resolver === 'function') value = resolver(context);
|
||||
if (value === undefined || value === null || value === '') {
|
||||
value = context.sourceSessionId
|
||||
?? context.sessionId
|
||||
?? context.source?.sessionId
|
||||
?? context.mcp?.sourceSessionId;
|
||||
}
|
||||
// 来源只取 MCP 连接上下文,绝不接受模型参数中的 sessionId。
|
||||
if (typeof value !== 'string' || value.length === 0) {
|
||||
throw new TaskBoardError('task_session_not_found', 'MCP 调用上下文缺少来源会话。');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function actorFromContext(context) {
|
||||
const actorId = isPlainObject(context)
|
||||
? (context.actorId || context.agentId || context.sourceAgentId || 'mcp')
|
||||
: 'mcp';
|
||||
return { source: 'mcp', id: actorId };
|
||||
}
|
||||
|
||||
function errorPayload(error) {
|
||||
if (error instanceof TaskBoardError) {
|
||||
const payload = { ok: false, code: error.code, message: error.message };
|
||||
if (error.details !== undefined) payload.details = error.details;
|
||||
return payload;
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
code: 'task_status_invalid',
|
||||
message: '任务状态工具执行失败。',
|
||||
};
|
||||
}
|
||||
|
||||
function serviceFromOptions(serviceOrOptions) {
|
||||
if (serviceOrOptions && typeof serviceOrOptions.getTask === 'function'
|
||||
&& typeof serviceOrOptions.updateStatus === 'function') {
|
||||
return serviceOrOptions;
|
||||
}
|
||||
if (isPlainObject(serviceOrOptions)) {
|
||||
if (serviceOrOptions.service) return serviceOrOptions.service;
|
||||
if (serviceOrOptions.taskBoardService) return serviceOrOptions.taskBoardService;
|
||||
}
|
||||
throw new TypeError('任务看板 MCP 需要 TaskBoardService。');
|
||||
}
|
||||
|
||||
function factoryOptionsFrom(serviceOrOptions, options) {
|
||||
return isPlainObject(serviceOrOptions) && !serviceOrOptions.getTask
|
||||
? { ...serviceOrOptions, ...(isPlainObject(options) ? options : {}) }
|
||||
: (isPlainObject(options) ? options : {});
|
||||
}
|
||||
|
||||
function enabledDefinitions(service) {
|
||||
const snapshot = typeof service.getStatusDefinitionSnapshot === 'function'
|
||||
? service.getStatusDefinitionSnapshot()
|
||||
: { definitions: service.getStatusDefinitions() };
|
||||
return (Array.isArray(snapshot.definitions) ? snapshot.definitions : [])
|
||||
.filter((definition) => definition?.enabled === true);
|
||||
}
|
||||
|
||||
function compactPrompt(value) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function taskUpdateDescription(task, definitions) {
|
||||
const current = task.status || definitions.find((definition) => (
|
||||
definition.id === task.taskTracking.statusId
|
||||
));
|
||||
const currentLabel = compactPrompt(current?.label) || task.taskTracking.statusId;
|
||||
const currentId = task.taskTracking.statusId;
|
||||
const mappings = definitions.map((definition) => (
|
||||
`- ${compactPrompt(definition.label)} / ${definition.id}: ${compactPrompt(definition.prompt)}`
|
||||
));
|
||||
return [
|
||||
`更新当前来源会话的任务看板列。当前状态:${currentLabel} / ${currentId}。`,
|
||||
'仅当当前对话任务状态真实发生变化时调用;单轮开始、结束、运行停止不代表任务状态变化。',
|
||||
'可选列分类映射:',
|
||||
...mappings,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function createTaskBoardMcpToolDefinitions(serviceOrOptions, context = {}, options = {}) {
|
||||
const service = serviceFromOptions(serviceOrOptions);
|
||||
const factoryOptions = factoryOptionsFrom(serviceOrOptions, options);
|
||||
const resolver = factoryOptions.sourceSessionIdResolver
|
||||
|| factoryOptions.getSourceSessionId
|
||||
|| null;
|
||||
try {
|
||||
const sessionId = contextSessionId(context, resolver);
|
||||
const task = service.getTask(sessionId);
|
||||
if (task.taskTracking.enabled !== true) return [];
|
||||
const definitions = enabledDefinitions(service);
|
||||
return [{
|
||||
name: 'ccweb_task_update',
|
||||
description: taskUpdateDescription(task, definitions),
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
statusId: {
|
||||
type: 'string',
|
||||
enum: definitions.map((definition) => definition.id),
|
||||
description: '选择一个当前已启用的看板列 ID。',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
maxLength: 2000,
|
||||
description: '可选,简要说明状态变化原因。',
|
||||
},
|
||||
summary: {
|
||||
type: 'string',
|
||||
maxLength: 4000,
|
||||
description: '可选,更新面向看板的任务摘要。',
|
||||
},
|
||||
},
|
||||
required: ['statusId'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
}];
|
||||
} catch (error) {
|
||||
if (error instanceof TaskBoardError
|
||||
&& (error.code === 'task_session_not_found' || error.code === 'task_tracking_disabled')) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function statusPayload(task) {
|
||||
return {
|
||||
ok: true,
|
||||
changed: task.changed === true,
|
||||
sessionId: task.sessionId,
|
||||
currentStatus: { ...task.taskTracking },
|
||||
currentStatusDefinition: task.status ? { ...task.status } : null,
|
||||
task,
|
||||
};
|
||||
}
|
||||
|
||||
function createTaskBoardMcpHandlers(serviceOrOptions, options = {}) {
|
||||
const service = serviceFromOptions(serviceOrOptions);
|
||||
const factoryOptions = factoryOptionsFrom(serviceOrOptions, options);
|
||||
const sourceSessionResolver = factoryOptions.sourceSessionIdResolver
|
||||
|| factoryOptions.getSourceSessionId
|
||||
|| null;
|
||||
|
||||
function updateStatus(args = {}, context = {}) {
|
||||
try {
|
||||
const input = assertArgsObject(args, new Set(['statusId', 'reason', 'summary']), 'ccweb_task_update');
|
||||
const sessionId = contextSessionId(context, sourceSessionResolver);
|
||||
const task = service.updateStatus(sessionId, input, actorFromContext(context));
|
||||
return statusPayload(task);
|
||||
} catch (error) {
|
||||
return errorPayload(error);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze({ ccweb_task_update: updateStatus });
|
||||
}
|
||||
|
||||
function createTaskBoardMcpHandler(serviceOrOptions, options = {}) {
|
||||
const handlers = createTaskBoardMcpHandlers(serviceOrOptions, options);
|
||||
return function handleTaskBoardMcpCall(name, args = {}, context = {}) {
|
||||
const handler = handlers[name];
|
||||
if (!handler) {
|
||||
return {
|
||||
ok: false,
|
||||
code: 'unknown_tool',
|
||||
message: `未知任务看板工具: ${String(name || '')}`,
|
||||
};
|
||||
}
|
||||
return handler(args, context);
|
||||
};
|
||||
}
|
||||
|
||||
const createTaskBoardMcpAdapter = createTaskBoardMcpHandler;
|
||||
|
||||
module.exports = {
|
||||
TASK_BOARD_MCP_TOOL_DEFINITIONS,
|
||||
TASK_BOARD_MCP_TOOLS,
|
||||
TOOLS,
|
||||
createTaskBoardMcpToolDefinitions,
|
||||
createTaskBoardMcpHandlers,
|
||||
createTaskBoardMcpHandler,
|
||||
createTaskBoardMcpAdapter,
|
||||
errorPayload,
|
||||
};
|
||||
Reference in New Issue
Block a user