galaxy-opc-plugin 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +207 -10
- package/index.ts +106 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/api/companies.ts +4 -0
- package/src/api/dashboard.ts +368 -16
- package/src/api/routes.ts +2 -2
- package/src/db/migrations.ts +146 -0
- package/src/db/schema.ts +104 -3
- package/src/db/sqlite-adapter.ts +39 -2
- package/src/opc/accounting-parser.ts +178 -0
- package/src/opc/daily-brief.ts +529 -0
- package/src/opc/onboarding-flow.ts +332 -0
- package/src/opc/proactive-service.ts +290 -3
- package/src/tools/finance-tool.test-payment.ts +326 -0
- package/src/tools/finance-tool.ts +653 -1
- package/src/tools/onboarding-tool.ts +233 -0
- package/src/tools/order-tool.ts +481 -0
- package/src/tools/smart-accounting-tool.ts +144 -0
- package/src/web/DASHBOARD_INTEGRATION_GUIDE.md +478 -0
- package/src/web/config-ui-patches.ts +389 -0
- package/src/web/config-ui.ts +4162 -3809
- package/src/web/dashboard-ui.ts +582 -0
- package/src/web/landing-page.ts +56 -6
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — opc_order 订单中台工具
|
|
3
|
+
*
|
|
4
|
+
* 核心功能:签单闭环管理
|
|
5
|
+
* - 报价单创建与管理
|
|
6
|
+
* - 报价转合同(自动创建里程碑和应收)
|
|
7
|
+
* - 签约确认与里程碑跟踪
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
11
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
12
|
+
import type { OpcDatabase } from "../db/index.js";
|
|
13
|
+
import { json, toolError } from "../utils/tool-helper.js";
|
|
14
|
+
|
|
15
|
+
const OrderSchema = Type.Union([
|
|
16
|
+
// 1. 创建报价单
|
|
17
|
+
Type.Object({
|
|
18
|
+
action: Type.Literal("create_quotation"),
|
|
19
|
+
company_id: Type.String({ description: "公司 ID" }),
|
|
20
|
+
contact_id: Type.Optional(Type.String({ description: "客户 ID(关联 opc_contacts)" })),
|
|
21
|
+
title: Type.String({ description: "报价标题" }),
|
|
22
|
+
valid_until: Type.String({ description: "有效期 (YYYY-MM-DD)" }),
|
|
23
|
+
items: Type.Array(Type.Object({
|
|
24
|
+
description: Type.String({ description: "服务/产品描述" }),
|
|
25
|
+
quantity: Type.Number({ description: "数量" }),
|
|
26
|
+
unit_price: Type.Number({ description: "单价(元)" }),
|
|
27
|
+
}), { description: "报价明细", minItems: 1 }),
|
|
28
|
+
notes: Type.Optional(Type.String({ description: "备注" })),
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
// 2. 查看报价单列表
|
|
32
|
+
Type.Object({
|
|
33
|
+
action: Type.Literal("list_quotations"),
|
|
34
|
+
company_id: Type.String({ description: "公司 ID" }),
|
|
35
|
+
status: Type.Optional(Type.String({ description: "按状态筛选: draft/sent/accepted/rejected/expired" })),
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
// 3. 更新报价单状态
|
|
39
|
+
Type.Object({
|
|
40
|
+
action: Type.Literal("update_quotation_status"),
|
|
41
|
+
quotation_id: Type.String({ description: "报价单 ID" }),
|
|
42
|
+
status: Type.String({ description: "新状态: sent(已发送)/accepted(已接受)/rejected(已拒绝)" }),
|
|
43
|
+
}),
|
|
44
|
+
|
|
45
|
+
// 4. 报价转合同(核心功能)
|
|
46
|
+
Type.Object({
|
|
47
|
+
action: Type.Literal("quotation_to_contract"),
|
|
48
|
+
quotation_id: Type.String({ description: "报价单 ID" }),
|
|
49
|
+
contract_type: Type.String({ description: "合同类型: 服务合同/采购合同/其他" }),
|
|
50
|
+
start_date: Type.String({ description: "合同开始日期 (YYYY-MM-DD)" }),
|
|
51
|
+
end_date: Type.String({ description: "合同结束日期 (YYYY-MM-DD)" }),
|
|
52
|
+
payment_terms: Type.String({ description: "付款条款(如:签约30%,交付70%)" }),
|
|
53
|
+
milestones: Type.Array(Type.Object({
|
|
54
|
+
title: Type.String({ description: "里程碑名称" }),
|
|
55
|
+
due_date: Type.String({ description: "到期日期 (YYYY-MM-DD)" }),
|
|
56
|
+
amount: Type.Number({ description: "该里程碑对应的收款金额" }),
|
|
57
|
+
description: Type.Optional(Type.String({ description: "里程碑描述" })),
|
|
58
|
+
}), { description: "里程碑列表", minItems: 1 }),
|
|
59
|
+
key_terms: Type.Optional(Type.String({ description: "核心条款摘要" })),
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
// 5. 确认签约
|
|
63
|
+
Type.Object({
|
|
64
|
+
action: Type.Literal("confirm_signing"),
|
|
65
|
+
contract_id: Type.String({ description: "合同 ID" }),
|
|
66
|
+
signed_date: Type.String({ description: "签约日期 (YYYY-MM-DD)" }),
|
|
67
|
+
}),
|
|
68
|
+
|
|
69
|
+
// 6. 完成里程碑
|
|
70
|
+
Type.Object({
|
|
71
|
+
action: Type.Literal("complete_milestone"),
|
|
72
|
+
milestone_id: Type.String({ description: "里程碑 ID" }),
|
|
73
|
+
completed_date: Type.Optional(Type.String({ description: "完成日期 (YYYY-MM-DD),默认今天" })),
|
|
74
|
+
notes: Type.Optional(Type.String({ description: "完成备注" })),
|
|
75
|
+
}),
|
|
76
|
+
|
|
77
|
+
// 7. 查看进行中的订单
|
|
78
|
+
Type.Object({
|
|
79
|
+
action: Type.Literal("list_active_orders"),
|
|
80
|
+
company_id: Type.String({ description: "公司 ID" }),
|
|
81
|
+
}),
|
|
82
|
+
|
|
83
|
+
// 8. 里程碑到期提醒
|
|
84
|
+
Type.Object({
|
|
85
|
+
action: Type.Literal("milestone_reminder"),
|
|
86
|
+
company_id: Type.String({ description: "公司 ID" }),
|
|
87
|
+
days_ahead: Type.Optional(Type.Number({ description: "提前天数,默认 7 天" })),
|
|
88
|
+
}),
|
|
89
|
+
|
|
90
|
+
// 9. 获取合同详情(含里程碑)
|
|
91
|
+
Type.Object({
|
|
92
|
+
action: Type.Literal("get_contract_details"),
|
|
93
|
+
contract_id: Type.String({ description: "合同 ID" }),
|
|
94
|
+
}),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
type OrderParams = Static<typeof OrderSchema>;
|
|
98
|
+
|
|
99
|
+
export function registerOrderTool(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
100
|
+
api.registerTool(
|
|
101
|
+
{
|
|
102
|
+
name: "opc_order",
|
|
103
|
+
label: "OPC 订单中台",
|
|
104
|
+
description:
|
|
105
|
+
"订单中台工具,管理签单全流程。操作: create_quotation(创建报价单), " +
|
|
106
|
+
"list_quotations(报价单列表), update_quotation_status(更新报价状态), " +
|
|
107
|
+
"quotation_to_contract(报价转合同,自动创建里程碑和应收), " +
|
|
108
|
+
"confirm_signing(确认签约), complete_milestone(完成里程碑), " +
|
|
109
|
+
"list_active_orders(查看进行中订单), milestone_reminder(里程碑到期提醒), " +
|
|
110
|
+
"get_contract_details(获取合同详情)",
|
|
111
|
+
parameters: OrderSchema,
|
|
112
|
+
async execute(_toolCallId, params) {
|
|
113
|
+
const p = params as OrderParams;
|
|
114
|
+
try {
|
|
115
|
+
switch (p.action) {
|
|
116
|
+
// ═══════════════════════════════════════════════════════
|
|
117
|
+
// 1. 创建报价单
|
|
118
|
+
// ═══════════════════════════════════════════════════════
|
|
119
|
+
case "create_quotation": {
|
|
120
|
+
const id = db.genId();
|
|
121
|
+
const now = new Date().toISOString();
|
|
122
|
+
const month = now.slice(0, 7).replace("-", "");
|
|
123
|
+
|
|
124
|
+
// 生成报价单号: QT-YYYYMM-NNN
|
|
125
|
+
const countRow = db.queryOne(
|
|
126
|
+
"SELECT COUNT(*) as cnt FROM opc_quotations WHERE company_id = ? AND quotation_number LIKE ?",
|
|
127
|
+
p.company_id,
|
|
128
|
+
`QT-${month}-%`,
|
|
129
|
+
) as { cnt: number };
|
|
130
|
+
const seq = String((countRow?.cnt ?? 0) + 1).padStart(3, "0");
|
|
131
|
+
const quotationNumber = `QT-${month}-${seq}`;
|
|
132
|
+
|
|
133
|
+
// 计算总金额
|
|
134
|
+
const totalAmount = p.items.reduce((sum, item) => sum + item.quantity * item.unit_price, 0);
|
|
135
|
+
|
|
136
|
+
// 插入报价单
|
|
137
|
+
db.exec(
|
|
138
|
+
`INSERT INTO opc_quotations (id, company_id, contact_id, quotation_number, title, total_amount, valid_until, status, notes, created_at, updated_at)
|
|
139
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'draft', ?, ?, ?)`,
|
|
140
|
+
id,
|
|
141
|
+
p.company_id,
|
|
142
|
+
p.contact_id ?? "",
|
|
143
|
+
quotationNumber,
|
|
144
|
+
p.title,
|
|
145
|
+
totalAmount,
|
|
146
|
+
p.valid_until,
|
|
147
|
+
p.notes ?? "",
|
|
148
|
+
now,
|
|
149
|
+
now,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// 插入报价明细
|
|
153
|
+
for (const item of p.items) {
|
|
154
|
+
const itemId = db.genId();
|
|
155
|
+
const totalPrice = item.quantity * item.unit_price;
|
|
156
|
+
db.exec(
|
|
157
|
+
`INSERT INTO opc_quotation_items (id, quotation_id, description, quantity, unit_price, total_price, created_at)
|
|
158
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
159
|
+
itemId,
|
|
160
|
+
id,
|
|
161
|
+
item.description,
|
|
162
|
+
item.quantity,
|
|
163
|
+
item.unit_price,
|
|
164
|
+
totalPrice,
|
|
165
|
+
now,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const quotation = db.queryOne("SELECT * FROM opc_quotations WHERE id = ?", id);
|
|
170
|
+
const items = db.query("SELECT * FROM opc_quotation_items WHERE quotation_id = ?", id);
|
|
171
|
+
|
|
172
|
+
return json({ ok: true, quotation, items, message: `✅ 报价单 ${quotationNumber} 创建成功` });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ═══════════════════════════════════════════════════════
|
|
176
|
+
// 2. 查看报价单列表
|
|
177
|
+
// ═══════════════════════════════════════════════════════
|
|
178
|
+
case "list_quotations": {
|
|
179
|
+
let sql = "SELECT * FROM opc_quotations WHERE company_id = ?";
|
|
180
|
+
const params: (string | number)[] = [p.company_id];
|
|
181
|
+
|
|
182
|
+
if (p.status) {
|
|
183
|
+
sql += " AND status = ?";
|
|
184
|
+
params.push(p.status);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
sql += " ORDER BY created_at DESC";
|
|
188
|
+
const quotations = db.query(sql, ...params);
|
|
189
|
+
|
|
190
|
+
return json({ ok: true, quotations, count: quotations.length });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ═══════════════════════════════════════════════════════
|
|
194
|
+
// 3. 更新报价单状态
|
|
195
|
+
// ═══════════════════════════════════════════════════════
|
|
196
|
+
case "update_quotation_status": {
|
|
197
|
+
const now = new Date().toISOString();
|
|
198
|
+
db.exec(
|
|
199
|
+
"UPDATE opc_quotations SET status = ?, updated_at = ? WHERE id = ?",
|
|
200
|
+
p.status,
|
|
201
|
+
now,
|
|
202
|
+
p.quotation_id,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const quotation = db.queryOne("SELECT * FROM opc_quotations WHERE id = ?", p.quotation_id);
|
|
206
|
+
return json({ ok: true, quotation, message: `✅ 报价单状态已更新为 ${p.status}` });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ═══════════════════════════════════════════════════════
|
|
210
|
+
// 4. 报价转合同(核心功能)
|
|
211
|
+
// ═══════════════════════════════════════════════════════
|
|
212
|
+
case "quotation_to_contract": {
|
|
213
|
+
const now = new Date().toISOString();
|
|
214
|
+
|
|
215
|
+
// 1. 获取报价单信息
|
|
216
|
+
const quotation = db.queryOne("SELECT * FROM opc_quotations WHERE id = ?", p.quotation_id) as {
|
|
217
|
+
id: string;
|
|
218
|
+
company_id: string;
|
|
219
|
+
contact_id: string;
|
|
220
|
+
title: string;
|
|
221
|
+
total_amount: number;
|
|
222
|
+
} | null;
|
|
223
|
+
|
|
224
|
+
if (!quotation) {
|
|
225
|
+
return toolError("QUOTATION_NOT_FOUND", `报价单 ${p.quotation_id} 不存在`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 2. 创建合同
|
|
229
|
+
const contractId = db.genId();
|
|
230
|
+
db.exec(
|
|
231
|
+
`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)
|
|
232
|
+
VALUES (?, ?, ?, '', ?, 'sales', ?, ?, ?, 'draft', ?, ?, ?, ?, ?)`,
|
|
233
|
+
contractId,
|
|
234
|
+
quotation.company_id,
|
|
235
|
+
quotation.title,
|
|
236
|
+
p.contract_type,
|
|
237
|
+
quotation.total_amount,
|
|
238
|
+
p.start_date,
|
|
239
|
+
p.end_date,
|
|
240
|
+
p.key_terms ?? "",
|
|
241
|
+
p.quotation_id,
|
|
242
|
+
p.payment_terms,
|
|
243
|
+
now,
|
|
244
|
+
now,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// 3. 创建里程碑
|
|
248
|
+
const milestoneIds: string[] = [];
|
|
249
|
+
for (const milestone of p.milestones) {
|
|
250
|
+
const milestoneId = db.genId();
|
|
251
|
+
milestoneIds.push(milestoneId);
|
|
252
|
+
|
|
253
|
+
db.exec(
|
|
254
|
+
`INSERT INTO opc_contract_milestones (id, contract_id, company_id, title, description, due_date, amount, status, created_at, updated_at)
|
|
255
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`,
|
|
256
|
+
milestoneId,
|
|
257
|
+
contractId,
|
|
258
|
+
quotation.company_id,
|
|
259
|
+
milestone.title,
|
|
260
|
+
milestone.description ?? "",
|
|
261
|
+
milestone.due_date,
|
|
262
|
+
milestone.amount,
|
|
263
|
+
now,
|
|
264
|
+
now,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// 4. 自动创建应收记录(关联里程碑)
|
|
268
|
+
const paymentId = db.genId();
|
|
269
|
+
db.exec(
|
|
270
|
+
`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)
|
|
271
|
+
VALUES (?, ?, 'receivable', '', ?, ?, 'pending', '', ?, ?, 'service', ?, ?, ?)`,
|
|
272
|
+
paymentId,
|
|
273
|
+
quotation.company_id,
|
|
274
|
+
milestone.amount,
|
|
275
|
+
milestone.due_date,
|
|
276
|
+
contractId,
|
|
277
|
+
milestoneId,
|
|
278
|
+
`${milestone.title} 应收款`,
|
|
279
|
+
now,
|
|
280
|
+
now,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 5. 更新报价单状态为已接受
|
|
285
|
+
db.exec(
|
|
286
|
+
"UPDATE opc_quotations SET status = 'accepted', updated_at = ? WHERE id = ?",
|
|
287
|
+
now,
|
|
288
|
+
p.quotation_id,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const contract = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", contractId);
|
|
292
|
+
const milestones = db.query("SELECT * FROM opc_contract_milestones WHERE contract_id = ?", contractId);
|
|
293
|
+
const payments = db.query("SELECT * FROM opc_payments WHERE contract_id = ?", contractId);
|
|
294
|
+
|
|
295
|
+
return json({
|
|
296
|
+
ok: true,
|
|
297
|
+
contract,
|
|
298
|
+
milestones,
|
|
299
|
+
payments,
|
|
300
|
+
message: `✅ 报价转合同成功!已创建 ${p.milestones.length} 个里程碑和应收记录`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ═══════════════════════════════════════════════════════
|
|
305
|
+
// 5. 确认签约
|
|
306
|
+
// ═══════════════════════════════════════════════════════
|
|
307
|
+
case "confirm_signing": {
|
|
308
|
+
const now = new Date().toISOString();
|
|
309
|
+
|
|
310
|
+
// 更新合同状态和签约日期
|
|
311
|
+
db.exec(
|
|
312
|
+
"UPDATE opc_contracts SET status = 'active', signed_date = ?, updated_at = ? WHERE id = ?",
|
|
313
|
+
p.signed_date,
|
|
314
|
+
now,
|
|
315
|
+
p.contract_id,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// 获取第一个里程碑,标记为进行中
|
|
319
|
+
const firstMilestone = db.queryOne(
|
|
320
|
+
"SELECT * FROM opc_contract_milestones WHERE contract_id = ? ORDER BY due_date ASC LIMIT 1",
|
|
321
|
+
p.contract_id,
|
|
322
|
+
) as { id: string } | null;
|
|
323
|
+
|
|
324
|
+
if (firstMilestone) {
|
|
325
|
+
db.exec(
|
|
326
|
+
"UPDATE opc_contract_milestones SET status = 'in_progress', updated_at = ? WHERE id = ?",
|
|
327
|
+
now,
|
|
328
|
+
firstMilestone.id,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const contract = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", p.contract_id);
|
|
333
|
+
const milestones = db.query("SELECT * FROM opc_contract_milestones WHERE contract_id = ?", p.contract_id);
|
|
334
|
+
|
|
335
|
+
return json({
|
|
336
|
+
ok: true,
|
|
337
|
+
contract,
|
|
338
|
+
milestones,
|
|
339
|
+
message: `✅ 合同已签约!第一个里程碑已启动`,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ═══════════════════════════════════════════════════════
|
|
344
|
+
// 6. 完成里程碑
|
|
345
|
+
// ═══════════════════════════════════════════════════════
|
|
346
|
+
case "complete_milestone": {
|
|
347
|
+
const now = new Date().toISOString();
|
|
348
|
+
const completedDate = p.completed_date ?? now.slice(0, 10);
|
|
349
|
+
|
|
350
|
+
// 更新里程碑状态
|
|
351
|
+
db.exec(
|
|
352
|
+
"UPDATE opc_contract_milestones SET status = 'completed', completed_date = ?, notes = ?, updated_at = ? WHERE id = ?",
|
|
353
|
+
completedDate,
|
|
354
|
+
p.notes ?? "",
|
|
355
|
+
now,
|
|
356
|
+
p.milestone_id,
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const milestone = db.queryOne("SELECT * FROM opc_contract_milestones WHERE id = ?", p.milestone_id) as {
|
|
360
|
+
contract_id: string;
|
|
361
|
+
title: string;
|
|
362
|
+
} | null;
|
|
363
|
+
|
|
364
|
+
if (!milestone) {
|
|
365
|
+
return toolError("MILESTONE_NOT_FOUND", `里程碑 ${p.milestone_id} 不存在`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 获取下一个里程碑,标记为进行中
|
|
369
|
+
const nextMilestone = db.queryOne(
|
|
370
|
+
"SELECT * FROM opc_contract_milestones WHERE contract_id = ? AND status = 'pending' ORDER BY due_date ASC LIMIT 1",
|
|
371
|
+
milestone.contract_id,
|
|
372
|
+
) as { id: string; title: string } | null;
|
|
373
|
+
|
|
374
|
+
if (nextMilestone) {
|
|
375
|
+
db.exec(
|
|
376
|
+
"UPDATE opc_contract_milestones SET status = 'in_progress', updated_at = ? WHERE id = ?",
|
|
377
|
+
now,
|
|
378
|
+
nextMilestone.id,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return json({
|
|
383
|
+
ok: true,
|
|
384
|
+
milestone: db.queryOne("SELECT * FROM opc_contract_milestones WHERE id = ?", p.milestone_id),
|
|
385
|
+
next_milestone: nextMilestone,
|
|
386
|
+
message: `✅ 里程碑「${milestone.title}」已完成${nextMilestone ? `,下一个里程碑「${nextMilestone.title}」已启动` : ""}`,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ═══════════════════════════════════════════════════════
|
|
391
|
+
// 7. 查看进行中的订单
|
|
392
|
+
// ═══════════════════════════════════════════════════════
|
|
393
|
+
case "list_active_orders": {
|
|
394
|
+
const contracts = db.query(
|
|
395
|
+
"SELECT * FROM opc_contracts WHERE company_id = ? AND status = 'active' ORDER BY signed_date DESC",
|
|
396
|
+
p.company_id,
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const orders = contracts.map((contract) => {
|
|
400
|
+
const milestones = db.query(
|
|
401
|
+
"SELECT * FROM opc_contract_milestones WHERE contract_id = ? ORDER BY due_date ASC",
|
|
402
|
+
(contract as { id: string }).id,
|
|
403
|
+
);
|
|
404
|
+
return { ...contract, milestones };
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return json({ ok: true, orders, count: orders.length });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ═══════════════════════════════════════════════════════
|
|
411
|
+
// 8. 里程碑到期提醒
|
|
412
|
+
// ═══════════════════════════════════════════════════════
|
|
413
|
+
case "milestone_reminder": {
|
|
414
|
+
const daysAhead = p.days_ahead ?? 7;
|
|
415
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
416
|
+
const futureDate = new Date(Date.now() + daysAhead * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
417
|
+
|
|
418
|
+
const milestones = db.query(
|
|
419
|
+
`SELECT m.*, c.title as contract_title
|
|
420
|
+
FROM opc_contract_milestones m
|
|
421
|
+
JOIN opc_contracts c ON m.contract_id = c.id
|
|
422
|
+
WHERE m.company_id = ?
|
|
423
|
+
AND m.status IN ('pending', 'in_progress')
|
|
424
|
+
AND m.due_date BETWEEN ? AND ?
|
|
425
|
+
ORDER BY m.due_date ASC`,
|
|
426
|
+
p.company_id,
|
|
427
|
+
today,
|
|
428
|
+
futureDate,
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
return json({
|
|
432
|
+
ok: true,
|
|
433
|
+
milestones,
|
|
434
|
+
count: milestones.length,
|
|
435
|
+
message: `未来 ${daysAhead} 天内有 ${milestones.length} 个里程碑到期`,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ═══════════════════════════════════════════════════════
|
|
440
|
+
// 9. 获取合同详情
|
|
441
|
+
// ═══════════════════════════════════════════════════════
|
|
442
|
+
case "get_contract_details": {
|
|
443
|
+
const contract = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", p.contract_id);
|
|
444
|
+
if (!contract) {
|
|
445
|
+
return toolError("CONTRACT_NOT_FOUND", `合同 ${p.contract_id} 不存在`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const milestones = db.query(
|
|
449
|
+
"SELECT * FROM opc_contract_milestones WHERE contract_id = ? ORDER BY due_date ASC",
|
|
450
|
+
p.contract_id,
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const payments = db.query(
|
|
454
|
+
"SELECT * FROM opc_payments WHERE contract_id = ? ORDER BY due_date ASC",
|
|
455
|
+
p.contract_id,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const quotation = (contract as { quotation_id: string }).quotation_id
|
|
459
|
+
? db.queryOne("SELECT * FROM opc_quotations WHERE id = ?", (contract as { quotation_id: string }).quotation_id)
|
|
460
|
+
: null;
|
|
461
|
+
|
|
462
|
+
return json({
|
|
463
|
+
ok: true,
|
|
464
|
+
contract,
|
|
465
|
+
milestones,
|
|
466
|
+
payments,
|
|
467
|
+
quotation,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
default:
|
|
472
|
+
return toolError("INVALID_ACTION", `未知操作: ${(p as { action: string }).action}`);
|
|
473
|
+
}
|
|
474
|
+
} catch (err) {
|
|
475
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
476
|
+
return toolError("EXECUTION_ERROR", `订单工具执行失败: ${message}`);
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
);
|
|
481
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — 智能记账工具
|
|
3
|
+
* 支持自然语言记账和智能分类
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
7
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
8
|
+
import type { OpcDatabase } from "../db/index.js";
|
|
9
|
+
import { json, toolError } from "../utils/tool-helper.js";
|
|
10
|
+
import { parseAccountingIntent, suggestCategory } from "../opc/accounting-parser.js";
|
|
11
|
+
|
|
12
|
+
const SmartAccountingSchema = Type.Union([
|
|
13
|
+
Type.Object({
|
|
14
|
+
action: Type.Literal("natural_language_record"),
|
|
15
|
+
company_id: Type.String({ description: "公司 ID" }),
|
|
16
|
+
input: Type.String({ description: "自然语言记账输入,如:今天请客户吃饭花了 500 块" }),
|
|
17
|
+
confirm: Type.Optional(Type.Boolean({ description: "是否确认记账(默认 false,先预览)" })),
|
|
18
|
+
}),
|
|
19
|
+
Type.Object({
|
|
20
|
+
action: Type.Literal("suggest_category"),
|
|
21
|
+
description: Type.String({ description: "交易描述" }),
|
|
22
|
+
}),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
type SmartAccountingParams = Static<typeof SmartAccountingSchema>;
|
|
26
|
+
|
|
27
|
+
export function registerSmartAccountingTool(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
28
|
+
api.registerTool(
|
|
29
|
+
{
|
|
30
|
+
name: "opc_smart_accounting",
|
|
31
|
+
label: "OPC 智能记账",
|
|
32
|
+
description:
|
|
33
|
+
"智能记账工具,支持自然语言记账和智能分类。操作: " +
|
|
34
|
+
"natural_language_record(自然语言记账), suggest_category(建议分类)",
|
|
35
|
+
parameters: SmartAccountingSchema,
|
|
36
|
+
async execute(_toolCallId, params) {
|
|
37
|
+
const p = params as SmartAccountingParams;
|
|
38
|
+
try {
|
|
39
|
+
switch (p.action) {
|
|
40
|
+
case "natural_language_record": {
|
|
41
|
+
// 使用 LLM 解析自然语言输入
|
|
42
|
+
const intent = await parseAccountingIntent(p.input, api);
|
|
43
|
+
|
|
44
|
+
if (intent.confidence < 50) {
|
|
45
|
+
return toolError(
|
|
46
|
+
`解析置信度过低(${intent.confidence}%),请提供更清晰的描述。\n` +
|
|
47
|
+
`解析结果:${intent.description}`,
|
|
48
|
+
"LOW_CONFIDENCE",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 如果是预览模式(默认)
|
|
53
|
+
if (!p.confirm) {
|
|
54
|
+
return json({
|
|
55
|
+
ok: true,
|
|
56
|
+
preview: true,
|
|
57
|
+
parsed_intent: intent,
|
|
58
|
+
message: "请确认以下解析结果是否正确。如果正确,请使用 confirm=true 参数确认记账。",
|
|
59
|
+
suggestions: [
|
|
60
|
+
intent.tax_note ? `💡 税务提示:${intent.tax_note}` : null,
|
|
61
|
+
intent.is_deductible ? "✅ 此笔支出可抵税,记得保存发票" : null,
|
|
62
|
+
intent.amount > 2000 && intent.category === "fixed_asset" ? "📊 大额固定资产购置,可按年折旧" : null,
|
|
63
|
+
].filter(Boolean),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 确认模式:创建交易记录
|
|
68
|
+
const transactionId = db.genId();
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
|
|
71
|
+
db.execute(
|
|
72
|
+
`INSERT INTO opc_transactions (id, company_id, type, category, amount, description, counterparty, transaction_date, created_at)
|
|
73
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
74
|
+
transactionId,
|
|
75
|
+
p.company_id,
|
|
76
|
+
intent.type,
|
|
77
|
+
intent.category,
|
|
78
|
+
intent.amount,
|
|
79
|
+
intent.description,
|
|
80
|
+
intent.counterparty ?? "",
|
|
81
|
+
intent.date,
|
|
82
|
+
now,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const transaction = db.queryOne("SELECT * FROM opc_transactions WHERE id = ?", transactionId);
|
|
86
|
+
|
|
87
|
+
// 生成记账成功提示
|
|
88
|
+
const successMessages = [
|
|
89
|
+
`✅ 记账成功!${intent.type === "income" ? "收入" : "支出"} ${intent.amount} 元`,
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
if (intent.tax_note) {
|
|
93
|
+
successMessages.push(`💡 ${intent.tax_note}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (intent.is_deductible && intent.type === "expense") {
|
|
97
|
+
successMessages.push("✅ 此笔支出可抵税,记得保存发票用于年度汇算清缴");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 检查月度预算(如果支出)
|
|
101
|
+
if (intent.type === "expense") {
|
|
102
|
+
const currentMonth = intent.date.slice(0, 7);
|
|
103
|
+
const monthlyExpense = db.queryOne(
|
|
104
|
+
`SELECT COALESCE(SUM(amount), 0) as total
|
|
105
|
+
FROM opc_transactions
|
|
106
|
+
WHERE company_id = ? AND type = 'expense' AND transaction_date LIKE ?`,
|
|
107
|
+
p.company_id, currentMonth + "%",
|
|
108
|
+
) as { total: number };
|
|
109
|
+
|
|
110
|
+
const totalExpense = monthlyExpense?.total ?? 0;
|
|
111
|
+
if (totalExpense > 10000) {
|
|
112
|
+
successMessages.push(`⚠️ 本月支出已达 ${totalExpense.toLocaleString()} 元`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return json({
|
|
117
|
+
ok: true,
|
|
118
|
+
transaction,
|
|
119
|
+
intent,
|
|
120
|
+
messages: successMessages,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case "suggest_category": {
|
|
125
|
+
const suggestion = await suggestCategory(p.description, api);
|
|
126
|
+
return json({
|
|
127
|
+
ok: true,
|
|
128
|
+
suggestion,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
default:
|
|
133
|
+
return toolError(`未知操作: ${(p as { action: string }).action}`, "UNKNOWN_ACTION");
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return toolError(err instanceof Error ? err.message : String(err), "EXECUTION_ERROR");
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{ name: "opc_smart_accounting" },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
api.logger.info("opc: 已注册 opc_smart_accounting 工具");
|
|
144
|
+
}
|