/** * 星环OPC中心 — opc_staff AI 员工岗位配置工具 * * 为每家一人公司配置 AI 员工角色(行政/HR/财务/法务等), * 实现"一人 = AI 团队"的核心理念。 */ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import crypto from "node:crypto"; import { Type, type Static } from "@sinclair/typebox"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { OpcDatabase } from "../db/index.js"; import { json, toolError } from "../utils/tool-helper.js"; import { TaskExecutor } from "../opc/task-executor.js"; /** 内置 AI 岗位定义 */ const BUILTIN_ROLES: Record = { admin: { name: "行政助理", prompt: "你是公司行政助理,负责日程管理、文件归档、会议安排、行政事务协调。用专业、简洁的方式处理行政工作。", skills: ["schedule", "document", "meeting"], }, hr: { name: "HR 专员", prompt: "你是公司 HR 专员,负责员工招聘、入职手续、薪酬核算、劳动合同管理、社保公积金事务。熟悉劳动法规。", skills: ["recruit", "payroll", "labor-law"], }, finance: { name: "财务顾问", prompt: "你是公司财务顾问,负责账务记录、发票管理、税务申报、现金流分析、财务报表。熟悉中国财税法规。", skills: ["bookkeeping", "tax", "invoice", "cashflow"], }, legal: { name: "法务助理", prompt: "你是公司法务助理,负责合同审查、风险评估、合规检查、法律文件起草。熟悉中国商业法律。", skills: ["contract-review", "compliance", "risk-assessment"], }, marketing: { name: "市场推广", prompt: "你是公司市场推广专员,负责品牌推广、内容营销、社交媒体运营、客户获取策略。", skills: ["content", "social-media", "brand"], }, ops: { name: "运营经理", prompt: "你是公司运营经理,负责项目管理、流程优化、供应链协调、KPI 跟踪与分析。", skills: ["project-mgmt", "process", "kpi"], }, }; const StaffSchema = Type.Union([ Type.Object({ action: Type.Literal("configure_staff"), company_id: Type.String({ description: "公司 ID" }), role: Type.String({ description: "岗位角色: admin/hr/finance/legal/marketing/ops 或自定义" }), role_name: Type.Optional(Type.String({ description: "岗位显示名称,不填则使用内置名称" })), enabled: Type.Optional(Type.Boolean({ description: "是否启用,默认 true" })), system_prompt: Type.Optional(Type.String({ description: "自定义系统提示词,不填则使用内置提示词" })), skills: Type.Optional(Type.String({ description: "技能列表 JSON 数组,如 [\"finance\",\"tax\"]" })), notes: Type.Optional(Type.String({ description: "备注" })), }), Type.Object({ action: Type.Literal("list_staff"), company_id: Type.String({ description: "公司 ID" }), enabled_only: Type.Optional(Type.Boolean({ description: "仅返回已启用岗位,默认 false" })), }), Type.Object({ action: Type.Literal("toggle_staff"), company_id: Type.String({ description: "公司 ID" }), role: Type.String({ description: "岗位角色" }), enabled: Type.Boolean({ description: "true=启用, false=停用" }), }), Type.Object({ action: Type.Literal("init_default_staff"), company_id: Type.String({ description: "公司 ID,将初始化 6 个默认 AI 岗位" }), }), Type.Object({ action: Type.Literal("list_builtin_roles"), }), Type.Object({ action: Type.Literal("assign_task"), company_id: Type.String({ description: "公司 ID" }), staff_role: Type.String({ description: "分配给哪个岗位: admin/hr/finance/legal/marketing/ops" }), title: Type.String({ description: "任务标题" }), description: Type.Optional(Type.String({ description: "任务详细描述" })), priority: Type.Optional(Type.String({ description: "优先级: urgent/high/normal/low,默认 normal" })), }), Type.Object({ action: Type.Literal("list_staff_tasks"), company_id: Type.String({ description: "公司 ID" }), staff_role: Type.Optional(Type.String({ description: "按岗位筛选" })), status: Type.Optional(Type.String({ description: "按状态筛选: pending/in_progress/completed/cancelled" })), }), Type.Object({ action: Type.Literal("update_task"), task_id: Type.String({ description: "任务 ID" }), status: Type.Optional(Type.String({ description: "新状态: pending/in_progress/completed/cancelled" })), result_summary: Type.Optional(Type.String({ description: "任务结果摘要" })), result_data: Type.Optional(Type.String({ description: "任务结果数据 JSON" })), }), Type.Object({ action: Type.Literal("staff_standup"), company_id: Type.String({ description: "公司 ID" }), staff_role: Type.Optional(Type.String({ description: "指定员工岗位,不填则全员" })), }), Type.Object({ action: Type.Literal("execute_task"), company_id: Type.String({ description: "公司 ID" }), staff_role: Type.String({ description: "执行员工岗位: admin/hr/finance/legal/marketing/ops" }), title: Type.String({ description: "任务标题" }), description: Type.Optional(Type.String({ description: "任务详细描述" })), }), Type.Object({ action: Type.Literal("run_daily_tasks"), company_id: Type.String({ description: "公司 ID" }), }), Type.Object({ action: Type.Literal("setup_schedule"), company_id: Type.String({ description: "公司 ID" }), schedule_type: Type.Optional(Type.String({ description: "调度类型: daily/weekly,默认 daily" })), cron_expr: Type.Optional(Type.String({ description: "cron 表达式,默认 '0 9 * * *'(每天9点)" })), timezone: Type.Optional(Type.String({ description: "时区,默认 Asia/Shanghai" })), }), ]); type StaffParams = Static; export function registerStaffTool(api: OpenClawPluginApi, db: OpcDatabase): void { api.registerTool( { name: "opc_staff", label: "OPC AI 员工配置", description: "AI 员工岗位配置与任务管理工具。实现\"一人公司 = AI 团队\"。" + "操作: configure_staff(配置/更新岗位), list_staff(岗位列表), " + "toggle_staff(启用/停用岗位), init_default_staff(一键初始化6个默认岗位), " + "list_builtin_roles(查看内置岗位模板), " + "assign_task(只记录任务), execute_task(让员工真正干活-启动独立会话执行), " + "run_daily_tasks(执行今日所有定时任务), " + "setup_schedule(设置cron定时调度-AI再调cron工具创建), " + "list_staff_tasks(查看员工任务), " + "update_task(更新任务状态/结果), staff_standup(员工站会报告)", parameters: StaffSchema, async execute(_toolCallId, params) { const p = params as StaffParams; try { switch (p.action) { case "configure_staff": { const builtin = BUILTIN_ROLES[p.role]; const roleName = p.role_name ?? builtin?.name ?? p.role; const prompt = p.system_prompt ?? builtin?.prompt ?? ""; const skills = p.skills ?? JSON.stringify(builtin?.skills ?? []); const now = new Date().toISOString(); // UPSERT: 存在则更新,不存在则插入 const existing = db.queryOne( "SELECT id FROM opc_staff_config WHERE company_id = ? AND role = ?", p.company_id, p.role, ); if (existing) { const sets: string[] = ["role_name = ?", "system_prompt = ?", "skills = ?", "updated_at = ?"]; const vals: unknown[] = [roleName, prompt, skills, now]; if (p.enabled !== undefined) { sets.push("enabled = ?"); vals.push(p.enabled ? 1 : 0); } if (p.notes !== undefined) { sets.push("notes = ?"); vals.push(p.notes); } vals.push(p.company_id, p.role); db.execute( `UPDATE opc_staff_config SET ${sets.join(", ")} WHERE company_id = ? AND role = ?`, ...vals, ); } else { const id = db.genId(); db.execute( `INSERT INTO opc_staff_config (id, company_id, role, role_name, enabled, system_prompt, skills, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, p.company_id, p.role, roleName, (p.enabled ?? true) ? 1 : 0, prompt, skills, p.notes ?? "", now, now, ); } return json(db.queryOne( "SELECT * FROM opc_staff_config WHERE company_id = ? AND role = ?", p.company_id, p.role, )); } case "list_staff": { let sql = "SELECT * FROM opc_staff_config WHERE company_id = ?"; const args: unknown[] = [p.company_id]; if (p.enabled_only) { sql += " AND enabled = 1"; } sql += " ORDER BY created_at ASC"; const rows = db.query(sql, ...args); return json({ staff: rows, count: (rows as unknown[]).length }); } case "toggle_staff": { const now = new Date().toISOString(); db.execute( "UPDATE opc_staff_config SET enabled = ?, updated_at = ? WHERE company_id = ? AND role = ?", p.enabled ? 1 : 0, now, p.company_id, p.role, ); const staffConfig = db.queryOne( "SELECT * FROM opc_staff_config WHERE company_id = ? AND role = ?", p.company_id, p.role, ); if (!staffConfig) return toolError("岗位配置不存在,请先调用 configure_staff 或 init_default_staff", "RECORD_NOT_FOUND"); return json(staffConfig); } case "init_default_staff": { const company = db.queryOne("SELECT * FROM opc_companies WHERE id = ?", p.company_id); if (!company) return toolError("公司不存在", "COMPANY_NOT_FOUND"); const now = new Date().toISOString(); const created: string[] = []; const skipped: string[] = []; for (const [role, def] of Object.entries(BUILTIN_ROLES)) { const exists = db.queryOne( "SELECT id FROM opc_staff_config WHERE company_id = ? AND role = ?", p.company_id, role, ); if (exists) { skipped.push(role); continue; } const id = db.genId(); db.execute( `INSERT INTO opc_staff_config (id, company_id, role, role_name, enabled, system_prompt, skills, notes, created_at, updated_at) VALUES (?, ?, ?, ?, 1, ?, ?, '', ?, ?)`, id, p.company_id, role, def.name, def.prompt, JSON.stringify(def.skills), now, now, ); created.push(role); } return json({ company_id: p.company_id, created, skipped, message: `已初始化 ${created.length} 个 AI 岗位${skipped.length > 0 ? `,跳过 ${skipped.length} 个已存在岗位` : ""}`, }); } case "list_builtin_roles": { const roles = Object.entries(BUILTIN_ROLES).map(([role, def]) => ({ role, name: def.name, skills: def.skills, prompt_preview: def.prompt.slice(0, 50) + "…", })); return json({ builtin_roles: roles, count: roles.length }); } case "assign_task": { const company = db.queryOne("SELECT id FROM opc_companies WHERE id = ?", p.company_id); if (!company) return toolError("公司不存在", "COMPANY_NOT_FOUND"); // 验证岗位存在且启用 const staffConfig = db.queryOne( "SELECT role_name FROM opc_staff_config WHERE company_id = ? AND role = ? AND enabled = 1", p.company_id, p.staff_role, ) as { role_name: string } | null; if (!staffConfig) return toolError(`岗位 ${p.staff_role} 不存在或未启用,请先调用 init_default_staff`, "STAFF_NOT_FOUND"); const id = db.genId(); const now = new Date().toISOString(); db.execute( `INSERT INTO opc_staff_tasks (id, company_id, staff_role, title, description, status, priority, assigned_at, created_at) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?)`, id, p.company_id, p.staff_role, p.title, p.description ?? "", p.priority ?? "normal", now, now, ); const task = db.queryOne("SELECT * FROM opc_staff_tasks WHERE id = ?", id); return json({ ...task as Record, staff_name: staffConfig.role_name, message: `已将任务「${p.title}」分配给 ${staffConfig.role_name}`, }); } case "list_staff_tasks": { let sql = "SELECT t.*, s.role_name as staff_name FROM opc_staff_tasks t LEFT JOIN opc_staff_config s ON t.company_id = s.company_id AND t.staff_role = s.role WHERE t.company_id = ?"; const args: unknown[] = [p.company_id]; if ("staff_role" in p && p.staff_role) { sql += " AND t.staff_role = ?"; args.push(p.staff_role); } if ("status" in p && p.status) { sql += " AND t.status = ?"; args.push(p.status); } sql += " ORDER BY CASE t.priority WHEN 'urgent' THEN 1 WHEN 'high' THEN 2 WHEN 'normal' THEN 3 ELSE 4 END, t.assigned_at DESC"; const tasks = db.query(sql, ...args); return json({ tasks, count: (tasks as unknown[]).length }); } case "update_task": { const existingTask = db.queryOne("SELECT * FROM opc_staff_tasks WHERE id = ?", p.task_id); if (!existingTask) return toolError("任务不存在", "TASK_NOT_FOUND"); const now = new Date().toISOString(); const sets: string[] = []; const vals: unknown[] = []; if ("status" in p && p.status) { sets.push("status = ?"); vals.push(p.status); if (p.status === "in_progress") { sets.push("started_at = ?"); vals.push(now); } else if (p.status === "completed" || p.status === "cancelled") { sets.push("completed_at = ?"); vals.push(now); } } if ("result_summary" in p && p.result_summary) { sets.push("result_summary = ?"); vals.push(p.result_summary); } if ("result_data" in p && p.result_data) { sets.push("result_data = ?"); vals.push(p.result_data); } if (sets.length === 0) return toolError("无更新字段", "NO_UPDATES"); vals.push(p.task_id); db.execute(`UPDATE opc_staff_tasks SET ${sets.join(", ")} WHERE id = ?`, ...vals); return json(db.queryOne("SELECT * FROM opc_staff_tasks WHERE id = ?", p.task_id)); } case "staff_standup": { // 查询启用的员工 let staffSql = "SELECT role, role_name FROM opc_staff_config WHERE company_id = ? AND enabled = 1"; const staffArgs: unknown[] = [p.company_id]; if ("staff_role" in p && p.staff_role) { staffSql += " AND role = ?"; staffArgs.push(p.staff_role); } staffSql += " ORDER BY created_at ASC"; const staffList = db.query(staffSql, ...staffArgs) as { role: string; role_name: string }[]; const standup: Record[] = []; for (const s of staffList) { // 进行中的任务 const activeTasks = db.query( `SELECT id, title, status, priority, assigned_at FROM opc_staff_tasks WHERE company_id = ? AND staff_role = ? AND status IN ('pending', 'in_progress') ORDER BY CASE priority WHEN 'urgent' THEN 1 WHEN 'high' THEN 2 WHEN 'normal' THEN 3 ELSE 4 END`, p.company_id, s.role, ); // 最近完成的任务 const completedTasks = db.query( `SELECT id, title, result_summary, completed_at FROM opc_staff_tasks WHERE company_id = ? AND staff_role = ? AND status = 'completed' ORDER BY completed_at DESC LIMIT 3`, p.company_id, s.role, ); // 该角色的观察洞察 const observations = db.query( `SELECT title, message, action_hint FROM opc_insights WHERE company_id = ? AND staff_role = ? AND status = 'active' AND (expires_at = '' OR expires_at > datetime('now')) ORDER BY priority DESC LIMIT 3`, p.company_id, s.role, ); standup.push({ role: s.role, role_name: s.role_name, active_tasks: activeTasks, active_task_count: (activeTasks as unknown[]).length, completed_tasks: completedTasks, observations, }); } return json({ standup, employee_count: standup.length }); } case "execute_task": { const company = db.queryOne("SELECT id FROM opc_companies WHERE id = ?", p.company_id); if (!company) return toolError("公司不存在", "COMPANY_NOT_FOUND"); const executor = new TaskExecutor(db); const result = executor.prepareBossTask( p.company_id, p.staff_role, p.title, "description" in p && p.description ? p.description : p.title, ); return json({ task_id: result.taskId, staff_name: result.staffRoleName, title: result.title, message: `已创建任务「${result.title}」并分配给 ${result.staffRoleName}。请立即调用 sessions_spawn 启动独立会话执行任务。`, _spawn_instruction: { action: "请调用 sessions_spawn 工具", task: result.spawnPrompt, }, }); } case "run_daily_tasks": { const company = db.queryOne("SELECT id FROM opc_companies WHERE id = ?", p.company_id); if (!company) return toolError("公司不存在", "COMPANY_NOT_FOUND"); const executor = new TaskExecutor(db); const dailyResults = executor.prepareScheduledTasks(p.company_id, "daily"); const weeklyResults = executor.prepareScheduledTasks(p.company_id, "weekly"); const allResults = [...dailyResults, ...weeklyResults]; if (allResults.length === 0) { return json({ message: "今日所有定时任务已执行过或无可执行任务", tasks: [], }); } const spawnInstructions = allResults.map(r => ({ task_id: r.taskId, staff_name: r.staffRoleName, title: r.title, task: r.spawnPrompt, })); return json({ message: `已创建 ${allResults.length} 个定时任务,请依次调用 sessions_spawn 启动员工独立会话执行。`, tasks: spawnInstructions.map(s => ({ task_id: s.task_id, staff_name: s.staff_name, title: s.title, })), _spawn_instructions: spawnInstructions.map(s => ({ action: "请调用 sessions_spawn 工具", task_id: s.task_id, staff_name: s.staff_name, task: s.task, })), }); } case "setup_schedule": { const company = db.queryOne( "SELECT id, name FROM opc_companies WHERE id = ?", p.company_id, ) as { id: string; name: string } | null; if (!company) return toolError("公司不存在", "COMPANY_NOT_FOUND"); const timezone = ("timezone" in p && p.timezone) || "Asia/Shanghai"; const agentId = `opc-${p.company_id}`; const nowMs = Date.now(); // 构建 3 个完整 CronJob 对象 const cronJobs = [ { id: crypto.randomUUID(), agentId, name: `opc-morning-${p.company_id}`, enabled: true, createdAtMs: nowMs, updatedAtMs: nowMs, schedule: { kind: "cron" as const, expr: ("cron_expr" in p && p.cron_expr) || "0 9 * * *", tz: timezone }, sessionTarget: "isolated" as const, wakeMode: "now" as const, payload: { kind: "agentTurn" as const, message: [ `你是「${company.name}」的 CEO 幕僚长。现在是每日晨报时间。`, "", "请执行以下步骤:", `1. 调用 opc_staff 工具,action=run_daily_tasks,company_id="${p.company_id}"`, "2. 根据返回的 _spawn_instructions 依次调用 sessions_spawn 启动员工执行", "3. 检查 pending_approval 状态的任务(需要老板决策的),整理决策清单", "4. 检查 24 小时内完成的任务,整理成果汇报", `5. 将晨报通过 sessions_send 发送给老板(sessionKey="agent:${agentId}:main")`, ].join("\n"), timeoutSeconds: 600, }, delivery: { mode: "announce" as const, channel: "last" as const }, state: {}, }, { id: crypto.randomUUID(), agentId, name: `opc-weekly-${p.company_id}`, enabled: true, createdAtMs: nowMs, updatedAtMs: nowMs, schedule: { kind: "cron" as const, expr: "0 10 * * 1", tz: timezone }, sessionTarget: "isolated" as const, wakeMode: "now" as const, payload: { kind: "agentTurn" as const, message: [ `你是「${company.name}」的 CEO 幕僚长。现在是每周一复盘时间。`, "", "请执行以下步骤:", `1. 调用 opc_staff 工具,action=staff_standup,company_id="${p.company_id}",获取全员站会报告`, `2. 调用 opc_staff 工具,action=list_staff_tasks,company_id="${p.company_id}",status=completed,查看本周完成的任务`, "3. 整理上周复盘:完成了什么、遗留了什么、下周重点", `4. 将周报通过 sessions_send 发送给老板(sessionKey="agent:${agentId}:main")`, ].join("\n"), timeoutSeconds: 600, }, delivery: { mode: "announce" as const, channel: "last" as const }, state: {}, }, { id: crypto.randomUUID(), agentId, name: `opc-monthly-${p.company_id}`, enabled: true, createdAtMs: nowMs, updatedAtMs: nowMs, schedule: { kind: "cron" as const, expr: "0 10 1 * *", tz: timezone }, sessionTarget: "isolated" as const, wakeMode: "now" as const, payload: { kind: "agentTurn" as const, message: [ `你是「${company.name}」的 CEO 幕僚长。现在是每月1日月度总结时间。`, "", "请执行以下步骤:", `1. 调用 opc_finance 工具查看本月财务概况(company_id="${p.company_id}")`, `2. 调用 opc_staff 工具,action=staff_standup,company_id="${p.company_id}",获取全员站会`, "3. 整理月度报告:收支汇总、客户变化、项目进展、合同状态、税务提醒", "4. 生成下月 OKR 建议", `5. 将月报通过 sessions_send 发送给老板(sessionKey="agent:${agentId}:main")`, ].join("\n"), timeoutSeconds: 600, }, delivery: { mode: "announce" as const, channel: "last" as const }, state: {}, }, ]; // ── 直接写入 cron/jobs.json(绕过 ownerOnly 限制) ── const cronDir = path.join(os.homedir(), ".openclaw", "cron"); const jobsFile = path.join(cronDir, "jobs.json"); let store: { version: number; jobs: Record[] } = { version: 1, jobs: [] }; // 读取现有 jobs try { const raw = fs.readFileSync(jobsFile, "utf-8"); const parsed = JSON.parse(raw); if (parsed && Array.isArray(parsed.jobs)) { store = parsed; } } catch { // 文件不存在或解析失败,使用空 store } // 移除该公司的旧 OPC cron jobs(按 name 前缀匹配) const opcPrefix = `opc-`; const companySuffix = `-${p.company_id}`; store.jobs = store.jobs.filter(j => { const name = typeof j.name === "string" ? j.name : ""; return !(name.startsWith(opcPrefix) && name.endsWith(companySuffix)); }); // 添加新的 3 个 jobs store.jobs.push(...cronJobs); // 原子写入 fs.mkdirSync(cronDir, { recursive: true }); const tmpFile = `${jobsFile}.${process.pid}.${crypto.randomBytes(8).toString("hex")}.tmp`; fs.writeFileSync(tmpFile, JSON.stringify(store, null, 2), "utf-8"); fs.renameSync(tmpFile, jobsFile); // 记录调度配置到 opc_tool_config db.execute( `INSERT INTO opc_tool_config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`, `cron_schedule_${p.company_id}`, JSON.stringify({ jobs: cronJobs.map(j => j.name), timezone, createdAt: new Date().toISOString(), }), ); return json({ message: `已为「${company.name}」创建 3 个定时任务并直接写入 cron 系统(无需额外操作)。`, schedule_config: { timezone, jobs: cronJobs.map(j => ({ name: j.name, cron_expr: j.schedule.expr, id: j.id, })), }, created_jobs: [ { name: cronJobs[0].name, schedule: "每天 9:00", desc: "每日晨报 + 执行定时任务" }, { name: cronJobs[1].name, schedule: "每周一 10:00", desc: "每周复盘 + 全员站会" }, { name: cronJobs[2].name, schedule: "每月1日 10:00", desc: "月度总结 + 财务报告" }, ], note: "cron 服务将在约 60 秒内自动加载新任务。重启 gateway 可立即生效。", }); } default: return toolError(`未知操作: ${(p as { action: string }).action}`, "UNKNOWN_ACTION"); } } catch (err) { return toolError(err instanceof Error ? err.message : String(err), "DB_ERROR"); } }, }, { name: "opc_staff" }, ); api.logger.info("opc: 已注册 opc_staff 工具"); }