galaxy-opc-plugin 0.2.1 → 0.2.2
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/index.ts +244 -8
- 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/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 +131 -0
- package/src/db/schema.ts +211 -0
- package/src/db/sqlite-adapter.ts +5 -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/event-triggers.ts +472 -0
- package/src/opc/intelligence-engine.ts +702 -0
- package/src/opc/milestone-detector.ts +251 -0
- package/src/opc/proactive-service.ts +179 -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.ts +238 -0
- package/src/tools/finance-tool.ts +922 -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/opc-tool.test.ts +250 -0
- package/src/tools/opc-tool.ts +251 -28
- 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/staff-tool.ts +395 -2
- package/src/web/config-ui.ts +299 -45
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — 业务闭环引擎
|
|
3
|
+
*
|
|
4
|
+
* 当 AI 调用 create_contract / add_transaction / add_employee 时,
|
|
5
|
+
* 代码自动创建关联记录(联系人、项目、任务、里程碑、采购单、发票等),
|
|
6
|
+
* 保证数据完整性,避免 AI 跳步骤或遗漏。
|
|
7
|
+
*
|
|
8
|
+
* 所有 workflow 方法在数据库事务中执行(BEGIN/COMMIT/ROLLBACK),
|
|
9
|
+
* 任何一步失败则全部回滚,不会产生半成品数据。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { OpcDatabase } from "../db/index.js";
|
|
13
|
+
|
|
14
|
+
/** 合法的合同方向值 */
|
|
15
|
+
export const VALID_DIRECTIONS = ["sales", "procurement", "outsourcing", "partnership"] as const;
|
|
16
|
+
export type ContractDirection = typeof VALID_DIRECTIONS[number];
|
|
17
|
+
|
|
18
|
+
/** 自动创建的记录摘要 */
|
|
19
|
+
export interface AutoCreated {
|
|
20
|
+
module: string; // "contact" | "project" | "task" | "milestone" | "procurement" | "hr" | "invoice"
|
|
21
|
+
action: string; // "created" | "updated"
|
|
22
|
+
id: string;
|
|
23
|
+
summary: string; // 人类可读摘要
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class BusinessWorkflows {
|
|
27
|
+
constructor(private db: OpcDatabase) {}
|
|
28
|
+
|
|
29
|
+
/** 校验 direction 值是否合法 */
|
|
30
|
+
static validateDirection(direction: string): direction is ContractDirection {
|
|
31
|
+
return (VALID_DIRECTIONS as readonly string[]).includes(direction);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── 合同创建后(事务保护) ────────────────────────────────────
|
|
35
|
+
afterContractCreated(contract: {
|
|
36
|
+
id: string; company_id: string; title: string; counterparty: string;
|
|
37
|
+
contract_type: string; direction: string; amount: number;
|
|
38
|
+
start_date: string; end_date: string;
|
|
39
|
+
}): AutoCreated[] {
|
|
40
|
+
if (!BusinessWorkflows.validateDirection(contract.direction)) {
|
|
41
|
+
throw new Error(`无效的合同方向「${contract.direction}」,合法值: ${VALID_DIRECTIONS.join(", ")}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return this.db.transaction(() => {
|
|
45
|
+
return this._doAfterContractCreated(contract);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── 交易创建后(事务保护) ────────────────────────────────────
|
|
50
|
+
afterTransactionCreated(tx: {
|
|
51
|
+
id: string; company_id: string; type: string; amount: number;
|
|
52
|
+
counterparty: string; description: string;
|
|
53
|
+
}): AutoCreated[] {
|
|
54
|
+
return this.db.transaction(() => {
|
|
55
|
+
return this._doAfterTransactionCreated(tx);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── 员工添加后(事务保护) ────────────────────────────────────
|
|
60
|
+
afterEmployeeAdded(emp: {
|
|
61
|
+
id: string; company_id: string; employee_name: string; position: string;
|
|
62
|
+
contract_type: string; salary: number;
|
|
63
|
+
}): AutoCreated[] {
|
|
64
|
+
return this.db.transaction(() => {
|
|
65
|
+
return this._doAfterEmployeeAdded(emp);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ══════════════════════════════════════════════════════════════
|
|
70
|
+
// 内部实现(在事务内执行)
|
|
71
|
+
// ══════════════════════════════════════════════════════════════
|
|
72
|
+
|
|
73
|
+
private _doAfterContractCreated(contract: {
|
|
74
|
+
id: string; company_id: string; title: string; counterparty: string;
|
|
75
|
+
contract_type: string; direction: ContractDirection; amount: number;
|
|
76
|
+
start_date: string; end_date: string;
|
|
77
|
+
}): AutoCreated[] {
|
|
78
|
+
const results: AutoCreated[] = [];
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
const today = now.slice(0, 10);
|
|
81
|
+
|
|
82
|
+
// 1. 联系人:精确匹配 name,不存在则创建,已存在则更新
|
|
83
|
+
const tagMap: Record<ContractDirection, string[]> = {
|
|
84
|
+
sales: ["客户"], procurement: ["供应商"],
|
|
85
|
+
outsourcing: ["外包方"], partnership: ["合作伙伴"],
|
|
86
|
+
};
|
|
87
|
+
const tags = JSON.stringify(tagMap[contract.direction]);
|
|
88
|
+
const existing = this.db.queryOne(
|
|
89
|
+
"SELECT id FROM opc_contacts WHERE company_id = ? AND name = ?",
|
|
90
|
+
contract.company_id, contract.counterparty,
|
|
91
|
+
) as { id: string } | null;
|
|
92
|
+
|
|
93
|
+
if (existing) {
|
|
94
|
+
this.db.execute(
|
|
95
|
+
"UPDATE opc_contacts SET last_contact_date = ?, notes = notes || ?, updated_at = ? WHERE id = ?",
|
|
96
|
+
today, `\n关联合同:${contract.title},金额${contract.amount}元`, now, existing.id,
|
|
97
|
+
);
|
|
98
|
+
results.push({ module: "contact", action: "updated", id: existing.id,
|
|
99
|
+
summary: `已更新联系人「${contract.counterparty}」的最近联系日期` });
|
|
100
|
+
} else {
|
|
101
|
+
const contactId = this.db.genId();
|
|
102
|
+
this.db.execute(
|
|
103
|
+
`INSERT INTO opc_contacts (id, company_id, name, company_name, tags, notes, last_contact_date, created_at, updated_at)
|
|
104
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
105
|
+
contactId, contract.company_id, contract.counterparty, contract.counterparty,
|
|
106
|
+
tags, `通过合同「${contract.title}」建立关系,金额${contract.amount}元`,
|
|
107
|
+
today, now, now,
|
|
108
|
+
);
|
|
109
|
+
results.push({ module: "contact", action: "created", id: contactId,
|
|
110
|
+
summary: `已添加${tagMap[contract.direction][0]}「${contract.counterparty}」` });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. 按方向分流
|
|
114
|
+
if (contract.direction === "sales") {
|
|
115
|
+
// 销售/服务合同 -> 建交付项目 + 任务
|
|
116
|
+
const projectId = this.db.genId();
|
|
117
|
+
const projName = `【交付】${contract.counterparty}-${contract.title.replace(/合同$/, "")}`;
|
|
118
|
+
this.db.execute(
|
|
119
|
+
`INSERT INTO opc_projects (id, company_id, name, description, status, start_date, end_date, budget, spent, created_at, updated_at)
|
|
120
|
+
VALUES (?, ?, ?, ?, 'planning', ?, ?, ?, 0, ?, ?)`,
|
|
121
|
+
projectId, contract.company_id, projName,
|
|
122
|
+
`关联合同ID:${contract.id},合同金额:${contract.amount}元`,
|
|
123
|
+
contract.start_date || today, contract.end_date || "", contract.amount, now, now,
|
|
124
|
+
);
|
|
125
|
+
results.push({ module: "project", action: "created", id: projectId, summary: `已创建交付项目「${projName}」` });
|
|
126
|
+
|
|
127
|
+
const tasks = this._createDeliveryTasks(projectId, contract.company_id, contract.start_date || today, contract.end_date);
|
|
128
|
+
for (const t of tasks) {
|
|
129
|
+
results.push({ module: "task", action: "created", id: t.id, summary: `任务:${t.title}` });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
} else if (contract.direction === "procurement") {
|
|
133
|
+
const orderId = this.db.genId();
|
|
134
|
+
this.db.execute(
|
|
135
|
+
`INSERT INTO opc_procurement_orders (id, service_id, company_id, title, amount, status, order_date, notes, created_at)
|
|
136
|
+
VALUES (?, '', ?, ?, ?, 'pending', ?, ?, ?)`,
|
|
137
|
+
orderId, contract.company_id, `采购:${contract.title}`, contract.amount,
|
|
138
|
+
today, `关联合同ID:${contract.id}`, now,
|
|
139
|
+
);
|
|
140
|
+
results.push({ module: "procurement", action: "created", id: orderId,
|
|
141
|
+
summary: `已创建采购单,金额${contract.amount}元` });
|
|
142
|
+
|
|
143
|
+
} else if (contract.direction === "outsourcing") {
|
|
144
|
+
const hrId = this.db.genId();
|
|
145
|
+
this.db.execute(
|
|
146
|
+
`INSERT INTO opc_hr_records (id, company_id, employee_name, position, salary, social_insurance, housing_fund, start_date, contract_type, status, notes, created_at, updated_at)
|
|
147
|
+
VALUES (?, ?, ?, '外包', ?, 0, 0, ?, 'contractor', 'active', ?, ?, ?)`,
|
|
148
|
+
hrId, contract.company_id, contract.counterparty, contract.amount,
|
|
149
|
+
contract.start_date || today, `关联合同:${contract.title}`, now, now,
|
|
150
|
+
);
|
|
151
|
+
results.push({ module: "hr", action: "created", id: hrId,
|
|
152
|
+
summary: `已添加外包人员「${contract.counterparty}」` });
|
|
153
|
+
}
|
|
154
|
+
// partnership: 只建联系人 + 里程碑
|
|
155
|
+
|
|
156
|
+
// 3. 里程碑
|
|
157
|
+
const msId = this.db.genId();
|
|
158
|
+
const dirLabel: Record<ContractDirection, string> = {
|
|
159
|
+
sales: "签约客户", procurement: "签订采购", outsourcing: "签约外包", partnership: "达成合作",
|
|
160
|
+
};
|
|
161
|
+
const msTitle = `${dirLabel[contract.direction]}${contract.counterparty},${contract.amount}元${contract.title}`;
|
|
162
|
+
this.db.execute(
|
|
163
|
+
`INSERT INTO opc_milestones (id, company_id, title, category, target_date, status, description, created_at)
|
|
164
|
+
VALUES (?, ?, ?, 'business', ?, 'completed', ?, ?)`,
|
|
165
|
+
msId, contract.company_id, msTitle, today,
|
|
166
|
+
`合同类型:${contract.contract_type},方向:${contract.direction},金额:${contract.amount}元`, now,
|
|
167
|
+
);
|
|
168
|
+
results.push({ module: "milestone", action: "created", id: msId, summary: `时间线:${msTitle}` });
|
|
169
|
+
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private _doAfterTransactionCreated(tx: {
|
|
174
|
+
id: string; company_id: string; type: string; amount: number;
|
|
175
|
+
counterparty: string; description: string;
|
|
176
|
+
}): AutoCreated[] {
|
|
177
|
+
const results: AutoCreated[] = [];
|
|
178
|
+
const now = new Date().toISOString();
|
|
179
|
+
const today = now.slice(0, 10);
|
|
180
|
+
|
|
181
|
+
// 支出交易且有交易对手 → 自动创建/更新供应商联系人
|
|
182
|
+
if (tx.type === "expense" && tx.counterparty) {
|
|
183
|
+
const existing = this.db.queryOne(
|
|
184
|
+
"SELECT id FROM opc_contacts WHERE company_id = ? AND name = ?",
|
|
185
|
+
tx.company_id, tx.counterparty,
|
|
186
|
+
) as { id: string } | null;
|
|
187
|
+
|
|
188
|
+
if (existing) {
|
|
189
|
+
this.db.execute(
|
|
190
|
+
"UPDATE opc_contacts SET last_contact_date = ?, notes = notes || ?, updated_at = ? WHERE id = ?",
|
|
191
|
+
today, `\n支出:${tx.description},${tx.amount}元`, now, existing.id,
|
|
192
|
+
);
|
|
193
|
+
results.push({ module: "contact", action: "updated", id: existing.id,
|
|
194
|
+
summary: `已更新供应商「${tx.counterparty}」最近联系日期` });
|
|
195
|
+
} else {
|
|
196
|
+
const contactId = this.db.genId();
|
|
197
|
+
this.db.execute(
|
|
198
|
+
`INSERT INTO opc_contacts (id, company_id, name, company_name, tags, notes, last_contact_date, created_at, updated_at)
|
|
199
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
200
|
+
contactId, tx.company_id, tx.counterparty, tx.counterparty,
|
|
201
|
+
JSON.stringify(["供应商"]),
|
|
202
|
+
`通过支出交易建立关系:${tx.description},${tx.amount}元`,
|
|
203
|
+
today, now, now,
|
|
204
|
+
);
|
|
205
|
+
results.push({ module: "contact", action: "created", id: contactId,
|
|
206
|
+
summary: `已添加供应商「${tx.counterparty}」` });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 收入交易且有交易对手 → 自动创建/更新客户联系人
|
|
211
|
+
if (tx.type === "income" && tx.counterparty) {
|
|
212
|
+
const existing = this.db.queryOne(
|
|
213
|
+
"SELECT id FROM opc_contacts WHERE company_id = ? AND name = ?",
|
|
214
|
+
tx.company_id, tx.counterparty,
|
|
215
|
+
) as { id: string } | null;
|
|
216
|
+
|
|
217
|
+
if (existing) {
|
|
218
|
+
this.db.execute(
|
|
219
|
+
"UPDATE opc_contacts SET last_contact_date = ?, notes = notes || ?, updated_at = ? WHERE id = ?",
|
|
220
|
+
today, `\n收入:${tx.description},${tx.amount}元`, now, existing.id,
|
|
221
|
+
);
|
|
222
|
+
results.push({ module: "contact", action: "updated", id: existing.id,
|
|
223
|
+
summary: `已更新客户「${tx.counterparty}」最近联系日期` });
|
|
224
|
+
} else {
|
|
225
|
+
const contactId = this.db.genId();
|
|
226
|
+
this.db.execute(
|
|
227
|
+
`INSERT INTO opc_contacts (id, company_id, name, company_name, tags, notes, last_contact_date, created_at, updated_at)
|
|
228
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
229
|
+
contactId, tx.company_id, tx.counterparty, tx.counterparty,
|
|
230
|
+
JSON.stringify(["客户"]),
|
|
231
|
+
`通过收入交易建立关系:${tx.description},${tx.amount}元`,
|
|
232
|
+
today, now, now,
|
|
233
|
+
);
|
|
234
|
+
results.push({ module: "contact", action: "created", id: contactId,
|
|
235
|
+
summary: `已添加客户「${tx.counterparty}」` });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 收入 -> 自动创建销项发票(含税拆分正确)
|
|
240
|
+
if (tx.type === "income" && tx.amount > 0) {
|
|
241
|
+
const invoiceId = this.db.genId();
|
|
242
|
+
const taxRate = 0.06;
|
|
243
|
+
const totalAmount = tx.amount; // 到账金额 = 含税金额
|
|
244
|
+
const pretaxAmount = Math.round(totalAmount / (1 + taxRate) * 100) / 100;
|
|
245
|
+
const taxAmount = Math.round((totalAmount - pretaxAmount) * 100) / 100;
|
|
246
|
+
this.db.execute(
|
|
247
|
+
`INSERT INTO opc_invoices (id, company_id, type, counterparty, amount, tax_rate, tax_amount, total_amount, status, issue_date, notes, created_at)
|
|
248
|
+
VALUES (?, ?, 'sales', ?, ?, ?, ?, ?, 'draft', ?, ?, ?)`,
|
|
249
|
+
invoiceId, tx.company_id, tx.counterparty || "", pretaxAmount, taxRate, taxAmount, totalAmount,
|
|
250
|
+
today, `关联交易:${tx.description}`, now,
|
|
251
|
+
);
|
|
252
|
+
results.push({ module: "invoice", action: "created", id: invoiceId,
|
|
253
|
+
summary: `已创建销项发票:含税${totalAmount}元,税额${taxAmount}元` });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 大额交易 -> 里程碑
|
|
257
|
+
if (tx.amount >= 5000) {
|
|
258
|
+
const msId = this.db.genId();
|
|
259
|
+
const label = tx.type === "income" ? "收到" : "支出";
|
|
260
|
+
const msTitle = `${label}${tx.counterparty ? tx.counterparty + "款项" : ""}${tx.amount}元`;
|
|
261
|
+
this.db.execute(
|
|
262
|
+
`INSERT INTO opc_milestones (id, company_id, title, category, target_date, status, description, created_at)
|
|
263
|
+
VALUES (?, ?, ?, 'finance', ?, 'completed', ?, ?)`,
|
|
264
|
+
msId, tx.company_id, msTitle, today, tx.description || "", now,
|
|
265
|
+
);
|
|
266
|
+
results.push({ module: "milestone", action: "created", id: msId, summary: `时间线:${msTitle}` });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return results;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private _doAfterEmployeeAdded(emp: {
|
|
273
|
+
id: string; company_id: string; employee_name: string; position: string;
|
|
274
|
+
contract_type: string; salary: number;
|
|
275
|
+
}): AutoCreated[] {
|
|
276
|
+
const results: AutoCreated[] = [];
|
|
277
|
+
const now = new Date().toISOString();
|
|
278
|
+
const today = now.slice(0, 10);
|
|
279
|
+
const typeLabel = emp.contract_type === "contractor" ? "外包" :
|
|
280
|
+
emp.contract_type === "part_time" ? "兼职" :
|
|
281
|
+
emp.contract_type === "intern" ? "实习" : "全职";
|
|
282
|
+
const msId = this.db.genId();
|
|
283
|
+
const msTitle = `团队+1:${emp.employee_name}加入,担任${emp.position}(${typeLabel})`;
|
|
284
|
+
this.db.execute(
|
|
285
|
+
`INSERT INTO opc_milestones (id, company_id, title, category, target_date, status, description, created_at)
|
|
286
|
+
VALUES (?, ?, ?, 'team', ?, 'completed', ?, ?)`,
|
|
287
|
+
msId, emp.company_id, msTitle, today,
|
|
288
|
+
`月薪${emp.salary}元,用工类型:${emp.contract_type}`, now,
|
|
289
|
+
);
|
|
290
|
+
results.push({ module: "milestone", action: "created", id: msId, summary: `时间线:${msTitle}` });
|
|
291
|
+
return results;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── 内部:创建标准交付任务 ──────────────────────────────────
|
|
295
|
+
private _createDeliveryTasks(projectId: string, companyId: string, startDate: string, endDate: string) {
|
|
296
|
+
const now = new Date().toISOString();
|
|
297
|
+
const tasks = [
|
|
298
|
+
{ title: "需求确认与方案设计", priority: "high", dueOffset: 14, dueOffsetFromEnd: null as number | null },
|
|
299
|
+
{ title: "核心交付/开发", priority: "high", dueOffset: null as number | null, dueOffsetFromEnd: null as number | null },
|
|
300
|
+
{ title: "验收与交付", priority: "high", dueOffset: null as number | null, dueOffsetFromEnd: 14 },
|
|
301
|
+
{ title: "尾款收取与项目结项", priority: "medium", dueOffset: null as number | null, dueOffsetFromEnd: 0 },
|
|
302
|
+
];
|
|
303
|
+
const start = startDate ? new Date(startDate) : new Date();
|
|
304
|
+
const end = endDate ? new Date(endDate) : null;
|
|
305
|
+
const result: { id: string; title: string }[] = [];
|
|
306
|
+
for (const t of tasks) {
|
|
307
|
+
const id = this.db.genId();
|
|
308
|
+
let dueDate = "";
|
|
309
|
+
if (t.dueOffset != null) {
|
|
310
|
+
const d = new Date(start); d.setDate(d.getDate() + t.dueOffset);
|
|
311
|
+
dueDate = d.toISOString().slice(0, 10);
|
|
312
|
+
} else if (t.dueOffsetFromEnd != null && end) {
|
|
313
|
+
const d = new Date(end); d.setDate(d.getDate() - t.dueOffsetFromEnd);
|
|
314
|
+
dueDate = d.toISOString().slice(0, 10);
|
|
315
|
+
}
|
|
316
|
+
this.db.execute(
|
|
317
|
+
`INSERT INTO opc_tasks (id, project_id, company_id, title, description, assignee, priority, status, due_date, hours_estimated, hours_actual, created_at, updated_at)
|
|
318
|
+
VALUES (?, ?, ?, ?, '', '', ?, 'todo', ?, 0, 0, ?, ?)`,
|
|
319
|
+
id, projectId, companyId, t.title, t.priority, dueDate, now, now,
|
|
320
|
+
);
|
|
321
|
+
result.push({ id, title: t.title });
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
}
|