/** * 星环OPC中心 — AI 员工任务执行引擎 * * 负责: * 1. 检查哪些定时任务到了执行时间 * 2. 创建 staff_task 记录 * 3. 构建 sessions_spawn 的 prompt(由 AI 调用 sessions_spawn 执行) * 4. 跟踪任务完成状态 * * 注意:sessions_spawn 是 OpenClaw 框架的 agent 工具, * 插件不直接调用,而是将 prompt 返回给 AI,由 AI 调用 sessions_spawn。 */ import type { OpcDatabase } from "../db/index.js"; import { BUILTIN_TASK_TEMPLATES, type TaskTemplate, type TaskContext } from "./task-templates.js"; export interface ExecuteTaskResult { taskId: string; alreadyExists: boolean; staffRoleName: string; title: string; /** 完整的 sessions_spawn task prompt,AI 用此调用 sessions_spawn */ spawnPrompt: string; } export class TaskExecutor { constructor(private db: OpcDatabase) {} /** * 为一个任务模板创建任务记录并生成 spawn prompt。 * 如果今日已有同类任务,返回已存在标记。 */ prepareTask(template: TaskTemplate, companyId: string): ExecuteTaskResult { const company = this.db.queryOne( "SELECT name, industry FROM opc_companies WHERE id = ?", companyId, ) as { name: string; industry: string } | null; if (!company) throw new Error(`公司 ${companyId} 不存在`); const staffConfig = this.db.queryOne( "SELECT role_name, system_prompt FROM opc_staff_config WHERE company_id = ? AND role = ? AND enabled = 1", companyId, template.role, ) as { role_name: string; system_prompt: string } | null; if (!staffConfig) throw new Error(`岗位 ${template.role} 未启用`); const today = new Date().toISOString().slice(0, 10); const agentId = `opc-${companyId}`; // 检查今日是否已执行过同类任务(防重复) const existingTask = this.db.queryOne( `SELECT id FROM opc_staff_tasks WHERE company_id = ? AND staff_role = ? AND task_type = ? AND DATE(created_at) = ? AND status != 'cancelled'`, companyId, template.role, template.taskType, today, ) as { id: string } | null; if (existingTask) { return { taskId: existingTask.id, alreadyExists: true, staffRoleName: staffConfig.role_name, title: template.title, spawnPrompt: "", }; } // 创建任务记录 const taskId = this.db.genId(); const now = new Date().toISOString(); this.db.execute( `INSERT INTO opc_staff_tasks (id, company_id, staff_role, title, description, status, priority, task_type, schedule, assigned_at, started_at, created_at) VALUES (?, ?, ?, ?, ?, 'in_progress', 'normal', ?, ?, ?, ?, ?)`, taskId, companyId, template.role, template.title, template.description, template.taskType, template.schedule, now, now, now, ); // 构建 prompt context const ctx: TaskContext = { companyId, companyName: company.name, industry: company.industry, agentId, staffRole: template.role, staffRoleName: staffConfig.role_name, staffSystemPrompt: staffConfig.system_prompt, date: today, }; const basePrompt = template.buildPrompt(ctx); // 追加通用尾部指令 const spawnPrompt = `${basePrompt} ## 任务 ID ${taskId} ## 完成后必须执行 **【必须】更新任务状态 — 这是你交付成果的唯一方式** 调用 opc_staff update_task: - task_id="${taskId}" - status="completed" - result_summary=**完整的工作报告**(不是一句话!要包含所有具体内容)。格式: ## ${template.title}(${ctx.date}) ### 工作成果 (列出你做的所有工作的完整内容,搜到了什么、分析出了什么,要写具体内容) ### 关键发现 (3-5 条最重要的发现/结论,每条有具体数据) ### 建议下一步 (基于工作成果,建议老板接下来做什么) - result_data=完整结果的 JSON 字符串(包含所有原始数据、搜索结果、分析明细) ⚠️ result_summary 是老板看到的唯一内容,必须详细、完整、有具体数据。 老板不会去翻 result_data,所以 result_summary 里要把关键信息写全。 **【可选】尝试直接汇报** 如果 sessions_send 工具可用,调用: - sessionKey="agent:${agentId}:main" - message=与 result_summary 相同的完整报告 (如果 sessions_send 不可用,跳过即可,老板会通过任务系统看到你的成果)`; return { taskId, alreadyExists: false, staffRoleName: staffConfig.role_name, title: template.title, spawnPrompt, }; } /** * 老板按需触发:为指定员工构建自定义任务。 */ prepareBossTask( companyId: string, staffRole: string, title: string, description: string, ): ExecuteTaskResult { const template: TaskTemplate = { role: staffRole, taskType: "boss_assigned", title, schedule: "on_demand", description, buildPrompt: (ctx) => `你是「${ctx.staffRoleName}」,为「${ctx.companyName}」(${ctx.industry}行业)服务。 ${ctx.staffSystemPrompt} ## 老板交办任务 ${title} ${description} ## 可用工具 - opc_search: 联网搜索 - opc_finance: 财务管理 - opc_legal: 合同法务 - opc_hr: 人力资源 - opc_media: 内容管理 - opc_project: 项目管理 - opc_staff: 更新任务状态 - exec: 执行脚本/命令 - read/write: 文件操作 - browser: 浏览器自动化`, }; // boss_assigned 任务不做防重复检查,每次都新建 const company = this.db.queryOne( "SELECT name, industry FROM opc_companies WHERE id = ?", companyId, ) as { name: string; industry: string } | null; if (!company) throw new Error(`公司 ${companyId} 不存在`); const staffConfig = this.db.queryOne( "SELECT role_name, system_prompt FROM opc_staff_config WHERE company_id = ? AND role = ? AND enabled = 1", companyId, staffRole, ) as { role_name: string; system_prompt: string } | null; if (!staffConfig) throw new Error(`岗位 ${staffRole} 未启用`); const today = new Date().toISOString().slice(0, 10); const agentId = `opc-${companyId}`; const taskId = this.db.genId(); const now = new Date().toISOString(); this.db.execute( `INSERT INTO opc_staff_tasks (id, company_id, staff_role, title, description, status, priority, task_type, schedule, assigned_at, started_at, created_at) VALUES (?, ?, ?, ?, ?, 'in_progress', 'normal', ?, ?, ?, ?, ?)`, taskId, companyId, staffRole, title, description, "boss_assigned", "on_demand", now, now, now, ); const ctx: TaskContext = { companyId, companyName: company.name, industry: company.industry, agentId, staffRole, staffRoleName: staffConfig.role_name, staffSystemPrompt: staffConfig.system_prompt, date: today, }; const basePrompt = template.buildPrompt(ctx); const spawnPrompt = `${basePrompt} ## 任务 ID ${taskId} ## 完成后必须执行 **【必须】更新任务状态 — 这是你交付成果的唯一方式** 调用 opc_staff update_task: - task_id="${taskId}" - status="completed" - result_summary=**完整的工作报告**(不是一句话!要包含所有具体内容)。格式: ## ${title}(${today}) ### 工作成果 (列出你做的所有工作的完整内容,搜到了什么、分析出了什么,要写具体内容) ### 关键发现 (3-5 条最重要的发现/结论,每条有具体数据) ### 建议下一步 (基于工作成果,建议老板接下来做什么) - result_data=完整结果的 JSON 字符串(包含所有原始数据、搜索结果、分析明细) ⚠️ result_summary 是老板看到的唯一内容,必须详细、完整、有具体数据。 老板不会去翻 result_data,所以 result_summary 里要把关键信息写全。 **【可选】尝试直接汇报** 如果 sessions_send 工具可用,调用: - sessionKey="agent:${agentId}:main" - message=与 result_summary 相同的完整报告 (如果 sessions_send 不可用,跳过即可,老板会通过任务系统看到你的成果)`; return { taskId, alreadyExists: false, staffRoleName: staffConfig.role_name, title, spawnPrompt, }; } /** * 获取某公司在某个调度频率下所有应执行的任务模板。 * 检查哪些岗位已启用,返回可执行的模板列表。 */ getScheduledTemplates(companyId: string, schedule: "daily" | "weekly" | "hourly"): TaskTemplate[] { const templates = BUILTIN_TASK_TEMPLATES.filter(t => t.schedule === schedule); const enabledRoles = this.db.query( "SELECT role FROM opc_staff_config WHERE company_id = ? AND enabled = 1", companyId, ) as { role: string }[]; const roleSet = new Set(enabledRoles.map(r => r.role)); return templates.filter(t => roleSet.has(t.role)); } /** * 为某公司创建所有到期的定时任务记录(不重复创建)。 * 返回所有新建任务的 spawn 结果。 */ prepareScheduledTasks(companyId: string, schedule: "daily" | "weekly"): ExecuteTaskResult[] { const templates = this.getScheduledTemplates(companyId, schedule); const results: ExecuteTaskResult[] = []; for (const template of templates) { try { const result = this.prepareTask(template, companyId); if (!result.alreadyExists) { results.push(result); } } catch { // 岗位未启用等,跳过 } } return results; } /** * 为所有活跃公司创建定时任务记录。 * 用于 proactive-service 后台调用。 */ createPendingScheduledTasks(schedule: "daily" | "weekly"): number { const companies = this.db.query( "SELECT id FROM opc_companies WHERE status = 'active'", ) as { id: string }[]; let created = 0; for (const company of companies) { const templates = this.getScheduledTemplates(company.id, schedule); const today = new Date().toISOString().slice(0, 10); for (const template of templates) { try { // 检查今日是否已有同类任务 const existing = this.db.queryOne( `SELECT id FROM opc_staff_tasks WHERE company_id = ? AND staff_role = ? AND task_type = ? AND DATE(created_at) = ? AND status != 'cancelled'`, company.id, template.role, template.taskType, today, ); if (existing) continue; // 创建 pending 状态的任务(等 AI 来触发执行) const taskId = this.db.genId(); const now = new Date().toISOString(); this.db.execute( `INSERT INTO opc_staff_tasks (id, company_id, staff_role, title, description, status, priority, task_type, schedule, assigned_at, created_at) VALUES (?, ?, ?, ?, ?, 'pending', 'normal', ?, ?, ?, ?)`, taskId, company.id, template.role, template.title, template.description, template.taskType, template.schedule, now, now, ); created++; } catch { // 跳过失败的 } } } return created; } }