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,702 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — 智能分析引擎
|
|
3
|
+
*
|
|
4
|
+
* P0 阶段实现 3 个分析器:
|
|
5
|
+
* 1. analyzeDataGaps — 检查缺失数据并建议补全
|
|
6
|
+
* 2. analyzeFinanceTrends — 财务趋势分析
|
|
7
|
+
* 3. generateNextSteps — 根据阶段生成建议
|
|
8
|
+
*
|
|
9
|
+
* 所有洞察写入 opc_insights 表,带防重复和自动过期逻辑。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { OpcDatabase } from "../db/index.js";
|
|
13
|
+
import { detectCompanyStage, type CompanyStage } from "./stage-detector.js";
|
|
14
|
+
|
|
15
|
+
type CountRow = { cnt: number };
|
|
16
|
+
type SumRow = { total: number };
|
|
17
|
+
type InsightRow = { id: string };
|
|
18
|
+
|
|
19
|
+
// ── 过期天数配置 ──────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const EXPIRY_DAYS: Record<string, number> = {
|
|
22
|
+
data_gap: 7,
|
|
23
|
+
trend: 3,
|
|
24
|
+
next_step: 14,
|
|
25
|
+
staff_observation: 7,
|
|
26
|
+
risk: 3,
|
|
27
|
+
opportunity: 7,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ── 防重复 ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/** 同 company_id + insight_type + title 前 20 字符在 7 天内不重复写入 */
|
|
33
|
+
function insightExists(db: OpcDatabase, companyId: string, insightType: string, title: string): boolean {
|
|
34
|
+
const titleKey = title.slice(0, 20);
|
|
35
|
+
const cutoff = new Date();
|
|
36
|
+
cutoff.setDate(cutoff.getDate() - 7);
|
|
37
|
+
const cutoffStr = cutoff.toISOString();
|
|
38
|
+
|
|
39
|
+
const row = db.queryOne(
|
|
40
|
+
`SELECT id FROM opc_insights
|
|
41
|
+
WHERE company_id = ? AND insight_type = ? AND SUBSTR(title, 1, 20) = ? AND created_at >= ? AND status = 'active'`,
|
|
42
|
+
companyId, insightType, titleKey, cutoffStr,
|
|
43
|
+
) as InsightRow | null;
|
|
44
|
+
return row !== null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 写入洞察 */
|
|
48
|
+
function createInsight(
|
|
49
|
+
db: OpcDatabase,
|
|
50
|
+
companyId: string,
|
|
51
|
+
params: {
|
|
52
|
+
insightType: string;
|
|
53
|
+
category: string;
|
|
54
|
+
priority: number;
|
|
55
|
+
title: string;
|
|
56
|
+
message: string;
|
|
57
|
+
actionHint?: string;
|
|
58
|
+
staffRole?: string;
|
|
59
|
+
},
|
|
60
|
+
): void {
|
|
61
|
+
if (insightExists(db, companyId, params.insightType, params.title)) return;
|
|
62
|
+
|
|
63
|
+
const expiryDays = EXPIRY_DAYS[params.insightType] ?? 7;
|
|
64
|
+
const expiresAt = new Date();
|
|
65
|
+
expiresAt.setDate(expiresAt.getDate() + expiryDays);
|
|
66
|
+
const now = new Date().toISOString();
|
|
67
|
+
|
|
68
|
+
db.execute(
|
|
69
|
+
`INSERT INTO opc_insights
|
|
70
|
+
(id, company_id, insight_type, category, priority, title, message, action_hint, staff_role, status, expires_at, created_at, updated_at)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)`,
|
|
72
|
+
db.genId(), companyId, params.insightType, params.category, params.priority,
|
|
73
|
+
params.title, params.message, params.actionHint ?? "", params.staffRole ?? "",
|
|
74
|
+
expiresAt.toISOString(), now, now,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── 分析器 1: 数据缺口分析 ───────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function analyzeDataGaps(db: OpcDatabase, companyId: string): void {
|
|
81
|
+
// 无交易
|
|
82
|
+
const txCount = (db.queryOne(
|
|
83
|
+
"SELECT COUNT(*) as cnt FROM opc_transactions WHERE company_id = ?", companyId,
|
|
84
|
+
) as CountRow).cnt;
|
|
85
|
+
if (txCount === 0) {
|
|
86
|
+
createInsight(db, companyId, {
|
|
87
|
+
insightType: "data_gap", category: "finance", priority: 80,
|
|
88
|
+
title: "还没有记录收支",
|
|
89
|
+
message: "公司目前没有任何收支记录。建议记录第一笔交易,无论是收入还是支出,这是财务管理的第一步。",
|
|
90
|
+
actionHint: "使用 opc_finance 工具的 record_transaction 记录第一笔收支",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 无客户
|
|
95
|
+
const contactCount = (db.queryOne(
|
|
96
|
+
"SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?", companyId,
|
|
97
|
+
) as CountRow).cnt;
|
|
98
|
+
if (contactCount === 0) {
|
|
99
|
+
createInsight(db, companyId, {
|
|
100
|
+
insightType: "data_gap", category: "marketing", priority: 70,
|
|
101
|
+
title: "客户池为空",
|
|
102
|
+
message: "还没有添加任何客户/联系人。客户是一人企业的核心资产,建议尽早开始建立客户池。",
|
|
103
|
+
actionHint: "使用 opc_manage 工具的 add_contact 添加第一个客户",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 无画布
|
|
108
|
+
const canvasCount = (db.queryOne(
|
|
109
|
+
"SELECT COUNT(*) as cnt FROM opc_opb_canvas WHERE company_id = ?", companyId,
|
|
110
|
+
) as CountRow).cnt;
|
|
111
|
+
if (canvasCount === 0) {
|
|
112
|
+
createInsight(db, companyId, {
|
|
113
|
+
insightType: "data_gap", category: "growth", priority: 90,
|
|
114
|
+
title: "建议用 OPB 画布梳理商业模式",
|
|
115
|
+
message: "OPB 画布是一人企业方法论的核心规划工具,可以帮你系统梳理目标客户、价值主张、收入模式等关键要素。",
|
|
116
|
+
actionHint: "使用 opc_opb 工具创建画布,或让 AI 助手帮你填写",
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
// 画布不完整
|
|
120
|
+
const canvas = db.queryOne(
|
|
121
|
+
"SELECT * FROM opc_opb_canvas WHERE company_id = ?", companyId,
|
|
122
|
+
) as Record<string, string> | null;
|
|
123
|
+
if (canvas) {
|
|
124
|
+
const fields = [
|
|
125
|
+
"track", "target_customer", "pain_point", "solution",
|
|
126
|
+
"unique_value", "channels", "revenue_model", "cost_structure",
|
|
127
|
+
"key_resources", "key_activities", "key_partners", "unfair_advantage",
|
|
128
|
+
"metrics", "non_compete", "scaling_strategy", "notes",
|
|
129
|
+
];
|
|
130
|
+
const filled = fields.filter(f => canvas[f] && canvas[f].trim() !== "").length;
|
|
131
|
+
const pct = Math.round((filled / fields.length) * 100);
|
|
132
|
+
if (pct < 100) {
|
|
133
|
+
const missing = fields.filter(f => !canvas[f] || canvas[f].trim() === "");
|
|
134
|
+
createInsight(db, companyId, {
|
|
135
|
+
insightType: "data_gap", category: "growth", priority: 60,
|
|
136
|
+
title: `OPB 画布已完成 ${pct}%`,
|
|
137
|
+
message: `画布还缺少以下字段:${missing.join("、")}。完善画布有助于更清晰地规划业务方向。`,
|
|
138
|
+
actionHint: "使用 opc_opb 工具更新画布字段",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 无 AI 员工
|
|
145
|
+
const staffCount = (db.queryOne(
|
|
146
|
+
"SELECT COUNT(*) as cnt FROM opc_staff_config WHERE company_id = ? AND enabled = 1",
|
|
147
|
+
companyId,
|
|
148
|
+
) as CountRow).cnt;
|
|
149
|
+
if (staffCount === 0) {
|
|
150
|
+
createInsight(db, companyId, {
|
|
151
|
+
insightType: "data_gap", category: "ops", priority: 65,
|
|
152
|
+
title: "建议初始化 AI 员工团队",
|
|
153
|
+
message: "AI 员工可以帮你处理财务、法务、HR、市场等专业工作。初始化后,你可以像管理真实团队一样分派任务。",
|
|
154
|
+
actionHint: "使用 opc_staff 工具的 init_default 一键初始化默认岗位",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 无内容
|
|
159
|
+
const contentCount = (db.queryOne(
|
|
160
|
+
"SELECT COUNT(*) as cnt FROM opc_media_content WHERE company_id = ?", companyId,
|
|
161
|
+
) as CountRow).cnt;
|
|
162
|
+
if (contentCount === 0 && txCount > 0) {
|
|
163
|
+
// 只有在已经有交易活动时才建议内容营销
|
|
164
|
+
createInsight(db, companyId, {
|
|
165
|
+
insightType: "data_gap", category: "marketing", priority: 40,
|
|
166
|
+
title: "建议开始内容营销",
|
|
167
|
+
message: "一人企业的增长引擎之一是内容营销。建议开始创作和发布内容,建立个人品牌和获客渠道。",
|
|
168
|
+
actionHint: "使用 opc_media 工具创建内容",
|
|
169
|
+
staffRole: "marketing",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── 分析器 2: 财务趋势分析 ───────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
type MonthlyRow = { month: string; income: number; expense: number };
|
|
177
|
+
|
|
178
|
+
function analyzeFinanceTrends(db: OpcDatabase, companyId: string): void {
|
|
179
|
+
// 获取近 3 个月的月度数据
|
|
180
|
+
const months = db.query(
|
|
181
|
+
`SELECT strftime('%Y-%m', transaction_date) as month,
|
|
182
|
+
COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END), 0) as income,
|
|
183
|
+
COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END), 0) as expense
|
|
184
|
+
FROM opc_transactions
|
|
185
|
+
WHERE company_id = ?
|
|
186
|
+
GROUP BY month
|
|
187
|
+
ORDER BY month DESC
|
|
188
|
+
LIMIT 3`,
|
|
189
|
+
companyId,
|
|
190
|
+
) as MonthlyRow[];
|
|
191
|
+
|
|
192
|
+
if (months.length < 2) return;
|
|
193
|
+
|
|
194
|
+
const current = months[0];
|
|
195
|
+
const previous = months[1];
|
|
196
|
+
|
|
197
|
+
// 月环比收入变化(> 20% 触发)
|
|
198
|
+
if (previous.income > 0) {
|
|
199
|
+
const changeRate = (current.income - previous.income) / previous.income;
|
|
200
|
+
if (changeRate > 0.2) {
|
|
201
|
+
createInsight(db, companyId, {
|
|
202
|
+
insightType: "trend", category: "finance", priority: 70,
|
|
203
|
+
title: `收入增长 ${Math.round(changeRate * 100)}%`,
|
|
204
|
+
message: `本月收入 ${current.income.toLocaleString()} 元,较上月增长 ${Math.round(changeRate * 100)}%。增长势头良好,建议关注可持续性。`,
|
|
205
|
+
staffRole: "finance",
|
|
206
|
+
});
|
|
207
|
+
} else if (changeRate < -0.2) {
|
|
208
|
+
createInsight(db, companyId, {
|
|
209
|
+
insightType: "risk", category: "finance", priority: 85,
|
|
210
|
+
title: `收入下降 ${Math.round(Math.abs(changeRate) * 100)}%`,
|
|
211
|
+
message: `本月收入 ${current.income.toLocaleString()} 元,较上月下降 ${Math.round(Math.abs(changeRate) * 100)}%。建议分析下降原因并制定应对措施。`,
|
|
212
|
+
staffRole: "finance",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 收入来源单一检测
|
|
218
|
+
const counterparties = db.query(
|
|
219
|
+
`SELECT counterparty, SUM(amount) as total
|
|
220
|
+
FROM opc_transactions
|
|
221
|
+
WHERE company_id = ? AND type = 'income' AND counterparty != ''
|
|
222
|
+
GROUP BY counterparty
|
|
223
|
+
ORDER BY total DESC`,
|
|
224
|
+
companyId,
|
|
225
|
+
) as { counterparty: string; total: number }[];
|
|
226
|
+
|
|
227
|
+
if (counterparties.length > 0) {
|
|
228
|
+
const totalIncome = (db.queryOne(
|
|
229
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'",
|
|
230
|
+
companyId,
|
|
231
|
+
) as SumRow).total;
|
|
232
|
+
|
|
233
|
+
if (totalIncome > 0 && counterparties[0].total / totalIncome > 0.8 && counterparties.length >= 1) {
|
|
234
|
+
createInsight(db, companyId, {
|
|
235
|
+
insightType: "risk", category: "finance", priority: 75,
|
|
236
|
+
title: "收入来源过于集中",
|
|
237
|
+
message: `${counterparties[0].counterparty} 贡献了超过 80% 的收入。单一客户依赖是一人企业的常见风险,建议拓展更多客户来源。`,
|
|
238
|
+
staffRole: "finance",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 连续支出无收入
|
|
244
|
+
if (current.income === 0 && current.expense > 0 && previous.income === 0 && previous.expense > 0) {
|
|
245
|
+
createInsight(db, companyId, {
|
|
246
|
+
insightType: "risk", category: "finance", priority: 80,
|
|
247
|
+
title: "连续两月无收入但有支出",
|
|
248
|
+
message: `已连续两个月只有支出没有收入。总支出 ${(current.expense + previous.expense).toLocaleString()} 元。建议关注现金流状况。`,
|
|
249
|
+
staffRole: "finance",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 本月尚无收入(月中提醒,15号之后)
|
|
254
|
+
const today = new Date();
|
|
255
|
+
if (today.getDate() >= 15) {
|
|
256
|
+
const thisMonth = today.toISOString().slice(0, 7);
|
|
257
|
+
const thisMonthIncome = (db.queryOne(
|
|
258
|
+
`SELECT COALESCE(SUM(amount), 0) as total
|
|
259
|
+
FROM opc_transactions
|
|
260
|
+
WHERE company_id = ? AND type = 'income' AND strftime('%Y-%m', transaction_date) = ?`,
|
|
261
|
+
companyId, thisMonth,
|
|
262
|
+
) as SumRow).total;
|
|
263
|
+
|
|
264
|
+
// 只在之前有过收入的公司才提醒
|
|
265
|
+
const hasHistoricalIncome = (db.queryOne(
|
|
266
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'",
|
|
267
|
+
companyId,
|
|
268
|
+
) as SumRow).total > 0;
|
|
269
|
+
|
|
270
|
+
if (thisMonthIncome === 0 && hasHistoricalIncome) {
|
|
271
|
+
createInsight(db, companyId, {
|
|
272
|
+
insightType: "trend", category: "finance", priority: 60,
|
|
273
|
+
title: "本月尚无收入记录",
|
|
274
|
+
message: `已过月中,本月尚未记录任何收入。如果有收入发生但未记录,建议及时补录。`,
|
|
275
|
+
staffRole: "finance",
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── 分析器 3: 阶段性建议 ─────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
type PlaybookItem = {
|
|
284
|
+
title: string;
|
|
285
|
+
message: string;
|
|
286
|
+
category: string;
|
|
287
|
+
priority: number;
|
|
288
|
+
/** 返回 true 表示用户尚未完成此项,需要推送 */
|
|
289
|
+
check: (db: OpcDatabase, companyId: string) => boolean;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const STAGE_PLAYBOOKS: Record<CompanyStage, PlaybookItem[]> = {
|
|
293
|
+
idea: [
|
|
294
|
+
{
|
|
295
|
+
title: "明确你的赛道方向",
|
|
296
|
+
message: "用 OPB 画布梳理你的赛道选择,聚焦「小众强需求」比「大众弱需求」更适合一人企业。",
|
|
297
|
+
category: "growth", priority: 90,
|
|
298
|
+
check: (db, cid) => (db.queryOne("SELECT COUNT(*) as cnt FROM opc_opb_canvas WHERE company_id = ?", cid) as CountRow).cnt === 0,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
title: "找到你的第一个潜在客户",
|
|
302
|
+
message: "在正式开始之前,至少找到 1 个可能愿意付费的潜在客户,验证需求是否真实存在。",
|
|
303
|
+
category: "marketing", priority: 80,
|
|
304
|
+
check: (db, cid) => (db.queryOne("SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?", cid) as CountRow).cnt === 0,
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
title: "初始化 AI 员工团队",
|
|
308
|
+
message: "配置 AI 员工可以帮你处理专业工作,让你聚焦核心业务。",
|
|
309
|
+
category: "ops", priority: 70,
|
|
310
|
+
check: (db, cid) => (db.queryOne("SELECT COUNT(*) as cnt FROM opc_staff_config WHERE company_id = ? AND enabled = 1", cid) as CountRow).cnt === 0,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
validation: [
|
|
314
|
+
{
|
|
315
|
+
title: "获得第一笔收入",
|
|
316
|
+
message: "验证阶段最重要的目标是证明有人愿意为你的产品/服务付费。哪怕是很小的金额,也是最关键的验证。",
|
|
317
|
+
category: "finance", priority: 95,
|
|
318
|
+
check: (db, cid) => (db.queryOne("SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", cid) as SumRow).total === 0,
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
title: "完善 OPB 商业画布",
|
|
322
|
+
message: "通过验证过程中的发现,更新和完善你的商业画布,让商业模式更清晰。",
|
|
323
|
+
category: "growth", priority: 75,
|
|
324
|
+
check: (db, cid) => {
|
|
325
|
+
const canvas = db.queryOne("SELECT * FROM opc_opb_canvas WHERE company_id = ?", cid) as Record<string, string> | null;
|
|
326
|
+
if (!canvas) return true;
|
|
327
|
+
const fields = ["track", "target_customer", "pain_point", "solution", "unique_value", "revenue_model"];
|
|
328
|
+
return fields.some(f => !canvas[f] || canvas[f].trim() === "");
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
title: "签订第一份合同",
|
|
333
|
+
message: "正式的合同关系有助于保护你的权益,也标志着业务正规化。",
|
|
334
|
+
category: "legal", priority: 70,
|
|
335
|
+
check: (db, cid) => (db.queryOne("SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ?", cid) as CountRow).cnt === 0,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
early_revenue: [
|
|
339
|
+
{
|
|
340
|
+
title: "建立稳定的收入来源",
|
|
341
|
+
message: "有了第一笔收入后,下一个目标是让收入可持续。思考如何让客户持续付费或带来新客户。",
|
|
342
|
+
category: "finance", priority: 90,
|
|
343
|
+
check: (db, cid) => (db.queryOne("SELECT COUNT(DISTINCT strftime('%Y-%m', transaction_date)) as cnt FROM opc_transactions WHERE company_id = ? AND type = 'income' AND amount > 0", cid) as CountRow).cnt < 2,
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
title: "开始记录支出以掌握成本",
|
|
347
|
+
message: "了解你的成本结构是实现盈利的前提。建议开始系统记录各项支出。",
|
|
348
|
+
category: "finance", priority: 75,
|
|
349
|
+
check: (db, cid) => (db.queryOne("SELECT COUNT(*) as cnt FROM opc_transactions WHERE company_id = ? AND type = 'expense'", cid) as CountRow).cnt === 0,
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
title: "开始内容营销建立品牌",
|
|
353
|
+
message: "一人企业的增长引擎是内容。开始在你擅长的平台上分享专业内容,吸引潜在客户。",
|
|
354
|
+
category: "marketing", priority: 65,
|
|
355
|
+
check: (db, cid) => (db.queryOne("SELECT COUNT(*) as cnt FROM opc_media_content WHERE company_id = ?", cid) as CountRow).cnt === 0,
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
growth: [
|
|
359
|
+
{
|
|
360
|
+
title: "拓展客户来源,避免单一依赖",
|
|
361
|
+
message: "增长阶段要注意分散风险。确保不超过 80% 的收入来自单一客户。",
|
|
362
|
+
category: "marketing", priority: 85,
|
|
363
|
+
check: (db, cid) => {
|
|
364
|
+
const rows = db.query("SELECT counterparty, SUM(amount) as total FROM opc_transactions WHERE company_id = ? AND type = 'income' AND counterparty != '' GROUP BY counterparty ORDER BY total DESC", cid) as { total: number }[];
|
|
365
|
+
const totalIncome = (db.queryOne("SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", cid) as SumRow).total;
|
|
366
|
+
return rows.length > 0 && totalIncome > 0 && rows[0].total / totalIncome > 0.8;
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
title: "建立项目管理流程",
|
|
371
|
+
message: "随着业务增长,需要系统化管理项目和任务,避免遗漏和延期。",
|
|
372
|
+
category: "ops", priority: 70,
|
|
373
|
+
check: (db, cid) => (db.queryOne("SELECT COUNT(*) as cnt FROM opc_projects WHERE company_id = ?", cid) as CountRow).cnt === 0,
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
title: "考虑合规和税务筹划",
|
|
377
|
+
message: "收入增长后,税务筹划变得重要。建议开始系统管理税务申报。",
|
|
378
|
+
category: "finance", priority: 65,
|
|
379
|
+
check: (db, cid) => (db.queryOne("SELECT COUNT(*) as cnt FROM opc_tax_filings WHERE company_id = ?", cid) as CountRow).cnt === 0,
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
stable: [
|
|
383
|
+
{
|
|
384
|
+
title: "优化利润率",
|
|
385
|
+
message: "稳定运营阶段应关注利润率而非单纯收入增长。审视成本结构,找到优化空间。",
|
|
386
|
+
category: "finance", priority: 80,
|
|
387
|
+
check: () => true, // 通用建议,始终显示
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
title: "建立标准化服务流程",
|
|
391
|
+
message: "将你的服务标准化和产品化,降低对个人时间的依赖,为规模化做准备。",
|
|
392
|
+
category: "ops", priority: 75,
|
|
393
|
+
check: (db, cid) => (db.queryOne("SELECT COUNT(*) as cnt FROM opc_services WHERE company_id = ?", cid) as CountRow).cnt === 0,
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
scaling: [
|
|
397
|
+
{
|
|
398
|
+
title: "考虑团队扩展",
|
|
399
|
+
message: "规模化阶段可能需要引入外包或兼职人员。一人企业不意味着所有事都自己做。",
|
|
400
|
+
category: "hr", priority: 80,
|
|
401
|
+
check: () => true,
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
title: "评估融资需求",
|
|
405
|
+
message: "如果增长受限于资金,可以考虑天使投资或银行信贷来加速扩张。",
|
|
406
|
+
category: "finance", priority: 70,
|
|
407
|
+
check: (db, cid) => (db.queryOne("SELECT COUNT(*) as cnt FROM opc_investment_rounds WHERE company_id = ?", cid) as CountRow).cnt === 0,
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
exit: [
|
|
411
|
+
{
|
|
412
|
+
title: "整理公司资产和数据",
|
|
413
|
+
message: "退出阶段需要整理好所有资产、合同、知识产权等,为交割做准备。",
|
|
414
|
+
category: "ops", priority: 90,
|
|
415
|
+
check: () => true,
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
function generateNextSteps(db: OpcDatabase, companyId: string): void {
|
|
421
|
+
const stageResult = detectCompanyStage(db, companyId);
|
|
422
|
+
const playbook = STAGE_PLAYBOOKS[stageResult.stage] ?? [];
|
|
423
|
+
|
|
424
|
+
for (const item of playbook) {
|
|
425
|
+
if (item.check(db, companyId)) {
|
|
426
|
+
createInsight(db, companyId, {
|
|
427
|
+
insightType: "next_step",
|
|
428
|
+
category: item.category,
|
|
429
|
+
priority: item.priority,
|
|
430
|
+
title: item.title,
|
|
431
|
+
message: item.message,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── 分析器 4: AI 员工观察 ─────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
function analyzeStaffObservations(db: OpcDatabase, companyId: string): void {
|
|
440
|
+
// ── 财务顾问观察 ──
|
|
441
|
+
// 本季度无税务申报
|
|
442
|
+
const quarter = Math.floor(new Date().getMonth() / 3) + 1;
|
|
443
|
+
const year = new Date().getFullYear();
|
|
444
|
+
const quarterStr = `${year}Q${quarter}`;
|
|
445
|
+
const taxFiled = (db.queryOne(
|
|
446
|
+
"SELECT COUNT(*) as cnt FROM opc_tax_filings WHERE company_id = ? AND period LIKE ?",
|
|
447
|
+
companyId, `${year}%`,
|
|
448
|
+
) as CountRow).cnt;
|
|
449
|
+
const hasIncome = (db.queryOne(
|
|
450
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'",
|
|
451
|
+
companyId,
|
|
452
|
+
) as SumRow).total > 0;
|
|
453
|
+
if (taxFiled === 0 && hasIncome) {
|
|
454
|
+
createInsight(db, companyId, {
|
|
455
|
+
insightType: "staff_observation", category: "finance", priority: 72,
|
|
456
|
+
title: `${quarterStr} 尚未申报税务`,
|
|
457
|
+
message: `公司已有收入但本年度尚未创建任何税务申报记录。建议及时了解纳税义务,避免逾期罚款。`,
|
|
458
|
+
staffRole: "finance",
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 未开票收入检测
|
|
463
|
+
const invoicedTotal = (db.queryOne(
|
|
464
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_invoices WHERE company_id = ? AND type = 'sales'",
|
|
465
|
+
companyId,
|
|
466
|
+
) as SumRow).total;
|
|
467
|
+
const incomeTotal = (db.queryOne(
|
|
468
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'",
|
|
469
|
+
companyId,
|
|
470
|
+
) as SumRow).total;
|
|
471
|
+
if (incomeTotal > 0 && invoicedTotal < incomeTotal * 0.5) {
|
|
472
|
+
const gap = incomeTotal - invoicedTotal;
|
|
473
|
+
createInsight(db, companyId, {
|
|
474
|
+
insightType: "staff_observation", category: "finance", priority: 68,
|
|
475
|
+
title: `约 ${gap.toLocaleString()} 元收入未开票`,
|
|
476
|
+
message: `累计收入 ${incomeTotal.toLocaleString()} 元中,仅开票 ${invoicedTotal.toLocaleString()} 元。建议核实是否有遗漏开票。`,
|
|
477
|
+
staffRole: "finance",
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── 法务助手观察 ──
|
|
482
|
+
// 合同缺少风险备注
|
|
483
|
+
const riskyContracts = db.query(
|
|
484
|
+
"SELECT id, title FROM opc_contracts WHERE company_id = ? AND risk_notes = '' AND status = 'active'",
|
|
485
|
+
companyId,
|
|
486
|
+
) as { id: string; title: string }[];
|
|
487
|
+
if (riskyContracts.length > 0) {
|
|
488
|
+
createInsight(db, companyId, {
|
|
489
|
+
insightType: "staff_observation", category: "legal", priority: 65,
|
|
490
|
+
title: `${riskyContracts.length} 份活跃合同缺少风险备注`,
|
|
491
|
+
message: `合同「${riskyContracts[0].title}」等 ${riskyContracts.length} 份活跃合同未填写风险备注。建议评估合同风险点并记录。`,
|
|
492
|
+
staffRole: "legal",
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 即将到期合同(30天内)
|
|
497
|
+
const soonExpiring = db.query(
|
|
498
|
+
`SELECT title, end_date FROM opc_contracts
|
|
499
|
+
WHERE company_id = ? AND status = 'active' AND end_date != ''
|
|
500
|
+
AND end_date <= date('now', '+30 days') AND end_date >= date('now')
|
|
501
|
+
ORDER BY end_date LIMIT 3`,
|
|
502
|
+
companyId,
|
|
503
|
+
) as { title: string; end_date: string }[];
|
|
504
|
+
if (soonExpiring.length > 0) {
|
|
505
|
+
const list = soonExpiring.map(c => `「${c.title}」(${c.end_date})`).join("、");
|
|
506
|
+
createInsight(db, companyId, {
|
|
507
|
+
insightType: "staff_observation", category: "legal", priority: 78,
|
|
508
|
+
title: `${soonExpiring.length} 份合同即将到期`,
|
|
509
|
+
message: `以下合同将在 30 天内到期:${list}。请提前安排续签或终止事宜。`,
|
|
510
|
+
staffRole: "legal",
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── 市场专员观察 ──
|
|
515
|
+
// 本月无内容发布
|
|
516
|
+
const thisMonth = new Date().toISOString().slice(0, 7);
|
|
517
|
+
const contentThisMonth = (db.queryOne(
|
|
518
|
+
`SELECT COUNT(*) as cnt FROM opc_media_content
|
|
519
|
+
WHERE company_id = ? AND strftime('%Y-%m', COALESCE(NULLIF(published_date,''), created_at)) = ?`,
|
|
520
|
+
companyId, thisMonth,
|
|
521
|
+
) as CountRow).cnt;
|
|
522
|
+
const totalContent = (db.queryOne(
|
|
523
|
+
"SELECT COUNT(*) as cnt FROM opc_media_content WHERE company_id = ?", companyId,
|
|
524
|
+
) as CountRow).cnt;
|
|
525
|
+
if (totalContent > 0 && contentThisMonth === 0) {
|
|
526
|
+
createInsight(db, companyId, {
|
|
527
|
+
insightType: "staff_observation", category: "marketing", priority: 55,
|
|
528
|
+
title: "本月尚未发布任何内容",
|
|
529
|
+
message: "上月有内容发布但本月还没有。持续的内容输出是个人品牌建设的关键,建议保持发布节奏。",
|
|
530
|
+
staffRole: "marketing",
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── HR 专员观察 ──
|
|
535
|
+
// 薪酬异常检测
|
|
536
|
+
const hrRecords = db.query(
|
|
537
|
+
"SELECT employee_name, salary FROM opc_hr_records WHERE company_id = ? AND status = 'active' AND salary > 0",
|
|
538
|
+
companyId,
|
|
539
|
+
) as { employee_name: string; salary: number }[];
|
|
540
|
+
if (hrRecords.length >= 2) {
|
|
541
|
+
const avg = hrRecords.reduce((s, r) => s + r.salary, 0) / hrRecords.length;
|
|
542
|
+
const outliers = hrRecords.filter(r => r.salary > avg * 2 || r.salary < avg * 0.3);
|
|
543
|
+
if (outliers.length > 0) {
|
|
544
|
+
createInsight(db, companyId, {
|
|
545
|
+
insightType: "staff_observation", category: "hr", priority: 60,
|
|
546
|
+
title: `${outliers.length} 名员工薪酬偏离均值`,
|
|
547
|
+
message: `团队平均薪酬 ${Math.round(avg).toLocaleString()} 元,${outliers.map(o => o.employee_name).join("、")} 的薪酬显著偏离。建议核实是否合理。`,
|
|
548
|
+
staffRole: "hr",
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ── 运营总监观察 ──
|
|
554
|
+
// 逾期任务
|
|
555
|
+
const overdueTasks = (db.queryOne(
|
|
556
|
+
`SELECT COUNT(*) as cnt FROM opc_tasks
|
|
557
|
+
WHERE company_id = ? AND status NOT IN ('done','completed','cancelled')
|
|
558
|
+
AND due_date != '' AND due_date < date('now')`,
|
|
559
|
+
companyId,
|
|
560
|
+
) as CountRow).cnt;
|
|
561
|
+
if (overdueTasks > 0) {
|
|
562
|
+
createInsight(db, companyId, {
|
|
563
|
+
insightType: "staff_observation", category: "ops", priority: 82,
|
|
564
|
+
title: `${overdueTasks} 个任务已逾期`,
|
|
565
|
+
message: `有 ${overdueTasks} 个任务已超过截止日期尚未完成。逾期任务会拖慢整体进度,建议尽快处理或调整计划。`,
|
|
566
|
+
staffRole: "ops",
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 停滞项目(状态 planning/active 但超过30天无更新)
|
|
571
|
+
const staleProjects = db.query(
|
|
572
|
+
`SELECT name FROM opc_projects
|
|
573
|
+
WHERE company_id = ? AND status IN ('planning','active')
|
|
574
|
+
AND updated_at < datetime('now', '-30 days')`,
|
|
575
|
+
companyId,
|
|
576
|
+
) as { name: string }[];
|
|
577
|
+
if (staleProjects.length > 0) {
|
|
578
|
+
createInsight(db, companyId, {
|
|
579
|
+
insightType: "staff_observation", category: "ops", priority: 70,
|
|
580
|
+
title: `${staleProjects.length} 个项目超过 30 天无更新`,
|
|
581
|
+
message: `项目「${staleProjects[0].name}」等已超过 30 天无任何更新。建议评估是否继续推进或归档。`,
|
|
582
|
+
staffRole: "ops",
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ── 分析器 5: 运营健康分析 ───────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
function analyzeOperationalHealth(db: OpcDatabase, companyId: string): void {
|
|
590
|
+
// 超预算项目
|
|
591
|
+
const overBudgetProjects = db.query(
|
|
592
|
+
`SELECT name, budget, spent FROM opc_projects
|
|
593
|
+
WHERE company_id = ? AND budget > 0 AND spent > budget AND status NOT IN ('completed','cancelled')`,
|
|
594
|
+
companyId,
|
|
595
|
+
) as { name: string; budget: number; spent: number }[];
|
|
596
|
+
for (const p of overBudgetProjects) {
|
|
597
|
+
const overPct = Math.round(((p.spent - p.budget) / p.budget) * 100);
|
|
598
|
+
createInsight(db, companyId, {
|
|
599
|
+
insightType: "risk", category: "ops", priority: 80,
|
|
600
|
+
title: `项目「${p.name}」已超预算 ${overPct}%`,
|
|
601
|
+
message: `预算 ${p.budget.toLocaleString()} 元,已支出 ${p.spent.toLocaleString()} 元,超出 ${(p.spent - p.budget).toLocaleString()} 元。建议审视后续支出计划。`,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// 90天以上未联系客户
|
|
606
|
+
const staleContacts = (db.queryOne(
|
|
607
|
+
`SELECT COUNT(*) as cnt FROM opc_contacts
|
|
608
|
+
WHERE company_id = ? AND last_contact_date != '' AND last_contact_date < date('now', '-90 days')`,
|
|
609
|
+
companyId,
|
|
610
|
+
) as CountRow).cnt;
|
|
611
|
+
if (staleContacts > 0) {
|
|
612
|
+
createInsight(db, companyId, {
|
|
613
|
+
insightType: "risk", category: "marketing", priority: 58,
|
|
614
|
+
title: `${staleContacts} 个客户超过 90 天未联系`,
|
|
615
|
+
message: `有 ${staleContacts} 个客户/联系人上次沟通已超过 3 个月。定期维护客户关系有助于续单和转介绍。`,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// 账龄超30天的未付发票
|
|
620
|
+
const agingInvoices = db.query(
|
|
621
|
+
`SELECT invoice_number, counterparty, total_amount, issue_date FROM opc_invoices
|
|
622
|
+
WHERE company_id = ? AND status IN ('sent','pending') AND type = 'sales'
|
|
623
|
+
AND issue_date != '' AND issue_date < date('now', '-30 days')
|
|
624
|
+
LIMIT 5`,
|
|
625
|
+
companyId,
|
|
626
|
+
) as { invoice_number: string; counterparty: string; total_amount: number; issue_date: string }[];
|
|
627
|
+
if (agingInvoices.length > 0) {
|
|
628
|
+
const totalAging = agingInvoices.reduce((s, i) => s + i.total_amount, 0);
|
|
629
|
+
createInsight(db, companyId, {
|
|
630
|
+
insightType: "risk", category: "finance", priority: 76,
|
|
631
|
+
title: `${agingInvoices.length} 张发票账龄超过 30 天`,
|
|
632
|
+
message: `共 ${agingInvoices.length} 张销售发票已开出超过 30 天未收款,总金额 ${totalAging.toLocaleString()} 元。建议及时催款。`,
|
|
633
|
+
staffRole: "finance",
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ── 公开 API ─────────────────────────────────────────────────────
|
|
639
|
+
|
|
640
|
+
/** 对单个公司运行完整智能扫描 */
|
|
641
|
+
export function runIntelligenceScanForCompany(db: OpcDatabase, companyId: string, log: (msg: string) => void): void {
|
|
642
|
+
try {
|
|
643
|
+
analyzeDataGaps(db, companyId);
|
|
644
|
+
analyzeFinanceTrends(db, companyId);
|
|
645
|
+
generateNextSteps(db, companyId);
|
|
646
|
+
analyzeStaffObservations(db, companyId);
|
|
647
|
+
analyzeOperationalHealth(db, companyId);
|
|
648
|
+
log(`opc-intel: 公司 [${companyId}] 智能扫描完成`);
|
|
649
|
+
} catch (err) {
|
|
650
|
+
log(`opc-intel: 公司 [${companyId}] 扫描异常: ${err instanceof Error ? err.message : String(err)}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** 对所有公司运行智能扫描 */
|
|
655
|
+
export function runIntelligenceScan(db: OpcDatabase, log: (msg: string) => void): void {
|
|
656
|
+
const companies = db.query("SELECT id FROM opc_companies") as { id: string }[];
|
|
657
|
+
for (const c of companies) {
|
|
658
|
+
runIntelligenceScanForCompany(db, c.id, log);
|
|
659
|
+
}
|
|
660
|
+
log(`opc-intel: 全量智能扫描完成,共 ${companies.length} 家公司`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** 获取公司的活跃洞察(供上下文注入用) */
|
|
664
|
+
export function getActiveInsights(db: OpcDatabase, companyId: string, limit = 10): {
|
|
665
|
+
id: string;
|
|
666
|
+
insight_type: string;
|
|
667
|
+
category: string;
|
|
668
|
+
priority: number;
|
|
669
|
+
title: string;
|
|
670
|
+
message: string;
|
|
671
|
+
action_hint: string;
|
|
672
|
+
staff_role: string;
|
|
673
|
+
}[] {
|
|
674
|
+
return db.query(
|
|
675
|
+
`SELECT id, insight_type, category, priority, title, message, action_hint, staff_role
|
|
676
|
+
FROM opc_insights
|
|
677
|
+
WHERE company_id = ? AND status = 'active' AND (expires_at = '' OR expires_at > datetime('now'))
|
|
678
|
+
ORDER BY priority DESC, created_at DESC
|
|
679
|
+
LIMIT ?`,
|
|
680
|
+
companyId, limit,
|
|
681
|
+
) as {
|
|
682
|
+
id: string;
|
|
683
|
+
insight_type: string;
|
|
684
|
+
category: string;
|
|
685
|
+
priority: number;
|
|
686
|
+
title: string;
|
|
687
|
+
message: string;
|
|
688
|
+
action_hint: string;
|
|
689
|
+
staff_role: string;
|
|
690
|
+
}[];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** 清理过期洞察 */
|
|
694
|
+
export function expireStaleInsights(db: OpcDatabase, log: (msg: string) => void): void {
|
|
695
|
+
const result = db.execute(
|
|
696
|
+
`UPDATE opc_insights SET status = 'expired', updated_at = datetime('now')
|
|
697
|
+
WHERE status = 'active' AND expires_at != '' AND expires_at <= datetime('now')`,
|
|
698
|
+
);
|
|
699
|
+
if (result.changes > 0) {
|
|
700
|
+
log(`opc-intel: 清理了 ${result.changes} 条过期洞察`);
|
|
701
|
+
}
|
|
702
|
+
}
|