galaxy-opc-plugin 0.2.0 → 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.
Files changed (46) hide show
  1. package/index.ts +244 -8
  2. package/package.json +17 -3
  3. package/skills/acquisition-management/SKILL.md +83 -0
  4. package/skills/ai-staff/SKILL.md +89 -0
  5. package/skills/asset-package/SKILL.md +142 -0
  6. package/skills/opb-canvas/SKILL.md +88 -0
  7. package/src/__tests__/e2e/company-lifecycle.test.ts +399 -0
  8. package/src/__tests__/integration/business-workflows.test.ts +366 -0
  9. package/src/__tests__/test-utils.ts +316 -0
  10. package/src/commands/opc-command.ts +422 -0
  11. package/src/db/index.ts +3 -0
  12. package/src/db/migrations.test.ts +324 -0
  13. package/src/db/migrations.ts +131 -0
  14. package/src/db/schema.ts +211 -0
  15. package/src/db/sqlite-adapter.ts +5 -0
  16. package/src/opc/autonomy-rules.ts +132 -0
  17. package/src/opc/briefing-builder.ts +1331 -0
  18. package/src/opc/business-workflows.test.ts +535 -0
  19. package/src/opc/business-workflows.ts +325 -0
  20. package/src/opc/context-injector.ts +366 -28
  21. package/src/opc/event-triggers.ts +472 -0
  22. package/src/opc/intelligence-engine.ts +702 -0
  23. package/src/opc/milestone-detector.ts +251 -0
  24. package/src/opc/proactive-service.ts +179 -0
  25. package/src/opc/reminder-service.ts +4 -43
  26. package/src/opc/session-task-tracker.ts +60 -0
  27. package/src/opc/stage-detector.ts +168 -0
  28. package/src/opc/task-executor.ts +332 -0
  29. package/src/opc/task-templates.ts +179 -0
  30. package/src/tools/acquisition-tool.ts +8 -5
  31. package/src/tools/document-tool.ts +1176 -0
  32. package/src/tools/finance-tool.test.ts +238 -0
  33. package/src/tools/finance-tool.ts +922 -14
  34. package/src/tools/hr-tool.ts +10 -1
  35. package/src/tools/legal-tool.test.ts +251 -0
  36. package/src/tools/legal-tool.ts +26 -4
  37. package/src/tools/lifecycle-tool.test.ts +231 -0
  38. package/src/tools/media-tool.ts +156 -1
  39. package/src/tools/monitoring-tool.ts +135 -2
  40. package/src/tools/opc-tool.test.ts +250 -0
  41. package/src/tools/opc-tool.ts +251 -28
  42. package/src/tools/project-tool.test.ts +218 -0
  43. package/src/tools/schemas.ts +80 -0
  44. package/src/tools/search-tool.ts +227 -0
  45. package/src/tools/staff-tool.ts +395 -2
  46. package/src/web/config-ui.ts +299 -45
@@ -0,0 +1,472 @@
1
+ /**
2
+ * 星环OPC中心 — 事件驱动触发引擎
3
+ *
4
+ * 每次 OPC 工具写入数据后,自动检查业务规则,触发后续动作。
5
+ * 不是轮询 — 是实时响应。
6
+ *
7
+ * 触发引擎不启动子会话(太重),只创建 pending 状态的 staff_task,
8
+ * 任务在下次 AI 交互时由 context-injector 呈现给老板,或由 cron 定时执行。
9
+ */
10
+
11
+ import type { OpcDatabase } from "../db/index.js";
12
+ import { checkAutonomy, type AutonomyResult } from "./autonomy-rules.js";
13
+
14
+ type LogFn = (msg: string) => void;
15
+
16
+ /**
17
+ * 在 after_tool_call 中调用。
18
+ * 根据工具名 + 参数 + 返回值,匹配触发规则,执行自动动作。
19
+ */
20
+ export function triggerEventRules(
21
+ db: OpcDatabase,
22
+ companyId: string,
23
+ toolName: string,
24
+ params: Record<string, unknown> | undefined,
25
+ result: Record<string, unknown> | undefined,
26
+ log?: LogFn,
27
+ ): void {
28
+ const l = log ?? (() => {});
29
+
30
+ try {
31
+ // 只处理写操作
32
+ const action = params?.action as string | undefined;
33
+ if (!action) return;
34
+
35
+ const writeActions = [
36
+ "add_transaction", "create_contract", "update_contract",
37
+ "add_employee", "create_content",
38
+ "create_project", "create_tax_filing", "create_invoice",
39
+ "register_company", "update_company",
40
+ "batch_import_transactions", "batch_import_invoices", "batch_import_contacts",
41
+ "add_interaction", "generate_document", "approve_content",
42
+ ];
43
+ if (!writeActions.includes(action)) return;
44
+
45
+ // ── 应收款逾期检查(交易记录后) ──
46
+ if (toolName === "opc_finance" && action === "add_transaction") {
47
+ checkOverdueReceivables(db, companyId, l);
48
+ }
49
+
50
+ // ── 现金流异常检查(交易记录后) ──
51
+ if (toolName === "opc_finance" && action === "add_transaction") {
52
+ checkCashFlowAnomaly(db, companyId, l);
53
+ }
54
+
55
+ // ── 合同到期检查(合同创建 / 更新后) ──
56
+ if (toolName === "opc_legal" && (action === "create_contract" || action === "update_contract")) {
57
+ checkContractExpiry(db, companyId, l);
58
+ }
59
+
60
+ // ── 新告警分配(监控工具触发后) ──
61
+ if (toolName === "opc_monitoring") {
62
+ assignAlertsToStaff(db, companyId, l);
63
+ }
64
+
65
+ // ── 跟进逾期检测(CRM) ──
66
+ checkOverdueFollowUps(db, companyId, l);
67
+
68
+ l(`opc-events: 事件触发检查完成 [${toolName}.${action}]`);
69
+ } catch (err) {
70
+ l(`opc-events: 触发检查异常: ${err instanceof Error ? err.message : String(err)}`);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 将新产生的告警自动分配给对应 AI 员工。
76
+ * 在 proactive-service 的 runReminderScan 之后调用。
77
+ */
78
+ export function alertsToStaffTasks(db: OpcDatabase, log: LogFn): void {
79
+ try {
80
+ // 查找最近 1 小时内新创建的 active alerts,且没有对应 staff_task 的
81
+ const recentAlerts = db.query(
82
+ `SELECT a.id, a.company_id, a.title, a.message, a.severity, a.category
83
+ FROM opc_alerts a
84
+ WHERE a.status = 'active'
85
+ AND a.created_at > datetime('now', '-1 hour')
86
+ AND NOT EXISTS (
87
+ SELECT 1 FROM opc_staff_tasks t
88
+ WHERE t.company_id = a.company_id
89
+ AND t.title LIKE '%' || a.title || '%'
90
+ AND t.status IN ('pending', 'in_progress', 'pending_approval')
91
+ )`,
92
+ ) as { id: string; company_id: string; title: string; message: string; severity: string; category: string }[];
93
+
94
+ if (recentAlerts.length === 0) return;
95
+
96
+ let created = 0;
97
+ const now = new Date().toISOString();
98
+
99
+ for (const alert of recentAlerts) {
100
+ const staffRole = mapAlertCategoryToRole(alert.category);
101
+
102
+ // 检查该公司是否有对应岗位
103
+ const staffExists = db.queryOne(
104
+ "SELECT id FROM opc_staff_config WHERE company_id = ? AND role = ? AND enabled = 1",
105
+ alert.company_id, staffRole,
106
+ );
107
+ if (!staffExists) continue;
108
+
109
+ // 通过自主行动规则判断是否需要老板审批
110
+ const autonomy = checkAutonomy(staffRole, mapAlertToTrigger(alert.category, alert.severity));
111
+ const status = autonomy.needsBossApproval ? "pending_approval" : "pending";
112
+ const priority = alert.severity === "critical" ? "urgent" : alert.severity === "warning" ? "high" : "normal";
113
+
114
+ const taskId = db.genId();
115
+ db.execute(
116
+ `INSERT INTO opc_staff_tasks (id, company_id, staff_role, title, description, status, priority, task_type, schedule, assigned_at, created_at)
117
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'auto_alert', 'on_demand', ?, ?)`,
118
+ taskId, alert.company_id, staffRole,
119
+ `[告警处理] ${alert.title}`,
120
+ `系统检测到告警:${alert.message}\n\n告警ID: ${alert.id}\n严重程度: ${alert.severity}\n类别: ${alert.category}`,
121
+ status, priority, now, now,
122
+ );
123
+ created++;
124
+
125
+ // 如果需要审批,同时写入洞察
126
+ if (autonomy.needsBossApproval && autonomy.escalationMsg) {
127
+ db.execute(
128
+ `INSERT INTO opc_insights (id, company_id, insight_type, category, priority, title, message, action_hint, staff_role, status, expires_at, created_at, updated_at)
129
+ VALUES (?, ?, 'escalation', ?, ?, ?, ?, ?, ?, 'active', datetime('now', '+7 days'), ?, ?)`,
130
+ db.genId(), alert.company_id, alert.category,
131
+ alert.severity === "critical" ? 95 : 80,
132
+ `需要决策: ${alert.title}`,
133
+ autonomy.escalationMsg,
134
+ `审批后系统将自动执行`,
135
+ staffRole, now, now,
136
+ );
137
+ }
138
+ }
139
+
140
+ if (created > 0) {
141
+ log(`opc-events: 已将 ${created} 条新告警自动分配给 AI 员工`);
142
+ }
143
+ } catch (err) {
144
+ log(`opc-events: 告警分配异常: ${err instanceof Error ? err.message : String(err)}`);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * 将洞察中含有 staff_role 的自动转化为员工待办。
150
+ */
151
+ export function insightsToStaffTasks(db: OpcDatabase, log: LogFn): void {
152
+ try {
153
+ // 查找有 staff_role 且类型为 staff_observation 的活跃洞察,且没有对应 staff_task 的
154
+ const actionableInsights = db.query(
155
+ `SELECT i.id, i.company_id, i.title, i.message, i.action_hint, i.staff_role, i.priority
156
+ FROM opc_insights i
157
+ WHERE i.status = 'active' AND i.staff_role != ''
158
+ AND i.insight_type = 'staff_observation'
159
+ AND i.action_hint != ''
160
+ AND (i.expires_at = '' OR i.expires_at > datetime('now'))
161
+ AND NOT EXISTS (
162
+ SELECT 1 FROM opc_staff_tasks t
163
+ WHERE t.company_id = i.company_id
164
+ AND t.staff_role = i.staff_role
165
+ AND t.title LIKE '%' || i.title || '%'
166
+ AND t.status IN ('pending', 'in_progress', 'pending_approval')
167
+ )`,
168
+ ) as { id: string; company_id: string; title: string; message: string; action_hint: string; staff_role: string; priority: number }[];
169
+
170
+ if (actionableInsights.length === 0) return;
171
+
172
+ let created = 0;
173
+ const now = new Date().toISOString();
174
+
175
+ for (const insight of actionableInsights) {
176
+ // 检查该公司是否有对应岗位
177
+ const staffExists = db.queryOne(
178
+ "SELECT id FROM opc_staff_config WHERE company_id = ? AND role = ? AND enabled = 1",
179
+ insight.company_id, insight.staff_role,
180
+ );
181
+ if (!staffExists) continue;
182
+
183
+ const priority = insight.priority >= 80 ? "high" : "normal";
184
+ const taskId = db.genId();
185
+ db.execute(
186
+ `INSERT INTO opc_staff_tasks (id, company_id, staff_role, title, description, status, priority, task_type, schedule, assigned_at, created_at)
187
+ VALUES (?, ?, ?, ?, ?, 'pending', ?, 'auto_insight', 'on_demand', ?, ?)`,
188
+ taskId, insight.company_id, insight.staff_role,
189
+ `[洞察建议] ${insight.action_hint}`,
190
+ `来源洞察:${insight.title}\n${insight.message}`,
191
+ priority, now, now,
192
+ );
193
+ created++;
194
+ }
195
+
196
+ if (created > 0) {
197
+ log(`opc-events: 已将 ${created} 条洞察建议转化为员工待办`);
198
+ }
199
+ } catch (err) {
200
+ log(`opc-events: 洞察转化异常: ${err instanceof Error ? err.message : String(err)}`);
201
+ }
202
+ }
203
+
204
+ // ── 内部检查函数 ──────────────────────────────────────────────
205
+
206
+ function checkOverdueReceivables(db: OpcDatabase, companyId: string, log: LogFn): void {
207
+ // 检查逾期超过 7 天的应收发票
208
+ const overdueInvoices = db.query(
209
+ `SELECT id, counterparty, total_amount, issue_date FROM opc_invoices
210
+ WHERE company_id = ? AND type = 'sales' AND status IN ('sent', 'pending')
211
+ AND issue_date != '' AND issue_date < date('now', '-7 days')`,
212
+ companyId,
213
+ ) as { id: string; counterparty: string; total_amount: number; issue_date: string }[];
214
+
215
+ if (overdueInvoices.length === 0) return;
216
+
217
+ const staffExists = db.queryOne(
218
+ "SELECT id FROM opc_staff_config WHERE company_id = ? AND role = 'finance' AND enabled = 1",
219
+ companyId,
220
+ );
221
+ if (!staffExists) return;
222
+
223
+ const now = new Date().toISOString();
224
+ let created = 0;
225
+
226
+ for (const inv of overdueInvoices) {
227
+ const days = Math.floor((Date.now() - new Date(inv.issue_date).getTime()) / 86400000);
228
+
229
+ // 检查是否已有催收任务
230
+ const existing = db.queryOne(
231
+ `SELECT id FROM opc_staff_tasks WHERE company_id = ? AND staff_role = 'finance'
232
+ AND title LIKE ? AND status IN ('pending', 'in_progress', 'pending_approval')`,
233
+ companyId, `%催收%${inv.counterparty}%`,
234
+ );
235
+ if (existing) continue;
236
+
237
+ const autonomy = checkAutonomy("finance", days > 30 ? "invoice_overdue_30d" : "invoice_overdue_7d");
238
+ const status = autonomy.needsBossApproval ? "pending_approval" : "pending";
239
+
240
+ const taskId = db.genId();
241
+ db.execute(
242
+ `INSERT INTO opc_staff_tasks (id, company_id, staff_role, title, description, status, priority, task_type, schedule, assigned_at, created_at)
243
+ VALUES (?, ?, 'finance', ?, ?, ?, ?, 'auto_trigger', 'on_demand', ?, ?)`,
244
+ taskId, companyId,
245
+ `催收 ${inv.counterparty} 逾期款 ${inv.total_amount.toLocaleString()} 元`,
246
+ `发票逾期 ${days} 天,金额 ${inv.total_amount} 元。发票ID: ${inv.id}`,
247
+ status, days > 30 ? "urgent" : "high", now, now,
248
+ );
249
+ created++;
250
+
251
+ if (autonomy.needsBossApproval && autonomy.escalationMsg) {
252
+ db.execute(
253
+ `INSERT INTO opc_insights (id, company_id, insight_type, category, priority, title, message, action_hint, staff_role, status, expires_at, created_at, updated_at)
254
+ VALUES (?, ?, 'escalation', 'finance', 90, ?, ?, '审批后自动催收', 'finance', 'active', datetime('now', '+7 days'), ?, ?)`,
255
+ db.genId(), companyId,
256
+ `需要决策: ${inv.counterparty} 逾期应收款`,
257
+ autonomy.escalationMsg,
258
+ now, now,
259
+ );
260
+ }
261
+ }
262
+
263
+ if (created > 0) {
264
+ log(`opc-events: 已为 ${companyId} 创建 ${created} 个应收款催收任务`);
265
+ }
266
+ }
267
+
268
+ function checkCashFlowAnomaly(db: OpcDatabase, companyId: string, log: LogFn): void {
269
+ const thisMonth = new Date().toISOString().slice(0, 7);
270
+ const lastMonth = (() => {
271
+ const d = new Date();
272
+ d.setMonth(d.getMonth() - 1);
273
+ return d.toISOString().slice(0, 7);
274
+ })();
275
+
276
+ const thisMonthNet = (db.queryOne(
277
+ `SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END), 0) as total
278
+ FROM opc_transactions WHERE company_id = ? AND strftime('%Y-%m', transaction_date) = ?`,
279
+ companyId, thisMonth,
280
+ ) as { total: number }).total;
281
+
282
+ const lastMonthNet = (db.queryOne(
283
+ `SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END), 0) as total
284
+ FROM opc_transactions WHERE company_id = ? AND strftime('%Y-%m', transaction_date) = ?`,
285
+ companyId, lastMonth,
286
+ ) as { total: number }).total;
287
+
288
+ // 本月现金流为负且恶化超过上月 50%
289
+ if (thisMonthNet >= 0) return;
290
+ if (lastMonthNet >= 0) return; // 上月为正,本月转负也值得关注但用不同逻辑
291
+ if (Math.abs(thisMonthNet) <= Math.abs(lastMonthNet) * 1.5) return;
292
+
293
+ // 检查是否已有任务
294
+ const existing = db.queryOne(
295
+ `SELECT id FROM opc_staff_tasks WHERE company_id = ? AND staff_role = 'finance'
296
+ AND title LIKE '%支出异常%' AND status IN ('pending', 'in_progress', 'pending_approval')
297
+ AND DATE(created_at) = DATE('now')`,
298
+ companyId,
299
+ );
300
+ if (existing) return;
301
+
302
+ const staffExists = db.queryOne(
303
+ "SELECT id FROM opc_staff_config WHERE company_id = ? AND role = 'finance' AND enabled = 1",
304
+ companyId,
305
+ );
306
+ if (!staffExists) return;
307
+
308
+ const now = new Date().toISOString();
309
+ const taskId = db.genId();
310
+ db.execute(
311
+ `INSERT INTO opc_staff_tasks (id, company_id, staff_role, title, description, status, priority, task_type, schedule, assigned_at, created_at)
312
+ VALUES (?, ?, 'finance', '分析本月支出异常', ?, 'pending', 'high', 'auto_trigger', 'on_demand', ?, ?)`,
313
+ taskId, companyId,
314
+ `本月现金流 ${thisMonthNet.toLocaleString()} 元,较上月 ${lastMonthNet.toLocaleString()} 元恶化超过 50%`,
315
+ now, now,
316
+ );
317
+
318
+ log(`opc-events: 已为 ${companyId} 创建现金流异常分析任务`);
319
+ }
320
+
321
+ function checkContractExpiry(db: OpcDatabase, companyId: string, log: LogFn): void {
322
+ // 检查 30 天内到期的合同(active 或 draft 状态都检查)
323
+ const expiringContracts = db.query(
324
+ `SELECT id, title, counterparty, end_date FROM opc_contracts
325
+ WHERE company_id = ? AND status IN ('active', 'draft') AND end_date != ''
326
+ AND end_date <= date('now', '+30 days') AND end_date >= date('now')`,
327
+ companyId,
328
+ ) as { id: string; title: string; counterparty: string; end_date: string }[];
329
+
330
+ if (expiringContracts.length === 0) return;
331
+
332
+ const staffExists = db.queryOne(
333
+ "SELECT id FROM opc_staff_config WHERE company_id = ? AND role = 'legal' AND enabled = 1",
334
+ companyId,
335
+ );
336
+ if (!staffExists) return;
337
+
338
+ const now = new Date().toISOString();
339
+ let created = 0;
340
+
341
+ for (const contract of expiringContracts) {
342
+ const existing = db.queryOne(
343
+ `SELECT id FROM opc_staff_tasks WHERE company_id = ? AND staff_role = 'legal'
344
+ AND title LIKE ? AND status IN ('pending', 'in_progress', 'pending_approval')`,
345
+ companyId, `%${contract.title}%续签%`,
346
+ );
347
+ if (existing) continue;
348
+
349
+ const daysLeft = Math.floor((new Date(contract.end_date).getTime() - Date.now()) / 86400000);
350
+ const autonomy = checkAutonomy("legal", daysLeft <= 7 ? "contract_expiry_7d" : "contract_expiry_30d");
351
+ const status = autonomy.needsBossApproval ? "pending_approval" : "pending";
352
+
353
+ const taskId = db.genId();
354
+ db.execute(
355
+ `INSERT INTO opc_staff_tasks (id, company_id, staff_role, title, description, status, priority, task_type, schedule, assigned_at, created_at)
356
+ VALUES (?, ?, 'legal', ?, ?, ?, ?, 'auto_trigger', 'on_demand', ?, ?)`,
357
+ taskId, companyId,
358
+ `审查合同「${contract.title}」续签方案`,
359
+ `合同将在 ${daysLeft} 天后到期(${contract.end_date})。\n对方: ${contract.counterparty}\n合同ID: ${contract.id}`,
360
+ status, daysLeft <= 7 ? "urgent" : "high", now, now,
361
+ );
362
+ created++;
363
+
364
+ if (autonomy.needsBossApproval && autonomy.escalationMsg) {
365
+ db.execute(
366
+ `INSERT INTO opc_insights (id, company_id, insight_type, category, priority, title, message, action_hint, staff_role, status, expires_at, created_at, updated_at)
367
+ VALUES (?, ?, 'escalation', 'legal', 90, ?, ?, '审批后法务将准备续签方案', 'legal', 'active', ?, ?, ?)`,
368
+ db.genId(), companyId,
369
+ `需要决策: ${contract.title} 续签`,
370
+ autonomy.escalationMsg,
371
+ contract.end_date, now, now,
372
+ );
373
+ }
374
+ }
375
+
376
+ if (created > 0) {
377
+ log(`opc-events: 已为 ${companyId} 创建 ${created} 个合同续签审查任务`);
378
+ }
379
+ }
380
+
381
+ function assignAlertsToStaff(db: OpcDatabase, companyId: string, log: LogFn): void {
382
+ // 调用通用的 alertsToStaffTasks 来处理该公司的新告警
383
+ alertsToStaffTasks(db, log);
384
+ }
385
+
386
+ /** 检查客户跟进逾期(CRM 联系人 follow_up_date 过期且未成交/流失) */
387
+ function checkOverdueFollowUps(db: OpcDatabase, companyId: string, log: LogFn): void {
388
+ try {
389
+ const today = new Date().toISOString().slice(0, 10);
390
+ const overdueContacts = db.query(
391
+ `SELECT id, name, follow_up_date, pipeline_stage, deal_value FROM opc_contacts
392
+ WHERE company_id = ? AND follow_up_date != '' AND follow_up_date < ?
393
+ AND pipeline_stage NOT IN ('won', 'lost', 'churned')`,
394
+ companyId, today,
395
+ ) as { id: string; name: string; follow_up_date: string; pipeline_stage: string; deal_value: number }[];
396
+
397
+ if (overdueContacts.length === 0) return;
398
+
399
+ // 检查是否有 marketing 或 ops 岗位
400
+ const staffRole = (db.queryOne(
401
+ "SELECT id FROM opc_staff_config WHERE company_id = ? AND role = 'marketing' AND enabled = 1",
402
+ companyId,
403
+ ) ? "marketing" : db.queryOne(
404
+ "SELECT id FROM opc_staff_config WHERE company_id = ? AND role = 'ops' AND enabled = 1",
405
+ companyId,
406
+ ) ? "ops" : null);
407
+ if (!staffRole) return;
408
+
409
+ const now = new Date().toISOString();
410
+ let created = 0;
411
+
412
+ for (const contact of overdueContacts) {
413
+ // 检查是否已有跟进任务
414
+ const existing = db.queryOne(
415
+ `SELECT id FROM opc_staff_tasks WHERE company_id = ? AND title LIKE ?
416
+ AND status IN ('pending', 'in_progress', 'pending_approval')`,
417
+ companyId, `%跟进%${contact.name}%`,
418
+ );
419
+ if (existing) continue;
420
+
421
+ const taskId = db.genId();
422
+ db.execute(
423
+ `INSERT INTO opc_staff_tasks (id, company_id, staff_role, title, description, status, priority, task_type, schedule, assigned_at, created_at)
424
+ VALUES (?, ?, ?, ?, ?, 'pending', 'high', 'auto_trigger', 'on_demand', ?, ?)`,
425
+ taskId, companyId, staffRole,
426
+ `跟进客户 ${contact.name}(逾期)`,
427
+ `客户 ${contact.name} 的跟进日期 ${contact.follow_up_date} 已逾期。\n当前阶段: ${contact.pipeline_stage}\n潜在金额: ${contact.deal_value} 元`,
428
+ now, now,
429
+ );
430
+ created++;
431
+
432
+ }
433
+
434
+ if (created > 0) {
435
+ log(`opc-events: 已为 ${companyId} 创建 ${created} 个客户跟进逾期任务`);
436
+ }
437
+ } catch (err) {
438
+ log(`opc-events: 跟进逾期检测异常: ${err instanceof Error ? err.message : String(err)}`);
439
+ }
440
+ }
441
+
442
+ // ── 映射辅助 ────────────────────────────────────────────────
443
+
444
+ function mapAlertCategoryToRole(category: string): string {
445
+ const mapping: Record<string, string> = {
446
+ tax: "finance",
447
+ cashflow: "finance",
448
+ finance: "finance",
449
+ contract: "legal",
450
+ legal: "legal",
451
+ hr: "hr",
452
+ employee: "hr",
453
+ marketing: "marketing",
454
+ content: "marketing",
455
+ project: "ops",
456
+ ops: "ops",
457
+ };
458
+ return mapping[category] ?? "admin";
459
+ }
460
+
461
+ function mapAlertToTrigger(category: string, severity: string): string {
462
+ if (category === "contract") {
463
+ return severity === "critical" ? "contract_expiry_7d" : "contract_expiry_30d";
464
+ }
465
+ if (category === "tax") {
466
+ return "monthly_close";
467
+ }
468
+ if (category === "cashflow") {
469
+ return "invoice_overdue_7d";
470
+ }
471
+ return "general_alert";
472
+ }