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.
- package/index.ts +244 -8
- package/package.json +17 -3
- package/skills/acquisition-management/SKILL.md +83 -0
- package/skills/ai-staff/SKILL.md +89 -0
- package/skills/asset-package/SKILL.md +142 -0
- package/skills/opb-canvas/SKILL.md +88 -0
- 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/acquisition-tool.ts +8 -5
- 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 +135 -2
- 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,1331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — 智能简报构建器
|
|
3
|
+
*
|
|
4
|
+
* 将预计算数据组装为上下文注入文本。
|
|
5
|
+
* 两个入口:
|
|
6
|
+
* - buildBriefingContext — 单公司 Agent 上下文(Path A)
|
|
7
|
+
* - buildPortfolioBriefing — 跨公司概览(Path C)
|
|
8
|
+
*
|
|
9
|
+
* 硬上限: 最多 5 条洞察 + 2 条庆祝 + 3 条告警
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { OpcDatabase } from "../db/index.js";
|
|
13
|
+
import { getActiveInsights } from "./intelligence-engine.js";
|
|
14
|
+
|
|
15
|
+
type CelebrationRow = {
|
|
16
|
+
id: string;
|
|
17
|
+
celebration_type: string;
|
|
18
|
+
title: string;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type AlertRow = {
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
message: string;
|
|
26
|
+
severity: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type StageRow = {
|
|
30
|
+
company_id: string;
|
|
31
|
+
stage: string;
|
|
32
|
+
stage_label: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type CompanyRow = {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
status: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type CountRow = { cnt: number };
|
|
42
|
+
type SumRow = { total: number };
|
|
43
|
+
type MonthlyTxRow = { month: string; income: number; expense: number };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 计算公司健康评分(0-100)。
|
|
47
|
+
* 维度:财务健康(30%) + 运营效率(20%) + 增长趋势(20%) + 数据完整度(15%) + 合规状态(15%)
|
|
48
|
+
*/
|
|
49
|
+
export function computeHealthScore(db: OpcDatabase, companyId: string): {
|
|
50
|
+
total: number;
|
|
51
|
+
dimensions: { name: string; score: number; weight: number; detail: string }[];
|
|
52
|
+
} {
|
|
53
|
+
const dims: { name: string; score: number; weight: number; detail: string }[] = [];
|
|
54
|
+
|
|
55
|
+
// ── 财务健康 (30%) ──
|
|
56
|
+
const finance = (() => {
|
|
57
|
+
const income = (db.queryOne(
|
|
58
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", companyId,
|
|
59
|
+
) as SumRow).total;
|
|
60
|
+
const expense = (db.queryOne(
|
|
61
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'expense'", companyId,
|
|
62
|
+
) as SumRow).total;
|
|
63
|
+
let score = 30; // baseline
|
|
64
|
+
if (income > 0) score += 25;
|
|
65
|
+
if (income > expense) score += 25; // profitable
|
|
66
|
+
const revenueMonths = (db.queryOne(
|
|
67
|
+
"SELECT COUNT(DISTINCT strftime('%Y-%m', transaction_date)) as cnt FROM opc_transactions WHERE company_id = ? AND type = 'income' AND amount > 0", companyId,
|
|
68
|
+
) as CountRow).cnt;
|
|
69
|
+
if (revenueMonths >= 2) score += 10;
|
|
70
|
+
if (revenueMonths >= 3) score += 10;
|
|
71
|
+
const detail = income === 0 ? "无收入记录" : `收入 ${income.toLocaleString()} 元,${income > expense ? "盈利" : "亏损"}`;
|
|
72
|
+
return { score: Math.min(score, 100), detail };
|
|
73
|
+
})();
|
|
74
|
+
dims.push({ name: "财务健康", score: finance.score, weight: 0.3, detail: finance.detail });
|
|
75
|
+
|
|
76
|
+
// ── 运营效率 (20%) ──
|
|
77
|
+
const ops = (() => {
|
|
78
|
+
let score = 60; // baseline
|
|
79
|
+
const overdueTasks = (db.queryOne(
|
|
80
|
+
"SELECT COUNT(*) as cnt FROM opc_tasks WHERE company_id = ? AND status NOT IN ('done','completed','cancelled') AND due_date != '' AND due_date < date('now')", companyId,
|
|
81
|
+
) as CountRow).cnt;
|
|
82
|
+
const totalTasks = (db.queryOne(
|
|
83
|
+
"SELECT COUNT(*) as cnt FROM opc_tasks WHERE company_id = ?", companyId,
|
|
84
|
+
) as CountRow).cnt;
|
|
85
|
+
if (totalTasks > 0 && overdueTasks === 0) score += 30;
|
|
86
|
+
else if (overdueTasks > 0) score -= Math.min(overdueTasks * 10, 40);
|
|
87
|
+
const overBudget = (db.queryOne(
|
|
88
|
+
"SELECT COUNT(*) as cnt FROM opc_projects WHERE company_id = ? AND budget > 0 AND spent > budget AND status NOT IN ('completed','cancelled')", companyId,
|
|
89
|
+
) as CountRow).cnt;
|
|
90
|
+
if (overBudget > 0) score -= overBudget * 15;
|
|
91
|
+
const detail = overdueTasks > 0 ? `${overdueTasks} 个逾期任务` : totalTasks > 0 ? "任务按时推进" : "暂无任务数据";
|
|
92
|
+
return { score: Math.max(Math.min(score, 100), 0), detail };
|
|
93
|
+
})();
|
|
94
|
+
dims.push({ name: "运营效率", score: ops.score, weight: 0.2, detail: ops.detail });
|
|
95
|
+
|
|
96
|
+
// ── 增长趋势 (20%) ──
|
|
97
|
+
const growth = (() => {
|
|
98
|
+
let score = 40;
|
|
99
|
+
const months = db.query(
|
|
100
|
+
"SELECT strftime('%Y-%m', transaction_date) as m, SUM(CASE WHEN type='income' THEN amount ELSE 0 END) as inc FROM opc_transactions WHERE company_id = ? GROUP BY m ORDER BY m DESC LIMIT 3", companyId,
|
|
101
|
+
) as { m: string; inc: number }[];
|
|
102
|
+
if (months.length >= 2 && months[1].inc > 0) {
|
|
103
|
+
const rate = (months[0].inc - months[1].inc) / months[1].inc;
|
|
104
|
+
if (rate > 0.1) score += 40;
|
|
105
|
+
else if (rate > 0) score += 20;
|
|
106
|
+
else if (rate > -0.1) score += 10;
|
|
107
|
+
}
|
|
108
|
+
const contactCount = (db.queryOne(
|
|
109
|
+
"SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?", companyId,
|
|
110
|
+
) as CountRow).cnt;
|
|
111
|
+
if (contactCount >= 5) score += 10;
|
|
112
|
+
else if (contactCount >= 1) score += 5;
|
|
113
|
+
const detail = months.length >= 2 && months[1].inc > 0
|
|
114
|
+
? `月收入环比 ${Math.round(((months[0].inc - months[1].inc) / months[1].inc) * 100)}%`
|
|
115
|
+
: contactCount > 0 ? `${contactCount} 个客户` : "数据不足";
|
|
116
|
+
return { score: Math.max(Math.min(score, 100), 0), detail };
|
|
117
|
+
})();
|
|
118
|
+
dims.push({ name: "增长趋势", score: growth.score, weight: 0.2, detail: growth.detail });
|
|
119
|
+
|
|
120
|
+
// ── 数据完整度 (15%) ──
|
|
121
|
+
const data = (() => {
|
|
122
|
+
let score = 0;
|
|
123
|
+
const checks = [
|
|
124
|
+
{ sql: "SELECT COUNT(*) as cnt FROM opc_transactions WHERE company_id = ?", pts: 15 },
|
|
125
|
+
{ sql: "SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?", pts: 15 },
|
|
126
|
+
{ sql: "SELECT COUNT(*) as cnt FROM opc_opb_canvas WHERE company_id = ?", pts: 20 },
|
|
127
|
+
{ sql: "SELECT COUNT(*) as cnt FROM opc_staff_config WHERE company_id = ? AND enabled = 1", pts: 15 },
|
|
128
|
+
{ sql: "SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ?", pts: 10 },
|
|
129
|
+
{ sql: "SELECT COUNT(*) as cnt FROM opc_projects WHERE company_id = ?", pts: 10 },
|
|
130
|
+
{ sql: "SELECT COUNT(*) as cnt FROM opc_media_content WHERE company_id = ?", pts: 10 },
|
|
131
|
+
{ sql: "SELECT COUNT(*) as cnt FROM opc_hr_records WHERE company_id = ?", pts: 5 },
|
|
132
|
+
];
|
|
133
|
+
let filled = 0;
|
|
134
|
+
for (const c of checks) {
|
|
135
|
+
if ((db.queryOne(c.sql, companyId) as CountRow).cnt > 0) {
|
|
136
|
+
score += c.pts;
|
|
137
|
+
filled++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const detail = `${filled}/${checks.length} 个模块已使用`;
|
|
141
|
+
return { score: Math.min(score, 100), detail };
|
|
142
|
+
})();
|
|
143
|
+
dims.push({ name: "数据完整度", score: data.score, weight: 0.15, detail: data.detail });
|
|
144
|
+
|
|
145
|
+
// ── 合规状态 (15%) ──
|
|
146
|
+
const compliance = (() => {
|
|
147
|
+
let score = 70; // baseline
|
|
148
|
+
const activeAlerts = (db.queryOne(
|
|
149
|
+
"SELECT COUNT(*) as cnt FROM opc_alerts WHERE company_id = ? AND status = 'active'", companyId,
|
|
150
|
+
) as CountRow).cnt;
|
|
151
|
+
const criticalAlerts = (db.queryOne(
|
|
152
|
+
"SELECT COUNT(*) as cnt FROM opc_alerts WHERE company_id = ? AND status = 'active' AND severity = 'critical'", companyId,
|
|
153
|
+
) as CountRow).cnt;
|
|
154
|
+
if (activeAlerts === 0) score += 30;
|
|
155
|
+
else {
|
|
156
|
+
score -= criticalAlerts * 20;
|
|
157
|
+
score -= (activeAlerts - criticalAlerts) * 5;
|
|
158
|
+
}
|
|
159
|
+
const hasContract = (db.queryOne(
|
|
160
|
+
"SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ?", companyId,
|
|
161
|
+
) as CountRow).cnt > 0;
|
|
162
|
+
if (hasContract) score += 10;
|
|
163
|
+
const detail = activeAlerts > 0 ? `${activeAlerts} 条活跃告警` : "无告警";
|
|
164
|
+
return { score: Math.max(Math.min(score, 100), 0), detail };
|
|
165
|
+
})();
|
|
166
|
+
dims.push({ name: "合规状态", score: compliance.score, weight: 0.15, detail: compliance.detail });
|
|
167
|
+
|
|
168
|
+
const total = Math.round(dims.reduce((s, d) => s + d.score * d.weight, 0));
|
|
169
|
+
return { total, dimensions: dims };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function healthScoreEmoji(score: number): string {
|
|
173
|
+
if (score >= 80) return "🟢";
|
|
174
|
+
if (score >= 60) return "🟡";
|
|
175
|
+
if (score >= 40) return "🟠";
|
|
176
|
+
return "🔴";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── 核心指标一览 ────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
function buildDashboardMetrics(db: OpcDatabase, companyId: string): string[] {
|
|
182
|
+
const income = (db.queryOne(
|
|
183
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'",
|
|
184
|
+
companyId,
|
|
185
|
+
) as SumRow).total;
|
|
186
|
+
const expense = (db.queryOne(
|
|
187
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'expense'",
|
|
188
|
+
companyId,
|
|
189
|
+
) as SumRow).total;
|
|
190
|
+
const thisMonth = new Date().toISOString().slice(0, 7);
|
|
191
|
+
const monthIncome = (db.queryOne(
|
|
192
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income' AND strftime('%Y-%m', transaction_date) = ?",
|
|
193
|
+
companyId, thisMonth,
|
|
194
|
+
) as SumRow).total;
|
|
195
|
+
const monthExpense = (db.queryOne(
|
|
196
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'expense' AND strftime('%Y-%m', transaction_date) = ?",
|
|
197
|
+
companyId, thisMonth,
|
|
198
|
+
) as SumRow).total;
|
|
199
|
+
const contactCount = (db.queryOne(
|
|
200
|
+
"SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?", companyId,
|
|
201
|
+
) as CountRow).cnt;
|
|
202
|
+
const activeContracts = (db.queryOne(
|
|
203
|
+
"SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ? AND status = 'active'", companyId,
|
|
204
|
+
) as CountRow).cnt;
|
|
205
|
+
const activeProjects = (db.queryOne(
|
|
206
|
+
"SELECT COUNT(*) as cnt FROM opc_projects WHERE company_id = ? AND status IN ('active','planning')", companyId,
|
|
207
|
+
) as CountRow).cnt;
|
|
208
|
+
const contentCount = (db.queryOne(
|
|
209
|
+
"SELECT COUNT(*) as cnt FROM opc_media_content WHERE company_id = ?", companyId,
|
|
210
|
+
) as CountRow).cnt;
|
|
211
|
+
// 运营时长
|
|
212
|
+
const companyRow = db.queryOne(
|
|
213
|
+
"SELECT created_at FROM opc_companies WHERE id = ?", companyId,
|
|
214
|
+
) as { created_at: string } | null;
|
|
215
|
+
let ageText = "";
|
|
216
|
+
if (companyRow?.created_at) {
|
|
217
|
+
const created = new Date(companyRow.created_at);
|
|
218
|
+
const now = new Date();
|
|
219
|
+
const days = Math.floor((now.getTime() - created.getTime()) / 86400000);
|
|
220
|
+
if (days < 30) ageText = `${days} 天`;
|
|
221
|
+
else if (days < 365) ageText = `${Math.floor(days / 30)} 个月`;
|
|
222
|
+
else ageText = `${(days / 365).toFixed(1)} 年`;
|
|
223
|
+
}
|
|
224
|
+
const stage = db.queryOne(
|
|
225
|
+
"SELECT stage_label FROM opc_company_stage WHERE company_id = ?", companyId,
|
|
226
|
+
) as { stage_label: string } | null;
|
|
227
|
+
|
|
228
|
+
const lines: string[] = ["### 📊 核心指标一览"];
|
|
229
|
+
lines.push(`- 💰 累计收入: ${income.toLocaleString()} 元 | 累计支出: ${expense.toLocaleString()} 元 | 净利润: ${(income - expense).toLocaleString()} 元`);
|
|
230
|
+
lines.push(`- 📈 本月收入: ${monthIncome.toLocaleString()} 元 | 本月支出: ${monthExpense.toLocaleString()} 元`);
|
|
231
|
+
lines.push(`- 👥 客户: ${contactCount} | 活跃合同: ${activeContracts} | 活跃项目: ${activeProjects} | 内容: ${contentCount} 篇`);
|
|
232
|
+
if (ageText) {
|
|
233
|
+
lines.push(`- 📅 运营时长: ${ageText} | 发展阶段: ${stage?.stage_label ?? "未检测"}`);
|
|
234
|
+
}
|
|
235
|
+
lines.push("");
|
|
236
|
+
return lines;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── 现金流预测 ──────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
function buildCashFlowForecast(db: OpcDatabase, companyId: string): string[] {
|
|
242
|
+
const months = db.query(
|
|
243
|
+
`SELECT strftime('%Y-%m', transaction_date) as month,
|
|
244
|
+
COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END), 0) as income,
|
|
245
|
+
COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END), 0) as expense
|
|
246
|
+
FROM opc_transactions WHERE company_id = ?
|
|
247
|
+
GROUP BY month ORDER BY month DESC LIMIT 6`,
|
|
248
|
+
companyId,
|
|
249
|
+
) as MonthlyTxRow[];
|
|
250
|
+
|
|
251
|
+
if (months.length === 0) return [];
|
|
252
|
+
|
|
253
|
+
const avgIncome = months.reduce((s, m) => s + m.income, 0) / months.length;
|
|
254
|
+
const avgExpense = months.reduce((s, m) => s + m.expense, 0) / months.length;
|
|
255
|
+
const netMonthly = avgIncome - avgExpense;
|
|
256
|
+
|
|
257
|
+
const lines: string[] = ["### 💰 现金流预测"];
|
|
258
|
+
lines.push(`- 月均收入: ${Math.round(avgIncome).toLocaleString()} 元 | 月均支出: ${Math.round(avgExpense).toLocaleString()} 元`);
|
|
259
|
+
|
|
260
|
+
if (netMonthly > 0) {
|
|
261
|
+
lines.push(`- 月均净收入: +${Math.round(netMonthly).toLocaleString()} 元 ✅`);
|
|
262
|
+
} else if (netMonthly < 0) {
|
|
263
|
+
const balance = (db.queryOne(
|
|
264
|
+
"SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END), 0) as total FROM opc_transactions WHERE company_id = ?",
|
|
265
|
+
companyId,
|
|
266
|
+
) as SumRow).total;
|
|
267
|
+
if (balance > 0) {
|
|
268
|
+
const runway = Math.floor(balance / Math.abs(netMonthly));
|
|
269
|
+
lines.push(`- 月均净亏损: ${Math.round(Math.abs(netMonthly)).toLocaleString()} 元 ⚠️ | 预计跑道: ${runway} 个月`);
|
|
270
|
+
} else {
|
|
271
|
+
lines.push(`- 月均净亏损: ${Math.round(Math.abs(netMonthly)).toLocaleString()} 元 ⚠️`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 收入里程碑预测
|
|
276
|
+
const totalIncome = (db.queryOne(
|
|
277
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'",
|
|
278
|
+
companyId,
|
|
279
|
+
) as SumRow).total;
|
|
280
|
+
if (avgIncome > 0) {
|
|
281
|
+
const milestones = [10_000, 50_000, 100_000, 500_000, 1_000_000];
|
|
282
|
+
const next = milestones.find(m => m > totalIncome);
|
|
283
|
+
if (next) {
|
|
284
|
+
const monthsNeeded = Math.ceil((next - totalIncome) / avgIncome);
|
|
285
|
+
const label = next >= 10_000 ? `${next / 10_000} 万` : `${next.toLocaleString()} 元`;
|
|
286
|
+
lines.push(`- 🎯 下一里程碑: 收入 ${label}(预计 ${monthsNeeded} 个月内)`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 环比增长
|
|
291
|
+
if (months.length >= 2 && months[1].income > 0) {
|
|
292
|
+
const rate = ((months[0].income - months[1].income) / months[1].income) * 100;
|
|
293
|
+
const icon = rate > 0 ? "📈" : rate < 0 ? "📉" : "➡️";
|
|
294
|
+
lines.push(`- ${icon} 收入趋势: 环比 ${rate > 0 ? "+" : ""}${Math.round(rate)}%`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
lines.push("");
|
|
298
|
+
return lines;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── 员工晨会 ──────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
const ROLE_EMOJI: Record<string, string> = {
|
|
304
|
+
finance: "💰",
|
|
305
|
+
legal: "⚖️",
|
|
306
|
+
hr: "👤",
|
|
307
|
+
marketing: "📣",
|
|
308
|
+
ops: "⚙️",
|
|
309
|
+
admin: "📋",
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
type InsightRow = {
|
|
313
|
+
insight_type: string;
|
|
314
|
+
staff_role?: string;
|
|
315
|
+
title: string;
|
|
316
|
+
message: string;
|
|
317
|
+
action_hint?: string;
|
|
318
|
+
priority: number;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
type StaffTaskRow = {
|
|
322
|
+
id: string;
|
|
323
|
+
staff_role: string;
|
|
324
|
+
title: string;
|
|
325
|
+
status: string;
|
|
326
|
+
priority: string;
|
|
327
|
+
result_summary: string;
|
|
328
|
+
assigned_at: string;
|
|
329
|
+
completed_at: string;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
type StaffConfigRow = {
|
|
333
|
+
role: string;
|
|
334
|
+
role_name: string;
|
|
335
|
+
enabled: number;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
function buildEmployeeStandup(
|
|
339
|
+
db: OpcDatabase,
|
|
340
|
+
companyId: string,
|
|
341
|
+
staffObs: InsightRow[],
|
|
342
|
+
): string[] {
|
|
343
|
+
// 查询启用的员工列表
|
|
344
|
+
const staffConfigs = db.query(
|
|
345
|
+
"SELECT role, role_name, enabled FROM opc_staff_config WHERE company_id = ? AND enabled = 1 ORDER BY created_at ASC",
|
|
346
|
+
companyId,
|
|
347
|
+
) as StaffConfigRow[];
|
|
348
|
+
|
|
349
|
+
if (staffConfigs.length === 0 && staffObs.length === 0) return [];
|
|
350
|
+
|
|
351
|
+
// 查询进行中和待处理的任务
|
|
352
|
+
const activeTasks = db.query(
|
|
353
|
+
`SELECT id, staff_role, title, status, priority, result_summary, assigned_at, completed_at
|
|
354
|
+
FROM opc_staff_tasks
|
|
355
|
+
WHERE company_id = ? AND status IN ('pending', 'in_progress')
|
|
356
|
+
ORDER BY CASE priority WHEN 'urgent' THEN 1 WHEN 'high' THEN 2 WHEN 'normal' THEN 3 ELSE 4 END, assigned_at DESC`,
|
|
357
|
+
companyId,
|
|
358
|
+
) as StaffTaskRow[];
|
|
359
|
+
|
|
360
|
+
// 查询最近完成的任务(24h 内)
|
|
361
|
+
const recentCompleted = db.query(
|
|
362
|
+
`SELECT id, staff_role, title, status, priority, result_summary, assigned_at, completed_at
|
|
363
|
+
FROM opc_staff_tasks
|
|
364
|
+
WHERE company_id = ? AND status = 'completed' AND completed_at > datetime('now', '-24 hours')
|
|
365
|
+
ORDER BY completed_at DESC LIMIT 5`,
|
|
366
|
+
companyId,
|
|
367
|
+
) as StaffTaskRow[];
|
|
368
|
+
|
|
369
|
+
// 按角色分组
|
|
370
|
+
const roleNames = new Map<string, string>();
|
|
371
|
+
for (const s of staffConfigs) {
|
|
372
|
+
roleNames.set(s.role, s.role_name);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const obsByRole = new Map<string, InsightRow[]>();
|
|
376
|
+
for (const obs of staffObs) {
|
|
377
|
+
const role = obs.staff_role ?? "general";
|
|
378
|
+
if (!obsByRole.has(role)) obsByRole.set(role, []);
|
|
379
|
+
obsByRole.get(role)!.push(obs);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const tasksByRole = new Map<string, StaffTaskRow[]>();
|
|
383
|
+
for (const t of activeTasks) {
|
|
384
|
+
if (!tasksByRole.has(t.staff_role)) tasksByRole.set(t.staff_role, []);
|
|
385
|
+
tasksByRole.get(t.staff_role)!.push(t);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const completedByRole = new Map<string, StaffTaskRow[]>();
|
|
389
|
+
for (const t of recentCompleted) {
|
|
390
|
+
if (!completedByRole.has(t.staff_role)) completedByRole.set(t.staff_role, []);
|
|
391
|
+
completedByRole.get(t.staff_role)!.push(t);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 收集所有有内容的角色
|
|
395
|
+
const allRoles = new Set<string>();
|
|
396
|
+
for (const s of staffConfigs) allRoles.add(s.role);
|
|
397
|
+
for (const role of obsByRole.keys()) allRoles.add(role);
|
|
398
|
+
for (const role of tasksByRole.keys()) allRoles.add(role);
|
|
399
|
+
for (const role of completedByRole.keys()) allRoles.add(role);
|
|
400
|
+
|
|
401
|
+
const lines: string[] = ["### 👥 员工晨会"];
|
|
402
|
+
|
|
403
|
+
for (const role of allRoles) {
|
|
404
|
+
const emoji = ROLE_EMOJI[role] ?? "🤖";
|
|
405
|
+
const name = roleNames.get(role) ?? role;
|
|
406
|
+
const obs = obsByRole.get(role) ?? [];
|
|
407
|
+
const tasks = tasksByRole.get(role) ?? [];
|
|
408
|
+
const completed = completedByRole.get(role) ?? [];
|
|
409
|
+
|
|
410
|
+
// 跳过没有任何内容的角色
|
|
411
|
+
if (obs.length === 0 && tasks.length === 0 && completed.length === 0) continue;
|
|
412
|
+
|
|
413
|
+
// 员工发言:观察 + 建议
|
|
414
|
+
if (obs.length > 0) {
|
|
415
|
+
const mainObs = obs[0];
|
|
416
|
+
const extraObs = obs.slice(1);
|
|
417
|
+
lines.push(`${emoji} **${name}**: "${mainObs.message}"`);
|
|
418
|
+
for (const extra of extraObs) {
|
|
419
|
+
lines.push(` - ${extra.title}: ${extra.message}`);
|
|
420
|
+
}
|
|
421
|
+
// 可安排的任务建议
|
|
422
|
+
const hints = obs.filter(o => o.action_hint).map(o => o.action_hint);
|
|
423
|
+
if (hints.length > 0) {
|
|
424
|
+
lines.push(` → 可安排: ${hints.join(" | ")}`);
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
lines.push(`${emoji} **${name}**:`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 当前任务
|
|
431
|
+
if (tasks.length > 0) {
|
|
432
|
+
for (const t of tasks) {
|
|
433
|
+
const statusIcon = t.status === "in_progress" ? "🔄" : "⏳";
|
|
434
|
+
const priorityTag = t.priority === "urgent" ? " [紧急]" : t.priority === "high" ? " [重要]" : "";
|
|
435
|
+
lines.push(` ${statusIcon} 进行中: ${t.title}${priorityTag}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 刚完成的任务
|
|
440
|
+
if (completed.length > 0) {
|
|
441
|
+
for (const t of completed) {
|
|
442
|
+
const summary = t.result_summary ? ` — ${t.result_summary}` : "";
|
|
443
|
+
lines.push(` ✅ 已完成: ${t.title}${summary}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 汇总统计
|
|
449
|
+
const totalActive = activeTasks.length;
|
|
450
|
+
const totalCompleted24h = recentCompleted.length;
|
|
451
|
+
if (totalActive > 0 || totalCompleted24h > 0) {
|
|
452
|
+
const parts: string[] = [];
|
|
453
|
+
if (totalActive > 0) parts.push(`${totalActive} 项进行中`);
|
|
454
|
+
if (totalCompleted24h > 0) parts.push(`${totalCompleted24h} 项24h内完成`);
|
|
455
|
+
lines.push(`📌 团队任务: ${parts.join(",")}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
lines.push(`💡 说"安排[员工]做..."可一键派遣任务`);
|
|
459
|
+
lines.push("");
|
|
460
|
+
return lines;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ── 本周聚焦 ────────────────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
function buildWeeklyFocus(db: OpcDatabase, companyId: string): string[] {
|
|
466
|
+
const items: { priority: number; text: string }[] = [];
|
|
467
|
+
|
|
468
|
+
// 逾期任务
|
|
469
|
+
const overdueTasks = db.query(
|
|
470
|
+
`SELECT title, due_date FROM opc_tasks
|
|
471
|
+
WHERE company_id = ? AND status NOT IN ('done','completed','cancelled')
|
|
472
|
+
AND due_date != '' AND due_date < date('now')
|
|
473
|
+
ORDER BY due_date ASC LIMIT 2`,
|
|
474
|
+
companyId,
|
|
475
|
+
) as { title: string; due_date: string }[];
|
|
476
|
+
for (const t of overdueTasks) {
|
|
477
|
+
items.push({ priority: 95, text: `完成逾期任务「${t.title}」(截止 ${t.due_date})` });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 即将到期合同
|
|
481
|
+
const expiringContracts = db.query(
|
|
482
|
+
`SELECT title, end_date FROM opc_contracts
|
|
483
|
+
WHERE company_id = ? AND status = 'active' AND end_date != ''
|
|
484
|
+
AND end_date <= date('now', '+14 days') AND end_date >= date('now')
|
|
485
|
+
LIMIT 2`,
|
|
486
|
+
companyId,
|
|
487
|
+
) as { title: string; end_date: string }[];
|
|
488
|
+
for (const c of expiringContracts) {
|
|
489
|
+
items.push({ priority: 90, text: `处理即将到期合同「${c.title}」(${c.end_date} 到期)` });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 账龄发票催收
|
|
493
|
+
const agingInvoices = db.query(
|
|
494
|
+
`SELECT counterparty, total_amount FROM opc_invoices
|
|
495
|
+
WHERE company_id = ? AND status IN ('sent','pending') AND type = 'sales'
|
|
496
|
+
AND issue_date != '' AND issue_date < date('now', '-30 days')
|
|
497
|
+
LIMIT 2`,
|
|
498
|
+
companyId,
|
|
499
|
+
) as { counterparty: string; total_amount: number }[];
|
|
500
|
+
for (const inv of agingInvoices) {
|
|
501
|
+
items.push({ priority: 85, text: `催收应收款「${inv.counterparty}」${inv.total_amount.toLocaleString()} 元` });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 久未联系客户
|
|
505
|
+
const staleContacts = db.query(
|
|
506
|
+
`SELECT name, last_contact_date FROM opc_contacts
|
|
507
|
+
WHERE company_id = ? AND last_contact_date != '' AND last_contact_date < date('now', '-60 days')
|
|
508
|
+
ORDER BY last_contact_date ASC LIMIT 2`,
|
|
509
|
+
companyId,
|
|
510
|
+
) as { name: string; last_contact_date: string }[];
|
|
511
|
+
for (const c of staleContacts) {
|
|
512
|
+
const days = Math.floor((Date.now() - new Date(c.last_contact_date).getTime()) / 86400000);
|
|
513
|
+
items.push({ priority: 80, text: `联系客户「${c.name}」(已 ${days} 天未联系)` });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 本月未记录支出
|
|
517
|
+
const thisMonth = new Date().toISOString().slice(0, 7);
|
|
518
|
+
const monthExpenseCount = (db.queryOne(
|
|
519
|
+
"SELECT COUNT(*) as cnt FROM opc_transactions WHERE company_id = ? AND type = 'expense' AND strftime('%Y-%m', transaction_date) = ?",
|
|
520
|
+
companyId, thisMonth,
|
|
521
|
+
) as CountRow).cnt;
|
|
522
|
+
if (monthExpenseCount === 0) {
|
|
523
|
+
const anyExpense = (db.queryOne(
|
|
524
|
+
"SELECT COUNT(*) as cnt FROM opc_transactions WHERE company_id = ? AND type = 'expense'", companyId,
|
|
525
|
+
) as CountRow).cnt;
|
|
526
|
+
items.push({ priority: 60, text: anyExpense > 0 ? "补录本月支出记录" : "开始记录支出,掌握成本结构" });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 无客户
|
|
530
|
+
const contactCount = (db.queryOne(
|
|
531
|
+
"SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?", companyId,
|
|
532
|
+
) as CountRow).cnt;
|
|
533
|
+
if (contactCount === 0) {
|
|
534
|
+
items.push({ priority: 75, text: "添加第一个客户/联系人到客户池" });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 增长阶段无项目
|
|
538
|
+
const stageRow = db.queryOne(
|
|
539
|
+
"SELECT stage FROM opc_company_stage WHERE company_id = ?", companyId,
|
|
540
|
+
) as { stage: string } | null;
|
|
541
|
+
const stage = stageRow?.stage ?? "idea";
|
|
542
|
+
|
|
543
|
+
const projectCount = (db.queryOne(
|
|
544
|
+
"SELECT COUNT(*) as cnt FROM opc_projects WHERE company_id = ? AND status IN ('active','planning')", companyId,
|
|
545
|
+
) as CountRow).cnt;
|
|
546
|
+
if (projectCount === 0 && ["growth", "stable", "scaling"].includes(stage)) {
|
|
547
|
+
items.push({ priority: 65, text: "创建项目来系统化管理当前业务" });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 内容营销缺口
|
|
551
|
+
const contentThisMonth = (db.queryOne(
|
|
552
|
+
`SELECT COUNT(*) as cnt FROM opc_media_content
|
|
553
|
+
WHERE company_id = ? AND strftime('%Y-%m', COALESCE(NULLIF(published_date,''), created_at)) = ?`,
|
|
554
|
+
companyId, thisMonth,
|
|
555
|
+
) as CountRow).cnt;
|
|
556
|
+
const totalContent = (db.queryOne(
|
|
557
|
+
"SELECT COUNT(*) as cnt FROM opc_media_content WHERE company_id = ?", companyId,
|
|
558
|
+
) as CountRow).cnt;
|
|
559
|
+
if (totalContent > 0 && contentThisMonth === 0) {
|
|
560
|
+
items.push({ priority: 50, text: "本月发布一篇内容,保持品牌曝光" });
|
|
561
|
+
} else if (totalContent === 0 && ["early_revenue", "growth", "stable"].includes(stage)) {
|
|
562
|
+
items.push({ priority: 55, text: "开始内容营销,建立个人品牌" });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// 无合同
|
|
566
|
+
const contractCount = (db.queryOne(
|
|
567
|
+
"SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ?", companyId,
|
|
568
|
+
) as CountRow).cnt;
|
|
569
|
+
if (contractCount === 0 && ["early_revenue", "growth"].includes(stage)) {
|
|
570
|
+
items.push({ priority: 68, text: "创建第一份服务合同,保护权益" });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (items.length === 0) return [];
|
|
574
|
+
|
|
575
|
+
items.sort((a, b) => b.priority - a.priority);
|
|
576
|
+
const top = items.slice(0, 3);
|
|
577
|
+
|
|
578
|
+
const lines: string[] = ["### 🎯 本周聚焦"];
|
|
579
|
+
top.forEach((item, i) => {
|
|
580
|
+
lines.push(`${i + 1}. ${item.text}`);
|
|
581
|
+
});
|
|
582
|
+
lines.push("");
|
|
583
|
+
return lines;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ── 阶段基准对标 ────────────────────────────────────────────────
|
|
587
|
+
|
|
588
|
+
type StageBenchmark = {
|
|
589
|
+
revenueTarget: string;
|
|
590
|
+
clientTarget: number;
|
|
591
|
+
contractTarget: number;
|
|
592
|
+
contentTarget: string;
|
|
593
|
+
upgradeCondition: string;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const STAGE_BENCHMARKS: Record<string, StageBenchmark> = {
|
|
597
|
+
idea: {
|
|
598
|
+
revenueTarget: "获得第一笔收入",
|
|
599
|
+
clientTarget: 1,
|
|
600
|
+
contractTarget: 0,
|
|
601
|
+
contentTarget: "开始规划",
|
|
602
|
+
upgradeCondition: "有画布/客户/项目 → 验证阶段",
|
|
603
|
+
},
|
|
604
|
+
validation: {
|
|
605
|
+
revenueTarget: "实现首次变现",
|
|
606
|
+
clientTarget: 2,
|
|
607
|
+
contractTarget: 1,
|
|
608
|
+
contentTarget: "开始内容输出",
|
|
609
|
+
upgradeCondition: "有收入 → 初始营收",
|
|
610
|
+
},
|
|
611
|
+
early_revenue: {
|
|
612
|
+
revenueTarget: "月收入稳定 > 5,000 元",
|
|
613
|
+
clientTarget: 3,
|
|
614
|
+
contractTarget: 1,
|
|
615
|
+
contentTarget: "保持月更",
|
|
616
|
+
upgradeCondition: "累计收入 > 1万 + 2月以上有收入 → 增长阶段",
|
|
617
|
+
},
|
|
618
|
+
growth: {
|
|
619
|
+
revenueTarget: "月均收入 > 1万",
|
|
620
|
+
clientTarget: 5,
|
|
621
|
+
contractTarget: 2,
|
|
622
|
+
contentTarget: "多平台运营",
|
|
623
|
+
upgradeCondition: "累计收入 > 10万 + 3月以上 + 2份活跃合同 → 稳定运营",
|
|
624
|
+
},
|
|
625
|
+
stable: {
|
|
626
|
+
revenueTarget: "月均收入 > 5万",
|
|
627
|
+
clientTarget: 10,
|
|
628
|
+
contractTarget: 3,
|
|
629
|
+
contentTarget: "品牌已建立",
|
|
630
|
+
upgradeCondition: "累计收入 > 50万 + 6月以上 + 3名员工 → 规模化",
|
|
631
|
+
},
|
|
632
|
+
scaling: {
|
|
633
|
+
revenueTarget: "月均收入 > 10万",
|
|
634
|
+
clientTarget: 20,
|
|
635
|
+
contractTarget: 5,
|
|
636
|
+
contentTarget: "团队协作",
|
|
637
|
+
upgradeCondition: "考虑退出或持续扩张",
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
function buildStageBenchmark(db: OpcDatabase, companyId: string): string[] {
|
|
642
|
+
const stageRow = db.queryOne(
|
|
643
|
+
"SELECT stage FROM opc_company_stage WHERE company_id = ?", companyId,
|
|
644
|
+
) as { stage: string } | null;
|
|
645
|
+
const stage = stageRow?.stage ?? "idea";
|
|
646
|
+
const bench = STAGE_BENCHMARKS[stage];
|
|
647
|
+
if (!bench) return [];
|
|
648
|
+
|
|
649
|
+
const contactCount = (db.queryOne(
|
|
650
|
+
"SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?", companyId,
|
|
651
|
+
) as CountRow).cnt;
|
|
652
|
+
const contractCount = (db.queryOne(
|
|
653
|
+
"SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ? AND status = 'active'", companyId,
|
|
654
|
+
) as CountRow).cnt;
|
|
655
|
+
|
|
656
|
+
const clientCheck = contactCount >= bench.clientTarget ? "✅" : `❌ (${contactCount}/${bench.clientTarget})`;
|
|
657
|
+
const contractCheck = contractCount >= bench.contractTarget ? "✅" :
|
|
658
|
+
bench.contractTarget === 0 ? "—" : `❌ (${contractCount}/${bench.contractTarget})`;
|
|
659
|
+
|
|
660
|
+
const lines: string[] = [`### 🏁 阶段目标对标`];
|
|
661
|
+
lines.push(`- 收入目标: ${bench.revenueTarget} | 客户: ${clientCheck} | 合同: ${contractCheck}`);
|
|
662
|
+
lines.push(`- 📌 升级条件: ${bench.upgradeCondition}`);
|
|
663
|
+
lines.push("");
|
|
664
|
+
return lines;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ── 自动 OKR 生成 ──────────────────────────────────────────────
|
|
668
|
+
|
|
669
|
+
type OKRItem = {
|
|
670
|
+
objective: string;
|
|
671
|
+
keyResults: { text: string; current: number; target: number }[];
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
function buildQuarterlyOKR(db: OpcDatabase, companyId: string): string[] {
|
|
675
|
+
const stageRow = db.queryOne(
|
|
676
|
+
"SELECT stage FROM opc_company_stage WHERE company_id = ?", companyId,
|
|
677
|
+
) as { stage: string } | null;
|
|
678
|
+
const stage = stageRow?.stage ?? "idea";
|
|
679
|
+
|
|
680
|
+
const totalIncome = (db.queryOne(
|
|
681
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", companyId,
|
|
682
|
+
) as SumRow).total;
|
|
683
|
+
const contactCount = (db.queryOne(
|
|
684
|
+
"SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?", companyId,
|
|
685
|
+
) as CountRow).cnt;
|
|
686
|
+
const contractCount = (db.queryOne(
|
|
687
|
+
"SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ?", companyId,
|
|
688
|
+
) as CountRow).cnt;
|
|
689
|
+
const contentCount = (db.queryOne(
|
|
690
|
+
"SELECT COUNT(*) as cnt FROM opc_media_content WHERE company_id = ?", companyId,
|
|
691
|
+
) as CountRow).cnt;
|
|
692
|
+
|
|
693
|
+
let okr: OKRItem;
|
|
694
|
+
|
|
695
|
+
switch (stage) {
|
|
696
|
+
case "idea":
|
|
697
|
+
okr = {
|
|
698
|
+
objective: "完成商业模式验证,迈出第一步",
|
|
699
|
+
keyResults: [
|
|
700
|
+
{ text: "完成 OPB 商业画布", current: (db.queryOne("SELECT COUNT(*) as cnt FROM opc_opb_canvas WHERE company_id = ?", companyId) as CountRow).cnt, target: 1 },
|
|
701
|
+
{ text: "添加潜在客户", current: contactCount, target: 2 },
|
|
702
|
+
{ text: "获得第一笔收入", current: totalIncome > 0 ? 1 : 0, target: 1 },
|
|
703
|
+
],
|
|
704
|
+
};
|
|
705
|
+
break;
|
|
706
|
+
case "validation":
|
|
707
|
+
okr = {
|
|
708
|
+
objective: "证明有人愿意付费,实现首次变现",
|
|
709
|
+
keyResults: [
|
|
710
|
+
{ text: "累计收入(元)", current: totalIncome, target: 5000 },
|
|
711
|
+
{ text: "正式客户数", current: contactCount, target: 3 },
|
|
712
|
+
{ text: "签约合同数", current: contractCount, target: 1 },
|
|
713
|
+
],
|
|
714
|
+
};
|
|
715
|
+
break;
|
|
716
|
+
case "early_revenue":
|
|
717
|
+
okr = {
|
|
718
|
+
objective: "建立稳定收入来源,夯实业务基础",
|
|
719
|
+
keyResults: [
|
|
720
|
+
{ text: "累计收入(元)", current: totalIncome, target: 10_000 },
|
|
721
|
+
{ text: "正式客户数", current: contactCount, target: 3 },
|
|
722
|
+
{ text: "签约合同数", current: contractCount, target: 1 },
|
|
723
|
+
],
|
|
724
|
+
};
|
|
725
|
+
break;
|
|
726
|
+
case "growth":
|
|
727
|
+
okr = {
|
|
728
|
+
objective: "加速增长,实现收入多元化",
|
|
729
|
+
keyResults: [
|
|
730
|
+
{ text: "累计收入(元)", current: totalIncome, target: 100_000 },
|
|
731
|
+
{ text: "客户数", current: contactCount, target: 5 },
|
|
732
|
+
{ text: "活跃合同数", current: contractCount, target: 2 },
|
|
733
|
+
],
|
|
734
|
+
};
|
|
735
|
+
break;
|
|
736
|
+
case "stable":
|
|
737
|
+
okr = {
|
|
738
|
+
objective: "优化利润率,建立标准化体系",
|
|
739
|
+
keyResults: [
|
|
740
|
+
{ text: "累计收入(元)", current: totalIncome, target: 500_000 },
|
|
741
|
+
{ text: "客户数", current: contactCount, target: 10 },
|
|
742
|
+
{ text: "内容发布篇数", current: contentCount, target: 20 },
|
|
743
|
+
],
|
|
744
|
+
};
|
|
745
|
+
break;
|
|
746
|
+
case "scaling":
|
|
747
|
+
okr = {
|
|
748
|
+
objective: "规模化运营,考虑团队扩展",
|
|
749
|
+
keyResults: [
|
|
750
|
+
{ text: "累计收入(元)", current: totalIncome, target: 1_000_000 },
|
|
751
|
+
{ text: "客户数", current: contactCount, target: 20 },
|
|
752
|
+
{ text: "活跃合同数", current: contractCount, target: 5 },
|
|
753
|
+
],
|
|
754
|
+
};
|
|
755
|
+
break;
|
|
756
|
+
default:
|
|
757
|
+
return [];
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const lines: string[] = [`### 🎯 本季度 OKR(AI 生成)`];
|
|
761
|
+
lines.push(`**O: ${okr.objective}**`);
|
|
762
|
+
for (let i = 0; i < okr.keyResults.length; i++) {
|
|
763
|
+
const kr = okr.keyResults[i];
|
|
764
|
+
const pct = kr.target > 0 ? Math.min(Math.round((kr.current / kr.target) * 100), 100) : 0;
|
|
765
|
+
const bar = pct >= 100 ? "✅" : `${pct}%`;
|
|
766
|
+
lines.push(`- KR${i + 1}: ${kr.text} → ${kr.target.toLocaleString()} (当前: ${kr.current.toLocaleString()}, ${bar})`);
|
|
767
|
+
}
|
|
768
|
+
lines.push("");
|
|
769
|
+
return lines;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* 构建单个公司的智能简报上下文(用于 Path A — 公司 Agent)。
|
|
774
|
+
* 按优先级从 opc_insights / opc_celebrations / opc_alerts / opc_company_stage 组装。
|
|
775
|
+
*/
|
|
776
|
+
export function buildBriefingContext(db: OpcDatabase, companyId: string): string {
|
|
777
|
+
const sections: string[] = [];
|
|
778
|
+
|
|
779
|
+
// 0. 健康评分
|
|
780
|
+
const health = computeHealthScore(db, companyId);
|
|
781
|
+
sections.push(`### 公司健康评分: ${healthScoreEmoji(health.total)} ${health.total}/100`);
|
|
782
|
+
const dimText = health.dimensions.map(d =>
|
|
783
|
+
`${d.name} ${Math.round(d.score)}分(${Math.round(d.weight * 100)}%) — ${d.detail}`,
|
|
784
|
+
);
|
|
785
|
+
sections.push(dimText.map(t => `- ${t}`).join("\n"));
|
|
786
|
+
sections.push("");
|
|
787
|
+
|
|
788
|
+
// 0a2. 增长评分卡
|
|
789
|
+
const scorecard = computeGrowthScorecard(db, companyId);
|
|
790
|
+
sections.push(`### 📋 增长评分卡: ${gradeEmoji(scorecard.overall)} 总评 ${scorecard.overall}`);
|
|
791
|
+
sections.push(scorecard.dimensions.map(d => `- ${gradeEmoji(d.grade)} ${d.name}: **${d.grade}** — ${d.detail}`).join("\n"));
|
|
792
|
+
|
|
793
|
+
// 0a3. 历史对比
|
|
794
|
+
const lastBriefing = getLastBriefing(db, companyId);
|
|
795
|
+
if (lastBriefing) {
|
|
796
|
+
const healthDiff = health.total - lastBriefing.healthScore;
|
|
797
|
+
const healthArrow = healthDiff > 0 ? `↑${healthDiff}` : healthDiff < 0 ? `↓${Math.abs(healthDiff)}` : "→";
|
|
798
|
+
sections.push("");
|
|
799
|
+
sections.push(`### 📈 vs 上次 (${lastBriefing.date})`);
|
|
800
|
+
sections.push(`- 健康评分: ${lastBriefing.healthScore} → ${health.total} (${healthArrow})`);
|
|
801
|
+
const currentIncome = (db.queryOne(
|
|
802
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", companyId,
|
|
803
|
+
) as SumRow).total;
|
|
804
|
+
if (lastBriefing.totalIncome !== currentIncome) {
|
|
805
|
+
const incomeDiff = currentIncome - lastBriefing.totalIncome;
|
|
806
|
+
sections.push(`- 累计收入: ${lastBriefing.totalIncome.toLocaleString()} → ${currentIncome.toLocaleString()} (+${incomeDiff.toLocaleString()} 元)`);
|
|
807
|
+
}
|
|
808
|
+
if (lastBriefing.scorecardGrade !== scorecard.overall) {
|
|
809
|
+
sections.push(`- 评级: ${lastBriefing.scorecardGrade} → ${scorecard.overall}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
sections.push("");
|
|
813
|
+
|
|
814
|
+
// 0a4. 阶段基准对标
|
|
815
|
+
sections.push(...buildStageBenchmark(db, companyId));
|
|
816
|
+
|
|
817
|
+
// 0a5. 本季度 OKR
|
|
818
|
+
sections.push(...buildQuarterlyOKR(db, companyId));
|
|
819
|
+
|
|
820
|
+
// 0b. 核心指标一览
|
|
821
|
+
sections.push(...buildDashboardMetrics(db, companyId));
|
|
822
|
+
|
|
823
|
+
// 0c. 现金流预测
|
|
824
|
+
sections.push(...buildCashFlowForecast(db, companyId));
|
|
825
|
+
|
|
826
|
+
// 1. 未展示的庆祝(最多 2 条)
|
|
827
|
+
const celebrations = db.query(
|
|
828
|
+
`SELECT id, celebration_type, title, message FROM opc_celebrations
|
|
829
|
+
WHERE company_id = ? AND shown = 0
|
|
830
|
+
ORDER BY created_at DESC LIMIT 2`,
|
|
831
|
+
companyId,
|
|
832
|
+
) as CelebrationRow[];
|
|
833
|
+
|
|
834
|
+
if (celebrations.length > 0) {
|
|
835
|
+
sections.push("### 成就达成");
|
|
836
|
+
for (const c of celebrations) {
|
|
837
|
+
sections.push(`- **${c.title}** — ${c.message}`);
|
|
838
|
+
}
|
|
839
|
+
// 标记为已展示
|
|
840
|
+
for (const c of celebrations) {
|
|
841
|
+
db.execute("UPDATE opc_celebrations SET shown = 1 WHERE id = ?", c.id);
|
|
842
|
+
}
|
|
843
|
+
sections.push("");
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// 2. 待处理告警(最多 3 条,按严重程度排序)
|
|
847
|
+
const alerts = db.query(
|
|
848
|
+
`SELECT id, title, message, severity FROM opc_alerts
|
|
849
|
+
WHERE company_id = ? AND status = 'active'
|
|
850
|
+
ORDER BY
|
|
851
|
+
CASE severity WHEN 'critical' THEN 1 WHEN 'warning' THEN 2 ELSE 3 END,
|
|
852
|
+
created_at DESC
|
|
853
|
+
LIMIT 3`,
|
|
854
|
+
companyId,
|
|
855
|
+
) as AlertRow[];
|
|
856
|
+
|
|
857
|
+
if (alerts.length > 0) {
|
|
858
|
+
sections.push("### 待处理告警");
|
|
859
|
+
for (const a of alerts) {
|
|
860
|
+
const icon = a.severity === "critical" ? "[紧急]" : a.severity === "warning" ? "[注意]" : "[提示]";
|
|
861
|
+
sections.push(`- ${icon} **${a.title}** — ${a.message}`);
|
|
862
|
+
}
|
|
863
|
+
sections.push("");
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// 2b. 本周聚焦
|
|
867
|
+
sections.push(...buildWeeklyFocus(db, companyId));
|
|
868
|
+
|
|
869
|
+
// 3. 智能洞察(最多 10 条,员工晨会需要更多观察数据)
|
|
870
|
+
const insights = getActiveInsights(db, companyId, 10);
|
|
871
|
+
|
|
872
|
+
// 分类整理
|
|
873
|
+
const nextSteps = insights.filter(i => i.insight_type === "next_step");
|
|
874
|
+
const dataGaps = insights.filter(i => i.insight_type === "data_gap");
|
|
875
|
+
const trends = insights.filter(i => i.insight_type === "trend" || i.insight_type === "risk" || i.insight_type === "opportunity");
|
|
876
|
+
const staffObs = insights.filter(i => i.insight_type === "staff_observation");
|
|
877
|
+
|
|
878
|
+
if (trends.length > 0) {
|
|
879
|
+
sections.push("### AI 分析观察");
|
|
880
|
+
for (const t of trends) {
|
|
881
|
+
sections.push(`- **${t.title}** — ${t.message}`);
|
|
882
|
+
}
|
|
883
|
+
sections.push("");
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// 员工晨会(按角色分组,含任务状态 + 洞察)
|
|
887
|
+
const standupLines = buildEmployeeStandup(db, companyId, staffObs);
|
|
888
|
+
if (standupLines.length > 0) {
|
|
889
|
+
sections.push(...standupLines);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (nextSteps.length > 0) {
|
|
893
|
+
sections.push("### 建议下一步行动");
|
|
894
|
+
for (const n of nextSteps) {
|
|
895
|
+
sections.push(`- **${n.title}** — ${n.message}`);
|
|
896
|
+
}
|
|
897
|
+
sections.push("");
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (dataGaps.length > 0) {
|
|
901
|
+
sections.push("### 数据完善建议");
|
|
902
|
+
for (const d of dataGaps) {
|
|
903
|
+
sections.push(`- **${d.title}** — ${d.message}`);
|
|
904
|
+
}
|
|
905
|
+
sections.push("");
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (sections.length === 0) return "";
|
|
909
|
+
|
|
910
|
+
const header = ["", "## 今日智能简报", ""];
|
|
911
|
+
const footer = [
|
|
912
|
+
"---",
|
|
913
|
+
"**交互指令**:先简洁汇报(健康评分 + 紧急事项 + 关键变化),然后根据数据主动建议今天应优先处理什么。",
|
|
914
|
+
"遵循「CEO幕僚长交互规范」和「数据闭环规范」:主动追问、主动建议、所有成果必须调用工具写入。",
|
|
915
|
+
];
|
|
916
|
+
|
|
917
|
+
return [...header, ...sections, ...footer].join("\n");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* 构建跨公司组合概览(用于 Path C — 普通对话)。
|
|
922
|
+
* 每家公司一行摘要 + top 3 洞察。
|
|
923
|
+
*/
|
|
924
|
+
export function buildPortfolioBriefing(db: OpcDatabase): string {
|
|
925
|
+
const companies = db.query(
|
|
926
|
+
"SELECT id, name, status FROM opc_companies ORDER BY created_at DESC LIMIT 10",
|
|
927
|
+
) as CompanyRow[];
|
|
928
|
+
|
|
929
|
+
if (companies.length === 0) return "";
|
|
930
|
+
|
|
931
|
+
// 组合级汇总
|
|
932
|
+
let portfolioIncome = 0;
|
|
933
|
+
let portfolioExpense = 0;
|
|
934
|
+
for (const c of companies) {
|
|
935
|
+
portfolioIncome += (db.queryOne(
|
|
936
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", c.id,
|
|
937
|
+
) as SumRow).total;
|
|
938
|
+
portfolioExpense += (db.queryOne(
|
|
939
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'expense'", c.id,
|
|
940
|
+
) as SumRow).total;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const lines: string[] = ["", "## 公司组合概览", ""];
|
|
944
|
+
lines.push(`**组合汇总**: ${companies.length} 家公司 | 总收入: ${portfolioIncome.toLocaleString()} 元 | 总支出: ${portfolioExpense.toLocaleString()} 元 | 净利润: ${(portfolioIncome - portfolioExpense).toLocaleString()} 元`);
|
|
945
|
+
lines.push("");
|
|
946
|
+
|
|
947
|
+
for (const c of companies) {
|
|
948
|
+
// 阶段
|
|
949
|
+
const stage = db.queryOne(
|
|
950
|
+
"SELECT stage, stage_label FROM opc_company_stage WHERE company_id = ?", c.id,
|
|
951
|
+
) as StageRow | null;
|
|
952
|
+
const stageLabel = stage?.stage_label ?? "未检测";
|
|
953
|
+
|
|
954
|
+
// 活跃告警数
|
|
955
|
+
const alertCount = (db.queryOne(
|
|
956
|
+
"SELECT COUNT(*) as cnt FROM opc_alerts WHERE company_id = ? AND status = 'active'",
|
|
957
|
+
c.id,
|
|
958
|
+
) as { cnt: number }).cnt;
|
|
959
|
+
|
|
960
|
+
// 新成就数(未展示的)
|
|
961
|
+
const newCelebrations = (db.queryOne(
|
|
962
|
+
"SELECT COUNT(*) as cnt FROM opc_celebrations WHERE company_id = ? AND shown = 0",
|
|
963
|
+
c.id,
|
|
964
|
+
) as { cnt: number }).cnt;
|
|
965
|
+
|
|
966
|
+
// 健康评分
|
|
967
|
+
const health = computeHealthScore(db, c.id);
|
|
968
|
+
|
|
969
|
+
let summary = `- ${healthScoreEmoji(health.total)} **${c.name}**(${stageLabel} | ${health.total}分)`;
|
|
970
|
+
const badges: string[] = [];
|
|
971
|
+
if (alertCount > 0) badges.push(`${alertCount} 条告警`);
|
|
972
|
+
if (newCelebrations > 0) badges.push(`${newCelebrations} 个新成就`);
|
|
973
|
+
if (badges.length > 0) summary += ` — ${badges.join("、")}`;
|
|
974
|
+
|
|
975
|
+
lines.push(summary);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Top 3 跨公司洞察
|
|
979
|
+
const topInsights = db.query(
|
|
980
|
+
`SELECT i.title, i.message, i.priority, c.name as company_name
|
|
981
|
+
FROM opc_insights i
|
|
982
|
+
LEFT JOIN opc_companies c ON i.company_id = c.id
|
|
983
|
+
WHERE i.status = 'active' AND (i.expires_at = '' OR i.expires_at > datetime('now'))
|
|
984
|
+
ORDER BY i.priority DESC, i.created_at DESC
|
|
985
|
+
LIMIT 3`,
|
|
986
|
+
) as { title: string; message: string; priority: number; company_name: string }[];
|
|
987
|
+
|
|
988
|
+
if (topInsights.length > 0) {
|
|
989
|
+
lines.push("");
|
|
990
|
+
lines.push("### 重点关注");
|
|
991
|
+
for (const ins of topInsights) {
|
|
992
|
+
lines.push(`- [${ins.company_name}] **${ins.title}** — ${ins.message}`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// 跨公司未展示庆祝
|
|
997
|
+
const newCelebs = db.query(
|
|
998
|
+
`SELECT cel.title, cel.message, c.name as company_name
|
|
999
|
+
FROM opc_celebrations cel
|
|
1000
|
+
LEFT JOIN opc_companies c ON cel.company_id = c.id
|
|
1001
|
+
WHERE cel.shown = 0
|
|
1002
|
+
ORDER BY cel.created_at DESC
|
|
1003
|
+
LIMIT 3`,
|
|
1004
|
+
) as { title: string; message: string; company_name: string }[];
|
|
1005
|
+
|
|
1006
|
+
if (newCelebs.length > 0) {
|
|
1007
|
+
lines.push("");
|
|
1008
|
+
lines.push("### 最新成就");
|
|
1009
|
+
for (const cel of newCelebs) {
|
|
1010
|
+
lines.push(`- [${cel.company_name}] **${cel.title}** — ${cel.message}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return lines.join("\n");
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// ── 增长评分卡 ──────────────────────────────────────────────────
|
|
1018
|
+
|
|
1019
|
+
type GradeDimension = {
|
|
1020
|
+
name: string;
|
|
1021
|
+
grade: string;
|
|
1022
|
+
score: number;
|
|
1023
|
+
detail: string;
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
function letterGrade(score: number): string {
|
|
1027
|
+
if (score >= 90) return "A";
|
|
1028
|
+
if (score >= 75) return "B";
|
|
1029
|
+
if (score >= 55) return "C";
|
|
1030
|
+
if (score >= 35) return "D";
|
|
1031
|
+
return "F";
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function gradeEmoji(g: string): string {
|
|
1035
|
+
if (g === "A") return "🟢";
|
|
1036
|
+
if (g === "B") return "🔵";
|
|
1037
|
+
if (g === "C") return "🟡";
|
|
1038
|
+
if (g === "D") return "🟠";
|
|
1039
|
+
return "🔴";
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* 计算增长评分卡(5 维度 A–F 评级)。
|
|
1044
|
+
*/
|
|
1045
|
+
export function computeGrowthScorecard(db: OpcDatabase, companyId: string): {
|
|
1046
|
+
overall: string;
|
|
1047
|
+
overallScore: number;
|
|
1048
|
+
dimensions: GradeDimension[];
|
|
1049
|
+
} {
|
|
1050
|
+
const dims: GradeDimension[] = [];
|
|
1051
|
+
|
|
1052
|
+
// ── 财务成熟度 ──
|
|
1053
|
+
const fin = (() => {
|
|
1054
|
+
const totalIncome = (db.queryOne(
|
|
1055
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", companyId,
|
|
1056
|
+
) as SumRow).total;
|
|
1057
|
+
const totalExpense = (db.queryOne(
|
|
1058
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'expense'", companyId,
|
|
1059
|
+
) as SumRow).total;
|
|
1060
|
+
const revenueMonths = (db.queryOne(
|
|
1061
|
+
"SELECT COUNT(DISTINCT strftime('%Y-%m', transaction_date)) as cnt FROM opc_transactions WHERE company_id = ? AND type = 'income' AND amount > 0", companyId,
|
|
1062
|
+
) as CountRow).cnt;
|
|
1063
|
+
let score = 0;
|
|
1064
|
+
if (totalIncome > 0) score += 20;
|
|
1065
|
+
if (totalIncome > 10_000) score += 15;
|
|
1066
|
+
if (totalIncome > 50_000) score += 15;
|
|
1067
|
+
if (totalExpense > 0) score += 10; // 记录支出
|
|
1068
|
+
if (revenueMonths >= 2) score += 15;
|
|
1069
|
+
if (revenueMonths >= 6) score += 10;
|
|
1070
|
+
if (totalIncome > totalExpense && totalExpense > 0) score += 15; // 盈利
|
|
1071
|
+
const profitRate = totalIncome > 0 ? ((totalIncome - totalExpense) / totalIncome * 100) : 0;
|
|
1072
|
+
const detail = totalIncome === 0 ? "尚无收入" :
|
|
1073
|
+
`收入 ${totalIncome.toLocaleString()} 元, ${revenueMonths} 个月, 利润率 ${Math.round(profitRate)}%`;
|
|
1074
|
+
return { score: Math.min(score, 100), detail };
|
|
1075
|
+
})();
|
|
1076
|
+
dims.push({ name: "财务成熟度", grade: letterGrade(fin.score), score: fin.score, detail: fin.detail });
|
|
1077
|
+
|
|
1078
|
+
// ── 客户基础 ──
|
|
1079
|
+
const cust = (() => {
|
|
1080
|
+
const contactCount = (db.queryOne(
|
|
1081
|
+
"SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?", companyId,
|
|
1082
|
+
) as CountRow).cnt;
|
|
1083
|
+
const counterpartyCount = (db.queryOne(
|
|
1084
|
+
"SELECT COUNT(DISTINCT counterparty) as cnt FROM opc_transactions WHERE company_id = ? AND type = 'income' AND counterparty != ''", companyId,
|
|
1085
|
+
) as CountRow).cnt;
|
|
1086
|
+
const activeContacts = (db.queryOne(
|
|
1087
|
+
"SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ? AND last_contact_date > date('now', '-90 days')", companyId,
|
|
1088
|
+
) as CountRow).cnt;
|
|
1089
|
+
let score = 0;
|
|
1090
|
+
if (contactCount > 0) score += 25;
|
|
1091
|
+
if (contactCount >= 3) score += 15;
|
|
1092
|
+
if (contactCount >= 5) score += 10;
|
|
1093
|
+
if (counterpartyCount >= 2) score += 20; // 多元收入来源
|
|
1094
|
+
if (counterpartyCount >= 3) score += 10;
|
|
1095
|
+
if (activeContacts > 0) score += 20; // 定期联系
|
|
1096
|
+
const detail = contactCount === 0 ? "尚无客户" :
|
|
1097
|
+
`${contactCount} 个客户, ${counterpartyCount} 个收入来源, ${activeContacts} 个活跃联系`;
|
|
1098
|
+
return { score: Math.min(score, 100), detail };
|
|
1099
|
+
})();
|
|
1100
|
+
dims.push({ name: "客户基础", grade: letterGrade(cust.score), score: cust.score, detail: cust.detail });
|
|
1101
|
+
|
|
1102
|
+
// ── 运营体系 ──
|
|
1103
|
+
const ops = (() => {
|
|
1104
|
+
const hasProject = (db.queryOne(
|
|
1105
|
+
"SELECT COUNT(*) as cnt FROM opc_projects WHERE company_id = ?", companyId,
|
|
1106
|
+
) as CountRow).cnt > 0;
|
|
1107
|
+
const hasContract = (db.queryOne(
|
|
1108
|
+
"SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ?", companyId,
|
|
1109
|
+
) as CountRow).cnt > 0;
|
|
1110
|
+
const hasInvoice = (db.queryOne(
|
|
1111
|
+
"SELECT COUNT(*) as cnt FROM opc_invoices WHERE company_id = ?", companyId,
|
|
1112
|
+
) as CountRow).cnt > 0;
|
|
1113
|
+
const hasStaff = (db.queryOne(
|
|
1114
|
+
"SELECT COUNT(*) as cnt FROM opc_staff_config WHERE company_id = ? AND enabled = 1", companyId,
|
|
1115
|
+
) as CountRow).cnt > 0;
|
|
1116
|
+
const hasCanvas = (db.queryOne(
|
|
1117
|
+
"SELECT COUNT(*) as cnt FROM opc_opb_canvas WHERE company_id = ?", companyId,
|
|
1118
|
+
) as CountRow).cnt > 0;
|
|
1119
|
+
const taskCount = (db.queryOne(
|
|
1120
|
+
"SELECT COUNT(*) as cnt FROM opc_tasks WHERE company_id = ?", companyId,
|
|
1121
|
+
) as CountRow).cnt;
|
|
1122
|
+
let score = 0;
|
|
1123
|
+
if (hasCanvas) score += 20;
|
|
1124
|
+
if (hasProject) score += 20;
|
|
1125
|
+
if (hasContract) score += 20;
|
|
1126
|
+
if (hasInvoice) score += 15;
|
|
1127
|
+
if (hasStaff) score += 15;
|
|
1128
|
+
if (taskCount > 0) score += 10;
|
|
1129
|
+
const items = [hasCanvas && "画布", hasProject && "项目", hasContract && "合同", hasInvoice && "发票", hasStaff && "AI员工"].filter(Boolean);
|
|
1130
|
+
const detail = items.length === 0 ? "尚未建立" : `已建立: ${items.join("、")}`;
|
|
1131
|
+
return { score: Math.min(score, 100), detail };
|
|
1132
|
+
})();
|
|
1133
|
+
dims.push({ name: "运营体系", grade: letterGrade(ops.score), score: ops.score, detail: ops.detail });
|
|
1134
|
+
|
|
1135
|
+
// ── 品牌建设 ──
|
|
1136
|
+
const brand = (() => {
|
|
1137
|
+
const contentTotal = (db.queryOne(
|
|
1138
|
+
"SELECT COUNT(*) as cnt FROM opc_media_content WHERE company_id = ?", companyId,
|
|
1139
|
+
) as CountRow).cnt;
|
|
1140
|
+
const publishedCount = (db.queryOne(
|
|
1141
|
+
"SELECT COUNT(*) as cnt FROM opc_media_content WHERE company_id = ? AND status = 'published'", companyId,
|
|
1142
|
+
) as CountRow).cnt;
|
|
1143
|
+
const platforms = (db.queryOne(
|
|
1144
|
+
"SELECT COUNT(DISTINCT platform) as cnt FROM opc_media_content WHERE company_id = ? AND platform != ''", companyId,
|
|
1145
|
+
) as CountRow).cnt;
|
|
1146
|
+
const thisMonth = new Date().toISOString().slice(0, 7);
|
|
1147
|
+
const recentContent = (db.queryOne(
|
|
1148
|
+
`SELECT COUNT(*) as cnt FROM opc_media_content
|
|
1149
|
+
WHERE company_id = ? AND strftime('%Y-%m', COALESCE(NULLIF(published_date,''), created_at)) = ?`,
|
|
1150
|
+
companyId, thisMonth,
|
|
1151
|
+
) as CountRow).cnt;
|
|
1152
|
+
let score = 0;
|
|
1153
|
+
if (contentTotal > 0) score += 25;
|
|
1154
|
+
if (contentTotal >= 5) score += 15;
|
|
1155
|
+
if (contentTotal >= 10) score += 10;
|
|
1156
|
+
if (publishedCount > 0) score += 15;
|
|
1157
|
+
if (platforms >= 2) score += 15;
|
|
1158
|
+
if (recentContent > 0) score += 20; // 本月有发布
|
|
1159
|
+
const detail = contentTotal === 0 ? "尚无内容" :
|
|
1160
|
+
`${contentTotal} 篇内容, ${publishedCount} 篇已发布, ${platforms} 个平台`;
|
|
1161
|
+
return { score: Math.min(score, 100), detail };
|
|
1162
|
+
})();
|
|
1163
|
+
dims.push({ name: "品牌建设", grade: letterGrade(brand.score), score: brand.score, detail: brand.detail });
|
|
1164
|
+
|
|
1165
|
+
// ── 合规完善度 ──
|
|
1166
|
+
const comp = (() => {
|
|
1167
|
+
const hasTax = (db.queryOne(
|
|
1168
|
+
"SELECT COUNT(*) as cnt FROM opc_tax_filings WHERE company_id = ?", companyId,
|
|
1169
|
+
) as CountRow).cnt > 0;
|
|
1170
|
+
const hasContract = (db.queryOne(
|
|
1171
|
+
"SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ?", companyId,
|
|
1172
|
+
) as CountRow).cnt > 0;
|
|
1173
|
+
const hasInvoice = (db.queryOne(
|
|
1174
|
+
"SELECT COUNT(*) as cnt FROM opc_invoices WHERE company_id = ?", companyId,
|
|
1175
|
+
) as CountRow).cnt > 0;
|
|
1176
|
+
const activeAlerts = (db.queryOne(
|
|
1177
|
+
"SELECT COUNT(*) as cnt FROM opc_alerts WHERE company_id = ? AND status = 'active'", companyId,
|
|
1178
|
+
) as CountRow).cnt;
|
|
1179
|
+
let score = 30; // baseline
|
|
1180
|
+
if (hasTax) score += 25;
|
|
1181
|
+
if (hasContract) score += 20;
|
|
1182
|
+
if (hasInvoice) score += 15;
|
|
1183
|
+
if (activeAlerts === 0) score += 10;
|
|
1184
|
+
else score -= activeAlerts * 5;
|
|
1185
|
+
const items = [hasTax && "税务", hasContract && "合同", hasInvoice && "发票"].filter(Boolean);
|
|
1186
|
+
const detail = items.length === 0 ? "尚无合规记录" :
|
|
1187
|
+
`已有: ${items.join("、")}${activeAlerts > 0 ? `, ${activeAlerts} 条告警` : ""}`;
|
|
1188
|
+
return { score: Math.max(Math.min(score, 100), 0), detail };
|
|
1189
|
+
})();
|
|
1190
|
+
dims.push({ name: "合规完善度", grade: letterGrade(comp.score), score: comp.score, detail: comp.detail });
|
|
1191
|
+
|
|
1192
|
+
const overallScore = Math.round(dims.reduce((s, d) => s + d.score, 0) / dims.length);
|
|
1193
|
+
return { overall: letterGrade(overallScore), overallScore, dimensions: dims };
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// ── 每日简报快照 ────────────────────────────────────────────────
|
|
1197
|
+
|
|
1198
|
+
/** 保存每日简报快照到 opc_briefings(每个公司每天仅一条) */
|
|
1199
|
+
export function saveDailyBriefing(db: OpcDatabase, companyId: string): void {
|
|
1200
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1201
|
+
// 检查今日是否已有快照
|
|
1202
|
+
const existing = db.queryOne(
|
|
1203
|
+
"SELECT id FROM opc_briefings WHERE company_id = ? AND briefing_date = ?",
|
|
1204
|
+
companyId, today,
|
|
1205
|
+
);
|
|
1206
|
+
if (existing) return; // 今日已保存
|
|
1207
|
+
|
|
1208
|
+
const stageRow = db.queryOne(
|
|
1209
|
+
"SELECT stage FROM opc_company_stage WHERE company_id = ?", companyId,
|
|
1210
|
+
) as { stage: string } | null;
|
|
1211
|
+
|
|
1212
|
+
const health = computeHealthScore(db, companyId);
|
|
1213
|
+
const scorecard = computeGrowthScorecard(db, companyId);
|
|
1214
|
+
|
|
1215
|
+
const totalIncome = (db.queryOne(
|
|
1216
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", companyId,
|
|
1217
|
+
) as SumRow).total;
|
|
1218
|
+
const totalExpense = (db.queryOne(
|
|
1219
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'expense'", companyId,
|
|
1220
|
+
) as SumRow).total;
|
|
1221
|
+
const contactCount = (db.queryOne(
|
|
1222
|
+
"SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?", companyId,
|
|
1223
|
+
) as CountRow).cnt;
|
|
1224
|
+
const contractCount = (db.queryOne(
|
|
1225
|
+
"SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ? AND status = 'active'", companyId,
|
|
1226
|
+
) as CountRow).cnt;
|
|
1227
|
+
|
|
1228
|
+
const summaryJson = JSON.stringify({
|
|
1229
|
+
health_score: health.total,
|
|
1230
|
+
health_dimensions: health.dimensions,
|
|
1231
|
+
scorecard_grade: scorecard.overall,
|
|
1232
|
+
scorecard_dimensions: scorecard.dimensions,
|
|
1233
|
+
total_income: totalIncome,
|
|
1234
|
+
total_expense: totalExpense,
|
|
1235
|
+
net_profit: totalIncome - totalExpense,
|
|
1236
|
+
contact_count: contactCount,
|
|
1237
|
+
contract_count: contractCount,
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
const insights = getActiveInsights(db, companyId, 10);
|
|
1241
|
+
const insightsJson = JSON.stringify(insights.map(i => ({
|
|
1242
|
+
type: i.insight_type, title: i.title, priority: i.priority,
|
|
1243
|
+
})));
|
|
1244
|
+
|
|
1245
|
+
db.execute(
|
|
1246
|
+
`INSERT INTO opc_briefings (id, company_id, briefing_date, stage, health_score, summary_json, insights_json, next_steps_json, created_at)
|
|
1247
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, '[]', datetime('now'))`,
|
|
1248
|
+
db.genId(), companyId, today, stageRow?.stage ?? "idea", health.total, summaryJson, insightsJson,
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/** 获取上一次的简报快照(用于历史对比) */
|
|
1253
|
+
export function getLastBriefing(db: OpcDatabase, companyId: string): {
|
|
1254
|
+
date: string;
|
|
1255
|
+
healthScore: number;
|
|
1256
|
+
totalIncome: number;
|
|
1257
|
+
totalExpense: number;
|
|
1258
|
+
contactCount: number;
|
|
1259
|
+
scorecardGrade: string;
|
|
1260
|
+
} | null {
|
|
1261
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1262
|
+
const row = db.queryOne(
|
|
1263
|
+
`SELECT briefing_date, health_score, summary_json FROM opc_briefings
|
|
1264
|
+
WHERE company_id = ? AND briefing_date < ?
|
|
1265
|
+
ORDER BY briefing_date DESC LIMIT 1`,
|
|
1266
|
+
companyId, today,
|
|
1267
|
+
) as { briefing_date: string; health_score: number; summary_json: string } | null;
|
|
1268
|
+
|
|
1269
|
+
if (!row) return null;
|
|
1270
|
+
|
|
1271
|
+
try {
|
|
1272
|
+
const summary = JSON.parse(row.summary_json) as Record<string, unknown>;
|
|
1273
|
+
return {
|
|
1274
|
+
date: row.briefing_date,
|
|
1275
|
+
healthScore: row.health_score,
|
|
1276
|
+
totalIncome: (summary.total_income as number) ?? 0,
|
|
1277
|
+
totalExpense: (summary.total_expense as number) ?? 0,
|
|
1278
|
+
contactCount: (summary.contact_count as number) ?? 0,
|
|
1279
|
+
scorecardGrade: (summary.scorecard_grade as string) ?? "?",
|
|
1280
|
+
};
|
|
1281
|
+
} catch {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/** 构建 Webhook 每日推送文本 */
|
|
1287
|
+
export function buildWebhookBriefingText(db: OpcDatabase): string {
|
|
1288
|
+
const companies = db.query(
|
|
1289
|
+
"SELECT id, name FROM opc_companies ORDER BY created_at DESC LIMIT 10",
|
|
1290
|
+
) as { id: string; name: string }[];
|
|
1291
|
+
|
|
1292
|
+
if (companies.length === 0) return "";
|
|
1293
|
+
|
|
1294
|
+
const lines: string[] = [`📋 星环OPC每日简报 (${new Date().toISOString().slice(0, 10)})`, ""];
|
|
1295
|
+
|
|
1296
|
+
for (const c of companies) {
|
|
1297
|
+
const health = computeHealthScore(db, c.id);
|
|
1298
|
+
const scorecard = computeGrowthScorecard(db, c.id);
|
|
1299
|
+
const stageRow = db.queryOne(
|
|
1300
|
+
"SELECT stage_label FROM opc_company_stage WHERE company_id = ?", c.id,
|
|
1301
|
+
) as { stage_label: string } | null;
|
|
1302
|
+
|
|
1303
|
+
const totalIncome = (db.queryOne(
|
|
1304
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", c.id,
|
|
1305
|
+
) as SumRow).total;
|
|
1306
|
+
|
|
1307
|
+
lines.push(`【${c.name}】${stageRow?.stage_label ?? "未知"} | 健康 ${health.total}分 | 评级 ${scorecard.overall}`);
|
|
1308
|
+
lines.push(` 收入: ${totalIncome.toLocaleString()} 元 | ${scorecard.dimensions.map(d => `${d.name}:${d.grade}`).join(" ")}`);
|
|
1309
|
+
|
|
1310
|
+
// 未展示庆祝
|
|
1311
|
+
const celebs = db.query(
|
|
1312
|
+
"SELECT title FROM opc_celebrations WHERE company_id = ? AND shown = 0 LIMIT 2", c.id,
|
|
1313
|
+
) as { title: string }[];
|
|
1314
|
+
if (celebs.length > 0) {
|
|
1315
|
+
lines.push(` 🎉 ${celebs.map(x => x.title).join("、")}`);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// 紧急告警
|
|
1319
|
+
const alerts = db.query(
|
|
1320
|
+
"SELECT title FROM opc_alerts WHERE company_id = ? AND status = 'active' AND severity IN ('critical','warning') LIMIT 2", c.id,
|
|
1321
|
+
) as { title: string }[];
|
|
1322
|
+
if (alerts.length > 0) {
|
|
1323
|
+
lines.push(` ⚠️ ${alerts.map(x => x.title).join("、")}`);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
lines.push("");
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
lines.push("— 星环OPC中心 AI 助手");
|
|
1330
|
+
return lines.join("\n");
|
|
1331
|
+
}
|