galaxy-opc-plugin 0.2.1 → 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 +350 -8
- package/openclaw.plugin.json +1 -1
- package/package.json +17 -3
- package/src/__tests__/e2e/company-lifecycle.test.ts +399 -0
- package/src/__tests__/integration/business-workflows.test.ts +366 -0
- package/src/__tests__/test-utils.ts +316 -0
- package/src/api/companies.ts +4 -0
- package/src/api/dashboard.ts +368 -16
- package/src/api/routes.ts +2 -2
- package/src/commands/opc-command.ts +422 -0
- package/src/db/index.ts +3 -0
- package/src/db/migrations.test.ts +324 -0
- package/src/db/migrations.ts +277 -0
- package/src/db/schema.ts +312 -0
- package/src/db/sqlite-adapter.ts +44 -2
- package/src/opc/accounting-parser.ts +178 -0
- package/src/opc/autonomy-rules.ts +132 -0
- package/src/opc/briefing-builder.ts +1331 -0
- package/src/opc/business-workflows.test.ts +535 -0
- package/src/opc/business-workflows.ts +325 -0
- package/src/opc/context-injector.ts +366 -28
- package/src/opc/daily-brief.ts +529 -0
- package/src/opc/event-triggers.ts +472 -0
- package/src/opc/intelligence-engine.ts +702 -0
- package/src/opc/milestone-detector.ts +251 -0
- package/src/opc/onboarding-flow.ts +332 -0
- package/src/opc/proactive-service.ts +466 -0
- package/src/opc/reminder-service.ts +4 -43
- package/src/opc/session-task-tracker.ts +60 -0
- package/src/opc/stage-detector.ts +168 -0
- package/src/opc/task-executor.ts +332 -0
- package/src/opc/task-templates.ts +179 -0
- package/src/tools/document-tool.ts +1176 -0
- package/src/tools/finance-tool.test-payment.ts +326 -0
- package/src/tools/finance-tool.test.ts +238 -0
- package/src/tools/finance-tool.ts +1574 -14
- package/src/tools/hr-tool.ts +10 -1
- package/src/tools/legal-tool.test.ts +251 -0
- package/src/tools/legal-tool.ts +26 -4
- package/src/tools/lifecycle-tool.test.ts +231 -0
- package/src/tools/media-tool.ts +156 -1
- package/src/tools/monitoring-tool.ts +134 -1
- package/src/tools/onboarding-tool.ts +233 -0
- package/src/tools/opc-tool.test.ts +250 -0
- package/src/tools/opc-tool.ts +251 -28
- package/src/tools/order-tool.ts +481 -0
- package/src/tools/project-tool.test.ts +218 -0
- package/src/tools/schemas.ts +80 -0
- package/src/tools/search-tool.ts +227 -0
- package/src/tools/smart-accounting-tool.ts +144 -0
- package/src/tools/staff-tool.ts +395 -2
- 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 -3555
- package/src/web/dashboard-ui.ts +582 -0
- package/src/web/landing-page.ts +56 -6
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — opc-tool (核心管理工具) 集成测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
6
|
+
import { createTestDb, factories } from "../__tests__/test-utils.js";
|
|
7
|
+
import { SqliteAdapter } from "../db/sqlite-adapter.js";
|
|
8
|
+
|
|
9
|
+
describe("opc-tool database integration", () => {
|
|
10
|
+
let db: SqliteAdapter;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
db = createTestDb();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
db.close();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("create_company", () => {
|
|
21
|
+
it("should create a company successfully", () => {
|
|
22
|
+
const companyData = factories.company({
|
|
23
|
+
name: "创业公司A",
|
|
24
|
+
industry: "互联网",
|
|
25
|
+
owner_name: "张三",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const company = db.createCompany(companyData);
|
|
29
|
+
|
|
30
|
+
expect(company).not.toBeNull();
|
|
31
|
+
expect(company.id).toBeDefined();
|
|
32
|
+
expect(company.name).toBe("创业公司A");
|
|
33
|
+
expect(company.industry).toBe("互联网");
|
|
34
|
+
expect(company.owner_name).toBe("张三");
|
|
35
|
+
expect(company.status).toBe("active");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should set default values", () => {
|
|
39
|
+
const companyData = factories.company({
|
|
40
|
+
name: "最小配置公司",
|
|
41
|
+
industry: "咨询",
|
|
42
|
+
owner_name: "李四",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const company = db.createCompany(companyData);
|
|
46
|
+
|
|
47
|
+
expect(company.registered_capital).toBeDefined();
|
|
48
|
+
expect(company.created_at).toBeDefined();
|
|
49
|
+
expect(company.updated_at).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should create multiple companies", () => {
|
|
53
|
+
const company1 = db.createCompany(factories.company({ name: "公司1" }));
|
|
54
|
+
const company2 = db.createCompany(factories.company({ name: "公司2" }));
|
|
55
|
+
|
|
56
|
+
expect(company1.id).not.toBe(company2.id);
|
|
57
|
+
expect(company1.name).toBe("公司1");
|
|
58
|
+
expect(company2.name).toBe("公司2");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should handle Chinese characters in company name", () => {
|
|
62
|
+
const company = db.createCompany(factories.company({
|
|
63
|
+
name: "星河科技有限公司",
|
|
64
|
+
description: "专注于AI技术研发",
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
expect(company.name).toBe("星河科技有限公司");
|
|
68
|
+
expect(company.description).toBe("专注于AI技术研发");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("get_company", () => {
|
|
73
|
+
it("should retrieve a company by id", () => {
|
|
74
|
+
const created = db.createCompany(factories.company({ name: "测试公司" }));
|
|
75
|
+
const retrieved = db.getCompany(created.id);
|
|
76
|
+
|
|
77
|
+
expect(retrieved).not.toBeNull();
|
|
78
|
+
expect(retrieved!.id).toBe(created.id);
|
|
79
|
+
expect(retrieved!.name).toBe("测试公司");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should return null for non-existent company", () => {
|
|
83
|
+
const result = db.getCompany("non-existent-id");
|
|
84
|
+
expect(result).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("list_companies", () => {
|
|
89
|
+
it("should list all companies", () => {
|
|
90
|
+
db.createCompany(factories.company({ name: "公司1" }));
|
|
91
|
+
db.createCompany(factories.company({ name: "公司2" }));
|
|
92
|
+
db.createCompany(factories.company({ name: "公司3" }));
|
|
93
|
+
|
|
94
|
+
const companies = db.listCompanies();
|
|
95
|
+
expect(companies.length).toBe(3);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should filter companies by status", () => {
|
|
99
|
+
db.createCompany(factories.company({ name: "活跃公司", status: "active" }));
|
|
100
|
+
db.createCompany(factories.company({ name: "暂停公司", status: "suspended" }));
|
|
101
|
+
db.createCompany(factories.company({ name: "已收购公司", status: "acquired" }));
|
|
102
|
+
|
|
103
|
+
const activeCompanies = db.listCompanies("active");
|
|
104
|
+
expect(activeCompanies.length).toBe(1);
|
|
105
|
+
expect(activeCompanies[0].name).toBe("活跃公司");
|
|
106
|
+
|
|
107
|
+
const suspendedCompanies = db.listCompanies("suspended");
|
|
108
|
+
expect(suspendedCompanies.length).toBe(1);
|
|
109
|
+
expect(suspendedCompanies[0].name).toBe("暂停公司");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should return empty array when no companies exist", () => {
|
|
113
|
+
const companies = db.listCompanies();
|
|
114
|
+
expect(companies).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("update_company", () => {
|
|
119
|
+
it("should update company status", () => {
|
|
120
|
+
const company = db.createCompany(factories.company({ status: "active" }));
|
|
121
|
+
|
|
122
|
+
db.execute(
|
|
123
|
+
"UPDATE opc_companies SET status = ?, updated_at = ? WHERE id = ?",
|
|
124
|
+
"suspended", new Date().toISOString(), company.id
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const updated = db.getCompany(company.id);
|
|
128
|
+
expect(updated!.status).toBe("suspended");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should update company description", () => {
|
|
132
|
+
const company = db.createCompany(factories.company({ description: "原描述" }));
|
|
133
|
+
|
|
134
|
+
const newDescription = "更新后的描述";
|
|
135
|
+
db.execute(
|
|
136
|
+
"UPDATE opc_companies SET description = ?, updated_at = ? WHERE id = ?",
|
|
137
|
+
newDescription, new Date().toISOString(), company.id
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const updated = db.getCompany(company.id);
|
|
141
|
+
expect(updated!.description).toBe(newDescription);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should update registered capital", () => {
|
|
145
|
+
const company = db.createCompany(factories.company({ registered_capital: 100000 }));
|
|
146
|
+
|
|
147
|
+
db.execute(
|
|
148
|
+
"UPDATE opc_companies SET registered_capital = ?, updated_at = ? WHERE id = ?",
|
|
149
|
+
500000, new Date().toISOString(), company.id
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const updated = db.getCompany(company.id);
|
|
153
|
+
expect(updated!.registered_capital).toBe(500000);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("delete_company", () => {
|
|
158
|
+
it("should delete a company", () => {
|
|
159
|
+
const company = db.createCompany(factories.company({ name: "待删除公司" }));
|
|
160
|
+
|
|
161
|
+
db.execute("DELETE FROM opc_companies WHERE id = ?", company.id);
|
|
162
|
+
|
|
163
|
+
const deleted = db.getCompany(company.id);
|
|
164
|
+
expect(deleted).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should prevent deleting company with related data (foreign key constraint)", () => {
|
|
168
|
+
const company = db.createCompany(factories.company());
|
|
169
|
+
|
|
170
|
+
// Add related transaction
|
|
171
|
+
const txId = db.genId();
|
|
172
|
+
db.execute(
|
|
173
|
+
`INSERT INTO opc_transactions (id, company_id, type, category, amount, description, counterparty, transaction_date, created_at)
|
|
174
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
175
|
+
txId, company.id, "income", "service_income", 10000, "测试", "客户", "2026-01-15"
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Try to delete company - should fail due to foreign key constraint
|
|
179
|
+
expect(() => {
|
|
180
|
+
db.execute("DELETE FROM opc_companies WHERE id = ?", company.id);
|
|
181
|
+
}).toThrow();
|
|
182
|
+
|
|
183
|
+
// Proper way: delete related data first, then delete company
|
|
184
|
+
db.execute("DELETE FROM opc_transactions WHERE company_id = ?", company.id);
|
|
185
|
+
db.execute("DELETE FROM opc_companies WHERE id = ?", company.id);
|
|
186
|
+
|
|
187
|
+
const deleted = db.getCompany(company.id);
|
|
188
|
+
expect(deleted).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("company search and filtering", () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
db.createCompany(factories.company({ name: "科技公司A", industry: "科技" }));
|
|
195
|
+
db.createCompany(factories.company({ name: "咨询公司B", industry: "咨询" }));
|
|
196
|
+
db.createCompany(factories.company({ name: "科技公司C", industry: "科技" }));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should filter by industry", () => {
|
|
200
|
+
const techCompanies = db.query(
|
|
201
|
+
"SELECT * FROM opc_companies WHERE industry = ?",
|
|
202
|
+
"科技"
|
|
203
|
+
) as any[];
|
|
204
|
+
expect(techCompanies.length).toBe(2);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should search by name pattern", () => {
|
|
208
|
+
const companies = db.query(
|
|
209
|
+
"SELECT * FROM opc_companies WHERE name LIKE ?",
|
|
210
|
+
"%科技%"
|
|
211
|
+
) as any[];
|
|
212
|
+
expect(companies.length).toBe(2);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("company statistics", () => {
|
|
217
|
+
it("should count companies by status", () => {
|
|
218
|
+
db.createCompany(factories.company({ status: "active" }));
|
|
219
|
+
db.createCompany(factories.company({ status: "active" }));
|
|
220
|
+
db.createCompany(factories.company({ status: "suspended" }));
|
|
221
|
+
db.createCompany(factories.company({ status: "acquired" }));
|
|
222
|
+
|
|
223
|
+
const stats = db.queryOne(
|
|
224
|
+
`SELECT
|
|
225
|
+
COUNT(*) as total,
|
|
226
|
+
COUNT(CASE WHEN status = 'active' THEN 1 END) as active,
|
|
227
|
+
COUNT(CASE WHEN status = 'suspended' THEN 1 END) as suspended,
|
|
228
|
+
COUNT(CASE WHEN status = 'acquired' THEN 1 END) as acquired
|
|
229
|
+
FROM opc_companies`
|
|
230
|
+
) as any;
|
|
231
|
+
|
|
232
|
+
expect(stats.total).toBe(4);
|
|
233
|
+
expect(stats.active).toBe(2);
|
|
234
|
+
expect(stats.suspended).toBe(1);
|
|
235
|
+
expect(stats.acquired).toBe(1);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should calculate total registered capital", () => {
|
|
239
|
+
db.createCompany(factories.company({ registered_capital: 100000 }));
|
|
240
|
+
db.createCompany(factories.company({ registered_capital: 200000 }));
|
|
241
|
+
db.createCompany(factories.company({ registered_capital: 300000 }));
|
|
242
|
+
|
|
243
|
+
const result = db.queryOne(
|
|
244
|
+
"SELECT SUM(registered_capital) as total_capital FROM opc_companies"
|
|
245
|
+
) as any;
|
|
246
|
+
|
|
247
|
+
expect(result.total_capital).toBe(600000);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
package/src/tools/opc-tool.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
8
8
|
import type { OpcDatabase } from "../db/index.js";
|
|
9
|
+
import { BusinessWorkflows } from "../opc/business-workflows.js";
|
|
9
10
|
import { CompanyManager } from "../opc/company-manager.js";
|
|
10
11
|
import type { OpcCompanyStatus, OpcTransactionCategory, OpcTransactionType } from "../opc/types.js";
|
|
11
12
|
import { ensureCompanyWorkspace } from "../opc/workspace-factory.js";
|
|
@@ -42,9 +43,15 @@ export function registerOpcTool(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
|
42
43
|
"操作: register_company(注册公司), get_company(查询公司), list_companies(公司列表),",
|
|
43
44
|
"update_company(更新公司), activate_company(激活公司), change_company_status(变更状态),",
|
|
44
45
|
"add_transaction(记账), list_transactions(交易列表), finance_summary(财务摘要),",
|
|
45
|
-
"add_contact(
|
|
46
|
+
"add_contact(添加客户,支持CRM字段pipeline_stage/follow_up_date/deal_value/source),",
|
|
47
|
+
"list_contacts(客户列表), update_contact(更新客户,支持CRM字段),",
|
|
46
48
|
"delete_contact(删除客户), dashboard(看板统计),",
|
|
47
|
-
"set_company_skills(设置公司Agent skills), get_company_skills(查询公司Agent skills)",
|
|
49
|
+
"set_company_skills(设置公司Agent skills), get_company_skills(查询公司Agent skills),",
|
|
50
|
+
"batch_import_contacts(批量导入联系人),",
|
|
51
|
+
"crm_pipeline(销售漏斗), add_interaction(添加客户交互记录),",
|
|
52
|
+
"list_interactions(交互历史), follow_up_reminders(跟进提醒),",
|
|
53
|
+
"setup_feishu_channel(配置飞书频道), feishu_channel_status(查询飞书状态),",
|
|
54
|
+
"switch_company(切换到指定公司的专属Agent)",
|
|
48
55
|
].join(" "),
|
|
49
56
|
parameters: OpcManageSchema,
|
|
50
57
|
async execute(_toolCallId, params) {
|
|
@@ -113,18 +120,25 @@ export function registerOpcTool(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
// ── 交易记录 ──
|
|
116
|
-
case "add_transaction":
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
);
|
|
123
|
+
case "add_transaction": {
|
|
124
|
+
const tx = db.createTransaction({
|
|
125
|
+
company_id: p.company_id,
|
|
126
|
+
type: p.type as OpcTransactionType,
|
|
127
|
+
category: (p.category ?? "other") as OpcTransactionCategory,
|
|
128
|
+
amount: p.amount,
|
|
129
|
+
description: p.description ?? "",
|
|
130
|
+
counterparty: p.counterparty ?? "",
|
|
131
|
+
transaction_date: p.transaction_date ?? new Date().toISOString().slice(0, 10),
|
|
132
|
+
});
|
|
133
|
+
// 业务闭环:收入自动开票、大额记里程碑
|
|
134
|
+
const workflows = new BusinessWorkflows(db);
|
|
135
|
+
const autoCreated = workflows.afterTransactionCreated({
|
|
136
|
+
id: tx.id, company_id: p.company_id, type: p.type,
|
|
137
|
+
amount: p.amount, counterparty: p.counterparty ?? "",
|
|
138
|
+
description: p.description ?? "",
|
|
139
|
+
});
|
|
140
|
+
return json({ ...tx as object, _auto_created: autoCreated });
|
|
141
|
+
}
|
|
128
142
|
|
|
129
143
|
case "list_transactions":
|
|
130
144
|
return json(
|
|
@@ -142,19 +156,29 @@ export function registerOpcTool(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
|
142
156
|
);
|
|
143
157
|
|
|
144
158
|
// ── 客户管理 ──
|
|
145
|
-
case "add_contact":
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
159
|
+
case "add_contact": {
|
|
160
|
+
const contact = db.createContact({
|
|
161
|
+
company_id: p.company_id,
|
|
162
|
+
name: p.name,
|
|
163
|
+
phone: p.phone ?? "",
|
|
164
|
+
email: p.email ?? "",
|
|
165
|
+
company_name: p.company_name ?? "",
|
|
166
|
+
tags: p.tags ?? "[]",
|
|
167
|
+
notes: p.notes ?? "",
|
|
168
|
+
last_contact_date: new Date().toISOString().slice(0, 10),
|
|
169
|
+
});
|
|
170
|
+
// Update CRM fields if provided
|
|
171
|
+
const crmFields: Record<string, unknown> = {};
|
|
172
|
+
if ((p as Record<string, unknown>).pipeline_stage) crmFields.pipeline_stage = (p as Record<string, unknown>).pipeline_stage;
|
|
173
|
+
if ((p as Record<string, unknown>).follow_up_date) crmFields.follow_up_date = (p as Record<string, unknown>).follow_up_date;
|
|
174
|
+
if ((p as Record<string, unknown>).deal_value !== undefined) crmFields.deal_value = (p as Record<string, unknown>).deal_value;
|
|
175
|
+
if ((p as Record<string, unknown>).source) crmFields.source = (p as Record<string, unknown>).source;
|
|
176
|
+
if (Object.keys(crmFields).length > 0) {
|
|
177
|
+
const sets = Object.keys(crmFields).map((k) => `${k} = ?`).join(", ");
|
|
178
|
+
db.execute(`UPDATE opc_contacts SET ${sets} WHERE id = ?`, ...Object.values(crmFields), contact.id);
|
|
179
|
+
}
|
|
180
|
+
return json(db.queryOne("SELECT * FROM opc_contacts WHERE id = ?", contact.id));
|
|
181
|
+
}
|
|
158
182
|
|
|
159
183
|
case "list_contacts":
|
|
160
184
|
return json(db.listContacts(p.company_id, p.tag));
|
|
@@ -170,7 +194,18 @@ export function registerOpcTool(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
|
170
194
|
if (p.last_contact_date) updateData.last_contact_date = p.last_contact_date;
|
|
171
195
|
const updatedContact = db.updateContact(p.contact_id, updateData);
|
|
172
196
|
if (!updatedContact) return toolError(`联系人 ${p.contact_id} 不存在`, "CONTACT_NOT_FOUND");
|
|
173
|
-
|
|
197
|
+
// Update CRM fields if provided
|
|
198
|
+
const pp = p as Record<string, unknown>;
|
|
199
|
+
const crmUpd: Record<string, unknown> = {};
|
|
200
|
+
if (pp.pipeline_stage) crmUpd.pipeline_stage = pp.pipeline_stage;
|
|
201
|
+
if (pp.follow_up_date) crmUpd.follow_up_date = pp.follow_up_date;
|
|
202
|
+
if (pp.deal_value !== undefined) crmUpd.deal_value = pp.deal_value;
|
|
203
|
+
if (pp.source) crmUpd.source = pp.source;
|
|
204
|
+
if (Object.keys(crmUpd).length > 0) {
|
|
205
|
+
const sets = Object.keys(crmUpd).map((k) => `${k} = ?`).join(", ");
|
|
206
|
+
db.execute(`UPDATE opc_contacts SET ${sets}, updated_at = ? WHERE id = ?`, ...Object.values(crmUpd), new Date().toISOString(), p.contact_id);
|
|
207
|
+
}
|
|
208
|
+
return json(db.queryOne("SELECT * FROM opc_contacts WHERE id = ?", p.contact_id));
|
|
174
209
|
}
|
|
175
210
|
|
|
176
211
|
case "delete_contact":
|
|
@@ -201,6 +236,194 @@ export function registerOpcTool(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
|
201
236
|
return json({ company_id: resolvedId, skills });
|
|
202
237
|
}
|
|
203
238
|
|
|
239
|
+
// ── 批量导入联系人 ──
|
|
240
|
+
case "batch_import_contacts": {
|
|
241
|
+
const records: unknown[] = [];
|
|
242
|
+
db.transaction(() => {
|
|
243
|
+
const now = new Date().toISOString();
|
|
244
|
+
const today = now.slice(0, 10);
|
|
245
|
+
for (const c of p.contacts) {
|
|
246
|
+
const id = db.genId();
|
|
247
|
+
db.execute(
|
|
248
|
+
`INSERT OR IGNORE INTO opc_contacts (id, company_id, name, phone, email, company_name, tags, notes, last_contact_date, created_at, updated_at)
|
|
249
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, '', ?, ?, ?)`,
|
|
250
|
+
id, p.company_id, c.name, c.phone ?? "", c.email ?? "",
|
|
251
|
+
c.company_name ?? "", c.tags ?? "[]", today, now, now,
|
|
252
|
+
);
|
|
253
|
+
records.push({ id, name: c.name });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
return json({ ok: true, imported_count: records.length, records });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── CRM 销售漏斗 ──
|
|
260
|
+
case "crm_pipeline": {
|
|
261
|
+
const stages = ["lead", "qualified", "proposal", "negotiation", "won", "lost", "churned"];
|
|
262
|
+
const pipeline: Record<string, { count: number; total_deal_value: number; contacts: unknown[] }> = {};
|
|
263
|
+
for (const stage of stages) {
|
|
264
|
+
const contacts = db.query(
|
|
265
|
+
`SELECT id, name, company_name, deal_value, follow_up_date, source
|
|
266
|
+
FROM opc_contacts WHERE company_id = ? AND pipeline_stage = ?
|
|
267
|
+
ORDER BY deal_value DESC`,
|
|
268
|
+
p.company_id, stage,
|
|
269
|
+
) as unknown[];
|
|
270
|
+
const totalValue = (contacts as { deal_value: number }[]).reduce((s, c) => s + (c.deal_value || 0), 0);
|
|
271
|
+
pipeline[stage] = { count: contacts.length, total_deal_value: totalValue, contacts };
|
|
272
|
+
}
|
|
273
|
+
return json({ ok: true, pipeline });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── 添加客户交互记录 ──
|
|
277
|
+
case "add_interaction": {
|
|
278
|
+
const id = db.genId();
|
|
279
|
+
const now = new Date().toISOString();
|
|
280
|
+
db.execute(
|
|
281
|
+
`INSERT INTO opc_contact_interactions (id, contact_id, company_id, interaction_type, content, created_at)
|
|
282
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
283
|
+
id, p.contact_id, p.company_id, p.interaction_type, p.content, now,
|
|
284
|
+
);
|
|
285
|
+
// 自动更新联系人的 last_contact_date
|
|
286
|
+
db.execute(
|
|
287
|
+
"UPDATE opc_contacts SET last_contact_date = ?, updated_at = ? WHERE id = ?",
|
|
288
|
+
now.slice(0, 10), now, p.contact_id,
|
|
289
|
+
);
|
|
290
|
+
return json(db.queryOne("SELECT * FROM opc_contact_interactions WHERE id = ?", id));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── 查看交互历史 ──
|
|
294
|
+
case "list_interactions": {
|
|
295
|
+
const limit = (p as Record<string, unknown>).limit ?? 20;
|
|
296
|
+
const interactions = db.query(
|
|
297
|
+
`SELECT i.*, c.name as contact_name FROM opc_contact_interactions i
|
|
298
|
+
LEFT JOIN opc_contacts c ON i.contact_id = c.id
|
|
299
|
+
WHERE i.contact_id = ? ORDER BY i.created_at DESC LIMIT ?`,
|
|
300
|
+
p.contact_id, limit,
|
|
301
|
+
);
|
|
302
|
+
return json(interactions);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── 跟进提醒 ──
|
|
306
|
+
case "follow_up_reminders": {
|
|
307
|
+
const days = (p as Record<string, unknown>).days ?? 7;
|
|
308
|
+
const futureDate = new Date();
|
|
309
|
+
futureDate.setDate(futureDate.getDate() + (days as number));
|
|
310
|
+
const futureDateStr = futureDate.toISOString().slice(0, 10);
|
|
311
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
312
|
+
|
|
313
|
+
const upcoming = db.query(
|
|
314
|
+
`SELECT id, name, company_name, pipeline_stage, deal_value, follow_up_date, source
|
|
315
|
+
FROM opc_contacts
|
|
316
|
+
WHERE company_id = ? AND follow_up_date != '' AND follow_up_date <= ?
|
|
317
|
+
AND pipeline_stage NOT IN ('won', 'lost', 'churned')
|
|
318
|
+
ORDER BY follow_up_date`,
|
|
319
|
+
p.company_id, futureDateStr,
|
|
320
|
+
) as { follow_up_date: string }[];
|
|
321
|
+
|
|
322
|
+
const overdue = upcoming.filter((c) => c.follow_up_date < today);
|
|
323
|
+
const dueSoon = upcoming.filter((c) => c.follow_up_date >= today);
|
|
324
|
+
|
|
325
|
+
return json({ ok: true, overdue_count: overdue.length, upcoming_count: dueSoon.length, overdue, upcoming: dueSoon });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── 飞书频道 ──
|
|
329
|
+
case "setup_feishu_channel": {
|
|
330
|
+
const cfg = api.runtime.config.loadConfig() as Record<string, unknown>;
|
|
331
|
+
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
332
|
+
channels.feishu = {
|
|
333
|
+
enabled: true,
|
|
334
|
+
dmPolicy: "pairing",
|
|
335
|
+
groupPolicy: "open",
|
|
336
|
+
streaming: true,
|
|
337
|
+
accounts: {
|
|
338
|
+
main: {
|
|
339
|
+
appId: p.app_id,
|
|
340
|
+
appSecret: p.app_secret,
|
|
341
|
+
botName: p.bot_name || "\u661F\u73AFOPC\u52A9\u624B",
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
cfg.channels = channels;
|
|
346
|
+
await api.runtime.config.writeConfigFile(cfg);
|
|
347
|
+
return json({ ok: true, message: "\u98DE\u4E66\u9891\u9053\u5DF2\u914D\u7F6E\uFF0C\u7CFB\u7EDF\u5C06\u81EA\u52A8\u91CD\u542F" });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
case "feishu_channel_status": {
|
|
351
|
+
const cfg = api.runtime.config.loadConfig();
|
|
352
|
+
const feishuCfg = (cfg as Record<string, unknown>).channels as Record<string, unknown> | undefined;
|
|
353
|
+
const feishu = feishuCfg?.feishu as Record<string, unknown> | undefined;
|
|
354
|
+
const accounts = feishu?.accounts as Record<string, Record<string, string>> | undefined;
|
|
355
|
+
const main = accounts?.main;
|
|
356
|
+
return json({
|
|
357
|
+
configured: !!(main?.appId && main.appId !== "YOUR_FEISHU_APP_ID"),
|
|
358
|
+
enabled: feishu?.enabled ?? false,
|
|
359
|
+
botName: main?.botName ?? "",
|
|
360
|
+
dmPolicy: feishu?.dmPolicy ?? "pairing",
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── 切换公司 Agent ──
|
|
365
|
+
case "switch_company": {
|
|
366
|
+
const realId = resolveCompanyId(db, p.company_id);
|
|
367
|
+
const company = manager.getCompany(realId);
|
|
368
|
+
if (!company) return toolError(`公司 "${p.company_id}" 不存在`, "COMPANY_NOT_FOUND");
|
|
369
|
+
|
|
370
|
+
const targetAgentId = `opc-${realId}`;
|
|
371
|
+
const channel = (p as Record<string, unknown>)._channel as string | undefined;
|
|
372
|
+
const peerId = (p as Record<string, unknown>)._peer_id as string | undefined;
|
|
373
|
+
|
|
374
|
+
// 检查目标 agent 是否存在
|
|
375
|
+
const switchCfg = api.runtime.config.loadConfig() as Record<string, unknown>;
|
|
376
|
+
const agents = ((switchCfg.agents as Record<string, unknown>)?.list as Array<Record<string, unknown>>) ?? [];
|
|
377
|
+
const agentExists = agents.some(a => a.id === targetAgentId);
|
|
378
|
+
if (!agentExists) {
|
|
379
|
+
return toolError(
|
|
380
|
+
`公司 "${company.name}" 的专属 Agent (${targetAgentId}) 尚未创建。请先注册并激活该公司。`,
|
|
381
|
+
"RECORD_NOT_FOUND",
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 如果有 channel + peer 信息,更新 bindings
|
|
386
|
+
if (channel && peerId) {
|
|
387
|
+
const bindings = ((switchCfg.bindings ?? []) as Array<Record<string, unknown>>);
|
|
388
|
+
|
|
389
|
+
// 移除该 peer 在该 channel 上的旧绑定
|
|
390
|
+
const filtered = bindings.filter(b => {
|
|
391
|
+
const match = b.match as Record<string, unknown> | undefined;
|
|
392
|
+
const peer = match?.peer as Record<string, unknown> | undefined;
|
|
393
|
+
return !(match?.channel === channel && peer?.kind === "direct" && peer?.id === peerId);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// 添加新绑定
|
|
397
|
+
filtered.push({
|
|
398
|
+
agentId: targetAgentId,
|
|
399
|
+
match: {
|
|
400
|
+
channel,
|
|
401
|
+
peer: { kind: "direct", id: peerId },
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
switchCfg.bindings = filtered;
|
|
406
|
+
await api.runtime.config.writeConfigFile(switchCfg);
|
|
407
|
+
|
|
408
|
+
return json({
|
|
409
|
+
ok: true,
|
|
410
|
+
message: `已切换到「${company.name}」。系统将自动重启,下一条消息将由公司专属 AI 员工接待。`,
|
|
411
|
+
company: { id: realId, name: company.name },
|
|
412
|
+
agent_id: targetAgentId,
|
|
413
|
+
restarting: true,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 没有 channel/peer 信息(webchat 等),返回提示
|
|
418
|
+
return json({
|
|
419
|
+
ok: true,
|
|
420
|
+
message: `已找到公司「${company.name}」(Agent: ${targetAgentId})。当前频道不支持自动绑定,请在管理后台手动配置 bindings。`,
|
|
421
|
+
company: { id: realId, name: company.name },
|
|
422
|
+
agent_id: targetAgentId,
|
|
423
|
+
restarting: false,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
204
427
|
default:
|
|
205
428
|
return toolError(`未知操作: ${(p as { action: string }).action}`, "UNKNOWN_ACTION");
|
|
206
429
|
}
|