/** * 星环OPC中心 — opc_order 订单中台工具 * * 核心功能:签单闭环管理 * - 报价单创建与管理 * - 报价转合同(自动创建里程碑和应收) * - 签约确认与里程碑跟踪 */ 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"; const OrderSchema = Type.Union([ // 1. 创建报价单 Type.Object({ action: Type.Literal("create_quotation"), company_id: Type.String({ description: "公司 ID" }), contact_id: Type.Optional(Type.String({ description: "客户 ID(关联 opc_contacts)" })), title: Type.String({ description: "报价标题" }), valid_until: Type.String({ description: "有效期 (YYYY-MM-DD)" }), items: Type.Array(Type.Object({ description: Type.String({ description: "服务/产品描述" }), quantity: Type.Number({ description: "数量" }), unit_price: Type.Number({ description: "单价(元)" }), }), { description: "报价明细", minItems: 1 }), notes: Type.Optional(Type.String({ description: "备注" })), }), // 2. 查看报价单列表 Type.Object({ action: Type.Literal("list_quotations"), company_id: Type.String({ description: "公司 ID" }), status: Type.Optional(Type.String({ description: "按状态筛选: draft/sent/accepted/rejected/expired" })), }), // 3. 更新报价单状态 Type.Object({ action: Type.Literal("update_quotation_status"), quotation_id: Type.String({ description: "报价单 ID" }), status: Type.String({ description: "新状态: sent(已发送)/accepted(已接受)/rejected(已拒绝)" }), }), // 4. 报价转合同(核心功能) Type.Object({ action: Type.Literal("quotation_to_contract"), quotation_id: Type.String({ description: "报价单 ID" }), contract_type: Type.String({ description: "合同类型: 服务合同/采购合同/其他" }), start_date: Type.String({ description: "合同开始日期 (YYYY-MM-DD)" }), end_date: Type.String({ description: "合同结束日期 (YYYY-MM-DD)" }), payment_terms: Type.String({ description: "付款条款(如:签约30%,交付70%)" }), milestones: Type.Array(Type.Object({ title: Type.String({ description: "里程碑名称" }), due_date: Type.String({ description: "到期日期 (YYYY-MM-DD)" }), amount: Type.Number({ description: "该里程碑对应的收款金额" }), description: Type.Optional(Type.String({ description: "里程碑描述" })), }), { description: "里程碑列表", minItems: 1 }), key_terms: Type.Optional(Type.String({ description: "核心条款摘要" })), }), // 5. 确认签约 Type.Object({ action: Type.Literal("confirm_signing"), contract_id: Type.String({ description: "合同 ID" }), signed_date: Type.String({ description: "签约日期 (YYYY-MM-DD)" }), }), // 6. 完成里程碑 Type.Object({ action: Type.Literal("complete_milestone"), milestone_id: Type.String({ description: "里程碑 ID" }), completed_date: Type.Optional(Type.String({ description: "完成日期 (YYYY-MM-DD),默认今天" })), notes: Type.Optional(Type.String({ description: "完成备注" })), }), // 7. 查看进行中的订单 Type.Object({ action: Type.Literal("list_active_orders"), company_id: Type.String({ description: "公司 ID" }), }), // 8. 里程碑到期提醒 Type.Object({ action: Type.Literal("milestone_reminder"), company_id: Type.String({ description: "公司 ID" }), days_ahead: Type.Optional(Type.Number({ description: "提前天数,默认 7 天" })), }), // 9. 获取合同详情(含里程碑) Type.Object({ action: Type.Literal("get_contract_details"), contract_id: Type.String({ description: "合同 ID" }), }), ]); type OrderParams = Static; export function registerOrderTool(api: OpenClawPluginApi, db: OpcDatabase): void { api.registerTool( { name: "opc_order", label: "OPC 订单中台", description: "订单中台工具,管理签单全流程。操作: create_quotation(创建报价单), " + "list_quotations(报价单列表), update_quotation_status(更新报价状态), " + "quotation_to_contract(报价转合同,自动创建里程碑和应收), " + "confirm_signing(确认签约), complete_milestone(完成里程碑), " + "list_active_orders(查看进行中订单), milestone_reminder(里程碑到期提醒), " + "get_contract_details(获取合同详情)", parameters: OrderSchema, async execute(_toolCallId, params) { const p = params as OrderParams; try { switch (p.action) { // ═══════════════════════════════════════════════════════ // 1. 创建报价单 // ═══════════════════════════════════════════════════════ case "create_quotation": { const id = db.genId(); const now = new Date().toISOString(); const month = now.slice(0, 7).replace("-", ""); // 生成报价单号: QT-YYYYMM-NNN const countRow = db.queryOne( "SELECT COUNT(*) as cnt FROM opc_quotations WHERE company_id = ? AND quotation_number LIKE ?", p.company_id, `QT-${month}-%`, ) as { cnt: number }; const seq = String((countRow?.cnt ?? 0) + 1).padStart(3, "0"); const quotationNumber = `QT-${month}-${seq}`; // 计算总金额 const totalAmount = p.items.reduce((sum, item) => sum + item.quantity * item.unit_price, 0); // 插入报价单 db.exec( `INSERT INTO opc_quotations (id, company_id, contact_id, quotation_number, title, total_amount, valid_until, status, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'draft', ?, ?, ?)`, id, p.company_id, p.contact_id ?? "", quotationNumber, p.title, totalAmount, p.valid_until, p.notes ?? "", now, now, ); // 插入报价明细 for (const item of p.items) { const itemId = db.genId(); const totalPrice = item.quantity * item.unit_price; db.exec( `INSERT INTO opc_quotation_items (id, quotation_id, description, quantity, unit_price, total_price, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, itemId, id, item.description, item.quantity, item.unit_price, totalPrice, now, ); } const quotation = db.queryOne("SELECT * FROM opc_quotations WHERE id = ?", id); const items = db.query("SELECT * FROM opc_quotation_items WHERE quotation_id = ?", id); return json({ ok: true, quotation, items, message: `✅ 报价单 ${quotationNumber} 创建成功` }); } // ═══════════════════════════════════════════════════════ // 2. 查看报价单列表 // ═══════════════════════════════════════════════════════ case "list_quotations": { let sql = "SELECT * FROM opc_quotations WHERE company_id = ?"; const params: (string | number)[] = [p.company_id]; if (p.status) { sql += " AND status = ?"; params.push(p.status); } sql += " ORDER BY created_at DESC"; const quotations = db.query(sql, ...params); return json({ ok: true, quotations, count: quotations.length }); } // ═══════════════════════════════════════════════════════ // 3. 更新报价单状态 // ═══════════════════════════════════════════════════════ case "update_quotation_status": { const now = new Date().toISOString(); db.exec( "UPDATE opc_quotations SET status = ?, updated_at = ? WHERE id = ?", p.status, now, p.quotation_id, ); const quotation = db.queryOne("SELECT * FROM opc_quotations WHERE id = ?", p.quotation_id); return json({ ok: true, quotation, message: `✅ 报价单状态已更新为 ${p.status}` }); } // ═══════════════════════════════════════════════════════ // 4. 报价转合同(核心功能) // ═══════════════════════════════════════════════════════ case "quotation_to_contract": { const now = new Date().toISOString(); // 1. 获取报价单信息 const quotation = db.queryOne("SELECT * FROM opc_quotations WHERE id = ?", p.quotation_id) as { id: string; company_id: string; contact_id: string; title: string; total_amount: number; } | null; if (!quotation) { return toolError("QUOTATION_NOT_FOUND", `报价单 ${p.quotation_id} 不存在`); } // 2. 创建合同 const contractId = db.genId(); db.exec( `INSERT INTO opc_contracts (id, company_id, title, counterparty, contract_type, direction, amount, start_date, end_date, status, key_terms, quotation_id, payment_terms, created_at, updated_at) VALUES (?, ?, ?, '', ?, 'sales', ?, ?, ?, 'draft', ?, ?, ?, ?, ?)`, contractId, quotation.company_id, quotation.title, p.contract_type, quotation.total_amount, p.start_date, p.end_date, p.key_terms ?? "", p.quotation_id, p.payment_terms, now, now, ); // 3. 创建里程碑 const milestoneIds: string[] = []; for (const milestone of p.milestones) { const milestoneId = db.genId(); milestoneIds.push(milestoneId); db.exec( `INSERT INTO opc_contract_milestones (id, contract_id, company_id, title, description, due_date, amount, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`, milestoneId, contractId, quotation.company_id, milestone.title, milestone.description ?? "", milestone.due_date, milestone.amount, now, now, ); // 4. 自动创建应收记录(关联里程碑) const paymentId = db.genId(); db.exec( `INSERT INTO opc_payments (id, company_id, direction, counterparty, amount, due_date, status, invoice_id, contract_id, milestone_id, category, notes, created_at, updated_at) VALUES (?, ?, 'receivable', '', ?, ?, 'pending', '', ?, ?, 'service', ?, ?, ?)`, paymentId, quotation.company_id, milestone.amount, milestone.due_date, contractId, milestoneId, `${milestone.title} 应收款`, now, now, ); } // 5. 更新报价单状态为已接受 db.exec( "UPDATE opc_quotations SET status = 'accepted', updated_at = ? WHERE id = ?", now, p.quotation_id, ); const contract = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", contractId); const milestones = db.query("SELECT * FROM opc_contract_milestones WHERE contract_id = ?", contractId); const payments = db.query("SELECT * FROM opc_payments WHERE contract_id = ?", contractId); return json({ ok: true, contract, milestones, payments, message: `✅ 报价转合同成功!已创建 ${p.milestones.length} 个里程碑和应收记录`, }); } // ═══════════════════════════════════════════════════════ // 5. 确认签约 // ═══════════════════════════════════════════════════════ case "confirm_signing": { const now = new Date().toISOString(); // 更新合同状态和签约日期 db.exec( "UPDATE opc_contracts SET status = 'active', signed_date = ?, updated_at = ? WHERE id = ?", p.signed_date, now, p.contract_id, ); // 获取第一个里程碑,标记为进行中 const firstMilestone = db.queryOne( "SELECT * FROM opc_contract_milestones WHERE contract_id = ? ORDER BY due_date ASC LIMIT 1", p.contract_id, ) as { id: string } | null; if (firstMilestone) { db.exec( "UPDATE opc_contract_milestones SET status = 'in_progress', updated_at = ? WHERE id = ?", now, firstMilestone.id, ); } const contract = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", p.contract_id); const milestones = db.query("SELECT * FROM opc_contract_milestones WHERE contract_id = ?", p.contract_id); return json({ ok: true, contract, milestones, message: `✅ 合同已签约!第一个里程碑已启动`, }); } // ═══════════════════════════════════════════════════════ // 6. 完成里程碑 // ═══════════════════════════════════════════════════════ case "complete_milestone": { const now = new Date().toISOString(); const completedDate = p.completed_date ?? now.slice(0, 10); // 更新里程碑状态 db.exec( "UPDATE opc_contract_milestones SET status = 'completed', completed_date = ?, notes = ?, updated_at = ? WHERE id = ?", completedDate, p.notes ?? "", now, p.milestone_id, ); const milestone = db.queryOne("SELECT * FROM opc_contract_milestones WHERE id = ?", p.milestone_id) as { contract_id: string; title: string; } | null; if (!milestone) { return toolError("MILESTONE_NOT_FOUND", `里程碑 ${p.milestone_id} 不存在`); } // 获取下一个里程碑,标记为进行中 const nextMilestone = db.queryOne( "SELECT * FROM opc_contract_milestones WHERE contract_id = ? AND status = 'pending' ORDER BY due_date ASC LIMIT 1", milestone.contract_id, ) as { id: string; title: string } | null; if (nextMilestone) { db.exec( "UPDATE opc_contract_milestones SET status = 'in_progress', updated_at = ? WHERE id = ?", now, nextMilestone.id, ); } return json({ ok: true, milestone: db.queryOne("SELECT * FROM opc_contract_milestones WHERE id = ?", p.milestone_id), next_milestone: nextMilestone, message: `✅ 里程碑「${milestone.title}」已完成${nextMilestone ? `,下一个里程碑「${nextMilestone.title}」已启动` : ""}`, }); } // ═══════════════════════════════════════════════════════ // 7. 查看进行中的订单 // ═══════════════════════════════════════════════════════ case "list_active_orders": { const contracts = db.query( "SELECT * FROM opc_contracts WHERE company_id = ? AND status = 'active' ORDER BY signed_date DESC", p.company_id, ); const orders = contracts.map((contract) => { const milestones = db.query( "SELECT * FROM opc_contract_milestones WHERE contract_id = ? ORDER BY due_date ASC", (contract as { id: string }).id, ); return { ...contract, milestones }; }); return json({ ok: true, orders, count: orders.length }); } // ═══════════════════════════════════════════════════════ // 8. 里程碑到期提醒 // ═══════════════════════════════════════════════════════ case "milestone_reminder": { const daysAhead = p.days_ahead ?? 7; const today = new Date().toISOString().slice(0, 10); const futureDate = new Date(Date.now() + daysAhead * 24 * 60 * 60 * 1000).toISOString().slice(0, 10); const milestones = db.query( `SELECT m.*, c.title as contract_title FROM opc_contract_milestones m JOIN opc_contracts c ON m.contract_id = c.id WHERE m.company_id = ? AND m.status IN ('pending', 'in_progress') AND m.due_date BETWEEN ? AND ? ORDER BY m.due_date ASC`, p.company_id, today, futureDate, ); return json({ ok: true, milestones, count: milestones.length, message: `未来 ${daysAhead} 天内有 ${milestones.length} 个里程碑到期`, }); } // ═══════════════════════════════════════════════════════ // 9. 获取合同详情 // ═══════════════════════════════════════════════════════ case "get_contract_details": { const contract = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", p.contract_id); if (!contract) { return toolError("CONTRACT_NOT_FOUND", `合同 ${p.contract_id} 不存在`); } const milestones = db.query( "SELECT * FROM opc_contract_milestones WHERE contract_id = ? ORDER BY due_date ASC", p.contract_id, ); const payments = db.query( "SELECT * FROM opc_payments WHERE contract_id = ? ORDER BY due_date ASC", p.contract_id, ); const quotation = (contract as { quotation_id: string }).quotation_id ? db.queryOne("SELECT * FROM opc_quotations WHERE id = ?", (contract as { quotation_id: string }).quotation_id) : null; return json({ ok: true, contract, milestones, payments, quotation, }); } default: return toolError("INVALID_ACTION", `未知操作: ${(p as { action: string }).action}`); } } catch (err) { const message = err instanceof Error ? err.message : String(err); return toolError("EXECUTION_ERROR", `订单工具执行失败: ${message}`); } }, }, ); }