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.
@@ -0,0 +1,332 @@
1
+ /**
2
+ * 星环OPC中心 — 新手引导流程
3
+ *
4
+ * 通过 3 个问题了解用户,生成个性化启动清单。
5
+ */
6
+
7
+ import type { OpcDatabase } from "../db/index.js";
8
+
9
+ /** 引导问题定义 */
10
+ export const ONBOARDING_QUESTIONS = [
11
+ {
12
+ id: "stage",
13
+ question: "你当前处于哪个阶段?",
14
+ options: [
15
+ { value: "idea", label: "想法阶段 - 还在构思商业模式" },
16
+ { value: "preparing", label: "准备注册 - 已有明确方向,准备注册公司" },
17
+ { value: "registered", label: "已注册 - 公司已注册,准备开展业务" },
18
+ { value: "operating", label: "运营中 - 已经开始运营并产生收入" },
19
+ ],
20
+ },
21
+ {
22
+ id: "business_type",
23
+ question: "你的主要业务类型是什么?",
24
+ options: [
25
+ { value: "content", label: "内容创作 - 自媒体、课程、写作等" },
26
+ { value: "tech_service", label: "技术服务 - 开发、设计、咨询等" },
27
+ { value: "product_sales", label: "产品销售 - 电商、实物产品等" },
28
+ { value: "consulting", label: "咨询顾问 - 专业咨询、培训等" },
29
+ { value: "other", label: "其他" },
30
+ ],
31
+ },
32
+ {
33
+ id: "revenue_expectation",
34
+ question: "你预计今年的收入目标是?",
35
+ options: [
36
+ { value: "under_10w", label: "10万以下 - 小额变现或副业" },
37
+ { value: "10_50w", label: "10-50万 - 全职创业基础收入" },
38
+ { value: "50_100w", label: "50-100万 - 稳定业务收入" },
39
+ { value: "over_100w", label: "100万以上 - 规模化发展" },
40
+ ],
41
+ },
42
+ ] as const;
43
+
44
+ /** 引导状态 */
45
+ export type OnboardingState = {
46
+ currentQuestionIndex: number;
47
+ answers: Record<string, string>;
48
+ completed: boolean;
49
+ checklist: OnboardingChecklistItem[];
50
+ };
51
+
52
+ /** 清单项 */
53
+ export type OnboardingChecklistItem = {
54
+ id: string;
55
+ title: string;
56
+ description: string;
57
+ completed: boolean;
58
+ estimatedMinutes: number;
59
+ };
60
+
61
+ /** 获取公司的引导状态 */
62
+ export function getOnboardingState(db: OpcDatabase, companyId: string): OnboardingState | null {
63
+ const company = db.queryOne(
64
+ "SELECT onboarding_data, onboarding_stage, onboarding_completed FROM opc_companies WHERE id = ?",
65
+ companyId,
66
+ ) as { onboarding_data: string; onboarding_stage: string; onboarding_completed: number } | null;
67
+
68
+ if (!company) return null;
69
+
70
+ try {
71
+ const data = JSON.parse(company.onboarding_data || "{}") as Partial<OnboardingState>;
72
+ return {
73
+ currentQuestionIndex: data.currentQuestionIndex ?? 0,
74
+ answers: data.answers ?? {},
75
+ completed: company.onboarding_completed === 1,
76
+ checklist: data.checklist ?? [],
77
+ };
78
+ } catch {
79
+ return {
80
+ currentQuestionIndex: 0,
81
+ answers: {},
82
+ completed: false,
83
+ checklist: [],
84
+ };
85
+ }
86
+ }
87
+
88
+ /** 保存引导状态 */
89
+ export function saveOnboardingState(
90
+ db: OpcDatabase,
91
+ companyId: string,
92
+ state: OnboardingState,
93
+ ): void {
94
+ db.execute(
95
+ `UPDATE opc_companies
96
+ SET onboarding_data = ?,
97
+ onboarding_stage = ?,
98
+ onboarding_completed = ?,
99
+ updated_at = datetime('now')
100
+ WHERE id = ?`,
101
+ JSON.stringify(state),
102
+ state.answers.stage ?? "",
103
+ state.completed ? 1 : 0,
104
+ companyId,
105
+ );
106
+ }
107
+
108
+ /** 回答问题 */
109
+ export function answerQuestion(
110
+ db: OpcDatabase,
111
+ companyId: string,
112
+ questionId: string,
113
+ answer: string,
114
+ ): { success: boolean; message: string; nextQuestion?: typeof ONBOARDING_QUESTIONS[number] } {
115
+ const state = getOnboardingState(db, companyId);
116
+ if (!state) {
117
+ return { success: false, message: "公司不存在" };
118
+ }
119
+
120
+ if (state.completed) {
121
+ return { success: false, message: "引导已完成,无需再次回答" };
122
+ }
123
+
124
+ const currentQuestion = ONBOARDING_QUESTIONS[state.currentQuestionIndex];
125
+ if (!currentQuestion || currentQuestion.id !== questionId) {
126
+ return { success: false, message: "问题 ID 不匹配" };
127
+ }
128
+
129
+ // 验证答案是否有效
130
+ const validOption = currentQuestion.options.find(opt => opt.value === answer);
131
+ if (!validOption) {
132
+ return { success: false, message: "无效的选项" };
133
+ }
134
+
135
+ // 保存答案
136
+ state.answers[questionId] = answer;
137
+ state.currentQuestionIndex++;
138
+
139
+ // 检查是否所有问题都已回答
140
+ if (state.currentQuestionIndex >= ONBOARDING_QUESTIONS.length) {
141
+ // 生成清单
142
+ state.checklist = generateChecklist(state.answers);
143
+ state.completed = false; // 清单未完成
144
+ }
145
+
146
+ saveOnboardingState(db, companyId, state);
147
+
148
+ const nextQuestion = ONBOARDING_QUESTIONS[state.currentQuestionIndex];
149
+ return {
150
+ success: true,
151
+ message: `已记录:${validOption.label}`,
152
+ nextQuestion,
153
+ };
154
+ }
155
+
156
+ /** 生成个性化清单 */
157
+ export function generateChecklist(answers: Record<string, string>): OnboardingChecklistItem[] {
158
+ const stage = answers.stage ?? "idea";
159
+ const businessType = answers.business_type ?? "other";
160
+ const revenue = answers.revenue_expectation ?? "under_10w";
161
+
162
+ const checklist: OnboardingChecklistItem[] = [];
163
+
164
+ // 基础任务(所有阶段)
165
+ if (stage === "idea" || stage === "preparing") {
166
+ checklist.push({
167
+ id: "register_company_info",
168
+ title: "填写公司基本信息",
169
+ description: "使用 opc_manage 工具注册你的公司信息(名称、行业、负责人等)",
170
+ completed: false,
171
+ estimatedMinutes: 5,
172
+ });
173
+ }
174
+
175
+ // OPB 商业画布
176
+ if (stage === "idea" || stage === "preparing") {
177
+ checklist.push({
178
+ id: "complete_opb_canvas",
179
+ title: "完成 OPB 商业画布",
180
+ description: "梳理你的商业模式:目标客户、痛点、解决方案、收入模式等",
181
+ completed: false,
182
+ estimatedMinutes: 30,
183
+ });
184
+ }
185
+
186
+ // 配置 AI 团队
187
+ checklist.push({
188
+ id: "configure_ai_team",
189
+ title: "配置 AI 员工团队",
190
+ description: "根据业务需求启用财务、法务、运营等 AI 员工",
191
+ completed: false,
192
+ estimatedMinutes: 10,
193
+ });
194
+
195
+ // 第一笔记账(已运营)
196
+ if (stage === "operating") {
197
+ checklist.push({
198
+ id: "first_transaction",
199
+ title: "记录第一笔交易",
200
+ description: "使用 opc_manage 添加收入或支出记录,开始财务管理",
201
+ completed: false,
202
+ estimatedMinutes: 5,
203
+ });
204
+ }
205
+
206
+ // 添加客户
207
+ if (businessType === "tech_service" || businessType === "consulting" || businessType === "content") {
208
+ checklist.push({
209
+ id: "add_first_customer",
210
+ title: "添加第一个客户",
211
+ description: "使用 opc_manage 添加潜在客户或现有客户信息",
212
+ completed: false,
213
+ estimatedMinutes: 5,
214
+ });
215
+ }
216
+
217
+ // 创建合同模板(高收入目标)
218
+ if (revenue === "50_100w" || revenue === "over_100w") {
219
+ checklist.push({
220
+ id: "create_contract_template",
221
+ title: "创建服务合同模板",
222
+ description: "使用 opc_legal 创建标准化的服务合同模板",
223
+ completed: false,
224
+ estimatedMinutes: 20,
225
+ });
226
+ }
227
+
228
+ // 内容营销规划(内容创作)
229
+ if (businessType === "content") {
230
+ checklist.push({
231
+ id: "plan_content_strategy",
232
+ title: "规划内容发布计划",
233
+ description: "使用 opc_media 创建内容日历和发布计划",
234
+ completed: false,
235
+ estimatedMinutes: 15,
236
+ });
237
+ }
238
+
239
+ // 税务基础设置(已注册或运营中)
240
+ if (stage === "registered" || stage === "operating") {
241
+ checklist.push({
242
+ id: "setup_tax_basics",
243
+ title: "了解税务基础",
244
+ description: "查看税务日历,了解需要申报的税种和时间节点",
245
+ completed: false,
246
+ estimatedMinutes: 10,
247
+ });
248
+ }
249
+
250
+ // 项目管理(技术服务)
251
+ if (businessType === "tech_service") {
252
+ checklist.push({
253
+ id: "create_first_project",
254
+ title: "创建第一个项目",
255
+ description: "使用 opc_project 管理你的项目进度和预算",
256
+ completed: false,
257
+ estimatedMinutes: 10,
258
+ });
259
+ }
260
+
261
+ // 浏览 Dashboard
262
+ checklist.push({
263
+ id: "explore_dashboard",
264
+ title: "浏览运营看板",
265
+ description: "访问管理后台,了解关键指标和运营状态",
266
+ completed: false,
267
+ estimatedMinutes: 5,
268
+ });
269
+
270
+ return checklist;
271
+ }
272
+
273
+ /** 完成清单项 */
274
+ export function completeChecklistItem(
275
+ db: OpcDatabase,
276
+ companyId: string,
277
+ itemId: string,
278
+ ): { success: boolean; message: string; allCompleted?: boolean } {
279
+ const state = getOnboardingState(db, companyId);
280
+ if (!state) {
281
+ return { success: false, message: "公司不存在" };
282
+ }
283
+
284
+ const item = state.checklist.find(i => i.id === itemId);
285
+ if (!item) {
286
+ return { success: false, message: "清单项不存在" };
287
+ }
288
+
289
+ if (item.completed) {
290
+ return { success: false, message: "该项已完成" };
291
+ }
292
+
293
+ // 标记为完成
294
+ item.completed = true;
295
+
296
+ // 检查是否全部完成
297
+ const allCompleted = state.checklist.every(i => i.completed);
298
+ if (allCompleted) {
299
+ state.completed = true;
300
+ }
301
+
302
+ saveOnboardingState(db, companyId, state);
303
+
304
+ return {
305
+ success: true,
306
+ message: `已完成:${item.title}`,
307
+ allCompleted,
308
+ };
309
+ }
310
+
311
+ /** 获取当前问题 */
312
+ export function getCurrentQuestion(
313
+ db: OpcDatabase,
314
+ companyId: string,
315
+ ): typeof ONBOARDING_QUESTIONS[number] | null {
316
+ const state = getOnboardingState(db, companyId);
317
+ if (!state || state.currentQuestionIndex >= ONBOARDING_QUESTIONS.length) {
318
+ return null;
319
+ }
320
+ return ONBOARDING_QUESTIONS[state.currentQuestionIndex];
321
+ }
322
+
323
+ /** 重置引导(用于重新开始) */
324
+ export function resetOnboarding(db: OpcDatabase, companyId: string): void {
325
+ const initialState: OnboardingState = {
326
+ currentQuestionIndex: 0,
327
+ answers: {},
328
+ completed: false,
329
+ checklist: [],
330
+ };
331
+ saveOnboardingState(db, companyId, initialState);
332
+ }
@@ -21,6 +21,7 @@ import { recalculateAllStages } from "./stage-detector.js";
21
21
  import { saveDailyBriefing } from "./briefing-builder.js";
22
22
  import { TaskExecutor } from "./task-executor.js";
23
23
  import { alertsToStaffTasks, insightsToStaffTasks } from "./event-triggers.js";
24
+ import { generateDailyBrief, formatDailyBriefMarkdown } from "./daily-brief.js";
24
25
 
25
26
  /** 为所有公司保存每日简报快照 */
26
27
  function saveDailyBriefings(db: OpcDatabase, log: (msg: string) => void): void {
@@ -79,6 +80,283 @@ function createScheduledStaffTasks(db: OpcDatabase, log: (msg: string) => void):
79
80
  }
80
81
  }
81
82
 
83
+ /** 逾期款项检测 */
84
+ function detectOverduePayments(db: OpcDatabase, log: (msg: string) => void): void {
85
+ try {
86
+ const today = new Date().toISOString().slice(0, 10);
87
+ const overduePayments = db.query(
88
+ `SELECT id, company_id, direction, counterparty, amount, due_date
89
+ FROM opc_payments
90
+ WHERE status IN ('pending', 'partial')
91
+ AND due_date != '' AND due_date < ?`,
92
+ today,
93
+ ) as { id: string; company_id: string; direction: string; counterparty: string; amount: number; due_date: string }[];
94
+
95
+ let count = 0;
96
+ for (const payment of overduePayments) {
97
+ const daysOverdue = Math.floor((Date.now() - new Date(payment.due_date).getTime()) / 86400000);
98
+
99
+ // 检查是否已有告警
100
+ const existingAlert = db.queryOne(
101
+ `SELECT id FROM opc_alerts
102
+ WHERE company_id = ? AND category = 'overdue_payment'
103
+ AND title LIKE ? AND status = 'active'`,
104
+ payment.company_id,
105
+ `%${payment.counterparty}%`,
106
+ );
107
+
108
+ if (!existingAlert) {
109
+ db.execute(
110
+ `INSERT INTO opc_alerts (id, company_id, title, severity, category, status, message, created_at)
111
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
112
+ db.genId(),
113
+ payment.company_id,
114
+ `逾期${payment.direction === "receivable" ? "应收" : "应付"}款:${payment.counterparty}`,
115
+ daysOverdue > 30 ? "critical" : "warning",
116
+ "overdue_payment",
117
+ "active",
118
+ `${payment.direction === "receivable" ? "应收" : "应付"}款项 ${payment.amount.toLocaleString()} 元已逾期 ${daysOverdue} 天`,
119
+ );
120
+ count++;
121
+ }
122
+ }
123
+
124
+ if (count > 0) log(`opc-proactive: 检测到 ${count} 条逾期款项告警`);
125
+ } catch (err) {
126
+ log(`opc-proactive: 逾期款项检测异常: ${err instanceof Error ? err.message : String(err)}`);
127
+ }
128
+ }
129
+
130
+ /** 合同到期检测 */
131
+ function detectExpiringContracts(db: OpcDatabase, log: (msg: string) => void): void {
132
+ try {
133
+ const today = new Date().toISOString().slice(0, 10);
134
+ const expiringContracts = db.query(
135
+ `SELECT id, company_id, title, counterparty, end_date
136
+ FROM opc_contracts
137
+ WHERE status = 'active'
138
+ AND end_date != ''
139
+ AND end_date >= ? AND end_date <= date(?, '+30 days')`,
140
+ today, today,
141
+ ) as { id: string; company_id: string; title: string; counterparty: string; end_date: string }[];
142
+
143
+ let count = 0;
144
+ for (const contract of expiringContracts) {
145
+ const daysUntilExpiry = Math.floor((new Date(contract.end_date).getTime() - Date.now()) / 86400000);
146
+
147
+ // 检查是否已有告警
148
+ const existingAlert = db.queryOne(
149
+ `SELECT id FROM opc_alerts
150
+ WHERE company_id = ? AND category = 'expiring_contract'
151
+ AND title LIKE ? AND status = 'active'`,
152
+ contract.company_id,
153
+ `%${contract.title}%`,
154
+ );
155
+
156
+ if (!existingAlert) {
157
+ db.execute(
158
+ `INSERT INTO opc_alerts (id, company_id, title, severity, category, status, message, created_at)
159
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
160
+ db.genId(),
161
+ contract.company_id,
162
+ `合同即将到期:${contract.title}`,
163
+ daysUntilExpiry <= 7 ? "critical" : daysUntilExpiry <= 14 ? "warning" : "info",
164
+ "expiring_contract",
165
+ "active",
166
+ `与 ${contract.counterparty} 的合同将在 ${daysUntilExpiry} 天后到期(${contract.end_date}),需要续约或重新协商`,
167
+ );
168
+ count++;
169
+ }
170
+ }
171
+
172
+ if (count > 0) log(`opc-proactive: 检测到 ${count} 条合同到期告警`);
173
+ } catch (err) {
174
+ log(`opc-proactive: 合同到期检测异常: ${err instanceof Error ? err.message : String(err)}`);
175
+ }
176
+ }
177
+
178
+ /** 客户回访检测 */
179
+ function detectStaleCustomers(db: OpcDatabase, log: (msg: string) => void): void {
180
+ try {
181
+ const staleContacts = db.query(
182
+ `SELECT id, company_id, name, last_contact_date, pipeline_stage
183
+ FROM opc_contacts
184
+ WHERE last_contact_date != ''
185
+ AND last_contact_date < date('now', '-60 days')
186
+ AND pipeline_stage NOT IN ('lost', 'won')`,
187
+ ) as { id: string; company_id: string; name: string; last_contact_date: string; pipeline_stage: string }[];
188
+
189
+ let count = 0;
190
+ for (const contact of staleContacts) {
191
+ const daysSinceContact = Math.floor((Date.now() - new Date(contact.last_contact_date).getTime()) / 86400000);
192
+
193
+ // 检查是否已有告警
194
+ const existingAlert = db.queryOne(
195
+ `SELECT id FROM opc_alerts
196
+ WHERE company_id = ? AND category = 'stale_customer'
197
+ AND title LIKE ? AND status = 'active'`,
198
+ contact.company_id,
199
+ `%${contact.name}%`,
200
+ );
201
+
202
+ if (!existingAlert) {
203
+ db.execute(
204
+ `INSERT INTO opc_alerts (id, company_id, title, severity, category, status, message, created_at)
205
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
206
+ db.genId(),
207
+ contact.company_id,
208
+ `客户需要回访:${contact.name}`,
209
+ daysSinceContact > 90 ? "warning" : "info",
210
+ "stale_customer",
211
+ "active",
212
+ `已 ${daysSinceContact} 天未联系,当前阶段:${contact.pipeline_stage}`,
213
+ );
214
+ count++;
215
+ }
216
+ }
217
+
218
+ if (count > 0) log(`opc-proactive: 检测到 ${count} 个客户需要回访`);
219
+ } catch (err) {
220
+ log(`opc-proactive: 客户回访检测异常: ${err instanceof Error ? err.message : String(err)}`);
221
+ }
222
+ }
223
+
224
+ /** 里程碑到期检测(新增:订单闭环功能) */
225
+ function detectMilestonesDue(db: OpcDatabase, log: (msg: string) => void): void {
226
+ try {
227
+ const today = new Date().toISOString().slice(0, 10);
228
+ const in7Days = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
229
+
230
+ // 查询 7 天内到期的里程碑
231
+ const dueMilestones = db.query(
232
+ `SELECT m.id, m.company_id, m.contract_id, m.title, m.due_date, m.amount,
233
+ c.title as contract_title, c.counterparty
234
+ FROM opc_contract_milestones m
235
+ JOIN opc_contracts c ON m.contract_id = c.id
236
+ WHERE m.status IN ('pending', 'in_progress')
237
+ AND m.due_date != '' AND m.due_date BETWEEN ? AND ?`,
238
+ today, in7Days,
239
+ ) as { id: string; company_id: string; contract_id: string; title: string; due_date: string; amount: number; contract_title: string; counterparty: string }[];
240
+
241
+ let count = 0;
242
+ for (const milestone of dueMilestones) {
243
+ const daysUntilDue = Math.floor((new Date(milestone.due_date).getTime() - Date.now()) / 86400000);
244
+
245
+ // 检查是否已有告警
246
+ const existingAlert = db.queryOne(
247
+ `SELECT id FROM opc_alerts
248
+ WHERE company_id = ? AND category = 'milestone_due'
249
+ AND title LIKE ? AND status = 'active'`,
250
+ milestone.company_id,
251
+ `%${milestone.title}%`,
252
+ );
253
+
254
+ if (!existingAlert) {
255
+ db.execute(
256
+ `INSERT INTO opc_alerts (id, company_id, title, severity, category, status, message, created_at)
257
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
258
+ db.genId(),
259
+ milestone.company_id,
260
+ `里程碑即将到期:${milestone.title}`,
261
+ daysUntilDue <= 3 ? "warning" : "info",
262
+ "milestone_due",
263
+ "active",
264
+ `合同「${milestone.contract_title}」的里程碑将在 ${daysUntilDue} 天后到期(${milestone.due_date}),应收款 ${milestone.amount.toLocaleString()} 元`,
265
+ );
266
+ count++;
267
+ }
268
+ }
269
+
270
+ if (count > 0) log(`opc-proactive: 检测到 ${count} 个即将到期的里程碑`);
271
+ } catch (err) {
272
+ log(`opc-proactive: 里程碑到期检测异常: ${err instanceof Error ? err.message : String(err)}`);
273
+ }
274
+ }
275
+
276
+ /** 应收风险分层(新增:订单闭环功能) */
277
+ function classifyPaymentRisks(db: OpcDatabase, log: (msg: string) => void): void {
278
+ try {
279
+ const today = new Date().toISOString().slice(0, 10);
280
+
281
+ // 更新所有应收的逾期天数
282
+ db.exec(
283
+ `UPDATE opc_payments
284
+ SET overdue_days = CAST((julianday('${today}') - julianday(due_date)) AS INTEGER)
285
+ WHERE direction = 'receivable' AND status IN ('pending', 'partial', 'overdue') AND due_date != ''`,
286
+ );
287
+
288
+ // 更新风险等级
289
+ db.exec(
290
+ `UPDATE opc_payments
291
+ SET risk_level = CASE
292
+ WHEN overdue_days <= 7 THEN 'normal'
293
+ WHEN overdue_days BETWEEN 8 AND 30 THEN 'warning'
294
+ ELSE 'critical'
295
+ END
296
+ WHERE direction = 'receivable' AND status IN ('pending', 'partial', 'overdue')`,
297
+ );
298
+
299
+ // 统计各风险等级
300
+ const riskStats = db.query(
301
+ `SELECT risk_level, COUNT(*) as count, SUM(amount - paid_amount) as total_amount
302
+ FROM opc_payments
303
+ WHERE direction = 'receivable' AND status IN ('pending', 'partial', 'overdue')
304
+ GROUP BY risk_level`,
305
+ ) as { risk_level: string; count: number; total_amount: number }[];
306
+
307
+ let warningCount = 0;
308
+ let criticalCount = 0;
309
+
310
+ for (const stat of riskStats) {
311
+ if (stat.risk_level === "warning") warningCount = stat.count;
312
+ if (stat.risk_level === "critical") criticalCount = stat.count;
313
+ }
314
+
315
+ if (warningCount > 0 || criticalCount > 0) {
316
+ log(`opc-proactive: 应收风险分层完成 - 预警: ${warningCount} 笔, 严重: ${criticalCount} 笔`);
317
+ }
318
+
319
+ // 为严重逾期的应收创建告警
320
+ const criticalPayments = db.query(
321
+ `SELECT id, company_id, counterparty, amount, paid_amount, overdue_days
322
+ FROM opc_payments
323
+ WHERE risk_level = 'critical' AND direction = 'receivable'`,
324
+ ) as { id: string; company_id: string; counterparty: string; amount: number; paid_amount: number; overdue_days: number }[];
325
+
326
+ let alertCount = 0;
327
+ for (const payment of criticalPayments) {
328
+ // 检查是否已有告警
329
+ const existingAlert = db.queryOne(
330
+ `SELECT id FROM opc_alerts
331
+ WHERE company_id = ? AND category = 'critical_receivable'
332
+ AND title LIKE ? AND status = 'active'`,
333
+ payment.company_id,
334
+ `%${payment.counterparty}%`,
335
+ );
336
+
337
+ if (!existingAlert) {
338
+ const remainingAmount = payment.amount - payment.paid_amount;
339
+ db.execute(
340
+ `INSERT INTO opc_alerts (id, company_id, title, severity, category, status, message, created_at)
341
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
342
+ db.genId(),
343
+ payment.company_id,
344
+ `严重逾期应收:${payment.counterparty}`,
345
+ "critical",
346
+ "critical_receivable",
347
+ "active",
348
+ `应收款 ${remainingAmount.toLocaleString()} 元已逾期 ${payment.overdue_days} 天,建议升级催收或法律途径`,
349
+ );
350
+ alertCount++;
351
+ }
352
+ }
353
+
354
+ if (alertCount > 0) log(`opc-proactive: 创建 ${alertCount} 条严重逾期应收告警`);
355
+ } catch (err) {
356
+ log(`opc-proactive: 应收风险分层异常: ${err instanceof Error ? err.message : String(err)}`);
357
+ }
358
+ }
359
+
82
360
  /** 清理超过 2 小时仍 in_progress 且有 session_key 的任务(subagent_ended 未触发时的兜底) */
83
361
  function reapStaleTasks(db: OpcDatabase, log: (msg: string) => void): void {
84
362
  try {
@@ -134,13 +412,22 @@ function runFullScan(db: OpcDatabase, log: (msg: string) => void, webhookUrl?: s
134
412
  // 7. 创建定时员工任务
135
413
  createScheduledStaffTasks(db, log);
136
414
 
137
- // 8. 清理超时的 in_progress 任务
415
+ // 8. 新增检测:逾期款项、合同到期、客户回访
416
+ detectOverduePayments(db, log);
417
+ detectExpiringContracts(db, log);
418
+ detectStaleCustomers(db, log);
419
+
420
+ // 8.1. 订单闭环检测:里程碑到期、应收风险分层
421
+ detectMilestonesDue(db, log);
422
+ classifyPaymentRisks(db, log);
423
+
424
+ // 9. 清理超时的 in_progress 任务
138
425
  reapStaleTasks(db, log);
139
426
 
140
- // 9. 将新告警自动分配给对应 AI 员工
427
+ // 10. 将新告警自动分配给对应 AI 员工
141
428
  alertsToStaffTasks(db, log);
142
429
 
143
- // 10. 将可执行洞察转化为员工待办
430
+ // 11. 将可执行洞察转化为员工待办
144
431
  insightsToStaffTasks(db, log);
145
432
 
146
433
  log("opc-proactive: 全量扫描完成");